В прошлом примере, для вывода картинки на экран я использовал GDI функцию. Это очень даже ужастно, потому что так вывод происходит тормознуто. В этой статье я покажу тебе, как эффективно выводить информацию на экран на высокой скорости. Для этого я буду использовать быстрый вариант функции Blt и переключающие поверхности. Хотя об этом я уже рассказывал, повториться будет не лишним.
Самое первое, чего коснулось изменение нового примера - объявление:
FImageSurface - поверхность для хранения картинки.
Теперь я буду размещать картинку в поверхности FImageSurface. Это значит, что мне не нужно исползовать тормознутое копирование GDI. Теперь я могу копировать картинку с помощью быстрой функции blt. Но я пойду дальше, я буду копировать с помощью более быстрой функции BltFast, но об этом чуть позже.
Я ещё завёл второстепенную поверхность, для того, чтобы можно было переключатся между поверхностями. Если ты программировал графику в DOS, то наверно знаешь, что был такой приём, как переключение страниц. Здесь он работает так же. Мы заводим две поверхности и говорим, что они будут переключаемыми. Одна из них будет главной, а другая второстепенной. Главная всегда отображает данные на экран, а во второстепенной мы можем смело рисовать. Когда вывод данных во второстепенную поверхность окончен, мы меняем местами поверхности. Так получается, что мы моментально выводим готовое изображение на экран без копирования.
Точнее сказать, ничего местами не меняется. Мы просто уазываем, какая область памяти отвечает за отображение данных. Когда мы посылаем команду поменять местами поверхности, просто меняется указатель на главную поверхность. Эта операция проходит мгновенно. Этому подтверждение громадное количество великолепных игр под DOS, которые прекрасно работали даже на самых слабых машинах.
Давай взглянём на процедуру FormCreate (обработчик события OnCreate):
procedure TForm1.FormCreate(Sender: TObject);
var
hRet : HRESULT;
SurfaceDesc : TDDSurfaceDesc2;
fcaps : TDDSCaps2;
begin
//Обнуляю все переменные поверхностей.
FPrimarySurface := nil;
FDirectDraw := nil;
FBackGround :=nil;
FImageSurface :=nil;
....
....
//Здесь идёт инициализация DirectDraw
//Она не изменилась, поэтому этот код я опускаю
....
....
//заполняю структуру SurfaceDesc необходимую при создании поверхностей
FillChar (SurfaceDesc, SizeOf(SurfaceDesc), 0);
SurfaceDesc.dwSize := SizeOf(SurfaceDesc);
//Появилась новая константа DDSD_BACKBUFFERCOUNT, указывающая на то, что
//поле SurfaceDesc.dwBackBufferCount заполнено.
SurfaceDesc.dwFlags := DDSD_CAPS or DDSD_BACKBUFFERCOUNT;
//Появились новые константы DDSCAPS_FLIP и DDSCAPS_COMPLEX
SurfaceDesc.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE or
DDSCAPS_FLIP or DDSCAPS_COMPLEX;
//Поле dwBackBufferCount указывает на необходимость создания
//одной вспомогательной поверхности
SurfaceDesc.dwBackBufferCount := 1;
hRet := FDirectDraw.CreateSurface(SurfaceDesc, FPrimarySurface, nil);
if hRet <> DD_OK then
begin
ErrorOut(hRet);
Close;
Exit;
end;
//Получаю указатель на вторичную поверхность и
//сохраняю указатель на неё в переменной FBackGround
FillChar(fcaps, SizeOf(fcaps), 0);
fcaps.dwCaps := DDSCAPS_BACKBUFFER;
hRet := FPrimarySurface.GetAttachedSurface(fcaps, FBackGround);
if hRet <> DD_OK then
begin
ErrorOut(hRet);
Close;
Exit;
end;
//Загружаю картинку в поверхность FImageSurface
FImageSurface := DDLoadBitmap(FDirectDraw, '1.bmp', 0, 0);
if FImageSurface = nil then
begin
ErrorOut(hRet);
Close;
Exit;
end;
end;
Некоторые вещи я постарался описать в комментариях, но кое какие вещи я разьясню подробнее.
При описании структуры SurfaceDesc я вставляю новый флаг DDSD_BACKBUFFERCOUNT в SurfaceDesc.dwFlags. Это означает, что поле SurfaceDesc.dwBackBufferCount будет заполнено и указывает на количество вспомогательных поверхностей.
Изменились и флаги поверхности, теперь их три DDSCAPS_PRIMARYSURFACE, DDSCAPS_FLIP и DDSCAPS_COMPLEX. Флаг DDSCAPS_FLIP указывает, что поверхности будут переключаемыми. DDSCAPS_COMPLEX - указывает на то, что поверхности будут комплексными.
Далее, я получаю указатель на вторичную поверхность. Я создал сразу две поверхности и получил указатель на главную. Но мне же надо пользоватся и вторичной поверхностью, поэтому мне нужна переменная, которая бы указывала на неё. Переменная есть - FBackGround, мы её уже завели. Осталось только записать туда указатель на вторичную поверхность.
Для получения вторичной поверхности я вызываю функцию FPrimarySurface.GetAttachedSurface. У неё два параметра: первый - TDDSCaps2 структура показывающая, что я хочу получить, второй - указывает на переменную типа IDirectDrawSurface7, в которую запишется указатель на вторичную поверхность.
Перед использованием структуры TDDSCaps2, её нужно обнулить, что я и делаю. А так же нужно поле dwCaps указать, что мы хотим получить. Я указываю флаг DDSCAPS_BACKBUFFER, который говорит о том, что нам нужен указатель на вторичную поверхность, что я и получаю.
И последнее, что я делаю - загружаю картинку в поверхность FImageSurface с помощью функции DDLoadBitmap. Эта функция описана в модуле DDUtil. Она загружает картинку из ресурса или файла. При этом она создаёт новую поверхность и возвращает её.
Объявление функции выглядит так:
function DDLoadBitmap(
pdd : IDirectDraw7;
szBitmap : PChar;
dx, dy : Integer
) : IDirectDrawSurface7;
У функции четыре параметра:
pdd - интерфейс DirectDraw.
szBitmap - cтрока, указывающая на имя ресурса или пусть к файлу с картинкой.
dx и dy - желаемые значения ширины и высоты картинки. Если ты укажешь здесь нули, то будут использоватся значения самой картинки.
Эта функция не из состава DirectX, поэтому она не возвращает кода ошибки. Для проверки правильности работы нужно проверить возвращённую поверхность. Если она не равна nil, значит всё ничтяк.
Всё. С инициализацией покончено. Больше ничего в проекте не изменилось кроме процедуры рисования. Так что нам осталось рассмотреть только её:
procedure TForm1.FormPaint(Sender: TObject);
var
hRet : HRESULT;
bltfx : TDDBLTFX;
begin
//Обнуляю bltfx
ZeroMemory(@bltfx, SizeOf(bltfx));
bltfx.dwSize := sizeof(bltfx);
bltfx.dwFillColor := 0;
while True do
begin
//Заполняю экран чёрным цветом.
hRet := FBackGround.Blt(nil, nil, nil,
DDBLT_COLORFILL or DDBLT_WAIT, @bltfx);
//Проверяю на ошибку
if hRet = DDERR_SURFACELOST then
begin
//Если потеряна поверхность, то восстанавливаю её
hRet := FPrimarySurface._Restore;
FImageSurface := DDLoadBitmap(FDirectDraw, '1.bmp', 0, 0);
if hRet <> DD_OK then exit;
end
else Break;
end;
//Рисую картинку
FBackGround.BltFast (95, 15, FImageSurface, nil, DDBLTFAST_WAIT);
//Переключаю поверхность
FPrimarySurface.Flip(nil, DDFLIP_WAIT);
end;
В самом начале, я так же заполняю весь экран чёрным цветом. Здесь никаких изменений, кроме того, что теперь я заполняю вторичный буфер.
Небольшое изменение произошло при восстановлении поверхностей. Если поверхность потеряна, то необходимо восстановить только первичную поверхность. Вторичная восстановлению не подлежит. Необходимо так же перезагрузить картинку, потому что она так же теряется.
После всего этого, происходит вывод картинки на экран с помощью очень быстрой функции BltFast. Её скорость заключается в меньших возможностях. Она не может производить масштабирование и некоторые ещё операции, с которыми мы познакомимся позже. Главное запомни, что эта функция работает только в полноэкранном режиме и ты не ошибёшься.
Функция выглядит так:
function BltFast (
dwX: DWORD; //Позиция Х приёмника, куда будет происходить копирования
dwY: DWORD; //Позиция Y приёмника, куда будет происходить копирования
lpDDSrcSurface: IDirectDrawSurface; //Поверхность для копирования
lpSrcRect: PRect; //Область из источника, которая будет копироваться
dwTrans: DWORD //Флаги
) : HResult; stdcall;
В качестве флага я использовал флаг ожидания DDBLTFAST_WAIT. Он говорит о том, что если вывод невозможен, необходимо дождатся разрешения.
Последняя функция - Flip, которая переключает поверхности. Функция выглядит так:
Первый параметр - поверхность, на которую надо переключиться. Параметр можно не указывать, а использовать nil. В этом случае переключение будет происходить по цепочке.
Второй параметр - флаги переключения. Я указал флаг одидания. Я думаю не надо объяснять, что если переключение не возможно, то функция подождёт разрешения.
Вот и все новинки. На последок я расскажу, как происходит вывод ошибок. В прошлой статье я не стал этого говорить, потому что и так статья была слишком большая, поэтому я делаю это сейчас.
Для вывода ошибок я использовал процедуру:
procedure TForm1.ErrorOut(hRet: HRESULT);
begin
MessageBox(0, PChar(DDErrorString(hRet)), 'Ошибка', MB_OK or MB_ICONSTOP);
end;
Здесь используется функция DDErrorString, которая возвращает текст ошибки по её коду. Ей нужно передать код возвращённый DirectDraw функцией и ты получишь её текстовое описание.
Ошибку я вывожу только на этапе инициализации, потом это безсмыслено, потому что окно может не появится и произойдёт зависон. Когда DirectDraw уже проинициализировался и ты начал вывод на экран лучше использовать что-нибудь другое. Например, многие советуют записывать ошибки в файл, указывая имя функции и сообщение, которые вызвали ошибку. Я поддерживаю этот метод и сам его использую, потому что он очень удобен. Мне кажется, это наилучший выход.
На этом всё. Увидимся в следующий раз. Надеюсь это будет очень скоро.