Книга: Графика DirectX в Delphi
Глава 2. Обзор библиотеки DirectDraw
Глава 2. Обзор библиотеки DirectDraw
Поверхности
Блиттинг
Буферы
Отладка приложений
Что вы узнали в этой главе
Настоящая глава содержит краткие сведения о DirectDraw API, вводит читателя в круг базовых терминов и понятий. Обучение начинается с самых простейших программ. Приводятся приемы отладки приложений, использующих DirectDraw.
Примеры к данной главе располагаются в каталоге ExamplesChapter02.
Поверхности
В первой главе мы узнали, что работа начинается с создания главного объекта DirectDraw, а его методы используются для создания остальных необходимых нам объектов. Здесь у вас не должно сложиться впечатление, что все методы главного объекта предназначены только для создания других объектов. Однако он имеет также методы с иными предназначениями.
Рассмотрим проект каталога Ex01. Имя формы frmDD задано, в разделе private описания класса формы объявлены две переменные и вспомогательная функция:
FDD : IDirectDraw7; // Главный объект
FDDSPrimary : IDirectDrawSurface7; // Поверхность
procedure ErrorOut(hRet : HRESULT; FuncName : String); // Вывод сообщений
Вспомогательная функция вывода сообщений получает в качестве аргументов код ошибки и поясняющее сообщение и выводит расшифровку кода, используя функцию DDErrorString:
procedure TfrmDD.ErrorOut(hRet : HRESULT; FuncName : String); begin
MessageBox(0, PChar(FuncName + ': ' + #13 + DDErrorString(hRet)),
PChar (Caption) , MBJDK or B_ICONSTOP);
end;
Обработчик события onCreate формы дополнился новыми для нас действиями, которые мы должны очень внимательно разобрать:
procedure TfrmDD.FormCreate(Sender: TObject); var
hRet : HRESULT; // Для анализа успешности действий
ddsd : TDDSurfaceDesc2; // Вспомогательная структура begin
FDDSPrimary := nil; //В начале работы обнуляем все СОМ-объекты
FDD := nil;
// Создание главного объекта DirectDraw
hRet := DirectDrawCreateEx (nil, FDD, IDirectDraw7, nil);
if hRet <> DD_OK then begin
ErrorOut(hRet, 'DirectDrawCreateEx'); Exit;
end;
// Задаем уровень кооперации
hRet := FDD.SetCooperativeLevel(Handle, DDSCL_FULLSCREEN or
DDSCL_EXCLUSIVE); if hRet <> DD_OK then begin
ErrorOut(hRet, 'SetCooperativeLevel'); Exit;
end;
// Заполняем поля вспомогательной структуры
FillChar(ddsd, SizeOf(ddsd), 0); // Для начала все поля обнуляем ddsd.dwSize := SizeOf(ddsd); // Поле размера структуры ddsd.dwFlags := DDSD_CAPS; // Будет создаваться первичная поверхность ddsd.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE;
// Собственно создание первичной поверхности
hRet := FDD.CreateSurface(ddsd, FDDSPrimary, nil); if hRet <> DD_OK then begin
ErrorOut(hRet, 'Create Primary Surface');
Exit;
end;
end;
Прежде чем мы разберем подробно все новое, обращаю внимание, что при завершении работы приложения привычным для нас способом освобождаем переменные в порядке, обратном их связыванию:
procedure TfrmDD.FormDestroy(Sender: TObject); begin
if Assigned(FDD) then begin // Связана ли переменная главного объекта
// Связана ли переменная первичной поверхности
if Assigned(FDDSPrimary) then FDDSPrimary := nil;
FDD := nil;
end;
end;
При запуске проекта чего-либо особенного не происходит, только окно оказывается распахнутым на весь экран, хотя в свойствах формы ничего подобного не указано. Обратите внимание также, что окно хоть и распахнуто, но не максимизировано, мы можем изменять его размеры привычным способом.
Возьмите за правило не запускать проекты, использующие DirectDraw, под управлением среды Delphi. Запускайте непосредственно откомпилированный модуль.
Сразу после создания главного объекта мы задаем уровень кооперации, используя метод setCooperativeLevei. Уровень кооперации определяет уровень взаимодействия приложения с экраном и с другими приложениями. И это действие - задание уровня кооперации - является обязательным для всех приложений, использующих DirectDraw. Метод имеет два параметра: первый является идентификатором окна приложения, здесь мы передаем значение свойства Handle формы, второй параметр представляет собой одно из строго определенных значений, либо комбинацию таких значений.
Используемая комбинация битовых флагов записана в этом примере как "DDSCL_FULLSCREEN or DDSCL_EXCLUSIVE". В подобных случаях всегда слово or можно заменить знаком +. Первый флаг задает полноэкранный режим работы приложения, второй - монопольный режим доступа к экрану. Оба флага в комбинации должны присутствовать.
В Delphi подобные комбинации битовых значений редко встречаются, поэтому для начинающих требуются пояснения. Такие комбинации используются для передачи одним аргументом нескольких параметров. Константы, указанные в комбинации, представляют собой степени двойки. Из суммы таких чисел легко выделить присутствие каждой константы: наличие единицы в разряде двоичного числа.
Итак, в рассматриваемом примере запрашивается полноэкранный режим работы приложения, и по правилам, установленным DirectX, необходимо для такого режима установить эксклюзивный уровень доступа. Теперь понятно, почему окно распахивается на весь экран.
Дальше по коду создается первичная поверхность. Поверхность является одним из основных понятий DirectDraw, за этим термином скрывается прямоугольный блок памяти. Поясню смысл этого понятия на примере анимации, которую можно организовать, скажем, следующим образом: кадры анимации хранятся в отдельных блоках памяти, и с течением времени на экран выводится нужный блок. Блок, хранящий отдельный кадр, и блок, соответствующий экрану, называются поверхностями. О поверхностях можете думать как о растрах, размещенных в памяти, или как об объектах класса TBitmap.
После изрядной практики в использовании компонентов Delphi и рисовании с помощью методов canvas, начинающие программисты в DirectDraw испытывают при знакомстве с термином поверхности вполне объяснимую неловкость. Некоторые вещи здесь могут показаться новичку неудобными и неясными. В дальнейшем, с практикой, придет полное понимание всех моментов смысла поверхности и работы с ней, а сейчас вы должны твердо для себя уяснить, что экран для будущей работы вы обязаны подготовить сами, и производится это в терминах поверхности. Даже если у вас не будет на экране картинок, а вы хотите просто порисовать так, как привыкли это делать с помощью методов Canvas.
Создаются поверхности с помощью метода CreateSurface главного объекта, но для задания свойств создаваемой поверхности используется вспомогательная величина типа TDDSurfaceDesc2. Представляет она собой структуру - запись в традиционной для Delphi терминологии. Значения ее полей содержат параметры создаваемой поверхности. Только в редких случаях необходимо определять значения абсолютно всех параметров. Как правило, можно обойтись только парой из них. Остальные поля DirectX заполнит за нас. В любом случае, следует обязательно обнулить все поля структуры. Это очень важное правило. В нашем примере поля обнуляются с помощью функции Filichar, но помните, что это же можно сделать посредством другой функции, ZeroMemory:
ZeroMemory (@ddsd, SizeOf(ddsd));
Следующее действие также является обязательным: в поле dwsize надо записать размер структуры. Оба действия, обнуление всех полей и установка размера, необходимо выполнять в начале работы с любой структурой, встречающейся нам в DirectX. Пренебречь каким-либо из них у нас просто не получится. Проверьте сейчас же: удалите любую из этих строк и запустите проект. Выполнение кода обработчика onCreate завершится аварийно. Авария, впрочем, для этого проекта не является фатальной. Исключение связано с DirectDraw и пока не приводит к полному провалу работы приложения. Запомните, как выглядит окно выдачи расшифровки ошибки - работа нашей пользовательской функции ErrorOut, чтобы сразу же отличать аварийные ситуации, связанные с DirectX.
Итак, неиспользуемые поля структуры будут нулевыми. Главное поле, которое нам надо обязательно заполнить, - это поле ddsCaps, представляющее описание возможностей (capabilities) - наиважнейших характеристик поверхности. Поле это также является структурой, из всех полей которой мы обязаны задать, как минимум, значение поля dwCaps.
Поле dwFiags структуры TDDSurfaceDesc2 содержит указания, какие из ее полей заполнены нами и должны быть приняты системой в расчет. Присвоив этому полю значение здесь DDSD_CAPS, мы указываем DirectDraw, что нами заполнено именно поле ddsCaps, а все остальные параметры создаваемой поверхности отдаются на откуп графической системе, и она будет распоряжаться ими по собственному усмотрению. Если мы поместим в поля структуры значения, но забудем указать это в поле флагов, графическая система установленные значения в расчет принимать не будет.
Вывод в DirectDraw осуществляется на поверхностях, которых может быть столько, сколько нам надо. Предназначение и параметры каждой из них мы задаем, исходя из логики приложения. Поверхности можно использовать и как хранилища вспомогательных ресурсов - битовых растров, чем мы и будем пользоваться достаточно часто.
Но среди всех поверхностей есть одна, особая, связанная непосредственно с экраном. Все, что на нее будет выведено, окажется отображенным прямо на экране монитора. Называется эта поверхность первичной. Она обязательно создается в любом проекте, рисующем что-либо на экране с помощью DirectDraw.
В поле dwCaps структуры ddsCaps, являющейся, в свою очередь, частью структуры ddsd, заносим значение DDSCAPS_PRIMARYSURFACE, вызывая метод CreateSurface главного объекта. У метода три аргумента: адрес структуры, описывающей параметры создаваемой структуры, переменная, в которую помещается результат - созданная поверхность. Третий параметр зарезервирован для будущих нужд, а пока обязан быть установлен в nil.
Далее мы традиционным образом оцениваем успешность проделанной операции, и в случае ее провала сообщаем об ошибке и выполняем выход из программы. Строго говоря, здесь мы завершаем работу приложения в любом случае, независимо от успеха создания поверхности - ведь код на этом заканчивается.
Точно так же, как и в примере первой главы, равенство nil переменной поверхности является признаком неудачи предыдущей операции.
Мы разобрали все действия в нашем примере, и все они должны быть вам понятны.
Давайте повторим порядок действий:
* создается главный объект; задается уровень взаимодействия приложения с системой и другими приложениями; заполняются поля структуры, хранящей параметры поверхности; создается, как минимум, одна поверхность - первичная, связанная с экраном.
Поначалу все это может показаться чересчур громоздким, но по мере накопления опыта у вас появится легкость понимания кода.
Начинающих программистов может смущать кажущаяся запутанность с цифрами в типах переменных, скажем, тип TDDSurfaceDesc2 заканчивается не на 7. Одни типы, например интерфейсы, менялись с каждой версией, другие же вспомогательные типы модифицировались реже, поэтому их цифры "отстают" от нумерации используемых интерфейсов.
Еще один вопрос, который надо разрешить, тоже связан с версией DirectX. Многое из того, что мы применяем, присутствует и в более ранних версиях, и для массы примеров вполне можно использовать интерфейсы не седьмой, а, например, пятой версии. Легко поддаться такому соблазну, ведь тогда круг потенциальных пользователей вашей программы существенно расширяется. Я не смогу придерживаться этого, и вам советую делать подобное лишь в случае крайней необходимости. Ведь наверняка со временем разношерстность кода приведет к полному беспорядку в ваших проектах. Согласно спецификации СОМ-интерфейс не может меняться после его определения. Новый интерфейс должен поддерживать как старые, так и новые возможности. Но интерфейсы со старшими номерами версий не обязательно должны быть образованы от соответствующих интерфейсов с меньшими номерами.
Еще одна потенциальная проблема связана со вспомогательными структурами. Мы уже начали с ними знакомиться. Доступ к этим структурам, используемым в методах интерфейсов, осуществляется в программе непосредственно, а не через интерфейс (мы сами заполняем поля записи). Структуры с каждой новой версией могут обрастать новыми полями, следовательно, размер их меняется.
Именно по этой причине каждая функция должна обязательно получать и размер передаваемой структуры.
Более старая версия DirectX не сможет обработать новые поля знакомой ей структуры, она просто ничего не знает о появившихся в ней новых полях. Разработчики попытались снять часть возникших проблем, вводя новые типы структур, те самые двойки в имени их появились из-за запрета на применение одноименных методов разных интерфейсов. Но самым лучшим решением для нас будет использование текущей версии DirectX.
Двигаемся дальше. Попробуем порисовать что-нибудь в привычном для нас антураже. Переходим к проекту каталога Ех02, отличающемуся от предыдущего тем, что здесь добавился обработчик события onPaint формы:
procedure TfrmDD.FormPaint(Sender: TObject);
var
// Вспомогательный дескриптор, идентификатор устройства вывода GDI
DC : HDC;
wrkCanvas : TCanvas; // Вспомогательный объект, рабочая канва begin
// Получение дескриптора, необходимого для функций GDI
if FDDSPrimary.GetDC(DC) = DD_OK then begin
wrkCanvas := TCanvas.Create; // Создаем вспомогательную канву
wrkCanvas.Handle := DC; // Задаем идентификатор канвы = DC
// Рисуем на канве кружок
wrkCanvas.Ellipse (Left + 50, Top + 50, Left + 100, Top + 100);
wrkCanvas.Free; // Освобождение памяти, удаление канвы
FDDSPrimary.ReleaseDC (DC); // Освобождение контекста устройства
end;
end;
Пример учебный, не ожидайте от него ничего выдающегося. Впереди нас ждут еще более впечатляющие программы. При запуске проекта ничего особенного не происходит, на поверхности окна рисуется кружок. Причем вы можете даже обнаружить, что появляется он на экране медленнее, чем нарисованный обычным способом.
Канва Delphi является оболочкой системных функций вывода GDI, ее свойство Handle в точности соответствует типу нос, ссылке на устройство вывода. В этой величине нуждаются все функции GDI для идентификации устройства, окна или блока памяти, в который осуществляется вывод.
Как видно из кода, метод поверхности Getoc позволяет в нужную переменную поместить значение такого идентификатора. Установив значение Handle канвы в найденное значение, мы добьемся того, что вывод на канве физически будет осуществляться на необходимое нам устройство. Все это нам знакомо по предыдущей главе.
Первичная поверхность в нашей программе является канвой рабочего стола, так мы сами задали ее свойства. Поэтому канва связана с областью всего экрана, и прямоугольник, ограничивающий кружок, мы задаем в координатах рабочего стола, а не в координатах окна приложения. Затем мы освобождаем память и контекст вывода. Это действие является предельно важным. Если его не выполнить, то приложение просто закроет доступ к рабочему столу для всех остальных приложений и для операционной системы. Страшная ситуация!
Поработайте с проектом. При перемещении окна все работает так, как мы того ожидаем, но вот если размеры окна сделать слишком маленькими, то кружок может выходить за его пределы. Это объяснимо. Мы знаем, что круг рисуется на поверхности всего экрана. Если же окно приложения перекрыть другим окном или минимизировать, а затем распахнуть, то кружок уже не появляется.
Мы подходим к очень важным вопросам, специфичным именно для DirectDraw. Ситуацию, когда приложение временно убирается с экрана, а затем восстанавливается, необходимо в приложениях фиксировать.
Посмотрите проект каталога Ех03, в котором отслеживается ошибка, возникающая при такой ситуации, и пользователю выдается осмысленное сообщение (рис. 2.1).
Код ошибки - DDERR_SURFACELOST. Как сообщается в его расшифровке, в таком случае необходимо использовать метод Restore поверхности.
Пример делает прозрачным причины потери поверхности. Первичную поверхность мы связали с рабочим столом, и, конечно, после того, как окно "ушло" с экрана, оно освободило память поверхности для других приложений. Теперь ему надо заново вернуть свои исключительные права на область памяти. Такая же ситуация возникает при изменении параметров рабочего стола по ходу работы приложения.
Также многие из функций DirectDraw могут вернуть код ошибки DDERR_WASSTILLDRAWING, означающий, что аппаратное обеспечение занято и запрос необходимо повторять до тех пор, пока не добьемся успеха или не получим иного сообщения об ошибке.
Взгляните на проект каталога Ех04, здесь решены все эти проблемы, и первое, что изменилось, - это код, связанный с перерисовкой окна. Теперь код собственно воспроизведения заключен внутрь цикла, из которого мы выходим либо в случае успешного воспроизведения, либо если поверхность восстановить не удается, либо код ошибки отличен от DDERR_WASSTILLDRAWING:
while True do begin возможно, // Код придется повторять неоднократно
hRet := FDDSPrimary.GetDC(DC); // Заново получаем дескриптор
if Succeeded (hRet) then begin
wrkCanvas := TCanvas.Create;
wrkCanvas.Handle := DC; wrkCanvas.Ellipse (Left + 50, Top + 50, Left + 100, Top + 100);
wrkCanvas.Free; FDDSPrimary.ReleaseDC (DC); Break;
end;
// Поверхность потеряна, надо восстановить if hRet = DDERR_SURFACELOST then begin
hRet := FDDSPrimary._Restore;
// Если не удалось восстановить, дальше продолжать нельзя
if hRet <> DD_OK then Break; end;
// Ошибка отлична от DDERR_WASSTILLDRAWING, следовательно непоправима if hRet <> DDERR_WASSTILLDRAWING then Break;
end;
Чтобы кружок не рисовался за пределами окна приложения, можно просто не разрешать уменьшать высоту окна. Таким образом, появился обработчик
события OnCanResize:
procedure TfrmDD.FormCanResize(Sender: TObject; var NewWidth,
NewHeight: Integer; var Resize: Boolean); begin
if NewHeight < 110 // Высота окна не должна быть меньше 110
then Resize := False
else Resize := True;
end;
Что еще надо сделать, так это при обработке события OnResize окна вызывать тот же код, что и при событии OnPaint.
Для обработки тех ситуаций, когда восстановить поверхность не удается, в проект добавлен компонент класса TAppLicationEvents, на события OnActivate и onRestore которого вызывается такой же код, как и при создании окна. То есть при восстановлении минимизированного окна и каждой активизации окна приложения заново создаем первичную поверхность.
Хорошенько поработайте с проектом: протестируйте его работу в самых различных ситуациях, минимизируйте и восстанавливайте окно, активизируйте самыми различными способами, поменяйте установки экрана по ходу работы этого приложения. Кружок должен появляться всегда, когда мы его ожидаем. При деактивизации окно может вести себя непривычно для обычных приложений, можете записать Application.Minimize в обработчике события OnDeactivate единственного компонента проекта. Восстанавливается окно тоже особым образом, распахиваясь на весь экран.
Такое использование полноэкранной первичной поверхности, как в этом примере, когда воспроизведение осуществляется только функциями GDI в пределах окна приложения, редко применяется в практических задачах.
В примере есть небольшое упрощение. Так как при восстановлении окна приложения и его активизации (пользователь переходит на него с помощью комбинации клавиш <Alt>+<Tab>) поверхность создается заново, то она никогда не будет потеряна. Такой прием можно использовать только для простейших приложений, поскольку весьма неэкономно тратить время подготовки работы при каждой активизации приложения.
Блиттинг
Блиттингом называется копирование графических изображений в память видеоустройств, и DirectDraw представляет собой просто механизм блиттинга.
Начнем знакомство с этим механизмом с помощью проекта каталога Ех05. Пример является продолжением предыдущей программы. Первичная поверхность все также полноэкранная, на нее с помощью вспомогательной канвы выводится растровое изображение. Это изображение, используемое здесь и во многих последующих примерах, взято из DirectX SDK производства Microsoft. Содержит эта картинка потрясающий по своей красоте пейзаж.
Главное отличие данного примера от предыдущего состоит в том, что поверхность, служащая фоном нашего растра, закрашивается. Работа приложения теперь выглядит естественной для полноэкранных приложений вообще, и для приложений, использующих DirectDraw, в частности.
Поскольку это полноэкранное приложение, то обработчики событий OnCanResize и OnResize ему не нужны. Не пропустите также, что свойство Borderstyle формы я установил в bsNone. Это важно, если этого не сделать, то приложение будет работать прекрасно, но при движении курсора вблизи границ экрана и в районе системного заголовка окна приложения сквозь "экран" будет проглядывать другое окно. Обязательно проверьте это, вернув обычное значение указанного свойства формы. Полноэкранная поверхность занимает рабочий стол, загораживает собой все окна, но они продолжают реагировать на поступающие им сообщения.
Чтобы при восстановлении приложения его окно появлялось распахнутым на весь экран, появился обработчик события onRestore компонента
ApplicationEventsL:
procedure TfrmDD.ApplicationEventslRestore(Sender: TObject);
begin
WindowState := wsMaximized; end;
Следует задавать это свойство именно динамически, т. е. принудительно распахивать окно при каждом его восстановлении.
Главные изменения коснулись кода, связанного с перерисовкой окна. Часть кода, предназначенную для вывода изображения, рассматривать не будем. Здесь ничего нового для нас нет. Посмотрим внимательно на то, что ему предшествует, а именно на заполнение фона черным цветом. Кстати, обратите внимание, что порядок действий такой: закрашиваем фон, на закрашенный фон выводим растр. Замечу, что работа с растром в примере выполнена не оптимально, растр загружается при каждой перерисовке окна. Пока наши примеры очень просты и вполне терпят подобное, но при интенсивной работе с растровыми изображениями, конечно, так писать программы не будем.
Процедура обработчика перерисовки окна имеет локальную вспомогательную переменную ddbltfx типа TDDBLTFX, который представляет собой запись И является вспомогательным, подобным использованному нам при создании поверхности типу TDDSurfaceDesc2, но применяется лишь для задания параметров блиттинга. Поля этой структуры мы изучим по ходу дальнейшего изложения. Пока ограничимся тем, что значение поля dwFillcoior задает цвет заполнения поверхности. Как указывалось ранее, работа с подобными структурами начинается с обнуления всех полей и задания ее размера указанием значения поля dwSize:
ZeroMemory(@ddbltfx, SizeOf (ddbitfx)); // Обнуляем все поля
ddbitfx.dwSize := SizeOf (ddbitfx); // Обязательно задаем размер
ddbitfx.dwFillColor := 0; // Цвет заполнения - черный
Для блиттинга используем метод Bit первичной поверхности:
hRet := FDDSPrimary.Blt(nil, nil, nil, DDBLT_COLORFILL or DDBLT_WAIT, @ddbltfx);
Первым трем аргументам присваиваем значение nil (смысл их раскроим позже). Четвертый аргумент метода является битовым флагом: у нас это комбинация двух констант. Константа DDBLT_WAIT задает режим ожидания для вывода. В этом режиме вывод станет осуществляться, когда устройство будет готово к нему, и поэтому метод не возвратит никогда значение
DDERR_WASSTILLDRAWING. Флаг DDBLT_COLORFILL сообщает методу Blt о том, что вместо блиттинга выполняется цветовое заполнение. Последний аргумент указывает адрес переменной, хранящей параметры блиттинга.
Возвращаемое методом значение мы анализируем, подобно тому, как это делали в предыдущем примере. Только здесь мы покидаем цикл при любой ошибке, отличной от ошибки, связанной с потерей поверхности, или при отсутствии ошибок.
Строго говоря, нам следовало бы выделить ситуацию с неустранимой ошибкой, ведь в этом случае надо прекращать выполнение кода, а не просто завершать цикл.
Протестируйте пример. Приложение должно функционировать надежно в самых различных ситуациях, но если по ходу его работы изменить настройки экрана и установить его новые размеры больше предыдущих, то при восстановлении приложения оно не будет закрывать весь экран, а размеры его станут такими же, как при первоначальном запуске. Заметна еще одна проблема: если до запуска приложения установить режим экрана в 256 цветов, образ выводится совсем не красочным. Чтобы избежать таких ошибок, обычно полноэкранные приложения устанавливают разрешения экрана сообразно со своими нуждами.
Переходим к иллюстрации - проекту каталога Ех06. От предыдущего он отличается только тем, что код обработчика onCreate формы дополнился строками:
hRet := FDD. SetDisplayMode (640, 480, 16, 0, 0); // Задаем режим
if hRet <> DD_OK then begin // Обязательно анализируем результат
ErrorOut (hRet, ' SetDisplayMode ') ;
Exit ; end;
Установка режима является методом главного объекта, поэтому должна происходить строго после его создания, но не обязательно первым же действием. Первые три аргумента метода, надеюсь, сразу же ясны: высота, ширина экрана и разрешение (число бит, необходимых для определения цвета пиксела). Последние два аргумента метода всегда будем задавать нулевыми. Первый из них определяет частоту регенерации. При нулевом значении параметра отдается на усмотрение DirectDraw. Последний аргумент задает дополнительные флаги, пока из них доступен только DDSDM_STANDARDVGAMODE, связанный с особым режимом Mode X (320x200x8).
Итак, на время работы приложения мы задаем режим 640x480x16. Эта тройка чисел не может браться наобум, а должна принадлежать набору поддерживаемых системой режимов.
Запустив утилиту диагностики DirectX, вы можете найти список поддерживаемых режимов.
Если на вашей карте выводимое изображение теряет в красочности по сравнению с исходным 24-разрядным растром, установите этот режим 24- или 32-битным, в зависимости от того, какой из них доступен.
Обратите внимание, что нет необходимости по окончании работы приложения возвращаться к режиму, установленному пользователем. Это произойдет автоматически.
Разобравшись с данным примером, мы можем перейти к рассмотрению процедуры настоящего блиттинга. Рассмотрим проект из каталога Ех07. Коренное отличие его от предыдущего состоит в том, что образ помещаем на вспомогательную поверхность, а при воспроизведении осуществляется блиттинг на первичную поверхность.
Раздел private описания класса формы дополнился строкой объявления дополнительной поверхности, предназначенной для хранения образа:
FDDSImage : IDirectDrawSurface7;
Код обработчика события onCreate начинается с того, что этой переменной присваивается значение nil, а при завершении работы приложения освобождается память в порядке, обратном связыванию переменных:
if Assigned(FDD) then begin
if Assigned(FDDSImage) then FDDSImage := nil; // Перед первичной
if Assigned(FDDSPrimary) then FDDSPrimary := nil; // поверхностью
FDD := nil
end;
Растровое изображение считывается только один раз. У обработчика OnCreate появился вспомогательный объект wrkBitmap класса TBitmap. Вторичная поверхность создается после первичной и заполняется считанным растром:
wrkBitmap := TBitmap.Create; // Создание объекта растра
wrkBitmap.LoadFromFile ('..lake.bmp'); // Считывание файла растра // Напоминаю, что обнулять поля можно и с помощью ZeroMemory FillChar (ddsd, SizeOf(ddsd), 0} ; with ddsd do begin // Как для любой записи, можно использовать with
dwSize := SizeOf(ddsd); // Обязательное действие
// Будем задавать размеры поверхности (+ DDSDJiEIGHT и DDSD_WIDTH)
dwFlags := DDSD_CAPS or DDSD_HEIGHT or DDSD_WIDTH;
ddsCaps.dwCaps := DDSCAPS_OFFSCREENPLAIN; // Внеэкранная поверхность
dwWidth := wrkBitmap.Width; // Ширина поверхности равна ширине растра
dwHeight := wrkBitmap.Height; // Задаем высоту поверхности end; // with
// Собственно создание вспомогательной поверхности
hRet := FDD.CreateSurfасе(ddsd, FDDSImage, nil);
if hRet <> DD_OK then begin // Анализируем на предмет успешности
ErrorOut(hRet, 'Create Image Surface');
Exit;
end;
// Копирование растра из wrkBitmap во вспомогательную поверхность
hRet := DDCopyBitmap (FDDSImage, wrkBitmap.Handle, 0, 0, wrkBitmap.Width,
wrkBitmap.Height);
if hRet <> DD_OK then begin // Обязательно анализируем результат
ErrorOut(hRet, 'DDCopyBitmap');
Exit;
end;
// Удаление вспомогательного объекта wrkBitmap.Free;
Вспомогательная, внеэкранная поверхность Foosimage создается с описанием DDSCAPSJDFFSCREENPLAIN. Здесь есть некоторые нюансы, но пока рассматривать их не будем.
После создания вторичной поверхности заполняем ее растровым изображением с помощью вспомогательной функции DDCopyBitmap из модуля DDUtil,
не забываем дописать имя модуля после uses. В тонкости того, как осуществляется копирование, можете не вникать или разберитесь позднее самостоятельно. Код данной функции основан на функциях API. Ключевым является вызов StretchBlt.
Вспомогательная поверхность создана и заполнена растром размером 256x256 пикселов. Среди аргументов операции блиттинга присутствуют структуры типа TRECT, задающие местоположение в принимающей поверхности и копируемую область. Поэтому код обработчика перерисовки окна дополнился переменными dstRect и srcRect типа TRECT. Заполняем их поля с помощью API-функции setRect:
SetRect (dstRect, 100, 100, 356, 356); // Для принимающей поверхности
SetRect (srcRect, 0, 0, 256, 256); // Для источника
Теоретически эта операция также может привести к провалу. Для важных приложений рекомендую здесь анализировать возвращаемое булево значение. К тому же соглашусь, что в данном примере оптимальным решением было бы использование глобальных переменных, заполняемых один раз, а не при каждой перерисовке окна. Просто код в таком виде удобнее читать, а перерисовка не станет производиться интенсивно.
Канву для вывода растра не используем, делаем теперь все традиционным для DirectDraw способом:
while True do begin // Возможно, придется производить неоднократно
hRet := FDDSPrimary.Blt (SdstRect, FDDSImage, @srcRect, DDBLT_WAIT,
nil); // Собственно блиттинг
if hRet = DDERR_SURFACELOST then begin // Поверхность потеряна
if Failed (RestoreAll) then Exit; // Пытаемся восстановить
end else Break; // Или все прошло успешно, или неустранимая ошибка
end;
Потеря любой поверхности является верным признаком того, что надо восстанавливать все поверхности. Поэтому каждый раз в случае ошибки обращаемся к пользовательской функции RestoreAll:
function TfrmDD.RestoreAll : HRESULT; begin
Result := DD_FALSE; // Определяемся с результатом // Пытаемся восстановить первичную поверхность
if Succeeded (FDDSPrimary._Restore) then begin
// Пытаемся восстановить вторичную поверхность
if Failed (FDDSImage._Restore) then Exit;
Result := DD_OK; // Все прошло успешно
end;
end;
Нажав комбинацию клавиш <Alt>+<Tab>, переключитесь с этого приложения, а затем верните ему фокус. Если восстановление поверхностей прошло успешно, вы увидите картинку с пейзажем. Но если это получилось с вашей картой, совсем не обязательно, что это произойдет и с другими. На иных компьютерах пользователи в такой ситуации могут получить бессмысленный узор. Согласно рекомендациям разработчиков, поверхности, содержащие растр, при восстановлении должны заново заполняться.
Если это окно из рассматриваемого примера у вас восстанавливается без потерь, можете двигаться дальше. Если же у вас картинка при восстановлении портится, функцию восстановления исправьте следующим образом:
function TfrmDD.RestoreAll : HRESULT; var
hRet : HRESULT; begin
hRet := FDDSPrimary._Restore;
if Succeeded (hRet) then begin hRet := FDDSImage._Restore;
if Failed (hRet} then begin Result := hRet;
Exit;
end;
// Перезагружаем на поверхность содержимое растра Result := DDReLoadBitmap(FDDSImage, imageBMP);
end else Result := hRet;
end;
Теперь мы можем узнать смысл первых трех аргументов метода Bit поверхности. Первый из них - указатель на структуру типа TRECT, задающую местоположение и размер области, в которую происходит копирование. Второй параметр - поверхность источника. Третий аргумент - указатель на структуру типа TRECT, задающую местоположение и размер области, из которой происходит копирование.
Флагом задаем константу DDBLT_WAIT, не комбинацию значений. Дополнительные параметры пока не указываем, поэтому последний аргумент метода устанавливаем в nil.
Пример простой, но очень важный. Осмыслим изученное. Естественным для DirectDraw способом воспроизведения является блиттинг. На вспомогательных поверхностях размещаем нужные нам образы, а в определенный момент времени копируем требуемые области с одной поверхности на другую, в простейшем случае - со вспомогательных поверхностей на первичную, связанную с экраном.
Вторичных поверхностей создают столько, сколько требуется приложению. Разработчик сам решает, что и где ему располагать, но здесь надо учесть небольшую тонкость: если видеокарта имеет малый размер памяти, то вторичную поверхность не получится создать размером больше первичной. Может быть, это происходит только с конкретными картами, но я действительно встречался с такой ситуацией.
Поверхностей вы можете создавать сколько угодно, но чем их меньше, тем быстрее будет осуществляться блиттинг. Поэтому лучше не плодить множество поверхностей, а, в идеальном случае, располагать все образы на одной большой поверхности.
В общем случае операция копирования блоков из системной памяти в видеопамять осуществляется медленнее, чем из видеопамяти в видеопамять. Поэтому образы, выводимые наиболее часто, старайтесь размешать в видеопамяти, а при ее нехватке те из образов, которые редко будут появляться, например заставки или меню, следует размещать в системной памяти.
Поддержка AGP если и стирает разницу в скоростях обмена, то незначительно. Если бы скорости работы с видеопамятью и системной памятью сосовпадали или были близки друг к другу, не было бы нужды производителям карт выпускать модели, различающиеся размером видеопамяти, а пользователям оставалось лишь наращивать размер системной памяти.
Для поверхности, создаваемой в видеопамяти, надо использовать комбинацию флагов DDSCAPS_OFFSCREENPLAIN or DDSCAPSJ/IDEOMEMORY. и наоборот, флаг DDSCAPS_SYSTEMMEMORY указывает, что поверхность должна располагаться в системной памяти.
Метод GetAvailablevidMem главного объекта DirectDraw позволяет выяснить, сколько видеопамяти осталось в распоряжении приложения.
При нехватке видеопамяти операция создания поверхности завершится провалом, код соответствующей ошибки - DDERR_OUTOFVIDEOMEMORY. В этом случае необходимо поменять флаг и заново попытаться создать поверхность.
Теперь обсудим вопросы масштабирования растров. Вспомогательная функция копирования растра поддерживает масштабирование. Задайте размер вторичной поверхности больше, чем размер используемого растра, например, так:
ddsd.dwWidth := wrkBitmap.Width * 2;
Теперь при воспроизведении мы увидим картинку, растянутую вдоль экрана, но не всю, а только ее половину. Для того чтобы вывести ее целиком, надо изменить значение поля Right структуры dstRect:
SetRect (dstRect, 100, 100, 100 + 256 * 2, 356);
Попробуем перемещать картинку по экрану (проект каталога Ех08). Переменная ift хранит текущее значение смещения картинки, на это значение опираемся при заполнении полей структуры dstRect:
SetRect (dstRect, 1ft, 100, 1ft + 256, 356);
Форма дополнилась обработчиком нажатия клавиши: демонстрационные программы, использующие DirectDraw, традиционно должны завершать работу при нажатии клавиши <Esc> или <F12>. Добавилась также обработка нажатий клавиш управления курсором:
case Key of
VK_ESCAPE, VK_F12 : begin // Традиция для DirectDraw
Close; Exit;
end;
VK_LEFT : begin // Клавиша "стрелка влево"
Dec (1ft, 1); // Уменьшаем 1ft
FormPaint (nil); // Перерисовываем экран end;
VK_RIGHT : begin // Клавиша "стрелка вправо"
Inc (1ft, 1); // Увеличиваем 1ft
FormPaint (nil); // Перерисовываем экран
end;
end;
Обратите внимание, что для перерисовки окна метод Refresh не годится, иначе сквозь экран будет проглядывать мелькнувшее окно приложения. Картинка движется с малым шагом с целю убедить вас, что если хоть один пиксел растра не помещается на первичную поверхность, не воспроизводится ничего.
Также наверняка вам бросится в глаза то, что картинка появляется медленно на экране, при ее воспроизведении вы можете увидеть мельтешение черных полос. Пока старайтесь не обращать на это внимания. В будущем мы устраним это - такого не должно быть в наших серьезных проектах.
Сейчас сделайте следующее. Запишите строку, задающую параметры области вывода так:
SetRect (dstRect, 1ft, 100, 1ft + 512, 356);
Картинка выводится растянутой, из чего делаем важный вывод: метод Bit поверхности поддерживает операцию масштабирования. Удобное для нас свойство, им можно пользоваться, чтобы задавать в качестве фона растровое изображение любого размера. Для этого измените ту же строку вот так:
SetRect (dstRect, 0, 0, ClientWidth, ClientHeight);
Теперь код, заполняющий первичную поверхность черным цветом, можно просто удалить. Он не нужен, его выполнение только отнимает драгоценное время.
Плохо в получающемся примере то, что размер растра используется в нем дважды: при задании размеров первичной поверхности и при задании области вывода. Здесь нас ждет хорошая новость: во втором случае можно ничего не указывать, по умолчанию будет использоваться вся поверхность, и строку блиттинга можно записать так:
hRet := FDDSPrimary.Blt (SdstRect, FDDSImage, nil, DDBLT_WAIT, nil);
To есть третий параметр равен nil. Обязательно проверьте это, все должно работать как следует.
С точки зрения оптимизации лучше явно задавать размер копируемой поверхности.
Протестируйте работу программы при переключении и восстановлении и, если картинка пейзажа теряется, скорректируйте код функции RestoreAli.
Я воспользуюсь случаем, чтобы посвятить вас в еще одну важную тему: в любой момент времени мы можем получить информацию обо всех свойствах поверхности, в том числе и о ее размерах. Для этого предназначен метод поверхности GetSurfaceDesc.
Иллюстрацией служит проект каталога Ех09. Код обработчика onPaint формы дополнился локальной переменной ddsd2 типа TDDSurfaceDesc2, перед блиттингом с ней производятся обычные действия (обнуление всех полей и задание размера), используется она с целью хранения информации о параметрах поверхности, для получения которых и вызывается изучаемый метод:
//В ddsd2 занести данные о поверхности
FDDSImage.GetSurfaceDesc (ddsd2);
// Размеры srcRect устанавливаются равными размерам поверхности
SetRect (srcRect, 0, 0, ddsd2.dwWidth, ddsd2.dwHeight);
Сейчас в качестве упражнения рекомендую выполнить следующее задание: создайте простейшую программу просмотра bmp-файлов. После загрузки приложения пользователь выбирает нужный файл с помощью стандартного диалога. Растр выводится на полный экран.
Еще один простой пример по поводу блиттинга - проект каталога Ех10. Здесь экран раскрашивается подобно радуге (рис. 2.2).
Используется растр размером 1024x1, т. е. высотой в один пиксел. Не забывайте, что карты с небольшой видеопамятью не способны создать вторичную поверхность больше первичной. Некоторые читатели не смогут насладиться всей красотой этого примера, но ничего не потеряют, поскольку следующий проект выводит все тот же растр, и должен работать на всех картах.
В проекте каталога Ex11 я напоминаю о другом способе масштабирования растров, обычном для Delphi. При создании вторичной поверхности растровое изображение все также загружается в объект wrkBitmap. Затем создается вспомогательный объект wrkBitmapl, его ширина - 640 пикселов, высота - 1 пиксел. После чего "масштабируется" прежний растр и выводится на канве wrkBitmapi с помощью метода StretchDraw:
wrkBitmapi.Canvas.StretchDraw (Rect (0, 0, wrkBitmapi.Width,
wrkBitmapi.Height), wrkBitmap);
Размеры вторичной поверхности теперь должны опираться на размеры именно второго растра.
Такой способ масштабирования более эффективен. Задайте высоту растра равной 60 пикселам, и радуга должна заполнить экран гораздо быстрее, чем в двух предыдущих способах, поскольку меньше тратится времени при окончательном растяжении вторичной поверхности.
Упражнение: сделав wrkBitmapl глобальной переменной, добейтесь уверенного восстановления изображения.
Аналогичный прием со вспомогательным объектом класса TBitmap используется в очередном примере (проекте каталога Ех12), в котором образ загружается из jpg-файла, а при выводе картинка заключается в рамку (рис. 2.3).
В списке uses добавлены модули extctris и jpeg для использования динамически создаваемого объекта image класса Timage, в который будет загружаться jpg-файл :
Image := Timage.Create (nil); // Создаем объект
Image.Picture.LoadFromFile ('..lake.jpg'); // Загружаем jpg
// Непосредственно Image использовать не сможем
wrkBitmap := TBitmap.Create; // Вспомогательный Bitmap
wrkBitmap.Width := 640; // Размеры - все окно, чтобы не было искажений
wrkBitmap.Height := 480;
// Фон прямоугольника рамки // Рамка обрамляется красным // Толщина карандаша
wrkBitmap.Canvas.Brush.Color := clBlue;
wrkBitmap.Canvas.Pen.Color := clRed;
wrkBitmap.Canvas.Pen.Width := 5;
wrkBitmap.Canvas.Rectangle (150, 100, 490, 380); // Рамка
// Воспроизводим jpg на канве
wrkBitmap.Canvas.Draw (192, 112, Image.Picture.Graphic);
Image.Free; // Image больше не нужен
Канва в примере используется только при подготовке поверхности, посему мы не потеряем в скорости при воспроизведении.
Будьте внимательны, основной фон экрана в рассматриваемом примере - серый, поскольку за нашей картинкой выступает поверхность основного окна. Такое сочетание вывода функциями GDI и командами DirectDraw вообще-то надо избегать, заполняя весь фон вторичной поверхности. Если вы внимательно исследуете содержимое заголовочного файла DirectDraw.pas, то легко сможете обнаружить, что свойства блиттинга гораздо шире изученных нами. Например, поверхность можно вращать при выводе. Удобная возможность, но предоставляется только акселератором, причем далеко не каждым. Поэтому изучить вам это придется самостоятельно. А мы перейдем к другому методу поверхности, осуществляющему блиттинг - методу BitFast. Рассмотрим пример, представленный в проекте каталога Ех13. Картинка загружается из jpg-файла, внеэкранная поверхность должна закрывать собой весь экран:
wrkBitmap. Width := 640; // По размерам совпадает с устанавливаемым wrkBitmap. Height := 480; // экранным режимом
wrkBitmap. Canvas. Brush. Color := clBlack; // Фон экрана установим черным wrkBitmap. Canvas. Rectangle (0, 0, 640, 480); // Закрасим весь экран wrkBitmap. Canvas . Draw (192, 112, Image. Picture. Graphic ) ; // Вывод jpg
Воспроизведение основано на методе BitFast:
hRet := FDDSPrimary. BitFast (0, 0, FDDSImage, nil, DDBLTFAST_WAIT) ;
Первые два аргумента задают координаты (х, у) левого верхнего угла размещаемого блока в принимающей поверхности. Дальше указывается вставляемая поверхность. Предпоследний аргумент - величина типа TRECT - задает вырезаемую из вставляемой поверхности область. Точно так же, как и в случае с методом Bit, желательно явно задавать размеры, даже в случае, когда поверхность вставляется целиком. Последний аргумент определяет условия работы блиттинга. Пока мы задаем одиночное значение. Константа изменилась, но смысл ее использования аналогичен DDBLT_WAIT.
Метод BitFast более привлекателен в использовании и работает быстрее. Но он имеет некоторые ограничения в сравнении с методом Bit, например, не предоставляет возможности автоматического масштабирования, не может использоваться для заполнения фона так, как мы это делали раньше.
Буферы
Итак, будем стараться использовать метод BitFast всегда, когда это возможно, т. к. скорость работы приложения является для графики наиважнейшей характеристикой. Посмотрим, как выглядит с применением этого метода перерисовка в проекте каталога Ех14. С помощью клавиш перемещения курсора можно управлять положением картинки на экране по горизонтальной оси. Хотя скорость воспроизведения и увеличилась, но мельтешение полос при перерисовке картинки осталось. Экран мы заполняем в два этапа: вначале все закрашиваем черным цветом, затем накладываем картинку. В промежуток времени между этими действиями экран успевает обновиться, и мы видим эти раздражающие полосы.
Попробуем проделать следующее: создадим внеэкранную вспомогательную поверхность, по размерам равную первичной. Воспроизводить изображение будем на нее, а окончательную картинку перенесем на экранную поверхность. Иллюстрацией этого ловкого приема служит проект каталога Ех15.
Поскольку значения устанавливаемых параметров экрана в коде используются неоднократно, введем соответствующие константы:
ScreenWidth = 640;
ScreenHeight = 480; ScreenBitDepth = 16;
Для загрузки растра вызовем вспомогательную функцию DDLoadBitmap из модуля DDUtii, объединяющую создание поверхности, собственно загрузку и копирование растра на поверхность:
FDDSImage := DDLoadBitmap (FDD, szBitmap, 0, 0); // Укороченный код
if FDDSImage = nil then begin // Произошла ошибка
ErrorOut (hRet, ' DDLoadBitmap' ) ;
Exit
end;
Функция пытается загрузить растр из ресурсов, в случае неудачи загружает файл. Первым аргументом задается главный объект DirectDraw, затем имя файла - у нас это константа szBitmap, - дальше указываются требуемые размеры поверхности или нули, если поверхность должна иметь размеры растра.
Вспомогательная поверхность носит имя FDDSBack и в начале и конце работы приложения "обнуляется" самой первой. Ее размеры задаются равными размерам первичной поверхности:
FillChar (ddsd, SizeOf (ddsd) , 0) ; with ddsd do begin
dwSize := SizeOf (ddsd) ;
dwFlags := DDSD_CAPS or DDSD_HEIGHT or DDSD_WIDTH;
ddsCaps . dwCaps := DDSCAPS_OFFSCREENPLAIN;
dwWidth := ScreenWidth;
dwHeight := ScreenHeight; end;
hRet := FDD. CreateSur face (ddsd, FDDSBack, nil); if hRet <> DD_OK then begin
ErrorOut (hRet, 'Create Back Surface');
Exit
end;
Перерисовка окна принципиально изменилась тем, что весь вывод осуществляется на вспомогательную поверхность, содержимое которой в законченном виде помещается на первичную:
ZeroMemory(@ddbltfx, SizeOf (ddbltfx) ) ; ddbltfx.dwSize := SizeOf (ddbltfx) ; ddbltfx. dwFillColor := 0;
while True do begin // Закрашиваем фон вторичной поверхности черным hRet := FDDSBack. Bit (nil, nil, nil, DDBLT COLORFILL or DDBLT WAIT, @ddbltfx) ;
// Внеэкранная поверхность также может быть потеряна
if hRet = DDERR^SURFACELOST then begin if Failed (RestoreAll) then Exit;
end
else Break; end;
// Помещаем растр на вспомогательную поверхность while True do begin
hRet := FDDSBack.BltFast (1ft, 112, FDDSImage, nil, DDBLTFAST_WAIT);
if hRet = DDERR_SURFACELOST then begin if Failed (RestoreAll) then Exit;
end
else Break; end;
// Вспомогательная поверхность заполнена, блиттинг производится //на первичную while True do begin
hRet := FDDSPrimary.BltFast (0, 0, FDDSBack, nil, DDBLTFAST_WAIT);
if hRet = DDERR_SURFACELOST then begin if Failed (RestoreAll) then Exit;
end
else Break;
end;
Функция восстановления поверхности использует вспомогательную функцию перезагрузки растра DDReLoadBitmap модуля DDUtil:
function TfrmDD.RestoreAll : HRESULT; begin
Result = DD_FALSE;
if Succeeded (FDDSPrimary._Restore) then begin if Failed (FDDSImage._Restore) then Exit;
// Рекомендуется перезагрузить растр после восстановления if Failed (DDReLoadBitmap(FDDSImage, szBitmap)) then Exit;
// Добавилось восстановление еще одной вспомогательной поверхности
if Failed (FDDSBack.^Restore) then Exit;
Result := DD_OK;
end;
end;
Протестируйте приложение: никаких полос не возникает, все выглядит прекрасно. DirectDraw предлагает автоматизированный механизм двойной буферизации, аналогичный проделанному нами вручную. Посмотрим на примере проекта каталога Ех1б, как это делается. При создании первичной поверхности указываем количество задних буферов. Вместо одной константы у нас появилась комбинация нескольких флагов. Создаваемая поверхность является комплексной, состоящей из двух поверхностей - первичной и присоединенной к ней вторичной поверхности заднего буфера. Чтобы оговорить то, что "перебрасывание" (flipping) содержимого заднего буфера на первичную поверхность будет осуществляться DirectDraw без нашего участия, необходимо добавить флаг DDSCAPS_FLIP:
FillChar (ddsd, SizeOf(ddsd), 0);
ddsd.dwSize := SizeOf(ddsd);
// Сообщаем, что надо учесть наши пожелания о буфер заднего плана
ddsd.dwFlags := DDSD_CAPS or DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE or DDSCAPS_FLIP or
DDSCAPS_COMPLEX; // + комплексная поверхность + разрешить перебрасывание
ddsd.dwBackBufferCount := 1; // У поверхности есть один задний буфер
hRet := FDD.CreateSurface(ddsd, FDDSPrimary, nil);
if hRet <> DD_OK then begin
ErrorOut(hRet, 'Create Primary Surface');
Exit;
end;
Поверхность заднего буфера нам создавать не нужно, она появится без нашего участия, но для осуществления вывода на нее необходимо связать нашу переменную FDDSBack, для чего предназначен метод поверхности GetAttachedSurface. Первый аргумент метода - запись типа TDDSCaps2. С таким типом мы встречались, он является частью структуры TDDSurfaceDesc2. Здесь же указываем, что нам требуется адрес поверхности заднего буфера:
FillChar(ddscaps, SizeOf(ddscaps), 0); // Обнуляем все поля записи
// Оговариваем, что требуется адрес поверхности заднего буфера
ddscaps.dwCaps := DDSCAPS_BACKBUFFER;
// Получаем адрес присоединенной поверхности
hRet := FDDSPrimary.GetAttachedSurface(ddscaps, FDDSBack);
if hRet <> DD_OK then begin
ErrorOut(hRet, 'GetAttachedSurface');
Exit;
end;
Код воспроизведения изменился только в финальной части, вместо использования метода BitFast первичной поверхности вызываем ее метод Flip:
while True do begin
hRet := FDDSPrimary.Flip(nil, DDFLIP_WAIT) ;_
if hRet = DDERR_SURFACELOST then begin if Failed (RestoreAll) then Exit;
end
else Break;
end;
Первым аргументом метода указывается, при необходимости, адрес конкретной поверхности; вторым аргументом задается набор параметров, определяющих режим переключения.
Присоединенная поверхность не требует отдельного восстановления, она будет восстановлена как часть первичной, комплексной поверхности.
Обращаю внимание, что в программах, написанных на Delphi, необходимо обязательно при завершении работы освобождать присоединяемые поверхности, иначе возникнет исключение.
При использовании метода поверхности Flip не происходит, на самом деле, простого воспроизведения на ней так, как вытекает из моих предыдущих рассуждений. Буферы меняются местами, вернее, происходит переключение указателей (адресов). Сами объекты при этом местами не меняются.
Переходим к очередному примеру - проекту каталога Ех17. Смысл примера состоит в следующем: поместим на переднюю и заднюю поверхности разные образы и с течением времени будем только переключать их, не перерисовывая.
На переднюю поверхность я помещаю растянутую картинку с пейзажем, на задней поверхности (она как раз и является задним буфером) нарисован тот же пейзаж посреди черного поля. Код переключения буферов из обработчика OnPaint формы переместился в обработчик единственного события таймера, а начинается код, связанный с перерисовкой окна, с заполнения переднего буфера:
hRet := FDDSPrimary.Bit (nil, FDDSImage, nil, 0, nil) ;
Эта строка, а также код, связанный с заполнением заднего буфера, может спокойно перекочевать в обработчик OnCreate формы, он в этом примере вызывается и при восстановлении окна.
Чтобы таймер не работал при неактивном состоянии приложения, применяется событие объекта ApplicationEventsi, связанное с "уходом" окна приложения:
procedure TfrmDD.ApplicationEventslDeactivate(Sender: TObject);
begin
Timer1.Enabled := False; // Выключаем таймер Application.Minimize; // Минимизируем приложение
end;
Включается же таймер в обработчике OnCreate, специально для обработки восстановления окна.
Заполнение черным или любым другим цветом поверхности заднего плана для подобных примеров является обязательным. Если этого не делать, то в незаполненных участках буфера будет выводиться "мусор", искаженные следы работы системы с канвой рабочего стола. Если вы удалите этот код, вам может показаться, что все работает прекрасно, но только потому, что перед этим вы запускали приложение, аккуратно заполнившее экран.
Итак, интересен для нас этот несложный пример демонстрацией того, что при переключении буферов их содержимое не теряется, а перекочевывает в следующий буфер. Буферы заполняются при создании и восстановлении окна. В обработчике таймера происходит только переключение буферов.
В рассмотренном примере существуют два буфера. Это обычно для приложений, использующих DirectDraw. Если же буферов больше, то содержимым они меняются по цепочке (рис. 2.4).
Последний пример главы, проект каталога Ех18, на страницах книги разбирать не будем. Проект этот станет для вас полезным тогда, когда вам потребуется код получения информации о системе.
Отладка приложений
Надеюсь, у вас уже выработалась привычка запускать наши проекты, использующие DirectDraw, отдельно от среды Delphi. Полноэкранные приложения на основе DirectDraw тяжело отлаживать так, как вы привыкли это делать с обычными проектами.
Если вы установите точку останова в коде, то при достижении этой строки среда IDE попытается осуществить вывод на занятой поверхности, и ничего хорошего из этого не получится - система может зависнуть.
Ошибки, возникающие при создании и подготовке поверхностей, легко нами обрабатываются. Но как только полноэкранное приложение заняло канву рабочего стола, сообщения, выводимые нашей функцией ErrorOut, просто перестанут быть видны.
У каждого есть свои способы работы в такой ситуации. Я могу порекомендовать свой: пользуйтесь подачей звукового сигнала в тех точках, прохождение которых ставится под вопрос. Если есть сомнения в успешности каких-либо действий, подавайте различные сигналы, в зависимости от значения проверяемого выражения.
В ситуациях, когда такой способ существенно не поможет, придется использовать вывод в файл. Ведите протокол ваших действий, дописывая в отладочный файл информацию о выполненных операциях.
В остальных примерах использования DirectDraw расшифровка произошедшей ошибки будет выводиться в текстовый файл:
procedure TfrmDD.ErrorOut(hRet : HRESULT; FuncName : String);
var
t : TextFile; begin
AssignFile (t, 'Debug.txt');
Rewrite (t);
WriteLn (t, FuncName + ' : ' + DDErrorString (hRet));
CloseFile (t);
Destroy;
end;
Что вы узнали в этой главе
Главное предназначение DirectDraw - вывод содержимого одной поверхности или части поверхности на другую. Поверхностей может быть столько, сколько требуется разработчику, но для построений должна быть, как минимум, одна, первичная поверхность. Первичная поверхность связана с экраном, и пользователь видит то, что в конечном итоге оказывается на ней.
Методы поверхности Bit и BitFast предназначены для осуществления блиттинга - вывода на поверхность.
В построениях, как правило, используется двойная буферизация: вывод осуществляется на поверхность заднего буфера, затем передний и задний буферы меняются местами.
- Введение
- Глава 1. Понятие о СОМ
- Глава 2. Обзор библиотеки DirectDraw
- Глава 3 Приемы использования DirectDraw
- Глава 4 Спрайтовая анимация
- Глава 5 Пишем игру
- Глава 6 Работа с AVI-файлами
- Глава 7 Обзор библиотеки Direct3D
- Глава 8 Подробнее о библиотеке Direct3D
- Глава 9 Трехмерные построения
- ГЛАВА 10 Визуальные эффекты
- Заключение
- ПРИЛОЖЕНИЕ 1 Глоссарий
- Содержание книги
- Популярные страницы
- Глава 1. Обзор Ruby
- Обзор основных причин повреждения базы данных
- Первый просмотр: краткий обзор
- Настройка библиотеки
- Обеспечение безопасности библиотеки
- Удаление библиотеки
- 4.1. Суть обзора задач в тайм-менеджменте. Основные понятия и определения
- 4.2. Инструменты создания обзора
- Глава 2 Выбираем рассылочный сервис (требования, обзор существующих решений и личные рекомендации)
- 12.4.1. Обзор
- 12.5. Другие библиотеки для создания графических интерфейсов
- typedef - КРАТКИЙ ОБЗОР