Здравствуйте, уважаемые читатели! Долгое время мы с вами ходили
вокруг да около, но сегодня мы наконец-то слепим полноценное
GUI-приложение!! Но прежде чем начать его лепить, придётся немного
поучить теорию…
Я думаю, читатели много раз слышали термин “интерфейс” (interface, что
дословно переводится как “междумордие”). Его применяют в описании как
hardware, так и software в значении “способ взаимодействия”. С кем/чем
же взаимодействует компьютерная программа?
Во-первых, программа взаимодействует с ОС, под которой она запущена, с
помощью API (Application Programming Interface). Во-вторых, прога
взаимодействует с запустившим её юзером
Пользовательские интерфейсы делят на две большие группы: консольные и
графические. С консольными всё понятно: средствами взаимодействия там
служат параметры командной строки. А в GUI-приложениях эту роль
выполняют элементы управления окна
Посмотрите на типичное окно типичного Windows-приложения:
Все эти edit’ы, button’ы, list’ы, другие многочисленные элементы
управления плюс-минус меню - всё это нужно для создания нормального
удобного интерфейса. И при программировании окон на Ассемблере можно
подойти к этому вопросу с двух сторон
Можно создавать каждую кнопку, каждый edit, каждый static, да и вообще
любую пимпу как отдельное окно - дочернее по отношению к главному окну
приложения. А можно объявить главное окно приложения диалоговым
Само название этого типа окон говорит о том, что они направлены на
взаимодействие с юзером. И такое окно само нарисует на себе все
необходимые элементы управления - от нас только требуется указать,
какие именно и в каком количестве. Такие элементы управления будут
контролами диалогового окна - их описание мы оставим в секции ресурсов.
А элементы управления в заголовке окна (кнопки “Свернуть”,
“Развернуть”, “Закрыть”) создадутся без нашего участия: они
определяются стилем окна
Создаётся диалоговое окно функцией DialogBoxParamA, имеющей следующий
прототип:
int DialogBoxParam(
HINSTANCE hInstance, // хэндл приложения
LPCTSTR lpTemplateName, // идентификатор шаблона
HWND hWndParent, // хэндл окна-владельца
DLGPROC lpDialogFunc, // указатель на оконную процедуру
LPARAM dwInitParam // инициализационная переменная
);
Коротко поясню основные моменты. Идентификатор шаблона - это номер записи в секции ресурсов, характеризующей диалоговое окно. Хэндл окна владельца смело задавайте равным 0 - так как наше приложение не имеет других окон, кроме этого диалога, то владельцем ему будет Рабочий стол Windows (HWND_DESKTOP=0). Последним параметром также передавайте 0, не парьтесь
Эта функция создаёт диалоговое окно согласно информации из секции
ресурсов, запускает оконную процедуру нашего диалога и посылает ему
сообщение WM_INITDIALOG, которое нужно обязательно обработать, вернув в
eax 1. Кроме обработчика WM_INITDIALOG нам понадобится обработчик
WM_CLOSE, иначе окно не сможет закрыться
Контролы диалогового окна имеют привычку посылать сообщение WM_COMMAND
при изменении их состояния, а кнопки - ещё и сообщение BN_CLICKED
(причём хитровывернутым способом - BN_CLICKED через WM_COMMAND). Их
тоже нужно обрабатывать, но при этом ещё и различать, какой же контрол
послал сообщение. Как же это сделать?
Каждый контрол диалогового окна кроме хэндла должен иметь уникальный
ID. Каждое сообщение состоит из двух dword’ов: wParam и lParam.
WM_COMMAND в lParam содержит хэндл контрола, в нижнем слове wParam -
код уведомления, в ВЕРХНЕМ слове wParam - ID контрола. BN_CLICKED
содержит в lParam хэндл кнопки, а её ID - в НИЖНЕМ слове wParam
ID’ы таких часто встречающихся кнопок, как “OK!” (1), “Cancel” (2),
“Abort” (3), “Retry” (4), “Ignore” (5), “Yes” (6), “No” (7), заранее
определены в инклудах и могут юзаться в программе как константы
Чтобы понять, какая кнопка была нажата, делают такой финт ушами:
берут сообщение BN_CLICKED, переносят ID кнопки из нижнего слова wParam
в верхнее, затем выполняют “логическое И” с проверяемым ID. Например,
чтобы проверить, не “OK!” ли нажал юзер, сначала выполняют BN_CLICKED
shl 16 (сдвигают биты в структуре сообщения на 16 позиций влево, т.о.
нижнее слово wParam встанет на место верхнего), затем and IDOK
(если в BN_CLICKED тоже был IDOK, ничего не изменится), а потом
командой cmp сравнивают с содержимым верхнего слова wParam сообщения
WM_COMMAND. Это на словах сложно, а в коде задаётся одной строкой в
обработчике сообщения WM_COMMAND: cmp [wparam],BN_CLICKED
shl 16 + IDOK. Если вы поймёте логику этой команды, то вам станет ясно:
если была нажата кнопка “OK!”, то результатом сравнения станет
равенство. А можете особо и не вникать, просто запомните этот приём -
он вам очень пригодится
Диалоговое окно также, как и обычное, крутит цикл обработки сообщений.
Чтобы прервать его, нужно вызывать функцию EndDialog, передав ей хэндл
диалогового окна и число. Какое число - вы определяете сами, оно будет
в eax после выхода из оконной процедуры
Но, само собой, диалоговое окно должно не просто щеголять контролами, а что-то делать. Это реализуется так: обработчики сообщений вызывают EndDialog, передавая ей различные числовые аргументы. Ваша задача - получить это число из eax, понять, обработчик какого сообщения спровоцировал выход, и выполнить необходимые действия. Непонятно? Не беда: сейчас быстренько набросаем простейшее приложение, и всё сразу станет ясно
Я не буду сильно мудрить: моё диалоговое окно будет содержать два
edit’а и две кнопки. В edit’ы юзер будет вводить всякую отсебятину, и
при нажатии на одну из кнопок выскочит MessageBox с этой отсебятиной в
заголовке и тексте сообщения
Чтобы считать юзерскую отсебятину из edit’ов, воспользуемся функцией GetDlgItemTextA:
UINT GetDlgItemText(
HWND hDlg, // хэндл диалогового окна
int nIDDlgItem, // ID контрола
LPTSTR lpString, // указатель на буфер
int nMaxCount // количество считываемых символов
);
Let’s code!!
include 'win32axp.inc'
include 'encoding\WIN1251.INC'
;подключаем инклуду с кириллической кодировкой,
;чтобы использовать в приложении русские буквы
ID_ZAGOL = 101
ID_SLOVO = 102
;определяем ID'ы edit'ов
;кнопки я обзову "Жми!!" и "Отмена",
;они обойдутся стандартными ID'ами:
;IDOK и IDCANCEL
.data
slovo db 41 dup 0
zagolovok db 41 dup 0
;буферы по 41 байт для сообщения и заголовка
;заранее забьём нулями
.code
fuck:
invoke GetModuleHandle,0
;в eax - хэндл нашего приложения
invoke DialogBoxParam,eax,69,HWND_DESKTOP,ProceduraDialoga,0
;создаём диалоговое окно согласно шаблону №69,
;который мы сами же и создали в секции ресурсов.
;ответственной за диалог назначаем процедуру
;ProceduraDialoga, которую сами и напишем
inc eax
je exit
;вышеописанные две инструкции проверяют,
;не равен ли eax -1
;если равен - либо провалилось выполнение
;DialogBoxParamA, либо была вызвана
;EndDialog из обработчика сообщения
;WM_CLOSE: в обоих случаях пора на выход.
;если не -1, то EndDialog вызвана из
;обработчика сообщения WM_COMMAND,
;а значит, выполняется нижележащий код.
;Число -1 взято мной от балды (хотя очень удобно),
;вы вправе использовать другие аргументы для
;вызова EndDialog
invoke MessageBox,0,slovo,zagolovok,MB_OK
exit:
invoke ExitProcess,0
;думаю, в вызове MessageBox и ExitProcess
;вы и сами разберётесь
proc ProceduraDialoga,hwnddlg,msg,wparam,lparam
push ebx
push esi
push edi
;начало процедуры ProceduraDialoga.
;объявили необходимые параметры,
;по древней традиции сохранили
;три регистра.
;далее идут jump'ы на обработчики сообщений.
cmp [msg],WM_INITDIALOG
je Initialization
cmp [msg],WM_COMMAND
je Command
cmp [msg],WM_CLOSE
je Close
xor eax,eax
jmp Finish
Command:
cmp [wparam],BN_CLICKED shl 16 + IDCANCEL
;про этот финт я вам уже говорил:
;если нажата кнопка "Отмена",
;то пора на выход
je Close
cmp [wparam],BN_CLICKED shl 16 + IDOK
;если что-то нажато, но не кнопка "Жми!!"
;(мало ли что может глюкануть:) ),
;то обработаем это как сообщение
;WM_INITDIALOG
jne Initialization
;если нажата "Жми!!",
;читаем текст из edit'ов
invoke GetDlgItemText,[hwnddlg],ID_ZAGOL,zagolovok,40
invoke GetDlgItemText,[hwnddlg],ID_SLOVO,slovo,40
;копировать будем по 40 символов,
;более длинные строки будут обрезаны.
;в буферах останется по одному null-байту,
;который в случае длинных строк
;сыграет роль завершающего нуля.
;вызываем EndDialog, передав ей число 1
invoke EndDialog,[hwnddlg],1
jmp Finish
Initialization:
xor eax,eax
inc eax
;обработать WM_INITDIALOG просто:
;нужно всего лишь обеспечить
;в eax 1 на выходе из
;оконной процедуры
Finish:
pop edi
pop esi
pop ebx
ret
Close:
invoke EndDialog,[hwnddlg],-1
;если нас закрывают или жмут "Отмена",
;вызываем EndDialog, передав ей -1
jmp Finish
endp
;конец оконной процедуры
section '.rsrc' resource data readable
;секция ресурсов
directory RT_DIALOG,dialogs
;директория с диалогами.
;у нас диалог всего один. Описываем его:
;присваиваем номер 69 (чисто от балды),
;делаем его "двуязычным" (English и
;язык в системе по умолчанию),
;даём имя rsrc_dialog (от балды)
resource dialogs,69,LANG_ENGLISH+SUBLANG_DEFAULT,rsrc_dialog
;описываем его подробнее: указываем
;заголовок окна, размеры и стили.
;цифры - это координаты верхнего левого угла,
;ширина и высота. Не забывайте, что за 0;0
;принимается верхний левый угол экрана
dialog rsrc_dialog,'Диалог',70,70,190,90,WS_CAPTION+WS_POPUP+WS_SYSMENU+DS_MODALFRAME
;теперь описываем отдельные контролы.
;Для static сперва указывается отображаемый текст, затем
;цифры: первая у всех static'ов, что я видел
;равна -1, далее идут координаты и размеры
dialogitem 'STATIC','Введи слово/фразу для заголовка:',-1,10,10,190,8,WS_VISIBLE
dialogitem 'EDIT','',ID_ZAGOL,10,20,170,13,WS_VISIBLE+WS_BORDER+WS_TABSTOP
;edit'у обязательно назначаем ID
dialogitem 'STATIC','Введи слово/фразу для сообщения:',-1,10,40,190,8,WS_VISIBLE
dialogitem 'EDIT','',ID_SLOVO,10,50,170,13,WS_VISIBLE+WS_BORDER+WS_TABSTOP
;кнопки лично я обозвал "Жми!!" и "Отмена",
;мог бы и ID'ы им персональные назначить.
;вот только лень было это делать:)
;потому обошёлся стандартными IDOK и IDCANCEL
dialogitem 'BUTTON','Жми!!',IDOK,85,70,45,15,WS_VISIBLE+WS_TABSTOP+BS_DEFPUSHBUTTON
dialogitem 'BUTTON','Отмена',IDCANCEL,135,70,45,15,WS_VISIBLE+WS_TABSTOP+BS_PUSHBUTTON
enddialog
;закончили секцию ресурсов,
;заканчиваем программу
.end fuck
Компилируем, запускаем… Работает! И русские буквы везде отображает правильно! Не правда ли, Ассемблер теперь стал как-то ближе?
В последующих статьях мы продолжим писать GUI-приложения, изучать элементы управления окон, оснащать .exe-шники разнообразными ресурсами, научимся создавать меню… Будет интересно, оставайтесь с нами!!!