Assembler для Windows

             

Глава 4. Примеры программ использующих ресурсы

Вопрос использования ресурсов при программировании в Windows весьма важен, поэтому я посвящаю ему еще одну главу. Здесь будет приведено три более сложных примера на использование ресурсов и подробное их разъяснение.

I

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

Программа открывает окно с кнопкой и меню. При нажатии кнопки текущее меню заменяется другим. Если нажать еще раз, то меню исчезает. Следующее нажатие приводит к появлению


первого меню и так далее, по кругу. Кроме того, в первом меню имеется пункт, который приводит к такому же результату, что и нажатие кнопки. Наконец, для этого пункта установлена акселераторная клавиша - F5. При передвижении по меню название пунктов меню и заголовков выпадающих (POPUP) подменю отображается в заголовке окна. Вот, вкратце, как работает программа. Механизмы работы программы будут подробно разобраны ниже.

// файл menu2.rc
// виртуальная клавиша F5
#define VK_F5 0х74
// ************** MENUP **************
MENUP MENU
{
POPUP "&Первый пункт"
{
MENUITEM "&Первый",1
MENUITEM "В&торой",2
}
POPUP "&Второй пункт"
{
MENUITEM "Трети&й",3
MENUITEM "Четверт&ый\tF5",4
MENUITEM SEPARATOR
POPUP "Еще подмен&ю"
{
MENUITEM "Дополнительный пу&нкт",6
}
}
MENUITEM "Вы&ход",5
}
//**************** MENUC ********************
MENUC MENU
{
POPUP "Набор первый"
{
MENUITEM "Белый",101
MENUITEM "Серый",102
MENUITEM "Черный",103
}
POPUP "Набор второй"
{
MENUITEM "Красный",104
MENUITEM "Синий",105
MENUITEM "Зеленый",106
}
}
// таблица акселераторов
// определен один акселератор для вызова
// пункта из меню MENUP
MENUP ACCELERATORS
{
VK_F5, 4, VIRTKEY, NOINVERT
}
; файл menu2.inc
; константы
; сообщение приходит при закрытии окна
WM_DESTROY equ 2
; сообщение приходит при создании окна
WM_CREATE equ 1
; сообщение при щелчке левой кнопкой мыши в области окна
WM_COMMAND equ 111h
WM_MENUSELECT equ 11Fh
WM_SETTEXT equ 0Ch
MIIM_TYPE equ 10h
MF_STRING equ 0h
MF_POPUP equ 10h
; свойства окна
CS_VREDRAW equ 1h
CS_HREDRAW equ 2h
CS_GLOBALCLASS equ 4000h
WS_OVERLAPPEDWINDOW equ 000CF0000H
STYLE equ CS_HREDRAW+CS_VREDRAW+CS_GLOBALCLASS
BS_DEFPUSHBUTTON equ 1h
WS_VISIBLE equ 10000000h
WS_CHILD equ 40000000h
STYLBTN equ WS_CHILD+BS_DEFPUSHBUTTON+WS_VISIBLE
; идентификатор стандартной иконки
IDI_APPLICATION equ 32512
; идентификатор курсора
IDC_ARROW equ 32512
; режим показа окна - нормальный
SW_SHOWNORMAL equ 1
SW_HIDE equ 0
SW_SHOWMINIMIZED equ 2
; прототипы внешних процедур
EXTERN wsprintfA:NEAR
EXTERN GetMenuItemInfoA@16:NEAR
EXTERN LoadMenuA@8:NEAR
EXTERN SendMessageA@16:NEAR
EXTERN MessageBoxA@16:NEAR
EXTERN CreateWindowExA@48:NEAR
EXTERN DefWindowProcA@16:NEAR
EXTERN DispatchMessageA@4:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetMessageA@16:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN LoadCursorA@8:NEAR
EXTERN LoadIconA@8:NEAR
EXTERN PostQuitMessage@4:NEAR
EXTERN RegisterClassA@4:NEAR
EXTERN ShowWindow@8:NEAR
EXTERN TranslateMessage@4:NEAR
EXTERN UpdateWindow@4:NEAR
EXTERN TranslateAcceleratorA@12:NEAR
EXTERN LoadAcceleratorsA@8:NEAR
EXTERN GetMenu@4:NEAR
EXTERN DestroyMenu@4:NEAR
EXTERN SetMenu@8:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ?
MSMESSAGE DD ?
MSWPARAM DD ?
MSLPARAM DD ?
MSTIME DD ?
MSPT DD ?
MSGSTRUCT ENDS
;----структура класса окон
WNDCLASS STRUC
CLSSTYLE DD ?
CLWNDPROC DD ?
CLSCBCLSEX DD ?
CLSCBWNDEX DD ?
CLSHINST DD ?
CLSHICON DD ?
CLSHCURSOR DD ?
CLBKGROUND DD ?
CLMENNAME DD ?
CLNAME DD ?
WNDCLASS ENDS
MENINFO STRUCT
cbSize DD ?
fMask DD ?
fType DD ?
fState DD ?
wID DD ?
hSubMenu DD ?
hbmpChecked DD ?
hbmpUnchecked DD ?
dwItemData DD ?
dwTypeData DD ?
cch DD ?
MENINFO ENDS
; файл menu2.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include menu2.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
; ------------------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
SPACE DB 30 dup(32),0
MENI MENINFO <0>
NEWHWND DD 0
MSG MSGSTRUCT <?>
WC WNDCLASS <?>
HINST DD 0 ; дескриптор приложения
CLASSNAME DB 'CLASS32',0
CPBUT DB 'Кнопка',0 ; выход
CLSBUTN DB 'BUTTON',0
HWNDBTN DD 0
CAP DB 'Сообщение',0
MES DB 'Конец работы программы',0
MEN DB 'MENUP',0
MENC DB 'MENUC',0
ACC DD ?
HMENU DD ?
PRIZN DD ?
BUFER DB 100 DUP(0)
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; инициализировать счетчик
MOV PRIZN, 2
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
REG_CLASS:
; заполнить структуру окна
; стиль
MOV [WC.CLSSTYLE], STYLE
; процедура обработки сообщений
MOV [WC.CLWNDPROC], OFFSET WNDPROC
MOV [WC.CLSCBCLSEX], 0
MOV [WC.CLSCBWNDEX], 0
MOV EAX, [HINST]
MOV [WC.CLSHINST], EAX
; ----------иконка окна
PUSH IDI_APPLICATION
PUSH 0
CALL LoadIconA@8
MOV [WC.CLSHICON], EAX
; ----------курсор окна
PUSH IDC_ARROW
PUSH 0
CALL LoadCursorA@8
MOV [WC.CLSHCURSOR], EAX
; ——————----
MOV [WC.CLBKGROUND], 17 ; цвет окна
MOV DWORD PTR [WC.CLMENNAME], OFFSET MEN
MOV DWORD PTR [WC.CLNAME], OFFSET CLASSNAME
PUSH OFFSET WC
CALL RegisterClassA@4
; создать окно зарегистрированного класса
PUSH 0
PUSH [HINST]
PUSH 0
PUSH 0
PUSH 400 ; DY - высота окна
PUSH 400 ; DX - ширина окна
PUSH 100 ; Y
PUSH 100 ; X
PUSH WS_OVERLAPPEDWINDOW
PUSH OFFSET SPACE ; имя окна
PUSH OFFSET CLASSNAME ; имя класса
PUSH 0
CALL CreateWindowExA@48
; проверка на ошибку
CMP EAX, 0
JZ _ERR
MOV [NEWHWND], EAX ; дескриптор окна
; определить идентификатор меню
PUSH EAX
CALL GetMenu@4
MOV HMENU, EAX
; загрузить акселераторы
PUSH OFFSET MEN
PUSH [HINST]
CALL LoadAcceleratorsA@8
MOV ACC, EAX
; --———————————————————-
PUSH SW_SHOWNORMAL
PUSH [NEWHWND]
CALL ShowWindow@8 ; показать созданное окно
; -------------------------
PUSH [NEWHWND]
CALL UpdateWindow@4 ; команда перерисовать видимую
; часть окна, сообщение WM_PAINT
; петля обработки сообщений
MSG_LOOP:
PUSH 0
PUSH 0
PUSH 0
PUSH OFFSET MSG
CALL GetMessageA@16
CMP EAX, 0
JE END_LOOP
PUSH OFFSET MSG
PUSH [ACC]
PUSH [NEWHWND]
CALL TranslateAcceleratorA@12
CMP EAX, 0
JNE MSG_LOOP
PUSH OFFSET MSG
CALL TranslateMessage@4
PUSH OFFSET MSG
CALL DispatchMessageA@4
JMP MSG_LOOP
END_LOOP:
; выход из программы (закрыть процесс)
PUSH [MSG.MSWPARAM]
CALL ExitProcess@4
_ERR:
JMP END_LOOP
;----------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н] ; LPARAM
; [EBP+10H] ; WAPARAM
; [EBP+0CH] ; MES
; [EBP+8] ; HWND
WNDPROC PROC
PUSH EBP
MOV EBP,ESP
PUSH EBX
PUSH ESI
PUSH EDI
; сообщение WM_DESTROY - при закрытии окна
CMP DWORD PTR [EBP+0CH], WM__DESTROY
JE WMDESTROY
; сообщение WM CREATE - при создании окна
CMP DWORD PTR [EBP+0CH], WM_CREATE
JE WMCREATE
; сообщение WM COMMAND - при событиях с элементами на окне
CMP DWORD PTR [EBP+0CH], WM_COMMAND
JE WMCOMMND
; сообщение WM_MENUSELECT - события, связанные с меню
CMP DWORD PTR [EBP+0CH], WM_MENUSELECT
JE WMMENUSELECT
; остальные события возвращаем обратно
JMP DEFWNDPROC
WMMENUSELECT:
; пропускаем первое сообщение при обращении к меню
CMP WORD PTR [EBP+14Н],0
JE FINISH
; проверяем, что активизировано - пункт меню
;или заголовок выпадающего меню
MOV EDX, 0
TEST WORD PTR [EBP+12H],MF_POPUP
SETNE DL
; заполнение структуры для вызова функции
; GetMenuItemInfo
MOVZX EAX,WORD PTR [EBP+10H] ; идентификатор
MOV MENI.cbSize,48
MOV MENI.fMask, MIIM_TYPE
MOV MENI.fType, MF_STRING
MOV EBX, DWORD PTR [EBP+14H]
MOV MENI.hSubMenu, EBX
MOV MENI.dwTypeData, OFFSET BUFER
MOV MENI.cch, 100
; получить информацию о выбранном пункте меню
PUSH OFFSET MENI
PUSH EDX
PUSH EAX
PUSH DWORD PTR [EBP+14H]
CALL GetMenuItemInfoA@16
; проверить результат выполнения функции
CMP EAX, 0
JE FINISH
; вывести название пункта меню
PUSH MENI.dwTypeData
PUSH 0
PUSH WM_SETTEXT
PUSH DWORD PTR [EBP+0BH]
CALL SendMessageA@16
MOV EAX, 0
JMP FINISH
WMCOMMND:
MOV EAX, HWNDBTN
; проверить, не нажата ли кнопка
CMP DWORD PTR [EBP+14Н], EAX
JE YES_BUT
; проверить, не выбран ли пункт меню MENUC - Выход
CMP WORD PTR [EBP+10Н],5
JE WMDESTROY
; проверить, не выбран ли пункт меню с идентификатором 5
CMP WORD PTR [EBP+10Н], 4
JNE L00
JMP YES_BUT
L00:
MOV EAX, 0
JMP FINISH
YES_BUT:
; здесь обработка нажатия кнопки
; вначале стереть надпись в заголовке
PUSH OFFSET SPACE
PUSH 0
PUSH WM_SETTEXT
PUSH DWORD PTR [EBP+08H]
CALL SendMessageA@16
; проверить загружено или нет меню
CMP PRIZN, 0
JE L1
CMP PRIZN, 1
JE L2
; загрузить меню MENC
PUSH OFFSET MENC
PUSH [HINST]
CALL LoadMenuA@8
; установить меню
MOV HMENU, EAX
PUSH EAX
PUSH DWORD PTR [EBP+08H]
CALL SetMenu@8
; установить признак
MOV PRIZN,0
MOV EAX,0
JMP FINISH
L2:
; загрузить меню MENUP
PUSH OFFSET MEN
PUSH [HINST]
CALL LoadMenuA@8
; установить меню
MOV HMENU, EAX
PUSH EAX
PUSH DWORD PTR [EBP+08H]
CALL SetMenu@8
; установить признак
MOV PRIZN, 2
MOV EAX,0
JMP FINISH
L1:
; удалить меню
PUSH HMENU
CALL DestroyMenu@4
; обновить содержимое окна
PUSH SW_SHOWMINIMIZED
PUSH DWORD PTR [EBP+08H]
CALL ShowWindow@8
PUSH SW_SHOWNORMAL
PUSH DWORD PTR [EBP+08H]
CALL ShowWindow@8
MOV PRIZN,1
MOV EAX, 0
JMP FINISH
WMCREATE:
; создать окно-кнопку
PUSH 0
PUSH [HINST]
PUSH 0
PUSH DWORD PTR [EBP+08H]
PUSH 20 ; DY
PUSH 60 ; DX
PUSH 10 ; Y
PUSH 10 ; X
PUSH STYLBTN
; имя окна (надпись на кнопке)
PUSH OFFSET CPBUT
PUSH OFFSET CLSBUTN ; имя класса
PUSH 0
CALL CreateWindowExA@48
MOV HWNDBTN, EAX ; запомнить дескриптор кнопки
MOV EAX, 0
JMP FINISH
DEFWNDPROC:
PUSH DWORD PTR [EBP+14H]
PUSH DWORD PTR [EBP+10H]
PUSH DWORD PTR [EBP+0CH]
PUSH DWORD PTR [EBP+08H]
CALL DefWindowProcA@16
JMP FINISH
WMDESTROY:
PUSH 0 ; MB_OK
PUSH OFFSET CAP
PUSH OFFSET MES
PUSH DWORD PTR [EBP+08H] ; дескриптор окна
CALL MessageBoxA@16
PUSH 0
CALL PostQuitMessage@4 ; сообщение WM_QUIT
MOV EAX, 0
FINISH:
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Puc. 2.4.1. Пример манипуляции с меню.

Программа на Рис. 2.4.1 имеет ряд механизмов, к обсуждению которых я намерен сейчас приступить. Для начала замечу, что в программе используются три ресурса: два меню и таблица акселераторов (см. файл ресурсов).

1. Первое, на что хочу обратить Ваше внимание, - это переменная PRIZN. В ней хранится состояние меню: 2 - загружено меню MENUP, 1 - меню отсутствует, 0 - загружено меню MENUC. Начальное состояние обеспечивается заданием меню при регистрации класса окна:

MOV DWORD PTR [WC.CLMENNAME], OFFSET MEN

2. Второе - это кнопка. Механизм распознавания нажатия кнопки мы уже разбирали, так что больше на этом останавливаться не будем. Одно из событий, которое может произойти при нажатии кнопки - это удаление меню. Удаляется меню при помощи функции DestroyMenu. После удаления необходимо обновить содержимое окна. Это достигается последовательностью двух команд ShowWindow.

3. Еще одно событие, которое происходит при нажатии кнопки, это смена меню. Интересно, что смена меню происходит автоматически, если мы загрузим и установим новое меню.

4. Выбор одного из пунктов меню MENUP также приводит к смене меню. Здесь должно быть все понятно, поскольку обращение идет к тому же участку программы, что и в случае нажатия кнопки.

5. Интересная ситуация возникает с акселератором. Акселераторная клавиша у нас F5. При ее нажатии генерируется такое же сообщение, как при выборе пункта "Четвертый" меню MENUP. Важно то, что такое же сообщение будет генерироваться и тогда, когда загружается меню MENUC и когда меню не будет. А поскольку наша процедура обрабатывает сообщение в любом случае, клавиша F5 будет срабатывать всегда.

6. Рассмотрим теперь то, как производится определение названия выбранного пункта меню. Центральную роль в этом механизме играет сообщение WM_MENUSELECT. Это сообщение приходит всегда, когда выбирается пункт меню. Тут важно отметить, что когда мы активизируем меню, то в начале приходит сообщение WM_MENUSELECT со значением LPARAM, которое определяет идентификатор меню равным нулю. Этим целям служат строки:

CMP WORD PTR [EBP+14H], 0
JE FINISH

7. По получении сообщения WM_MENUSELECT в младшем слове параметра WPARAM может содержаться либо идентификатор пункта меню, либо номер заголовка выпадающего меню. Это ключевой момент. Нам важно это знать, так как строка заголовка выпадающего меню и строка пункта меню получаются по-разному. Определить, что выбрано, можно по старшему слову WPARAM. Мы используем для этого константу MF_POPUP: TEST WORD PTR [EBP+12H], MF_POPUP. Обратите внимание, как удобна и как кстати здесь команда SETNE.

8. Далее, для получения строки-названия используется функция GetMenuItemInfo. Третьим параметром этой функции как раз и может быть либо ноль, либо единица. Если ноль, то второй параметр - это идентификатор пункта меню, если единица, то второй параметр - номер заголовка выпадающего меню. Четвертым параметром является указатель на структуру, которая и будет заполняться в результате выполнения функции. Некоторые поля этой структуры должны быть, однако, заполнены заранее. Обращаю внимание на поле dwTypeData, которое должно содержать указатель на буфер, получающий необходимую нам строку. При этом поле cch должно содержать длину этого буфера. Но для того чтобы поля dwTypeData и cch трактовались функцией именно как указатель на буфер и его длину, поля fMask и fType должны быть правильно заполнены (см. программу). Наконец, поле cbSize должно содержать длину всей структуры.

9. После получения нужной информации, т.е. строки-названия пункта меню, при помощи функции SendMessage мы посылаем сообщение WM_SETTEXT, которое дает команду установить заголовок окна.

II

Итак, продолжим рассматривать ресурсы. Хочется рассказать о весьма интересном приеме, который можно использовать при работе с окнами редактирования. Наверное, Вы работали с визуальными языками типа Visual Basic, Delphi и пр. и обратили внимание, что окна редактирования можно так запрограммировать, а точнее, задать их свойства, что они позволят вводить только вполне определенные символы. В Delphi это свойство называется EditMask. Я думаю. Вам хотелось бы понять, как подобное реализовать только API-средствами. Но обо всем по порядку.

Обычное окно при нажатии клавиши (если в нем находится фокус) получает сообщения WM_KEYDOWN, WM_KEYUP и их квинтэссенцию WM_CHAR. Но в данном случае мы имеем дело не с обычным окном, а с диалоговым. Диалоговое окно таких сообщений не получает. Остается надеяться на сообщения, посылаемые на события, происходящие с самим элементом "окном редактирования". Но, увы, и здесь нас ждут разочарования. Данный элемент получает лишь два сообщения из тех, которые нас хоть как-то могут заинтересовать. Это сообщение EN_UPDATE и сообщение EN_CHANGE. Оба сообщения приходят, когда уже произведено изменение в окне редактирования. Но сообщение EN_UPDATE приходит, когда изменения на экране еще не произведены, а EN_CHANGE - после таких изменений. Нам придется сначала получить содержимое окна редактирования, определить, какой символ туда поступил последним, и если он недопустим, удалить его из строки и послать строку в окно снова. Добавьте сюда еще проблему, связанную с положением курсора и вторичным приходом сообщения EN_UPDATE. Лично я по такому пути бы не пошел.

Есть другой более изящный и короткий путь: использовать понятие горячей клавиши (HOTKEY). Мы ограничимся лишь программными свойствами горячих клавиш, то есть свойствами, которые необходимо знать программисту, чтобы использовать горячие клавиши в своих программах.

Горячая клавиша может быть определена для любой виртуальной клавиши, клавиши, определяемой через макроконстанты с префиксом VK. Для обычных алфавитно-цифровых клавиш значение этих констант просто совпадает с кодами ASCII. Возможны также сочетания с управляющими клавишами Alt, Control, Shift. После того как для данного окна определена горячая клавиша, при ее нажатии на функцию окна приходит сообщение WM_HOTKEY. По параметрам можно определить, какая именно горячая клавиша была нажата. Существенно, что понятие горячей клавиши глобально, т.е. она будет срабатывать, если будут активны другие окна и даже окна других приложений. Это требует от программиста весьма аккуратной работы, так как вы можете заблокировать нормальную работу других приложений. Т.е. необходимо отслеживать, когда данное окно активно, а когда нет. Этому весьма могут помочь сообщения WM_ACTIVATE и WM_ACTIVATEAPP. Первое сообщение всегда приходит на функцию окна тогда, когда окно активизируется или деактивизируется. Первый раз сообщение приходит при создании окна. Вот при получении этого сообщения и есть резон зарегистрировать горячие клавиши. Второе сообщение всегда приходит на функцию окна, когда окно теряет "фокус" - активизируется другое окно. Соответственно, при получении этого сообщения и следует отменить регистрацию этих клавиш.

Для работы с горячими клавишами используют в основном две функции: RegisterHotKey и UnregisterHotKey. Функция RegisterHotKey имеет следующие параметры:

  • первый - дескриптор окна;
  • второй - идентификатор горячей клавиши;
  • третий - модификатор, определяющий, не нажата ли управляющая клавиша;
  • четвертый - виртуальный код клавиши.

Функция UnregisterHotKey имеет всего два параметра:

  • первый - дескриптор окна;
  • второй - идентификатор.

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

Рассмотрим простой пример диалогового окна, на котором расположены два окна редактирования и кнопка выхода. Поставим перед собой такую цель. Первое окно редактирования должно пропускать только цифры от 0 до 9. Во второе окно можно вводить все символы. Выше рассматривался возможный механизм использования горячих клавиш с сообщениями WM_ACTIVATE и WM_ ACTIVATEAPP. Ясно, что эти события в данном случае нам ничем не помогут. Здесь дело тоньше, надо использовать сообщения, относящиеся к одному окну редактирования. Это сообщения EN_SETFOCUS и EN_KILLFOCUS, передаваемые, естественно, через сообщение WM_COMMAND. Ниже представлена программа, демонстрирующая этот механизм, и комментарий к ней. Сообщение EN_SETFOCUS говорит о том, что окно редактирования приобрело фокус (стало активным), а сообщение EN_KILLFOCUS - что окно редактирования потеряло фокус.

// файл dial1.rc
// определение констант
// стили окна
#define WS_SYSMENU 0x00080000L
#define WS_MINIMIZEBOX 0x00020000L
#define WS_MAXIMIZEBOX 0x00010000L
// текст в окне редактирования прижат к левому краю
#define ES_LEFT 0x0000L
// стиль всех элементов на окне
#define WS_CHILD 0x40000000L
// элементы на окне должны быть изначально видимы
#define WS_VISIBLE 0x10000000L
// бордюр вокруг элемента
#define WS_BORDER 0x00800000L
// при помощи TAB можно по очереди активизировать элементы
#define WS_TABSTOP 0x00010000L
// прижать строку к левому краю отведенного поля
#define SS_LEFT 0x00000000L
// стиль кнопка
#define BS_PUSHBUTTON 0x00000000L
// центрировать текст на кнопке
#define BS_CENTER 0x00000300L
#define DS_LOCALEDIT 0x20L
// определение диалогового окна
DIAL1 DIALOG 0, 0, 240, 120
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
// поле редактирования, идентификатор 1
CONTROL "", 1, "edit", ES_LEFT | WS_CHILD
| WS_VISIBLE | WS_BORDER | WS_TABSTOP, 24, 20, 128, 12
// еще одно поле редактирования, идентификатор 2
CONTROL "", 2, "edit", ES_LEFT | WS_CHILD
| WS_VISIBLE | WS_BORDER | WS_TABSTOP, 24, 52, 127, 12
// текст, идентификатор 3
CONTROL "Строка 1", 3, "static", SS_LEFT
| WS_CHILD | WS_VISIBLE, 164, 22, 60, 8
// еще текст, идентификатор 4
CONTROL "Строка 2", 4, "static", SS_LEFT
| WS_CHILD | WS_VISIBLE, 163, 54, 60, 8
// кнопка, идентификатор 5
CONTROL "Выход", 5, "button", BS_PUSHBUTTON
| BS_CENTER | WS_CHILD | WS_VISlBLE | WS_TABSTOP,
180, 76, 50, 14
}
;файл dial1.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE equ 10h
WM_INITDIALOG equ 110h
WM_COMMAND equ 111h
WM_SETTEXT equ 0Ch
WM_HOTKEY equ 312h
EN_SETFOCUS equ 100h
EN_KILLFOCUS equ 200h
; прототипы внешних процедур
EXTERN UnregisterHotKey@8:NEAR
EXTERN RegisterHotKey@16:NEAR
EXTERN MessageBoxA@16:NEAR
EXTERN ExitProcess@4:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN DialogBoxParamA@20:NEAR
EXTERN EndDialog@8:NEAR
EXTERN SendMessageA@16:NEAR
EXTERN GetDlgItem@8:NEAR
EXTERN MessageBoxA@16:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DWORD ?
MSMESSAGE DWORD ?
MSWPARAM DWORD ?
MSLPARAM DWORD ?
MSTIME DWORD ?
MSPT DWORD ?
MSGSTRUCT ENDS
;файл dial.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include dial1.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
; ------------------------------------------------------------
; сегмент данных DATA SEGMENT DWORD PUBLIC USE32 'DATA'
MSG MSGSTRUCT <?>
HINST DD 0 ; дескриптор приложения
PA DB "DIAL1",0
STR1 DB "Неправильный символ !",0
STR2 DB "Ошибка !",0
; таблица для создания горячих клавиш
TAB DB 32,33,34,35,36,37,38,39,40
DB 41,42,43,44,45,46,47,58,59,60
DB 61,62,63,64,65,66,67,68,69,70
DB 71,72,73,74,75,76,77,78,79,80
DB 81,82,83,84,85,86,87,88,89,90
DB 91,92,93,94,95,96,97,98,99,100
DB 101,102,103,104,105,106,107,108,109,110
DB 111,112,113,114,115,116,117,118,119,120
DB 121,122,123,124,125,126,127,128,129,130
DB 131,132,133,134,135,136,137,138,139,140
DB 141,142,143,144,145,146,147,148,149,150
DB 151,152,153,154,155,156,157,158,159,160
DB 161,162,163,164,165,166,167,168,169,170
DB 171,172,173,174,175,176,177,178,179,180
DB 181,182,183,184,185,186,187,188,189,190
DB 191,192,193,194,195,196,197,198,199,200
DB 201,202,203,204,205,206,207,208,209,210
DB 211,212,213,214,215,216,217,218,219,220
DB 221,222,223,224,225,226,227,228,229,230
DB 231,232,233,234,235,236,237,238,239,240
DB 241,242,243,244,245,246,247,248,249,250
DB 251,252,253,254,255
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
;----------------------------------------
PUSH 0
PUSH OFFSET WNDPROC
PUSH 0
PUSH OFFSET PA
PUSH [HINST]
CALL DialogBoxParamA@20
CMP EAX,-1
JNE KOL
KOL:
;----------------------------------------
PUSH 0 CALL
ExitProcess@4
;----------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н] ; LPARAM
; [EBP+10H] ; WAPARAM
; [EBP+0CH] ; MES
; [EBP+8] ; HWND
WNDPROC PROC
PUSH EBP
MOV EBP,ESP
PUSH EBX
PUSH ESI
PUSH EDI
;----------------------------------------
CMP DWORD PTR [EBP+0CH], WM_CLOSE
JNE L1
PUSH 0
PUSH DWORD PTR [EBP+08H]
CALL EndDialog@8
MOV EAX, 1
JMP FIN
L1:
CMP DWORD PTR [EBP+0CH],WM_INITDIALOG
JNE L2
; здесь заполнить окна редактирования, если надо
;
;
MOV EAX, 1
JMP FIN
L2:
CMP DWORD PTR [EBP+0CH],WM_COMMAND
JNE L5
; кнопка выхода ?
CMP WORD PTR [EBP+10H], 5
JNE L3
PUSH 0
PUSH DWORD PTR [EBP+08H]
CALL EndDialog@8
MOV EAX, 1
JMP FIN
L3:
CMP WORD PTR [EBP+10H], 1
JNE FINISH
; блок обработки сообщений первого окна редактирования
CMP WORD PTR [EBP+12H], EN_KILLFOCUS
JNE L4
; окно редактирования с идентификатором 1 теряет фокус
MOV EBX, 0
; снимаем все горячие клавиши
L33:
MOVZX EAX,BYTE PTR [ТАВ+EBX]
PUSH EAX
PUSH DWORD PTR [EBP+08Н]
CALL UnregisterHotKey@8
INC EBX
CMP EBX, 214
JNE L33
MOV EAX, 1
JMP FIN
L4:
CMP WORD PTR [EBP+12H],EN_SETFOCUS
JNE FINISH
; окно редактирования с идентификатором 1 получает фокус
MOV EBX, 0
; устанавливаем горячие клавиши
L44:
MOVZX EAX,BYTE PTR [ТАВ+EBX]
PUSH EAX
PUSH 0
PUSH EAX
PUSH DWORD PTR [EBP+08Н]
CALL RegisterHotKey@16
INC EBX
CMP EBX, 214
JNE L44
MOV EAX, 1
JMP FIN
L5:
CMP DWORD PTR [EBP+0CH],WM_HOTKEY
JNE FINISH
; здесь реакция на неправильно введенный символ
PUSH 0 ; МВ_ОК
PUSH OFFSET STR2
PUSH OFFSET STR1
PUSH DWORD PTR [EBP+08Н] ; дескриптор окна
CALL MessageBoxA@16
FINISH:
MOV EAX, 0
FIN:
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Рис. 2.4.2. Пример использования горячих клавиш с диалоговым окном.

Комментарий к программе на Рис. 2.4.2.

1. Самое главное: разберитесь с тем, как мы определяем, когда первое окно редактирования теряет, когда приобретает фокус. В начале определяется, что сообщение пришло от окна редактирования с идентификатором 1, а затем - какое сообщение пришло: EN_SETFOCUS или EN_KILLFOCUS. В первом случае мы устанавливаем горячие клавиши, а во втором снимаем горячие клавиши.

2. В области данных задаем таблицу горячих клавиш. Функция RegisterHotKey имеет следующие параметры:

  • 1-й параметр - идентификатор окна.
  • 2-й параметр - идентификатор горячей клавиши.
  • 3-й параметр - флаг нажатия управляющих клавиш.
  • 4-й параметр - виртуальный код клавиши.

В нашем случае виртуальный код клавиши и идентификатор горячей клавиши совпадают. Конечно, здесь есть поле для дальнейшего усовершенствования. Скажем, исключить из обработки клавиши управления курсором. Я думаю, читатель справится с этим самостоятельно.

III

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

Средства управления списком можно разделить на сообщения и свойства32. Свойства задаются в файле ресурсов. Например, свойство LBS_SORT приводит к тому, что содержимое списка будет автоматически сортироваться при добавлении туда элемента. Очень важным является свойство LBS_WANTKEYBOARDINPUT. При наличии такого свойства приложение получает сообщение WM_VKEYTOITEM, которое посылается приложению, когда нажимается какая-либо клавиша при наличии фокуса на данном списке. Вы можете выбрать самостоятельную обработку - клавиша PgUp, или оставить стандартную обработку. В том случае, если стандартная обработка не нужна, следует возвратить из функции диалогового окна отрицательное значение.


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


// файл diallst.rc
// определение констант
#define WS_SYSMENU 0x00080000L
#define WS_MINIMIZEBOX 0x00020000L
#define WS_MAXIMIZEBOX 0x00010000L
#define WS_VISIBLE 0x10000000L
#define WS_TABSTOP 0x00010000L
#define WS_VSCROLL 0x00200000L
#define WS_THICKFRAME 0x00040000L
#define LBS_NOTIFY 0x0001L
#define LBS_SORT 0x0002L
#define LBS_WANTKEYBOARDINPUT 0x0400L
// идентификаторы
#define LIST1 101
#define LIST2 102
#define IDI_ICON1 3
// определили иконку
IDI_ICON1 ICON "ico1.ico"
// определение диалогового окна
DIAL1 DIALOG 0, 0, 210, 110
STYLE WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
CAPTION "Пример диалогового окна"
FONT 8, "Arial"
{
CONTROL "ListBox1",LIST1, "listbox", WS_VISIBLE |
WS_TABSTOP | WS_VSCROLL | WS_THICKFRAME | LBS_NOTIFY |
LBS_WANTKEYBOARDINPUT,
16, 16, 70, 75
CONTROL "ListBox2", LIST2, "listbox", WS_VISIBLE |
WS_TABSTOP | WS_VSCROLL | WS_THICKFRAME | LBS_NOTIFY | LBS_SORT,
116, 16, 70, 75
}
; файл diallst.inc
; константы
; сообщение приходит при закрытии окна
WM_CLOSE equ 10h
WM_INITDIALOG equ 110h
WM_SETICON equ 80h
WM_COMMAND equ 111h
WM_VKEYTOITEM equ 2Eh
LB_ADDSTRING equ 180h
LBN_DBLCLK equ 2
LB_GETCURSEL equ 188h
LB_GETTEXT equ 189h
LB_FINDSTRING equ 18Fh
VK_INSERT equ 2Dh
; прототипы внешних процедур
EXTERN ExitProcess@4:NEAR
EXTERN GetModuleHandleA@4:NEAR
EXTERN DialogBoxParamA@20:NEAR
EXTERN EndDialog@8:NEAR
EXTERN LoadIconA@8:NEAR
EXTERN SendMessageA@16:NEAR
EXTERN SendDlgItemMessageA@20:NEAR
EXTERN MessageBoxA@16:NEAR
; структуры
; структура сообщения
MSGSTRUCT STRUC
MSHWND DD ?
MSMESSAGE DD ?
MSWPARAM DD ?
MSLPARAM DD ?
MSTIME DD ?
MSPT DD ?
MSGSTRUCT ENDS
; файл diallst.asm
.386P
; плоская модель
.MODEL FLAT, stdcall
include dial.inc
; директивы компоновщику для подключения библиотек
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib
;-------------------------------------------------
; сегмент данных
_DATA SEGMENT DWORD PUBLIC USE32 'DATA'
MSG MSGSTRUCT <?>
HINST DD 0 ; дескриптор приложения
PA DB "DIAL1",0
BUFER DB 100 DUP (0)
STR1 DB "Первый",0
STR2 DB "Второй",0
STR3 DB "Третий",0
STR4 DB "Четвертый",0
STR5 DB "Пятый ",0
STR6 DB "Шестой",0
STR7 DB "Седьмой",0
STR8 DB "Восьмой",0
STR9 DB "Девятый",0
STR10 DB "Десятый",0
STR11 DB "Одиннадцатый",0
STR12 DB "Двенадцатый",0
STR13 DB "Тринадцатый",0
STR14 DB "Четырнадцатый",0
STR15 DB "Пятнадцатый",0
INDEX DD OFFSET STR1
DD OFFSET STR2
DD OFFSET STR3
DD OFFSET STR4
DD OFFSET STR5
DD OFFSET STR6
DD OFFSET STR7
DD OFFSET STR8
DD OFFSET STR9
DD OFFSET STR10
DD OFFSET STR11
DD OFFSET STR12
DD OFFSET STR13
DD OFFSET STR14
DD OFFSET STR15
_DATA ENDS
; сегмент кода
_TEXT SEGMENT DWORD PUBLIC USE32 'CODE'
START:
; получить дескриптор приложения
PUSH 0
CALL GetModuleHandleA@4
MOV [HINST], EAX
;----------------------------------------
PUSH 0
PUSH OFFSET WNDPROC
PUSH 0
PUSH OFFSET PA
PUSH [HINST]
CALL DialogBoxParamA@20
CMP EAX,-1
JNE KOL
; сообщение об ошибке
KOL:
;----------------------------------------
PUSH 0
CALL ExitProcess@4
;----------------------------------------
; процедура окна
; расположение параметров в стеке
; [EBP+014Н] ; LPARAM
; [EBP+10H] ; WAPARAM
; [EBP+0CH] ; MES
; [EBP+8] ; HWND
WNDPROC PROC
PUSH EBP
MOV EBP, ESP
PUSH EBX
PUSH ESI
PUSH EDI
;----------------------------------------
CMP DWORD PTR [EBP+0CH],WM_CLOSE
JNE L1
PUSH 0
PUSH DWORD PTR [EBP+08H]
CALL EndDialog@8
JMP FINISH
L1:
CMP DWORD PTR [EBP+0CH], WM_INITDIALOG
JNE L2
; загрузить иконку
PUSH 3 ; идентификатор иконки
PUSH [HINST] ; идентификатор процесса
CALL LoadIconA@8
; установить иконку
PUSH EAX
PUSH 0 ; тип иконки (маленькая)
PUSH WM_SETICON
PUSH DWORD PTR [EBP+08H]
CALL SendMessageA@16
; заполнить левый список
MOV ECX, 15
MOV ESI, 0
L01:
PUSH ECX ; сохранить параметр цикла
PUSH INDEX[ESI]
PUSH 0
PUSH LB_ADDSTRING
PUSH 101
PUSH DWORD PTR [EBP+08H]
CALL SendDlgItemMessageA@20
ADD ESI, 4
POP ECX
LOOP L01
JMP FINISH
L2:
CMP DWORD PTR [EBP+0CH],WM_COMMAND
JNE L3
; не сообщение ли от левого списка?
CMP WORD PTR [EBP+10Н],101
JNE FINISH
; не было ли двойного щелчка?
CMP WORD PTR [EBP+12H], LBN_DBLCLK
JNE FINISH
; был двойной щелчок, теперь определим элемент
; получить индекс выбранного элемента
L4:
PUSH 0
PUSH 0
PUSH LB_GETCURSEL
PUSH 101
PUSH DWORD PTR [EBP+08H]
CALL SendDlgItemMessageA@20
; скопировать элемент списка в буфер
PUSH OFFSET BUFER
PUSH EAX ; индекс записи
PUSH LB_GETTEXT
PUSH 101
PUSH DWORD PTR [EBP+08H]
CALL SendDlgItemMessageA@20
; определить, нет ли элемента в правом списке
PUSH OFFSET BUFER
PUSH -1 ; искать во всем списке
PUSH LB_FINDSTRING
PUSH 102
PUSH DWORD PTR [EBP+08H]
CALL SendDlgItemMessageA@20
CMP EAX,-1
JNE FINISH ; элемент нашли
; не нашли, можно добавлять
PUSH OFFSET BUFER
PUSH 0
PUSH LB_ADDSTRING
PUSH 102
PUSH DWORD PTR [EBP+08H]
CALL SendDlgItemMessageA@20
MOV EAX,-1
JMP FIN
L3:
; здесь проверка, не нажата ли клавиша
CMP DWORD PTR [EBP+0CH],WM_VKEYTOITEM
JNE FINISH
CMP WORD PTR [EBP+10H],VK_INSERT
JE L4
MOV EAX,-1
JMP FIN
FINISH:
MOV EAX, 0
FIN:
POP EDI
POP ESI
POP EBX
POP EBP
RET 16
WNDPROC ENDP
_TEXT ENDS
END START

Puc. 2.4.3. Пример программы с двумя списками. Перебросить запись из левого списка в правый можно двойным щелчком мыши или клавишей INSERT.

Комментарий к программе на Рис. 2.4.3.

  1. В первую очередь обратите внимание на функцию SendDIgItemMessage. Для посылки сообщения элементам диалогового окна эта функция более удобна, чем SendMessage, так как элемент в ней идентифицируется не дескриптором (который еще надо узнать), а номером, определенным в файле ресурсов.
  2. Взглянув на файл ресурсов. Вы увидите, что второму (правому) списку присвоено свойство LBS_SORT. Если такое свойство присвоено списку, то при добавлении в него элемента (сообщение LB_ADDSTRING) этот элемент помещается в список так, что список остается упорядоченным. Свойство LBS_SORT стоит системе Windows довольно большой работы. Посредством сообщения WM_COMPAREITEM она определяет нужное положение нового элемента в списке, а затем вставляет его при помощи сообщения LB_INSERTSTRING.
  3. Хотелось бы также обратить внимание на цикл заполнения левого списка. Нам приходится хранить регистр ECX в стеке. Вы скажете - дело обыкновенное при организации цикла при помощи команды LOOP. А я Вам скажу, что это совсем не очевидно. К сожалению, в документации по функциям API и сообщениям не указывается, какие регистры микропроцессора сохраняются, а какие нет. Все это придется устанавливать экспериментально. Что и было сделано мною в данном примере.
  4. Сообщение WM_VKEYTOITEM приходит при нажатии какой-либо клавиши, при наличии фокуса на списке. При этом список должен иметь свойство LBS_WANTKEYBOARDINPUT. Именно потому, что данное свойство установлено только у левого списка, у нас нет необходимости проверять, от какого списка пришло сообщение.