Ассемблер под Windows для чайников. Часть 21
Сегодня мы продолжим изучение основных графических функций Windows и их использование в программах, написанных для компилятора FASM. Чаще всего в Windows-приложениях используются стандартные окна. Но иногда программисты, желая выделиться оригинальностью интерфейса программы, прибегают к использованию нестандартных форм окошек. Об этих "наворотах" мы сегодня и поговорим. Сразу скажу, что я не являюсь профессиональным дизайнером, поэтому прошу не судить строго мой скромный рисунок, используемый в качестве фона окна в сегодняшнем примере. Целью данного интерфейса является лишь знакомство с некоторыми способами создания альтернативной формы окна.
Для того, чтобы создать окно нестандартной формы, нам придется использовать функции работы с регионами. Регион — это, говоря простым языком, отображаемая область окна. Регион может быть прямоугольником, многоугольником, эллипсом или комбинацией этих форм. Регионы можно заполнять, закрашивать, можно инвертировать цвета в указанном регионе, а также регион может использоваться для определения места клика мышью. Начнем с того, что создадим простое окно без стандартной шапки, как мы уже делали это ранее с диалоговым окном. Для этого не будем указывать в стиле окна значение WS_CAPTION. Естественно, не стоит указывать стили, включающие в себя WS_CAPTION — такие, как WS_OVERLAPPEDWINDOW или WS_TILEDWINDOW:
format PE GUI 4.0entry start
include 'win32a.inc'include 'macro\if.inc'
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 MSGps PAINTSTRUCTrect RECT
hdc dd ?hMemDC dd ?hBitmap dd ?
section '.code' code readable executable
invoke GetModuleHandle,0mov [wc.hInstance],eaxinvoke LoadIcon,[wc.hInstance],17mov [wc.hIcon],eaxinvoke LoadCursor,[wc.hInstance],27mov [wc.hCursor],eaxinvoke RegisterClass,wc
invoke CreateWindowEx,0,_class,_title,\WS_VISIBLE+WS_POPUP+WS_SYSMENU+WS_MINIMIZEBOX,\320,240,320,240,0,0,[wc.hInstance],0test eax,eaxjz error
msg_loop:invoke GetMessage,msg,NULL,0,0cmp eax,1jb end_loopjne msg_loopinvoke TranslateMessage,msginvoke DispatchMessage,msgjmp msg_loop
proc WindowProc uses ebx esi edi,hWnd,uMsg,wParam,lParam
.if [uMsg]=WM_CREATEinvoke LoadBitmap,[wc.hInstance],37mov [hBitmap],eax.elseif [uMsg]=WM_LBUTTONDOWNinvoke ReleaseCaptureinvoke SendMessage,[hWnd],WM_NCLBUTTONDOWN,HTCAPTION,0.elseif [uMsg]=WM_RBUTTONUPinvoke DestroyWindow,[hWnd].elseif [uMsg]=WM_PAINTinvoke GetClientRect,[hWnd],rectinvoke BeginPaint,[hWnd],psmov [hdc],eaxinvoke CreateCompatibleDC,[hdc]mov [hMemDC],eaxinvoke SelectObject,[hMemDC],[hBitmap]invoke BitBlt,[hdc],0,0,[rect.right],[rect.bottom],[hMemDC],0,0,SRCCOPYinvoke DeleteDC,[hMemDC]invoke EndPaint,[hWnd],ps.elseif [uMsg]=WM_DESTROYinvoke DeleteObject,[hBitmap]invoke PostQuitMessage,0.elseinvoke DefWindowProc,[hWnd],[uMsg],[wParam],[lParam]ret.endifxor eax,eaxretendp
section '.idata' import data readable writeable
include 'api\gdi32.inc'include 'api\kernel32.inc'include 'api\user32.inc'
section '.rsrc' resource data readable
resource group_icons,\17,LANG_NEUTRAL,main_iconresource cursors,\2,LANG_NEUTRAL,cursor_dataresource group_cursors,\27,LANG_NEUTRAL,main_cursorresource 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_CREATEinvoke LoadBitmap,[wc.hInstance],37mov [hBitmap],eaxinvoke CreateEllipticRgn,0,0,320,240mov ebx,eaxinvoke CreateRectRgn,219,0,320,44mov edi,eaxinvoke CombineRgn,ebx,ebx,edi,RGN_ORinvoke DeleteObject,ediinvoke SetWindowRgn,[hWnd],ebx,TRUE.elseif [uMsg]=WM_LBUTTONDOWN…На этапе создания окна сразу после загрузки картинки (а можно и до этого) мы создаем регион в форме эллипса и сохраняем его дескриптор в заведомо неизменяемом функциями регистре EBX, чтобы не создавать лишних переменных. Потом создаем прямоугольный регион под кнопки и задвигаем дескриптор в EDI. Объединяем оба региона в один, сохранив результат в первом из двух созданных регионов. Удаляем прямоугольный регион функцией DeleteObject, так как он нам более не понадобится. Применяем комплексный регион к окну, рекомендуя перерисовать содержимое. Но этот регион мы не удаляем, потому что он будет использоваться системой на всем протяжении жизни окна. Что же, теперь мы уже получили нестандартную форму окна, хотя и остались 4 небольших эллипса на фоне основного эллипса окна, которые я изначально запланировал под дырки в окне. Еще осталась небольшая область между кнопками, которая по замыслу художника (то есть меня =)) тоже не должна отображаться. Но с этим мы разберемся позже. Сейчас главное то, что вы поняли, как работает система регионов применительно к окнам. Теперь давайте займемся нашими кнопками. Не стану повторять общую часть про оконные кнопки — если забыли, то перечитайте ранние части курса. Понятно, что для того, чтобы под нашими изображениями появились кнопки, их следует создать при обработке сообщения WM_CREATE. Вот только при обычном способе создания кнопки перекроют своим цветом наши нарисованные муляжи. Поэтому нам придется немного исхитриться:
…_button db 'BUTTON',0….if [uMsg]=WM_CREATEinvoke CreateWindowEx,0,_button,0,BS_OWNERDRAW+WS_VISIBLE+WS_CHILD, 271,0,49,44,[hWnd],1001,[wc.hInstance],0invoke CreateWindowEx,0,_button,0,BS_OWNERDRAW+WS_VISIBLE+WS_CHILD, 219,0,49,44,[hWnd],1002,[wc.hInstance],0invoke LoadBitmap,[wc.hInstance],37….elseif [uMsg]=WM_CTLCOLORBTNinvoke GetStockObject,NULL_BRUSHret.elseif [uMsg]=WM_COMMAND.if [wParam]=1001invoke DestroyWindow,[hWnd].elseif [wParam]=1002invoke 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_CREATEinvoke CreateWindowEx,0,_button,0,BS_OWNERDRAW+WS_VISIBLE+WS_CHILD,\271,0,49,44,[hWnd],1001,[wc.hInstance],0invoke CreateWindowEx,0,_button,0,BS_OWNERDRAW+WS_VISIBLE+WS_CHILD,\219,0,49,44,[hWnd],1002,[wc.hInstance],0invoke LoadBitmap,[wc.hInstance],37mov [hBitmap],eaxinvoke GetWindowDC,[hWnd]mov [hdc],eaxinvoke CreateCompatibleDC,[hdc]mov [hMemDC],eaxinvoke SelectObject,[hMemDC],[hBitmap]; вызов процедуры создания регионаinvoke GetClientRect,[hWnd],rectpush [rect.bottom]push [rect.right]push [hMemDC]call MakeRegion; применить регион к окнуinvoke SetWindowRgn,[hWnd],eax,TRUEinvoke 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],TRUEmov [var_Inline],FALSEmov [var_X],0mov [var_Y],0mov [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]=TRUEmov [var_Inline],FALSEmov ebx,[var_Y]inc ebxinvoke CreateRectRgn,[var_StartLineX],[var_Y],[var_X],ebxmov [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_ORinvoke DeleteObject,[var_LineRegion].endif.endif.else.if [var_Inline]=FALSE; текущий пиксель не прозрачный, это не конец ряда, и мы не на этапе создания линии; значит, этот пиксель будем считать первым пикселем очередной линииmov [var_Inline],TRUEpush [var_X]pop [var_StartLineX].endif.endifinc [var_X]mov ebx,[var_PicWidth].endwinc [var_Y]mov [var_X],0mov ebx,[var_PicHeight].endw
mov eax,[var_FullRegion] ; возвращаем готовый регион в точку вызова процедуры
Теперь перед вызовом процедуры мы помещаем в стек 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 год в рубрике программирование