DirectX (Игровой SDK)

В статье описано, как использовать DirectDraw для рисования с аппаратным ускорением при помощи TCanvas.
== Модель компонентных объектов (СОМ) ==
Перед углублением в изнурительные подробности DirectDraw, сначала несколько слов о модели компонентных объектов - кратко СОМ. Delphi использует объектно-ориентированный язык программирования Object Pascal. Дизайнеры Delphi решили сделать родные Delphi объекты полностью совместимыми с СОМ и OLE. Это большая новость для нас, потому что DirectDraw использует интерфейс СОМ и поэтому из Delphi получить к нему доступ достаточно просто.
Объекты СОМ подробно освещены в разделе Delphi. Но для того, чтобы сэкономить ваше время, предоставлю краткий обзор. В Delphi вы работаете с объектом СОМ практически так же, как и с другим объектом. Объекты СОМ выглядят по сути как обычные объекты Delphi. Они имеют методы, которые вы вызываете для доступа к их услугам. Тем не менее, они не имеют полей или свойств. Главным отличием является то, что вы вызываете метод Release вместо метода Free, если вы хотите освободить эти объекты.
Вы также никогда не создаете объект СОМ путем вызова конструктора. Вместо этого вы вызываете функцию в DirectDraw для создания главного объекта DirectDraw. Этот объект имеет дальнейшие методы, которые вы используете для создания других методов. Помимо этих двух вопросов вы можете фактически думать о них как об объектах Delphi.
Объекты СОМ DirectDraw определяются в довольно сложном файле-заголовке на С, который поставляется с Game SDK. Однако я перевел это в модуль импорта, который вы можете использовать в Delphi. Это файл DDraw.pas на сопровождающем CD-ROM. Для того, чтобы получить доступ к DirectDraw, просто добавьте DDraw в предложение uses.
== DirectDraw ==
DirectDraw может оказаться довольно каверзным в использовании. На первый взгляд он кажется простым; существует только несколько СОМ-классов и они не имеют большого количества методов. Однако DirectDraw использует записи для определения всех видов различных параметров при создании своих объектов. На первый взгляд они выглядят действительно устрашающе. Вы можете найти их в справочных файлах Game SDK, начиная с букв DD, например DDSurfaceDesc. Являясь API низкого уровня, существует множество опций и параметров, которые допускают разницу в спецификациях аппаратного обеспечения и возможностях. К счастью, в большинстве случаев можно проигнорировать множеством этих опций. Самой большой проблемой в момент написания этой книги является недостаток информации в SDK документации, которая описывает, какие комбинации опций разрешаются, поэтому для того, чтобы помочь вам найти путь через минное поле, эта глава поэтапно проходит по всем стадиям создания приложения DirectDraw. Я представляю код, который добавляется на каждом этапе и использует его для объяснения аспекта DirectDraw, также как и рабочий пример, на основании которого можно строить свои собственные программы.
=== Объект IDirectDraw ===
DLL с DirectDraw фактически имеет самый простой из интерфейсов. Она экспортирует только одну функцию: DirectDrawCreate. Вы используете эту функцию для создания СОМ-объекта IDirectDraw, который открывает остальную часть API. Таким образом, первое, что должен сделать пример - создать один из таких объектов. Вы делаете это в обработчике события OnCreate формы и разрушаете его в OnDestroy. Лучшим местом хранения объекта является приватное поле главной формы. Листинг 1 содержит базовый код для осуществления этого.
Листинг 1. Создание объекта IDirectDraw.
unit Uniti;
interface
uses
 Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, DDraw;
type
 TForm1 = class(TForm)
 procedure FormCreate(Sender: TObject);
 procedure FormDestroy(Sender: TObject);
 private
 DirectDraw: IDirectDraw ; // главный объект DirectDraw
 end;
var
 Form1: TForm1;
implementation
procedure TForm1.FormCreate(Sender: TObject);
begin
 { создать СОМ-объект DirectDraw }
 if DirectDrawCreate(nil, DirectDraw, nil) <> DD_OK then
 raise Exception.Create('Failed to create IDirectDraw object');
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
 { освободить СОМ-объект DirectDraw за счет вызова его метода Release }
 if Assigned(DirectDraw) then
 DirectDraw.Release;
end;
end.
Вы можете скачать этот тривиальный пример DDDemo1 здесь (пока не можете, я надеюсь это будет позже, прим. Lel). Он не делает что-либо очевидного, когда вы запускаете его, поэтому не ожидайте слишком многого. Я включаю его для того, чтобы показать, как мало кода требуется для создания и освобождения СОМ-объекта DirectDraw. Это действительно очень просто.
=== Коды возврата DirectDraw и исключения Delphi ===
Подавляющее большинство функций DirectDraw возвращает результирующий код целого типа с именем HResult, о котором вы можете думать как об Integer. Файл DDraw.pas имеет все возможные константы ошибок, занесенные в список, а справочный файл Game SDK указывает на возможные коды ошибки, возвращаемые каждой функцией. Вы можете проверить результаты этих функций, и в большинстве случаев возбудить исключение, если результат отличается от DD_OK.
Однако имеется ряд проблем с использованием исключений, поскольку вы переключаетесь на специальный экранный режим. Это означает, что вы не способны видеть Delphi IDE, когда он разрушается или прерывается в момент исключения, и ваша программа кажется замороженной. Установка точки прерывания обычно приводит в результате к одной и той же проблеме: приложение останавливается как раз в точке прерывания, но вы не имеете возможность увидеть Delphi. Добро пожаловать в программирование игр в среде Windows! Я обсуждаю это более подробно несколько позже.
=== Переключение на полноэкранный режим ===
Следующее, что необходимо сделать, - это переключить дисплей в режим перелистывания страниц. Когда вы это делаете, становится видимым только ваше приложение. Оно занимает весь экран. Любые другие приложения Windows, которые находятся в режиме выполнения, подобные Windows Explorer, продолжают работать и могут записывать выходные данные на то, что они считают экраном. Вы не видите, как выглядят выходные данные, потому что другие приложения все еще используют GDI для выходных данных, которому ничего не известно о DirectDraw. Но вам вовсе нет необходимости беспокоиться об этом. GDI будет продолжать беспечно писать в экранную память, хотя вы в действительности не сможете увидеть его выходные данные.
Путем переключения в специальный режим отображения данных вы занимаете весь экран. Как правило, вы можете запускать множество регулярных приложений среды Windows в одно и то же время; их окна перекрываются и благодаря GDI дела идут прекрасно. Но что произойдет, если вы попытаетесь запустить два и более полноэкранных DirectDraw-приложений в одно и то же время? Ответ - только одному разрешен доступ к полному экрану. DirectDraw управляет этим, предполагая, что вы имеете исключительный доступ к видеокарте перед изменением режима. Вы сделаете это установкой кооперативного уровня объекта DirectDraw в Exclusive. DirectDraw поддерживает эксклюзивный уровень доступа только для одного приложения одновременно. Если вы попытаетесь получить эксклюзивный доступ и какое-нибудь другое приложение уже его имеет, вызов не удастся. Подобным же образом, если вы попытаетесь изменить режимы отображения данных без приобретения эксклюзивного доступа, этот вызов не удастся. Таким образом, попытайтесь получить эксклюзивный доступ и затем переключите режимы отображения.
Здесь необходимо отметить, что вы должны предоставить описатель окна SetCooperativeLevel. DirectDraw изменяет размеры этого окна автоматически, так что оно заполняет экран в новом режиме отображения данных. Вы должны передать описатель формы в SetCooperativeLevel. Ввиду того, что описатель окна не был создан до времени вызова OnCreate, вы должны все это сделать и событии OnShow. Листинг 2 показывает, как это сделать.
Листинг 2. Переключение в полноэкранный режим в OnShow.
procedure TForm1.FormShow(Sender: TObject);
begin
 if DirectDraw.SetCooperativeLevel(Handle, DDSCI_EXCLUSIVE or DDSCI_FULLSCREEN) <> DD_OK then
 raise Exception.Create('Unable to acquire exclusive full-screen access');
 if DirectDraw.SetDisplayMode(640, 480, 8) <> DD_OK then
 raise Exception.Create('Unable to set new display mode');
end;
Пока все в порядке. Ничего тут сложного нет. Если вы запустите пример прямо сейчас, ваше приложение переключит режимы и вы увидите, как форма заполнит экран. Если вы передвинете или измените ее размеры, вы увидите за ним Delphi. Вы все еще смотрите на поверхность вывода GDI. GDI может благополучно выводить данные в этих различных режимах, так что вы увидите свои обычные приложения Windows так долго, сколько эта поверхность будет оставаться на переднем плане. Но ввиду того, что вы создаете приложение с мелькающими страницами, это не совсем то, что нам нужно. Директория DDDemo2 содержит изложенные примеры
=== Добавление обработчика исключений приложения ===
Как я уже упоминал ранее, тот факт, что DirectDraw занимает полный экран может вызвать проблему с обработкой исключений. Когда исключение возбуждается, по умолчанию Delphi IDE попадает в отладчик программы и приостанавливает исполнение программы, устанавливая кодовое окно на строке, содержащей ошибку. Проблема заключается в том, что когда происходит мелькание страниц вы, вероятно, не сможете увидеть IDE и приложение будет выглядеть замороженным. Еще хуже, если вам удастся продолжить исполнение, или на опции IDE окажется выключенной Break on exception (Останавливаться, если возбуждено исключение), то вы можете не увидеть окна сообщения, которое появляется с сообщением исключения.
Один из способов избежать этот сценарии отменить маркер на флажке Break on exception в IDE (TooIsjOptions menu) и установить в своем приложении специальный обработчик исключений приложения. Этот обработчик должен переключаться на поверхность GDI перед тем, как показать сообщение исключения. Это намного легче, чем может показаться. Все, что вам необходимо сделать - создать собственный private-метод в форме и присвоить его Application.OnException в OnCreate формы. Не забывайте установить его обратно в nil в OnDestroy. Новый обработчик может использовать метод SwitchToGDISurface объекта IDirectDraw перед вызовом MessageDlg. Листинг 3 показывает обработчик исключения.
Листинг 3. Обработчик исключений приложения.
procedure TForm1.ExceptionHandler(Sender: TObject; E: Exception);
begin
 if Assigned(DirectDraw) then
 DirectDraw.FlipToGDISurface;
 MessageDlg(E.Message, mtError, [mbOK], 0);
end;
Для того, чтобы установить обработчик исключения мы добавим следующую строку в OnCreate:
Application.OnException := ExceptionHandler;
Помните, что нужно выключить Break on exception (в TooIsfOptions). Как только вы наберетесь больше опыта, вы сможете включить эту опцию снова для специфических заданий отладки. Однако, если ваше приложение вызовет исключение, пока поверхность GDI невидима, IDE возьмет свое и вы ничего не увидите. Нажатие F9 должно вызвать повторное исполнение, а нажатие Ctrl-F2 вернет приложение в исходное состояние и возвратить вас в Delphi.
== Поверхности отображения ==
Теперь вы готовы создавать поверхности отображения. В DirectDraw поверхность отображения представляет собой линейную область экранной памяти, к которой можно получить непосредственный доступ для манипуляций. Поверхность отображения, которую вы видите на экране, называется основной поверхностью. Она представляет память видимого кадрового буфера на карте отображения. Вы также можете иметь невидимые поверхности, которые определяются как внеэкранные, или оверлейные поверхности. Подобное может существовать либо в регулярной системной памяти, либо во внеэкранной области памяти на самой графической карте. Для того, чтобы создать чередующиеся страницы, необходима основная поверхность и, по крайней мере, одна внеэкранная поверхность для осуществления отображения. Для того, чтобы внеэкранная поверхность могла появляться и исчезать на экране, она должна находится в видеопамяти. Тем не менее, DirectDraw пытается создать поверхности в видеопамяти по умолчанию, поэтому нет необходимости предпринимать что-либо специально.
Существует способ для создания основной поверхности и одной и более сменных поверхностей в одно и то же время за счет создания комплексной поверхности. Еще один аспект в создании комплексной (составной) поверхности заключается в том, что вы можете освободить все поверхности в комплексной цепи сменных поверхностей путем высвобождения самой комплексной поверхности. Для примера создадим комплексную поверхность посредством одной вспомогательной буферной поверхности.
Фоновые поверхности, которые создаются в качестве части комплексной, известны как Неявные поверхности. Существует большое число операций, которые вы не сможете осуществить с помощью Неявных поверхностей, например, отсоединить их от основной поверхности или освободить их независимо от основной поверхности. Однако, комплексные поверхности намного проще создавать, потому что DirectDraw создает фоновые буфера и соединяет их с основной поверхностью.
В этой связи я должен затронуть вопрос сложности DirectDraw, поскольку необходимо заполнять поля и записи TDDSurfaceDesc. Если вы прочитаете об этом в справке DirectDraw, вы сможете увидеть, что все это выглядит довольно ужасно! Но как я уже говорил, вы можете счастливо игнорировать большинство из этих полей. Листинг 4 представляет код, который необходимо добавить в обработчик OnShow для создания комплексной поверхности.
Листинг 4. Создание комплексной поверхности.
{ заполнить описатель DirectDrawSurface перед созданием поверхности }
FillChar(DDSurfaceDesc, SizeOf(DDSurfaceDesc), 0);
with DDSurfaceDesc do
begin
 dwSize := SizeOf(DDSurfaceDesc);
 dwFlags := DDSD_CAPS or DDSD_BACKBUFFERCOUNT;
 ddSCaps.dwCaps := DDSCAPS_COMPLEX or DDSCAPS FLIP or DDSCAPS_PRIMARYSURFACE;
 dwBackBufferCount: = 1;
end;
Листинг 7. Тестирование нажатия клавиш Esc и F12.
procedure TForm1.FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
 // если нажаты клавиши Escape или F12, завершить приложение
 case Key of
 VK_ESCAPE, VK_F12: Close;
 end;
end;
Вы можете скачать этот пример и DDDemo3 здесь. Если вы запустите его, вы увидите на экране мелькание сменяющих друг друга поверхностей GDI, которые содержат формы размером с экран, и фоновый буфер, который, вероятно, заполнен различными битами "остатков" отображения. Помните, для выхода следует нажать Esc или F12 (или конечно же Alt+F4).
=== Получение доступа к фоновому буферу ===
Теперь, когда вы познали основы приложения смены страниц, вы, вероятно, захотите что-нибудь с ним сделать. Вы нуждаетесь в том, чтобы иметь возможность рисовать на поверхности фонового буфера. Однако, в последней секции вы создали комплексную поверхность, которая автоматически создала для нас фоновый буфер. Проблема заключается в том, что функция CreateSurface заполнила поле PrimaryField (основная поверхность), и вы должны получить доступ к фоновому буферу. Для этого можно вызвать метод GetAttachedSurface. Добавьте поле фонового буфера BackBuffer к форме и код из листинга 8 - к OnShow:
Листинг 8. Доступ к поверхности фонового буфера.
{ получить фоновый буфер }
DDSCaps.dwCaps: = DDSCAPS_BACKBUFFER;
if PrimarySurface.GetAttachedSurface(DDSCaps, BackBuffer) <> DD_OK then
 raise Exception.Create('Failed to get back buffer surface');
DDSCaps является локальной переменной типа TDDSCaps, которая добавляется к обработчику FormShow. Вы заполняете флажки для необходимой присоединенной поверхности и вызываете GetAttachedSurface. В этом случае вам необходим фоновый буфер. Метод может вернуть только одну поверхность. Вызов напрасен, если более чем одна присоединенная поверхность соответствует переданным флажкам DDSCaps. Однако, не имеет значения, сколько фоновых поверхностей вы создали, существует только одна поверхность с флажком фонового буфера, и она является первой в цепи сменных поверхностей после основной. Если необходимо получить все присоединенные поверхности, можно вызвать функцию EnumAttachedSurfaces.
=== Восстановление поверхностей ===
Одна из многих особенностей DirectDraw заключается в том, что поверхности могут потерять свою память по многим причинам; например, когда изменяется режим отображения. Если это происходит, вы должны вызвать метод Restore поверхности, чтобы получить свою память обратно. Вы также должны перерисовать поверхность. Это несколько напоминает то, как у вас возникает необходимость нарисовать окно в обычном программировании для Windows, когда оно перекрывается и нуждается в обновлении. Большинство из функций IDirectDrawSurface могут возвратить результат DDERR_SURFACELOST. Когда это происходит, вы должны восстановить поверхность и перерисовать ее. Многие из этих функций также могут вернуть DDERR_WASSTILLDRAWING, что по сути означает, что аппаратное обеспечение занято и запрос необходимо повторять до тех пор, пока вы не добьетесь успеха, или пока вы не получите иное сообщение об ошибке.
Вот основополагающая логическая схема, использующая метод Flip. Этот пример предназначен только для того, чтобы ввести вас в курс. Он не перерисовывает поверхности. Смотрите листинг 9.
Листинг 9. "Традиционный" код для проверки и восстановления поверхности.
repeat
 DDResult := PrimarySurfасе.Flip(nil, 0);
 case DDResult of
 DD_OK:
 Break;
 DDERR_SURFACELOST:
 begin
 DDResult := PrimarySurface.Restore();
 if DDResult <> DD_OK then
 Break;
 end;
 else
 if DDResult <> DDERR_WASSTILLDRAWING then
 Break
 end;
until False;
Самое неприятное то, что вам необходим подобный код практически для каждого вызова метода IDirectDrawSurface. Всякий раз, когда спецификация вызова в справке Game SDK содержит DDERR_SURFACELOST в качестве возможного результата, это необходимо. Но Pascal - структурированный язык высокого уровня, не так ли? Таким образом, почему бы не написать небольшой сервисный метод для оказания такой помощи? Вот этот метод с именем одного из моих любимых шоу. (Оно не дает возможности себя забыть!) Оно представлено в листинге 10.
Листинг 10. Функция MakeItSo для оказание помощи в восстановлении поверхности.
function TForm1.MakeItSo(DDResult: HResult): Boolean;
begin
 { утилита для предоставления помощи в восстановлении поверхностей }
 case DDResult of
 DD_OK: Result := True;
 DDERR_SURFACELOST: Result := RestoreSurfaces <> DD_OK;
 else
 Result := DDResult <> DDERR_WASSTILLDRAWING;
 end;
end;
Последний метод восстанавливает поверхность в случае необходимости и затем вызывает функцию RestoreSurface, которую я вам сейчас представлю. Но сначала вот как следует ее использовать, применяя Flip, как в предыдущем примере:
repeat
 ...
until MakeItSo(PrimarySurfасе.Flip(nil, DDFLIP_WAIT));
Теперь я уверен, вы согласитесь, что это намного аккуратней и приятней, чем постоянно дублировать код, который я продемонстрировал ранее. Flip вызывается непрерывно, пока не достигнет успеха, либо пока не возникнет серьезная проблема. Я мог бы вызвать исключение в MakeItSo, если бы возникла неисправимая проблема. Примеры Game SDK, будучи написанными на С без обработки исключений, просто игнорируют результаты ошибки. Однако, если вы хотите использовать исключения, измените MakeItSo, как показано в листинге 11.
Листинг 11. Необязательная MakeItSo, которая вызывает исключения.
function TForm1.MakeItSo(DDResult: HResult): Boolean;
begin
 { утилита для предоставления помощи в восстановлении поверхностей - версия с исключениями }
 Result := False;
 case DDResult of
 DD_OK: Result := True;
 DDEKR_SURFACELOST: if RestoreSurfaces <> DD_OK then
 raise Exception.Create('MakeItSo failed');
 else
 if DDResult <> DDERR_WASSTILLDRAWING then
 raise Exception.Create('MakeItSo failed');
 end;
end;
Хорошо, теперь перейдем к методу RestoreSurfaces, при необходимости вызываемому в MakeItSo. Листинг 12 показывает метод RestoreSurfaces.
Листинг 12. Восстановление и перерисовка поверхности DirectDraw.
function TForm1.RestoreSurfaces: HResult;
begin
 { вызывается MakeItSo, если поверхности "потерялись" - восстановить и перерисовать их }
 Result := PrimarySurface.Restore;
 if Result = DD_OK then
 DrawSurfaces;
end;
Ничего удивительного. Вызывается метод Restore объекта основной поверхности. Ввиду того, что вы создали ее как комплексный объект, он автоматически восстанавливает любые неявные поверхности. Поэтому нет необходимости вызывать Restore для фонового буфера. Если Restore успешно восстановил память поверхности, вы вызываете DrawSurfaces, которую мы обсудим подробно далее.
=== Рисование на поверхностях DirectDraw ===
Существует два способа рисовать на поверхности DirectDraw. Вы можете получить указатель непосредственно на область памяти поверхности и непосредственно ею манипулировать. Это очень мощный способ, но требует написания специального кода и часто для скорости - на ассемблере. Все-таки вам редко придется это делать, потому что DirectDraw может создавать контекст устройства (DC), совместимый с GDI. Это означает, что вы можете рисовать на ней, используя стандартные вызовы GDI, а также любой DC. Однако, вызовы GDI достаточно утомительны, и Delphi уже включает DC в свой класс TCanvas. Таким образом, в примере я создаю TCanvas и использую его для облегчения себе жизни. Разве невозможно полюбить Delphi за это!
Все, что необходимо сделать, - создать объект TCanvas и вызвать метод GetDC поверхности. Затем вы назначаете DCCanvas.Handle, убедившись, что вы по завершению переустановили Handle в ноль. Создание холста и размещение контекстов устройств требует памяти и ресурсов. Контексты устройства представляют собой особенно скудный ресурс. Существенно важно освободить их, когда вы закончите. Для того, чтобы сделать код непробиваемым, используйте блоки try ... finally.
Листинг 13 представляет этот код. Он просто заполняет основную поверхность голубым цветом и выводит текст "Primary surface" (Основная поверхность) в центре слева. Фоновый буфер закрашивается в красный цвет и содержит текст "Back buffer" (Фоновый буфер) в центре справа. Листинг 13 с примером DDDemo4 можно скачать здесь.
Листинг 13 Данная процедура заполняет основную поверхность голубым цветом и выводит текст "Primary surface" (Основная поверхность) в центре слева. Фоновый буфер закрашивается в красный цвет и содержит текст "Back buffer" (Фоновый буфер) в центре справа.
procedure TForm1.DrawSurfaces;
var
 DC: HDC;
 ARect: TRect;
 DDCanvas: TCanvas;
 ATopPos: Integer;
begin
 // fill the primary surface with red and the back buffer with blue
 // and put some text on each. Using a canvas makes this trivial.
 DDCanvas := TCanvas.Create;
 try
 // first output to the primary surface
 if PrimarySurface.GetDC(DC) = DD_OK then
 try
 ARect := Rect(0, 0, 640, 480);
 with DDCanvas do
 begin
 Handle := DC; // make the canvas output to the DC
 Brush.Color := clRed;
 FillRect(ARect);
 Brush.Style := bsClear; // transparent text background
 Font.Name := 'Arial';
 Font.Size := 24;
 Font.Color := clWhite;
 ATopPos := (480 - TextHeight('A')) div 2;
 TextOut(10, ATopPos, 'Primary surface');
 end;
 finally
 // make sure we tidy up and release the DC
 DDCanvas.Handle := 0;
 PrimarySurface.ReleaseDC(DC);
 end;
 // now do back buffer
 if BackBuffer.GetDC(DC) = DD_OK then
 try
 with DDCanvas do
 begin
 Handle := DC; // make the canvas output to the DC
 Brush.Color := clBlue;
 FillRect(ARect);
 Brush.Style := bsClear; // transparent text background
 Font.Name := 'Arial';
 Font.Size := 24;
 Font.Color := clWhite;
 TextOut(630 - TextWidth('Back buffer'), ATopPos, 'Back buffer');
 end;
 finally
 // make sure we tidy up and release the DC
 DDCanvas.Handle := 0;
 BackBuffer.ReleaseDC(DC);
 end;
 finally
 // make sure the canvas is freed
 DDCanvas.Free;
 end;
end;
=== Непригодность основной формы ===
В предыдущих примерах форма была явно видима, заполняя собой основную поверхность. Однако, вы же не хотите, чтобы пользователь видел форму. Это приложение смены страниц и оно рисует по всему экрану. Поэтому вы должны предотвратить отображение формы на экране. Также необходимо избавиться от системного меню и неклиентских клавиш. Все это можно достичь просто установкой BorderStyle формы в bsNone в методе FormCreate. Вам также не нужен и курсор, поэтому установите его в crNone. Добавьте эти три строки к FormCreate:
BorderStyle := bsNone;
Color := clBlack;
Cursor := crNone;
Единственно, что остается сделать - убедиться в том, что поверхности рисуются правильно в самом начале. Сделайте проверку, вызвав DrawSurfaces в обработчике события OnPaint формы. Если вы этого не сделаете, основная поверхность изначально отобразит форму; то есть, экран будет полностью черным. Листинг 14 представляет обработчик события OnPaint формы.
Листинг 14. Обработчик события OnPaint просто вызывает DrawSurfaces.
procedure TForml.FormPaint(Sender: TObject);
begin
 // рисовать что-нибудь на основной поверхности и на фоновом буфере
 DrawSurfaces;
end;
Ну, все! Вы можете найти измененный код в примере DDDemo4 (скачать).
== Мощь Delphi: пользовательский класс холста (Canvas) ==
До этого вы наблюдали, как использовать прекрасное средство Delphi TCanvas для получения доступа к контексту устройства, который позволяет рисовать на поверхности DirectDraw. Однако, мы можем значительно все упростить благодаря применению объектно-ориентированного подхода. Сейчас вы создадите наследника класса TCanvas для того, чтобы иметь возможность рисовать на поверхности даже намного проще. Это очень просто; код представлен в листинге 15.
Листинг 15. Объект холста DirectDraw в Delphi. 
unit DDCanvas;
interface
uses Windows, SysUtils, Graphics, DDraw;
type
 TDDCanvas = class(TCanvas)
 private
 FSurface: IDirectDrawSurface;
 FDeviceContext: HDC;
 protected
 procedure CreateHandle; override;
 public
 constructor Create(ASurface: IDirectDrawSurface);
 destructor Destroy; override;
 procedure Release;
 end;
implementation
constructor TDDCanvas.Create(ASurface: IDirectDrawSurface);
begin
 inherited Create;
 if ASurface = nil then
 raise Exception.Create('Cannot create canvas for NIL surface');
 FSurface := ASurface;
end;
destructor TDDCanvas.Destroy;
begin
 Release;
 inherited Destroy;
end;
procedure TDDCanvas.CreateHandle;
begin
 if FDeviceContext = 0 then
 begin
 FSurface.GetDC(FDeviceContext);
 Handle := FDeviceContext;
 end;
end;
procedure TDDCanvas.Release;
begin
 if FDeviceContext <> 0 then
 begin
 Handle := 0;
 FSurface.ReleaseDC(FDeviceContext)
 FDeviceContext := 0;
 end;
end;
end.
=== Использование класса DDCanvas. ===
Для того, чтобы использовать этот класс, следует скопировать модуль DDCanvas.pas в каталог Lib, который находится в каталоге Delphi 3.0, или и другой каталог, обозначенный в пути поиска библиотеки.
Помните ли вы злополучное взаимное исключение Win, которое приостанавливает многозадачную работу? Хорошо, я еще раз подчеркну необходимость освобождения DC. Класс TDDCanvas имеет и использует в своих целях метод Release. Всегда заворачивайте любой доступ к холсту в блок try .. finally, например:
try
 DDCanvas.TextOut(0, 0, 'Hello Flipping World!');
 { и т.д. }
finally
 DDCanvas.Release;
end;
Или, как я часто делаю, используйте конструкцию with для того, чтобы сэкономить время набора:
with DDCanvas do
 try
 TextOut(0, 0, 'Hello Withering World!');
 { и т.д. }
 finally
 Release;
 end;
end;
Итак, теперь вы можете добавить пару таких холстов в объявления формы, создавая их в FormShow, например:
{ создать два TDDCanvas для наших двух поверхностей }
PrimaryCanvas := TDDCanvas.Create(PrimarySurface);
BackCanvas := TDDCanvas.Create(BackBuffer);
Освободите их в FormDestroy перед тем, как освободить поверхности:
{ освободить объекты TDDCanvas перед освобождением поверхностей }
PrimaryCanvas.Free;
BackCanvas.Free;
Теперь можно осуществлять вывод либо на основную поверхность, либо на фоновый буфер, просто применяя эти полотна. Таким образом, вы изменяете DrawSurfaces для их использования, значительно упрощая код, что продемонстрировано в листинге 16.
Листинг 16. DrawSurfaces использует объекты TDDCanvas.
procedure TForm1.DrawSurfaces;
var
 ARect: TRect;
 ATopPos: Integer;
begin
 // вначале выводить на основную поверхность.
 ARect := Rect(0, 0, 640, 480);
 with PrimaryCanvas do
 try
 Brush.Color := clRed;
 FillRect(ARect);
 Brush.Style := bsClear;
 Font.Name := 'Arial';
 Font.Size := 24;
 Font.Color := clWhite;
 ATopPos := (480 - TextHeight('A')) div 2;
 Text0ut(10, ATopPos, 'Primary surface');
 finally
 // убедиться, что мы сразу же освободили DC,
 // поскольку Windows замораживается, пока мы владеем DC.
 Release;
 end;
 // теперь работаем с фоновым буфером
 with BackCanvas do
 try
 Brush.Color := clBlue;
 FillRect(ARect);
 Brush.Style := bsClear;
 Font.Name := 'Arial';
 Font.Size := 24;
 Font.Color := clWhite;
 Text0ut(630 - TextWidth('Back buffer'), ATopPos, 'Back buffer');
 finally
 // убедиться, что DC освобожден
 Release;
 end;
end;
Обратите внимание на блоки try .. finally с вызовом Release. Помимо этого, теперь мы добрались до этапа, на котором уже можно рисовать на поверхностях DirectDraw, не используя скверные коды DirectDraw, а просто приятные методы холста Delphi!
== Загрузка и отрисовка растровых изображений ==
Теперь, когда у вас прекрасно работает смена страниц, самое время научиться загружать растровое изображение на поверхность отображения. Процесс загрузки растрового изображения значительно упрощен по сравнению с тем, как это происходило в Windows 3.х, за счет введения функций LoadImage() и CreateDIBSection() в WIN32 API. В Windows 95 вы можете использовать LoadImage для загрузки растрового изображения либо с дискового файла, либо из ресурса. В окончательном приложении вы несомненно встроите свои изображения в ЕХЕ-файл в виде ресурсов. Однако, полезно иметь возможность загружать их из файла во время разработки.
Первой из них, на которую следует обратить внимание, является DDReLoadBitmap. Вы можете смело использовать ее без понимания того, что она делает, но с целью обучения полезно немного заглянуть в этот код. Бывают моменты, когда вам может понадобиться самостоятельно написать специализированный код по обслуживанию растровых изображений. Это даст вам определенное понимание того, как это сделать. Листинг 17 представляет эту процедуру.
Листинг 17. Сервисная процедура DDReLoadBitmap для загрузки изображений.
procedure DDReLoadBitmap(Surface: IDirectDrawSurface; const BitmapName: string);
var
 Bitmap: HBitmap;
begin
 // попытаться загрузить изображение как ресурс;
 // если это не удается, то как файл
 Bitmap := Loadimage(GetModuleHandle(nil), PChar(BitmapName),
 IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);
 try
 if Bitmap = 0 then
 Bitmap := Loadimage(0, PChar(BitmapName), IMAGE_BITMAP,
 0, 0, LR_LOADFROMFILE or LR_CREATEDIBSECTION);
 if Bitmap = 0 then
 raise Exception.CreateFmt('Unable to load bitmap %s', [BitmapName]);
 DDCopyBitmap(Surface, Bitmap, 0, 0, 0, 0);
 finally
 DeleteObject(Bitmap);
 end;
end;
Вы указываете в DDReLoadBitmap поверхность DirectDraw и имя растрового изображения, которое вы хотите загрузить в поверхность. Процедура сначала попытается произвести загрузку из ресурса, предполагая, что BitmapName является именем ресурса. Если это не удается, она предполагает, что вы указали имя файла и попытается загрузить его из файла. На самом деле в этом случае при помощи LoadImage создается секция DIB. Это HBitmap из Windows с форматом аппаратно-независимого растрового изображения (DIB). Вы можете использовать DIB-секцию как обычный HBitmap, например, выбрав ее для DC и вызвав стандартную функцию GDI BitBlt().
DDReLoadBitmap вызывает другую сервисную программу - DDCopyBitmap, которая копирует изображение секции DIB на поверхность DirectDraw. Затем блок try .. finally избавляется от секции DIB, поскольку она больше не нужна. В отличие от кода обеспечения растровых изображений Windows 3.х, эта процедура достаточно проста. Теперь, как по поводу DDCopyBitmap? Как показано в листинге 18, это не намного сложнее.
Листинг 18. Сервисная процедура для копирования растрового изображения на поверхность.
procedure DDCopyBitmap(Surface: IDirectDrawSurface; Bitmap: HBITMAP;
 х, y, Width, Height: Integer);
var
 ImageDC: HDC;
 DC: HDC;
 BM: Windows.TBitmap;
 SurfaceDesc: TDDSurfaceDesc;
begin
 if (Surface = nil) or (Bitmap = 0) then
 raise Exception.Create('Invalid parameters for DDCopyBitmap');
 // убедиться, что поверхность восстановлена.
 Surfасе.Restore;
 // выбрать изображение для memoryDC, чтобы его использовать.
 ImageDC: = CreateCompatibleDC(0);
 try
 SelectObject(ImageDC, Bitmap);
 // получить размер изображения.
 GetObject(Bitmap, SizeOf(BM), @BM);
 if Width = 0 then
 Width := = BM.bmWidth;
 if Height = 0 then
 Height := BM.bmHeight;
 // получить размер поверхности.
 SurfaceDesc.dwSize := SizeOf(SurfaceDesc);
 SurfaceDesc.dwFlags := DDSD_HEIGHT or DDSD_WIDTH;
 Surface.GetSurfaceDesc(SurfaceDesc);
 if Surface.GetDC(DC) <> DD_OK then
 raise Exception.Create('GetDC failed for DirectDraw surface' )
 try
 StretchBlt(DC, 0, 0, SurfaceDesc.dwWidth, SurfaceDesc.dwHeight,
 ImageDC, x, y, Width, Height, SRCCOPY);
 finally
 Surface.ReleaseDC(DC);
 end;
 finally
 DeleteDC(ImageDC);
 end;
end;
После проверки некоторых параметров DDCopyBitmap вызывает Restore, чтобы обеспечить корректность память поверхности, Затем она обращается к обычной программе Windows для копирования растрового изображения с одного DC на другой. Исходное растровое изображение выбирается для первого DC, стандартная память DC обеспечивается вызовом CreateCompatibleDC. Передача нулевых параметров ширины и высоты в программу заставляет использовать фактическую ширину и высоту растрового изображения. Для того, чтобы получить эту информацию, программа использует функцию GetObject
Затем заготавливается запись SurfaceDesc путем включения флажков DDSD_HEIGHT и DDSD_WIDTH. Это передается в GetSurfaceDesc, которое реагирует путем заполнения полей dwHeight и dwWidth дескриптора. Программа получает второй DC из поверхности, используя вызов GetDC и осуществляя простое StretchBlt(). Как обычно, блоки try .. finally используются для обязательного освобождения DC. Все это довольно простые вещи. Это развеивает по ветру устаревшую истину о том, что код обработки растровых изображений для Windows тяжело писать. К счастью, теперь вы сможете прибегнуть к сочинению подобного кода без чувства опасения за будущее!
Kод DrawSurface упрощается еще больше, потому что фоновый буфер теперь можно загружать где угодно, используя DDReLoadBitmap. Упрощенный DrawSurface представлен в листинге 19.
Листинг 19. DrawSurface без кода отрисовки фоновой поверхности.
procedure TForm1.DrawSurfaces;
var
 ARect: TRect;
 ATopPos: Integer;
begin
 // вывод на основной холст.
 ARect := Rect(0, 0, 640, 480);
 with PrimaryCanvas do
 try
 Brush.Color := clBlue;
 FillRect(ARect);
 Brush.Style := bsClear;
 Font.Name := 'Arial';
 Font.Size := 24;
 Font.Color := clWhite;
 ATopPos := (480 - TextHeight('A')) div 2;
 TextOut(10, ATopPos, 'Primary surface');
 finally
 // убедиться, что мы сразу же освободили DC,
 // поскольку Windows замораживается, пока мы владеем DC.
 Release;
 end;
 { загрузить изображение в фоновый буфер }
 DDReloadBitmap(BackBuffer, GetBitmapName);
end;
== А что по поводу палитр? ==
Я знал, что об этом вы обязательно бы меня спросили! Хорошо, мы все еще вынуждены работать с палитрами. Настало время представить еще один СОМ-объект DirectDraw, На этот раз это IDirectDrawPalette. Этот маленький полезный объект обслужит большинство компонент палитры, не утруждая этим нас с вами. Для того, чтобы использовать IDirectDraw, вы создаете его с IDirectDraw.CreatePalette, которая устанавливает указатель на массив вводимых данных палитры, который использовался для инициализации объекта палитры. Затем вы присоединяете ее к поверхности DrawSurface и она станет использоваться автоматически для всех последующих операций. Конечно же, прекрасно.
Итак, как же получить эти значения цветов? Хорошо, я написал еще одну небольшую функцию для их загрузки из растрового изображения или создания цветов по умолчанию, и для создания и возврата объекта IDirectDrawPalette. Она также находится в DDUtils.pas и называется DDLoadPalette. Вы просто передайте ей имя вашего объекта IDirectDraw либо с именем растрового изображения, либо (если вы хотите палитру по умолчанию) с пустой строкой. (Как и другие программы, DDLoadPalette сначала пытается загрузить растровое изображение из ресурса приложения. Если это не удается, она пытается загрузить растровое изображение из файла. Я не повторяю здесь код, поскольку он несколько длиннее других функций. Он главным образом имеет дело с проверкой наличия у DIB таблицы цветов, которую он затем копирует в массив вводимых данных палитры).
Я добавил объект палитры к объявлению формы, загрузил его в FormShow и присоединил объект палитры к основной поверхности следующим образом:
{ загрузить палитру из растрового изображения
 и присоединить ее к основной поверхности }
DDPalette := DDLoadPalette(DirectDraw, GetBitmapName);
PrimarySurface.SetPalette(DDPalette);
Создав, вы должны освободить его из основной поверхности в FormDestroy:
{ освободить DD-палитру }
if Assigned(DDPalette) then
 DDPalette.Release;
Проделав все изменения, вы можете теперь приступить к проверке. DDDemo5 содержит все изменения, обозначенные до настоящего момента.
== Объединение всего вместе ==
В настоящий момент вы можете составить DirectDraw-приложсние со сменой страниц, а также загрузить растровое изображение и палитру. У вас имеется все необходимое для создания смены страниц и причем на полной скорости! Для того, чтобы было еще интересней, как насчет анимации? DirectDraw в одной из демонстрационных программ использует файл с именем ALL.BMP. Вы также скачать его вместе с примером DDDemo5. В ней содержится еще одно более интересное фоновое изображение и набор анимационных кадров с красным вращающимся трехмерным тором.
Перед очередной сменой страницы вы захотите отобразить фоновое изображение и затем текущий анимационный кадр с тором. Вы создаете три тора в разных позициях на экране, которые будут вращаться с разной скоростью. Ввиду того, что фоновый буфер будет непрерывно перерисовываться, вы должны хранить где-нибудь еще исходное изображение, загруженное из ALL.BMP. Поэтому создайте для него еще одну поверхность DirectDraw. Это внеэкранная поверхность и она не имеет отношения к смене страниц; на ней мы будем хранить изображение.
Существенно важно отметить, что по умолчанию DirectDraw создает исходное изображение в экранной памяти. Это означает, что когда вы используете изображение для обновления фонового буфера, любой производимый битовый перенос использует аппаратный перенос битов, если таковой имеется на графической карте. Практически все персональные компьютеры в настоящее время оснащены ускоренной графической картой, которую как раз и использует DirectDraw. Ввиду того, что это аппаратное обеспечение работает намного быстрее, чем процессор во время битового переноса, игры DirectDraw должны иметь большую эффективность по отношению к играм DOS, где процессор делает все.
Битовый перенос (bit-blitting) - термин, используемый для описания переноса областей растровых изображений в, из или в пределах других растровых изображений. Термин иногда записывается более точно как bitblting, но он сложен для чтения, поэтому вы часто найдете его в расчлененным в виде двух слов bit-blitting. BitBlt - краткое описание термина Bitmap Block Transfer (перенос блока растрового изображения).
Итак, за работу. Создайте эту дополнительную поверхность и назовите ее Image (изображение). Добавьте ее в объявление формы. Это как раз и есть IDirectDrawSurface, поэтому нет необходимости представлять здесь эту тривиальную строку кода. Затем добавьте код в FormShow, который создает растровое изображение. Используйте DDLoadBitmap, это только одна строка! Вот она:
Image := DDLoadBitmap(DirectDraw, GetBitmapName, 0, 0);
Помните, что вам необходимо пополнить метод RestoreSurfaces и тогда вы получите новую неявную поверхность. Если восстановление основной памяти поверхности пройдет нормально, попытайтесь восстановить поверхностную память Image. Если оба типа восстановлений будут иметь место, вызовите DrawSurfaces, как показано в листинге 20.
Листинг 20. Восстановление всех поверхностей.
function TForm1.RestoreSurfaces: HResult;
begin
 Result := PrimarySurface.Restore;
 if Result = DD_OK then
 begin
 Result := Image - Restore;
 if Result = DD_OK then
 DrawSurfaces;
 end;
end;
Author: Стас Бакулин 
Source: http://delphiworld.narod.ru
ID: 03912
Category:Графика
Category:DirectX
Category:Рисование
Category:Статьи