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

Сегодня мы продолжим изучение основных графических функций Windows и их использование в программах, написанных для компилятора FASM. Чаще всего в Windows-приложениях используются стандартные окна. Но иногда программисты, желая выделиться оригинальностью интерфейса программы, прибегают к использованию нестандартных форм окошек. Об этих "наворотах" мы сегодня и поговорим. Сразу скажу, что я не являюсь профессиональным дизайнером, поэтому прошу не судить строго мой скромный рисунок, используемый в качестве фона окна в сегодняшнем примере. Целью данного интерфейса является лишь знакомство с некоторыми способами создания альтернативной формы окна.

Для того, чтобы создать окно нестандартной формы, нам придется использовать функции работы с регионами. Регион — это, говоря простым языком, отображаемая область окна. Регион может быть прямоугольником, многоугольником, эллипсом или комбинацией этих форм. Регионы можно заполнять, закрашивать, можно инвертировать цвета в указанном регионе, а также регион может использоваться для определения места клика мышью. Начнем с того, что создадим простое окно без стандартной шапки, как мы уже делали это ранее с диалоговым окном. Для этого не будем указывать в стиле окна значение WS_CAPTION. Естественно, не стоит указывать стили, включающие в себя WS_CAPTION — такие, как WS_OVERLAPPEDWINDOW или WS_TILEDWINDOW:

format PE GUI 4.0
entry start

include 'win32a.inc'
include 'macro\if.inc'

HTCAPTION = 2

section '.data' data readable writeable

_class db 'FASMWIN32',0
_title db 'Регион',0
_error db 'Ошибка запуска.',0

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

msg MSG
ps PAINTSTRUCT
rect RECT

hdc dd ?
hMemDC dd ?
hBitmap dd ?

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

invoke CreateWindowEx,0,_class,_title,\
WS_VISIBLE+WS_POPUP+WS_SYSMENU+WS_MINIMIZEBOX,\
320,240,320,240,0,0,[wc.hInstance],0
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 uses ebx esi edi,hWnd,uMsg,wParam,lParam

.if [uMsg]=WM_CREATE
invoke LoadBitmap,[wc.hInstance],37
mov [hBitmap],eax
.elseif [uMsg]=WM_LBUTTONDOWN
invoke ReleaseCapture
invoke SendMessage,[hWnd],WM_NCLBUTTONDOWN,HTCAPTION,0
.elseif [uMsg]=WM_RBUTTONUP
invoke DestroyWindow,[hWnd]
.elseif [uMsg]=WM_PAINT
invoke GetClientRect,[hWnd],rect
invoke BeginPaint,[hWnd],ps
mov [hdc],eax
invoke CreateCompatibleDC,[hdc]
mov [hMemDC],eax
invoke SelectObject,[hMemDC],[hBitmap]
invoke BitBlt,[hdc],0,0,[rect.right],[rect.bottom],[hMemDC],0,0,SRCCOPY
invoke DeleteDC,[hMemDC]
invoke EndPaint,[hWnd],ps
.elseif [uMsg]=WM_DESTROY
invoke DeleteObject,[hBitmap]
invoke PostQuitMessage,0
.else
invoke DefWindowProc,[hWnd],[uMsg],[wParam],[lParam]
ret
.endif
xor eax,eax
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

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 bitmaps,\
37,LANG_NEUTRAL,pict

bitmap pict,'bitmap.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'


Сейчас наша программа лишь создает обычное квадратное окно размером 320x240. Но нашей целью является окно нестандартной формы. Чтобы сделать окошко овальным, используем функцию CreateEllipticRgn. Она имеет четыре параметра: X и Y левого верхнего угла и X и Y правого нижнего угла прямоугольника, в который будет вписан эллипс создаваемого региона. При успешном выполнении функция возвращает дескриптор созданного региона, который можно применить к окну функцией SetWindowRgn. У этой функции три параметра: дескриптор окна, дескриптор региона и флаг перерисовки содержимого окна после применения региона (TRUE либо FALSE). Если мы создадим регион в форме эллипса с координатами 0,0,320,240 и применим его к нашему окну, то получим окно овальной формы — как раз как эллипс, изображенный на рисунке. Однако на рисунке у нас есть еще выступающая прямоугольная область под нарисованные кнопки "свернуть" и "закрыть". Чтобы включить их в отображаемую область нашего окна, нам понадобятся еще две функции: CreateRectRgn и CombineRgn. Функция CreateRectRgn работает аналогично функции CreateEllipticRgn и имеет такие же параметры, но создает регион прямоугольной формы. Для простоты и наглядности примера создадим один прямоугольный регион под обе наши кнопки. Его координаты будут: 219,0,320,44. Теперь надо объединить прямоугольный регион с овальным в один. Функция CombineRgn объединяет два региона в один и сохраняет результат в отдельный регион или в один из исходных.

Параметры этой функции:
1. Дескриптор региона, в котором будет сохранен результат.
2. Дескриптор первого исходного региона.
3. Дескриптор второго исходного региона.
4. Способ объединения, который может быть одним из следующих значений:
RGN_AND - Сохранить в результат только пересекающиеся области регионов.
RGN_COPY - Создать копию первого из двух исходных регионов.
RGN_DIFF - Оставить лишь те области первого региона, которые не пересекаются со вторым.
RGN_OR - Полное объединение исходных регионов.
RGN_XOR - Объединение исходных регионов за исключением пересекающихся областей.



Возвращаемые функцией значения определяют тип получившегося региона:
NULLREGION — пустой регион;
SIMPLEREGION — одна прямоугольная область;
COMPLEXREGION — две и более прямоугольных областей;
ERROR — не удалось создать регион.

В нашем случае стоит выбрать способ полного объединения двух исходных регионов, потому что мы хотим добиться отображения и эллипса, и кнопок, и пересекающихся областей. Посмотрим, что нам надо добавить в код для достижения результата:

.if [uMsg]=WM_CREATE
invoke LoadBitmap,[wc.hInstance],37
mov [hBitmap],eax
invoke CreateEllipticRgn,0,0,320,240
mov ebx,eax
invoke CreateRectRgn,219,0,320,44
mov edi,eax
invoke CombineRgn,ebx,ebx,edi,RGN_OR
invoke DeleteObject,edi
invoke SetWindowRgn,[hWnd],ebx,TRUE
.elseif [uMsg]=WM_LBUTTONDOWN

На этапе создания окна сразу после загрузки картинки (а можно и до этого) мы создаем регион в форме эллипса и сохраняем его дескриптор в заведомо неизменяемом функциями регистре EBX, чтобы не создавать лишних переменных. Потом создаем прямоугольный регион под кнопки и задвигаем дескриптор в EDI. Объединяем оба региона в один, сохранив результат в первом из двух созданных регионов. Удаляем прямоугольный регион функцией DeleteObject, так как он нам более не понадобится. Применяем комплексный регион к окну, рекомендуя перерисовать содержимое. Но этот регион мы не удаляем, потому что он будет использоваться системой на всем протяжении жизни окна. Что же, теперь мы уже получили нестандартную форму окна, хотя и остались 4 небольших эллипса на фоне основного эллипса окна, которые я изначально запланировал под дырки в окне. Еще осталась небольшая область между кнопками, которая по замыслу художника (то есть меня =)) тоже не должна отображаться. Но с этим мы разберемся позже. Сейчас главное то, что вы поняли, как работает система регионов применительно к окнам. Теперь давайте займемся нашими кнопками. Не стану повторять общую часть про оконные кнопки — если забыли, то перечитайте ранние части курса. Понятно, что для того, чтобы под нашими изображениями появились кнопки, их следует создать при обработке сообщения WM_CREATE. Вот только при обычном способе создания кнопки перекроют своим цветом наши нарисованные муляжи. Поэтому нам придется немного исхитриться:


_button db 'BUTTON',0

.if [uMsg]=WM_CREATE
invoke CreateWindowEx,0,_button,0,BS_OWNERDRAW+WS_VISIBLE+WS_CHILD, 271,0,49,44,[hWnd],1001,[wc.hInstance],0
invoke CreateWindowEx,0,_button,0,BS_OWNERDRAW+WS_VISIBLE+WS_CHILD, 219,0,49,44,[hWnd],1002,[wc.hInstance],0
invoke LoadBitmap,[wc.hInstance],37

.elseif [uMsg]=WM_CTLCOLORBTN
invoke GetStockObject,NULL_BRUSH
ret
.elseif [uMsg]=WM_COMMAND
.if [wParam]=1001
invoke DestroyWindow,[hWnd]
.elseif [wParam]=1002
invoke ShowWindow,[hWnd],SW_MINIMIZE
.endif
.elseif [uMsg]=WM_LBUTTONDOWN


Свойство BS_OWNERDRAW (его, кстати, нельзя совмещать с другими BS-стилями кнопок) у кнопок необходимо для того, чтобы главное окно получало сообщение WM_DRAWITEM при изменении внешнего вида кнопки при ее нажатии и т.п. Данное сообщение мы сами обрабатывать не будем — оставим это системе. Нас интересует сообщение WM_CTLCOLORBTN, которое тоже приходит лишь если кнопки имеют свойство BS_OWNERDRAW. Такое сообщение приходит главному окну перед отрисовкой его кнопки и содержит дескриптор холста (HDC) кнопки в первом параметре, и дескриптор самой кнопки — во втором. Обработка данного сообщения процедурой окна может изменить цвет текста и цвет фона кнопки. Причем, если мы обрабатываем данное сообщение сами и хотим, чтобы ОС знала об этом, то после обработки сообщения нам обязательно надо вернуть в EAX дескриптор кисти, которую будет использовать система для закраски фона окна. Вот, собственно, и вся хитрость. Функцию GetStockObject используют для получения дескриптора на стандартные перья, кисти, шрифты или палитры, предопределенные системой. Значения HOLLOW_BRUSH или NULL_BRUSH позволяют получить дескриптор на пустую прозрачную кисть. Другие значения этой функции мы рассмотрим при подробном изучении функций рисования, а сейчас не будем переполнять мозг лишней информацией. Лучше заметьте, что сразу же после выполнения функции мы осуществляем возврат из процедуры (ret), чтобы не затереть полученный дескриптор в EAX обычным для других обработчиков нулевым результатом. Дескриптор, возвращенный функцией GetStockObject, мы больше нигде не сохраняем, так как удалять такой объект функцией DeleteObject не обязательно.

Для обработки сообщений о клике на кнопке используем вложенный макрос ".if". При получении сообщения WM_COMMAND старшая 16-битная половина его первого параметра содержит значение события, а младшая половина — идентификатор элемента. Поэтому, учитывая то, что значение события BN_CLICKED (клик по кнопке) равно нулю, мы выполняем обработку, если первый параметр равен идентификатору кнопки. Для событий, отличных от BN_CLICKED, следует использовать сравнение с учетом значения соответствующего события. Если значение равно идентификатору кнопки закрытия окна (1001 в нашем случае), то вызываем DestroyWindow. Если нажата кнопка "свернуть" (1002), то вызываем ShowWindow. Эта функция изменяет отображаемое положение окна. Первый ее параметр — дескриптор окна, второй — значение устанавливаемого состояния. Другие значения отображения окна вы можете найти в файле .. \FASM\INCLUDE\EQUATES \USER32.INC (группа: ShowWindow commands). Когда кнопки "закрыть" и "свернуть" заработали, мы можем отключить закрытие окна по правому клику в его клиентской области (сообщение WM_RBUTTONUP). Хотя можно и оставить эту фишку на всякий случай. Давайте же, наконец, уберем из отображаемого региона те оставшиеся в нем области, которые не должны отображаться! В нашем случае этого можно было бы достаточно легко достичь, создавая овальные и прямоугольные регионы и объединяя их различными в отображаемый регион. Однако при каждом изменении форм и размеров фонового рисунка нам придется заново переписывать всю цепочку команд и вызовов, создающих комплексный регион отображения. Это не есть путь программиста! Попытаемся автоматизировать это дело так, чтобы комплексный регион создавался в отдельной процедуре:


.if [uMsg]=WM_CREATE
invoke CreateWindowEx,0,_button,0,BS_OWNERDRAW+WS_VISIBLE+WS_CHILD,\
271,0,49,44,[hWnd],1001,[wc.hInstance],0
invoke CreateWindowEx,0,_button,0,BS_OWNERDRAW+WS_VISIBLE+WS_CHILD,\
219,0,49,44,[hWnd],1002,[wc.hInstance],0
invoke LoadBitmap,[wc.hInstance],37
mov [hBitmap],eax
invoke GetWindowDC,[hWnd]
mov [hdc],eax
invoke CreateCompatibleDC,[hdc]
mov [hMemDC],eax
invoke SelectObject,[hMemDC],[hBitmap]
; вызов процедуры создания региона
invoke GetClientRect,[hWnd],rect
push [rect.bottom]
push [rect.right]
push [hMemDC]
call MakeRegion
; применить регион к окну
invoke SetWindowRgn,[hWnd],eax,TRUE
invoke ReleaseDC,[hWnd],[hdc]
invoke DeleteDC,[hMemDC]

proc MakeRegion var_hDC,var_PicWidth,var_PicHeight

local var_X:DWORD ; столбец
local var_Y:DWORD ; ряд
local var_StartLineX:DWORD ; левый край текущего региона
local var_FullRegion:DWORD ; комплексный регион
local var_LineRegion:DWORD ; текущий регион
local var_TransparantColor:DWORD ; прозрачный цвет (исключается из региона)
local var_InFirstRegion:DWORD ; первый регион (Да/Нет)
local var_Inline:DWORD ; создается линия региона (Да/Нет)

mov [var_InFirstRegion],TRUE
mov [var_Inline],FALSE
mov [var_X],0
mov [var_Y],0
mov [var_StartLineX],0

invoke GetPixel,[var_hDC],0,0 ; получаем прозрачный цвет из левого верхнего пикселя картинки
mov [var_TransparantColor],eax

mov ebx, [var_PicHeight]
.while [var_Y] < ebx ; повторяем до нижнего ряда
mov ebx,[var_PicWidth]

.while [var_X] <= ebx ; повторяем до правого столбца в каждом ряду

invoke GetPixel,[var_hDC],[var_X],[var_Y] ; получаем текущий пиксель
mov ebx,[var_PicWidth]

; проверяем, является ли пиксель прозрачным или достигнут конец ряда
.if eax=[var_TransparantColor] | [var_X]=ebx

; если это так, то проверим еще, имеется ли у нас линия, которую надо добавить в регион
; или это лишь очередной прозрачный пиксель, и мы должны просто двигаться дальше

.if [var_Inline]=TRUE
mov [var_Inline],FALSE
mov ebx,[var_Y]
inc ebx
invoke CreateRectRgn,[var_StartLineX],[var_Y],[var_X],ebx
mov [var_LineRegion],eax
.if [var_InFirstRegion]=TRUE
; если это первый созданный регион, то мы просто
; сохраним его дескриптор в комплексный регион
push [var_LineRegion]
pop [var_FullRegion]
mov [var_InFirstRegion],FALSE
.else
; иначе — добавим полученный регион к комплексному региону
; а затем удалим текущий для освобождения памяти

invoke CombineRgn,[var_FullRegion],[var_FullRegion],[var_LineRegion],RGN_OR
invoke DeleteObject,[var_LineRegion]
.endif
.endif
.else
.if [var_Inline]=FALSE
; текущий пиксель не прозрачный, это не конец ряда, и мы не на этапе создания линии
; значит, этот пиксель будем считать первым пикселем очередной линии
mov [var_Inline],TRUE
push [var_X]
pop [var_StartLineX]
.endif
.endif
inc [var_X]
mov ebx,[var_PicWidth]
.endw
inc [var_Y]
mov [var_X],0
mov ebx,[var_PicHeight]
.endw

mov eax,[var_FullRegion] ; возвращаем готовый регион в точку вызова процедуры

ret

endp


Теперь перед вызовом процедуры мы помещаем в стек HDC нашего фонового изображения, его ширину и высоту и вызываем процедуру создания региона. По возвращении из процедуры в EAX будет дескриптор созданного региона, в котором будет отображаться все, кроме цвета крайнего левого верхнего пикселя картинки. Нам останется лишь применить регион к окну. Данная процедура работает по нижеописанному принципу. Цвет, условно считаемый прозрачным, берется из пикселя с координатами 0,0. Для этого используется функция GetPixel, которая возвращает цвет заданного пикселя заданного DC в виде значения RGB. Параметры функции интуитивно понятны: HDC, X-координата, Y-координата. Затем осуществляется проход по рядам пикселей от верхнего ряда до нижнего для выявления прозрачных и отображаемых пикселей. При встрече первого отображаемого пикселя его X- координата заносится в переменную [var_StartLineX], хранящую точку начала текущей непрозрачной линии, а также устанавливается в единицу флаг [var_Inline], означая тем самым, что линия начата, но не закончена. Линия может быть завершена в случае встречи прозрачного пикселя или в случае достижения последнего пикселя по оси X. По завершении линии создается прямоугольный регион от переменной текущего начала линии [var_StartLineX] до текущего пикселя [var_X]. Высота региона составляет один пиксель. Такой "линейный" регион объединяется с результирующим регионом [var_FullRegion], если только это не самая первая созданная линия. Дескриптор первого же созданного региона помещается в переменную [var_FullRegion], и далее к нему добавляются следующие отображаемые линии нашего фона. Естественно, при создании каждой новой линии флаг [var_Inline] снимается. Конечно, для простых форм не стоит использовать столь сложную процедуру, но, если вы захотите "вырезать" замысловатый регион, то тут вам без нее не обойтись. Помните, что цвет для прозрачности всегда надо выбирать максимально отличающимся от остальных цветов изображения, иначе вы рискуете не заметить несколько "битых" точек в середине изображения, которые стали прозрачными без вашего желания. Если вдруг вам понадобится сделать левый верхний пиксель видимым, то вы всегда можете выбрать другие координаты для получения прозрачного цвета в переменную процедуры.

На сегодня все. Желаю вам всего самого наилучшего, и до новых встреч!

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

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


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

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