Ассемблер под Windows для чайников. Часть 20

Продолжим знакомиться с азами воспроизведения картинок в окошках. В прошлый раз мы сумели загрузить картинку из ресурсов во вторичный буфер в памяти и скопировать ее в первичный буфер. Не надо пугаться двойной буферизации, ведь она присутствует повсюду, даже в смывном бачке, простите за ассоциацию. Это вполне естественно: аккуратно вырезать печать, а потом — бах, бах — и надпись с рисунком уже на бумаге. В программе нам даже не надо ничего вырезать, ведь у нас под рукой — всем знакомая ОС с удобными функциями для работы, в том числе, с графикой. Хорошо, с отображением рисунка все ясно, а как нам быть, если хочется отобразить подвижное изображение — анимацию? Не будем сейчас о всяких DirectX и OpenGL. Поговорим о простейшем способе, который реализуется при помощи стандартных GDI- и User-функций, с которыми мы в прошлый раз познакомились.

Об использовании анимированных GIF-файлов пока говорить рано, ведь для этого нам потребуется детально изучить формат файла и написать достаточно сложную процедуру распаковки изображения в память. Можно было бы тупо создать в ресурсах кучу BMP-картинок, для каждого кадра — отдельный BitMap. Однако выгоднее использовать одну битовую карту для всех кадров анимации. Например, десять кадров размером 64x64 можно разместить в одном файле разрешением 640x64 или 320x128, тем самым сэкономив немного памяти. Сжатие, конечно, позволило бы нам сэкономить гораздо больше места, но всему свое время — не будем прыгать сразу через три ступеньки. Чтобы не тратить время на рисование специальной анимации для нашего примера, я взял картинку из примера в ..\FASM\EXAMPLES\DDRAW\ и, отрезав фон, сохранил лишь раскадровку вращающегося кольца в файл BMP-формата (рис. 1).

В этой раскадровке мы имеем 60 кадров с изображением кольца 64x64, сохраненных в один файл размером 640x384. Даже в данной ситуации со столь большим числом кадров экономия составляет всего лишь около трех килобайт по сравнению с 60-ю отдельными изображениями 64x64, тем не менее, импортировать один файл в секцию ресурсов, как вы понимаете, намного проще, чем 60 файлов. Кроме того, вряд ли вам захочется программировать загрузку 60 отдельных картинок из ресурсов в буферную память. Смотрите, насколько просто реализуется воспроизведение этой анимации:

format PE GUI 4.0
entry start

include 'win32a.inc'

section '.data' data readable writeable

_class TCHAR 'FASMWIN32',0
_title TCHAR 'Анимация',0
_error TCHAR 'Ошибка запуска.',0

wc WNDCLASS 0,WindowProc,0,0,NULL,NULL,NULL,COLOR_WINDOWTEXT,NULL,_class

msg MSG
ps PAINTSTRUCT

hBitmap dd ?
hdc dd ?
hMemDC dd ?

src_X dd ?
src_Y dd ?
frame db ?

section '.code' code readable executable

start:

invoke GetModuleHandle,0
mov [wc.hInstance],eax
invoke LoadIcon,[wc.hInstance],17
mov [wc.hIcon],eax
invoke LoadCursor,[wc.hInstance],27
mov [wc.hCursor],eax
invoke RegisterClass,wc
test eax,eax
jz error

invoke CreateWindowEx,0,_class,_title,WS_VISIBLE+WS_DLGFRAME+ WS_SYSMENU,128,128,150,150,NULL,NULL,[wc.hInstance],NULL
test eax,eax
jz error

msg_loop:
invoke GetMessage,msg,NULL,0,0
cmp eax,1
jb end_loop
jne msg_loop
invoke TranslateMessage,msg
invoke DispatchMessage,msg
jmp msg_loop

error:
invoke MessageBox,NULL,_error,NULL,MB_ICONERROR+MB_OK

end_loop:
invoke ExitProcess,[msg.wParam]

proc WindowProc hwnd,wmsg,wparam,lparam
push ebx esi edi
mov eax,[wmsg]
cmp eax,WM_CREATE
je .wmcreate
cmp eax,WM_TIMER
je .wmtimer
cmp eax,WM_PAINT
je .wmpaint
cmp eax,WM_DESTROY
je .wmdestroy
.defwndproc:
invoke DefWindowProc,[hwnd],[wmsg],[wparam],[lparam]
jmp .finish
.wmcreate:
invoke LoadBitmap,[wc.hInstance],37
mov [hBitmap],eax
invoke CreateCompatibleDC,[hdc]
mov [hMemDC],eax
invoke SelectObject,[hMemDC],[hBitmap]
invoke SetTimer,[hwnd],0,25,0
jmp .finish
.wmtimer:
movzx eax,[frame]
xor edx,edx
mov ebx,10
div ebx

sal edx,6
mov [src_X],edx
sal eax,6
mov [src_Y],eax

invoke InvalidateRect,[hwnd],NULL,FALSE

inc [frame]
cmp [frame],60
jb .finish
mov [frame],0
jmp .finish
.wmpaint:
invoke BeginPaint,[hwnd],ps
mov [hdc],eax

invoke BitBlt,[hdc],40,30,64,64,[hMemDC],[src_X],[src_Y],SRCCOPY

invoke EndPaint,[hwnd],ps
jmp .finish
.wmdestroy:
invoke DeleteDC,[hMemDC]
invoke PostQuitMessage,0
xor eax,eax
.finish:
pop edi esi ebx
ret
endp

section '.idata' import data readable writeable

library gdi32,'GDI32.DLL',\
kernel32,'KERNEL32.DLL',\
user32,'USER32.DLL'

include 'api\gdi32.inc'
include 'api\kernel32.inc'
include 'api\user32.inc'

section '.rsrc' resource data readable

directory RT_BITMAP,bitmaps,\
RT_ICON,icons,\
RT_GROUP_ICON,group_icons,\
RT_CURSOR,cursors,\
RT_GROUP_CURSOR,group_cursors,\
RT_VERSION,versions

resource icons,\
48,LANG_NEUTRAL,icon_48,\
32,LANG_NEUTRAL,icon_32,\
24,LANG_NEUTRAL,icon_24,\
16,LANG_NEUTRAL,icon_16

resource group_icons,\
17,LANG_NEUTRAL,main_icon
resource cursors,\
2,LANG_NEUTRAL,cursor_data
resource group_cursors,\
27,LANG_NEUTRAL,main_cursor
resource versions,\
8,LANG_ENGLISH+SUBLANG_DEFAULT,version
resource bitmaps,\
37,LANG_NEUTRAL,pict

bitmap pict,'ani.bmp'
icon main_icon,icon_48,'48.ico',icon_32,'32.ico',\
icon_24,'24.ico',icon_16,'16.ico'
cursor main_cursor,cursor_data,'cursor.cur'

versioninfo version,VOS__WINDOWS32,VFT_APP,VFT2_UNKNOWN,LANG_ENGLISH+SUBLANG_DEFAULT,0,\
'FileDescription','Bitmap example',\
'LegalCopyright',<'Copyright ',0A9h,' BarMentaLisk 2008'>,\
'FileVersion','0.1',\
'ProductVersion','0.1',\
'OriginalFilename','bmp_example'

В секции данных мы определяем новые переменные. 32-битные src_X и src_Y будут хранить текущие координаты левого верхнего угла
воспроизводимого кадра анимации. 8-битная переменная frame будет содержать номер текущего кадра, из которого мы будем вычислять координаты его расположения в картинке раскадровки. При создании окна (сообщение WM_CREATE) мы загружаем картинку в память и сразу создаем совместимый с клиентской областью окна вторичный буфер. Изображение в окне будет перерисовываться много раз в секунду, поэтому нам нет смысла создавать и удалять вторичный буфер для отрисовки каждого кадра. После выбора картинки для вторичного буфера (SelectObject) мы создаем таймер. Функция SetTimer создает системный таймер, отсылающий сообщения программе через определенные равные промежутки времени. Он понадобится нам для равномерного воспроизведения анимации. Смена текущего кадра следующим будет осуществляться по получению сообщения от таймера. Параметры функции SetTimer:

1. Дескриптор окна вызывающего процесса, к которому будет привязан таймер. Если указать ноль, то привязки к окну не произойдет, а второй параметр будет игнорироваться.
2. Идентификатор таймера (на случай, если надо установить несколько таймеров), который будет отсылаться окну в первом параметре сообщения WM_TIMER.
3. Время задержки в миллисекундах.
4. Указатель на отдельную процедуру обработки сообщений таймера, если таковая имеется в процессе, создающем таймер.

Таймер у нас один, поэтому мы указываем нулевой идентификатор и не создаем отдельную процедуру для обработки его сообщений. Указываем лишь дескриптор нашего основного и единственного окна и задержку в 25 миллисекунд, которая соответствует скорости воспроизведения 40 кадров в секунду. Захотите увеличить скорость — уменьшайте задержку, но учтите, что отрисовка анимации через простейшие графические функции работает довольно медленно, и при особо коротких задержках система может просто не успевать производить смену кадров с заданной скоростью и будет "играть как умеет". Рассмотрим действия программы по получению сообщения от таймера. Если бы таймеров у нас было несколько, то первым делом нам надо было бы определить, какой из них прислал напоминание, путем сравнения wparam с заданными при создании таймеров значениями идентификаторов. Но, так как таймер у нас единственный, мы сразу переходим к запланированной операции. Нам необходимо вычислить координаты очередного кадра анимации. Не забыли еще, как производится деление нацело? Делимое размером с учетверенное слово (64 бита) должно находиться в регистрах EDX:EAX, поэтому мы копируем номер текущего кадра в EAX, а старшую часть (EDX) просто обнуляем. Делитель помещаем в свободный регистр (по желанию можно использовать и 32-битную переменную, но в нашем случае проще в регистр) и делим на него: div ebx. В результате в EAX мы получим частное, а в EDX — остаток от деления. Номер текущего кадра мы делим на 10 лишь потому, что на нашей картинке 10 кадров по горизонтали. Тогда, после деления, в EDX мы получим номер кадра в горизонтальном ряду, а в EAX — номер в вертикальном столбце (не забываем, что первый кадр — кадр номер ноль!). Для получения точных координат в пикселях нам достаточно умножить эти номера на 64 (наши кадры имеют размер 64x64). Ну, естественно, на степень двойки умножаем методом арифметического сдвига влево на определенное количество бит (sal edx,6). Вызываем функцию InvalidateRect, которая внесет необходимые сведения в структуру PAINTSTRUCT и пошлет нашему окну сообщение WM_PAINT для того, чтобы мы перерисовали содержимое окна.

Параметры функции InvalidateRect:
1. Дескриптор окна с изменившимся содержимым.
2. Указатель на структуру RECT, содержащую координаты прямоугольника, который следует добавить к региону, ожидающему перерисовки. Если указан ноль, то будет добавлена вся клиентская область окна.
3. Флаг очищения фона при перерисовке заданной области окна. Если указано значение TRUE, то фон будет очищен при последующем вызове функции BeginPaint. FALSE указывает на то, что фон следует оставить без изменений.

Таким образом, при внесении изменений во вторичный буфер можно добавлять области, которые будут перерисованы при очередной обработке сообщения WM_PAINT. Чтобы исключить область из ожидающих перерисовки областей, можно использовать функцию ValidateRect (параметры: дескриптор окна и указатель на структуру RECT с координатами исключаемой области). Но это уже для тех, кто решит самостоятельно запрограммировать простенькую двухмерную игру. Нам же сейчас проще добавить в перерисовку всю клиентскую область окна, хотя энтузиастам советую поэкспериментировать с точным указанием перерисовываемого прямоугольника через структуру RECT. Итак, после обозначения перерисовываемой области окна система уже, наверное, поставила сообщение WM_PAINT в очередь сообщений нашему окну. Но нам еще надо сделать одну мелочь в обработчике сообщения таймера — увеличить номер текущего кадра для следующего раза и обнулить его, если это значение сравнялось с цифрой 60, чтобы анимация пошла по очередному кругу. Теперь при получении сообщения WM_PAINT (оно, кстати, может приходить как после нашего вызова InvalidateRect, так и после перемещения/изменения окна, требующего перерисовки содержимого), мы копируем кадр 64x64 из координат, указанных в переменных [src_X], [src_Y], в координаты первичного буфера 40,30 (чтобы колечко было примерно посередине окна). В отличие от примера из прошлой статьи, мы не удаляем вторичный буфер после копирования картинки из него, поэтому по-хорошему нам надо будет не забыть сделать это при обработке сообщения WM_DESTROY.

Посмотрим, сможем ли мы, кроме имитации вращения кольца путем последовательной отрисовки кадров, еще и перемещать его в пределах окна. Местоположение анимации определено у нас сейчас константами (40 и 30). Заменим их переменными [dst_X] и [dst_Y]:

section '.data' data readable writeable

dst_X dd ?
dst_Y dd ?

invoke BitBlt,[hdc],[dst_X],[dst_Y],64,64,[hMemDC],[src_X],[src_Y],SRCCOPY


Теперь, изменяя содержимое этих переменных, мы сможем изменять координаты вывода нашей анимации в клиентской области окна. Давайте сделаем так, чтобы можно было стрелками клавиатуры передвигать наше вращающееся колечко. Для этого нам понадобится функция проверки состояния клавиши. Если, например, нажата клавиша вниз — значит, надо увеличить значение переменной [dst_Y], если вверх — то уменьшить, причем необходимо еще проверить, не достигло ли значение максимального или минимального предела, чтобы колечко не уползло дальше обозначенных нами границ. Функция GetKeyState возвращает состояние указанной виртуальной клавиши. Виртуальной потому, что клавиша может быть реально нажата, а может быть симулирована другой программой. У функции лишь один параметр — значение виртуального кода клавиши. Некоторые коды клавиш вы можете найти в файле ..\FASM\INCLUDE\EQUATES\USER32.INC, остальные — узнать при помощи прилагаемой к примерам программки KeyInfo.exe. Значения клавиш, обозначающих буквы латинского алфавита, можно заменять заглавным символом этой буквы, заключенным в кавычки — например, значение клавиши A (русская Ф) можно заменить на 'A' (кавычки не убирать!). Возвращаемое значение при нормальном исполнении функции определяет состояние заданной клавиши. В случае, когда клавиша нажата, старший бит возвращаемого значения будет установлен в единицу, иначе он будет нулевым. На всякий случай скажу, что младший бит означает состояние переключаемых клавиш вроде Caps Lock и Num Lock. Если он установлен в единицу, значит, данный Lock включен (горит светодиод), иначе переключаемое состояние клавиши — "выключено". Ну да ладно, сейчас нас интересуют обычные клавиши. После вызова функции проверки состояния мы сравним полученное значение с нулем и будем считать, что клавиша нажата, если значение меньше нуля (старший бит, установленный в единицу, означает отрицательное значение на языке машин). Значит так, для применения этой теории на практике нам надо внести в код следующие изменения:


;константы
min_X = -8
max_X = 88
min_Y = 0
max_Y = 68

.wmtimer:
;перемещение картинки
invoke GetKeyState,VK_UP
or eax,eax
jns @f
cmp [dst_Y],min_Y
je @f
dec [dst_Y]
@@:
invoke GetKeyState,VK_DOWN
or eax,eax
jns @f
cmp [dst_Y],max_Y
je @f
inc [dst_Y]
@@:
invoke GetKeyState,VK_LEFT
or eax,eax
jns @f
cmp [dst_X],min_X
je @f
dec [dst_X]
@@:
invoke GetKeyState,VK_RIGHT
or eax,eax
jns @f
cmp [dst_X],max_X
je @f
inc [dst_X]
@@:
;перемещение выполнено
movzx eax,[frame]


Теперь каждый раз по приходу сообщения от таймера координаты вывода изображения будут изменяться, если нажаты стрелки на клавиатуре. Например, если нажата клавиша "вверх" (VK_UP), будет выполнен декремент переменной [dst_Y] при условии, что она еще не равна минимальному значению (min_Y). Иначе будет произведен переход на следующую безымянную метку, где будет произведена проверка следующей клавиши. Единственная проблема, с которой мы сейчас столкнемся, — это то, что при перемещении картинки от нее будут оставаться следы в той области, где она была отрисована ранее. Для того, чтобы этого избежать, нам надо перед отрисовкой очередного кадра нарисовать фон заново. Если вы соберетесь программировать, к примеру, простую игру, то знайте, что вам придется каждый раз отрисовывать фон, а уже потом — подвижные объекты. В общем, для этого желательно все это дело отрисовывать во второй буфер из третьих, но сейчас не про это. Нам сейчас не нужен фон, кроме черного, поэтому мы можем ограничиться простой заливкой клиентской области окна черным цветом. Но для заливки всей клиентской области нам понадобится снова, как в примере из прошлого раза, получить размеры клиентской области в структуру RECT и обозначить границы заливки:

section '.data' data readable writeable
rect RECT

.wmpaint:
invoke BeginPaint,[hwnd],ps
mov [hdc],eax
invoke GetClientRect,[hwnd],rect
invoke BitBlt,[hdc],0,0,[rect.right],[rect.bottom],[hMemDC],0,0,BLACKNESS
invoke BitBlt,[hdc],[dst_X],[dst_Y],64,64,[hMemDC],[src_X],[src_Y],SRCCOPY
invoke EndPaint,[hwnd],ps
jmp .finish


Первым вызовом функции BitBlt мы осуществляем заливку всей клиентской области окна черным цветом, а вторым — как и прежде, копируем изображение очередного кадра в заданную область. Вы можете изменять минимальное и максимальное место отрисовки анимации по своему усмотрению. Я ничего не высчитывал, а просто выбрал эти границы подбором по своему усмотрению. Надеюсь, что этот пример даст вам повод для дальнейших экспериментов. Вы, наверное, заметили, что в сегодняшнем примере оказалось слишком много иконок в секции ресурсов. Я привел их здесь, чтобы вы лучше поняли на конкретном примере, как работает организация нескольких иконок в группу. Одна функция загружает сразу всю группу иконок, а потом ОС сама выбирает самую подходящую для каждого отображения. Если в проводнике отображается иконка разрешением 48 или 32 в зависимости от выбранного масштаба, то в окне программы будет показана иконка размером 16x16. Думаю, теперь вы будете иметь точное представление о значении иконок разного разрешения в ресурсах программы. Желаю вам четко понять сегодняшнее занятие. В сегодняшней теме нет ничего сложного ни для опытного кодера, ни для начинающего программера. Если что-то вам показалось непонятным, — советую внимательнее перечитать предыдущие статьи, и все станет на свои места. Оставайтесь на нашем канале — дальше будет еще интересней.

Все приводимые примеры были протестированы на правильность работы под Windows XP и, скорее всего, будут работать под другими версиями Windows, однако я не даю никаких гарантий их правильной работы на вашем компьютере. Исходные тексты программ вы можете найти на форуме: сайт

BarMentaLisk, SASecurity gr., q@sa-sec.org


Компьютерная газета. Статья была опубликована в номере 46 за 2008 год в рубрике программирование

©1997-2024 Компьютерная газета