На этом шаге мы рассмотрим создание и использование динамического меню.
Вы, наверное, обращали внимание, что во многих программах меню может динамически меняться во время работы: исчезают и добавляются некоторые пункты, одно меню встраивается в другое. Пример простейших манипуляций с меню приведен в расположенной ниже программе.
Программа открывает окно с кнопкой и меню. При нажатии кнопки текущее меню заменяется другим. Если нажать еще раз, то меню исчезает. Следующее нажатие приводит к появлению первого меню и так далее, по кругу. Кроме того, в первом меню имеется пункт, который приводит к такому же результату, что и нажатие кнопки. Наконец, для этого пункта установлена акселераторная клавиша F5. При передвижении по меню название пунктов меню и заголовков выпадающих (POPUP) подменю отображается в заголовке окна. Вот, вкратце, как работает программа. Механизмы работы программы будут подробно разобраны далее.
Рис.1. Внешний вид приложения
//Файл menu2.rc. //Виртуальная клавиша <F5> #define VK_F5 0x74 //******** MENUP *********** MENUP MENU { POPUP "&Первый пункт" { MENUITEM "&Первый", 1 MENUITEM "В&торой", 2 } POPUP "&Второй пункт" { MENUITEM "Трети&й", 3 MENUITEM "Чeтвepт&ый\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 }
;Константы. ;Сообщение приходит при закрытии окна. 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 SW_RESTORE equ 9 ;Прототипы внешних процедур. 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
.386P ;Плоская модель. .MODEL FLAT, STDCALL include pr41_1.asm ;Директивы компоновщику для подключения библиотек. includelib \masm32\lib\user32.lib includelib \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),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 ;----------------------------------------- ;Процедура окна. ;Расположение параметров в стеке: ;[ЕВР+014Н] LPARAM ;[ЕВР+10Н] WAPARAM ;[ЕВР+0СН] MES ;[ЕВР+8] HWND WNDPROC PROC PUSH EBP MOV EBP, ESP PUSH EBX PUSH ESI PUSH EDI ;Cообщение 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+14H],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+08H] CALL SendMessageA@16 MOV EAX,0 JMP FINISH WMCOMMND: MOV EAX,HWNDBTN ;Проверить, не нажата ли кнопка. CMP DWORD PTR [EBP+14H], EAX JE YES_BUT ;Проверить, не выбран ли пункт меню MENUC - выход. CMP WORD PTR [EBP+10H],5 JE WMDESTROY ;Проверить, не выбран ли пункт меню с идентификатором 5. CMP WORD PTR [EBP+10H],4 JNE LOO JMP YES_BUT LOO: 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 0 PUSH [HINST] CALL LoadMenuA@8 ;Установить "пустое" меню. PUSH EAX PUSH DWORD PTR [EBP+08H] CALL SetMenu@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
Приведенная программа имеет ряд механизмов, к обсуждению которых мы сейчас приступим. Для начала отметим, что в программе используются три ресурса: два меню и таблица акселераторов.
Первое, на что хотим обратить ваше внимание, - это переменная PRIZN. В ней хранится состояние меню: 2 - загружено меню MENUP, l - меню отсутствует, 0 - загружено меню MENUC. Начальное состояние обеспечивается заданием меню при регистрации класса окна:
MOV DWORD PTR [WC.CLMENNAME] , OFFSET MEN
Второе - это кнопка. Механизм распознавания нажатия кнопки мы уже разбирали, так что больше на этом останавливаться не будем. Одно из событий, которое может произойти при нажатии кнопки, - это удаление меню. Удаляется меню при помощи функции DestroyMenu. После удаления необходимо обновить содержимое окна, что достигается установкой "пустого" меню.
Еще одно событие, которое происходит при нажатии кнопки, - это смена меню. Интересно, что смена меню происходит автоматически, если мы загрузим и установим новое меню.
Выбор одного из пунктов меню MENUP также приводит к смене меню. Здесь должно быть все понятно, поскольку обращение идет к тому же участку программы, что и при нажатии кнопки.
Интересная ситуация возникает с акселератором. Акселераторная клавиша у нас F5. При ее нажатии генерируется такое же сообщение, как при выборе пункта Четвертый меню MENUP. Важно то, что такое же сообщение будет генерироваться и тогда, когда загружается меню MENUC, и когда меню не будет. А поскольку наша процедура обрабатывает сообщение в любом случае, клавиша F5 будет срабатывать всегда.
Рассмотрим теперь то, как производится определение названия выбранного пункта меню. Центральную роль в этом механизме играет сообщение WM_MENUSELECT. Это сообщение приходит всегда, когда выбирается пункт меню. Тут важно отметить, что когда мы активизируем меню, то в начале приходит сообщение WM_MENUSELECT со значением LPARAM, которое определяет идентификатор меню равным нулю. Этим целям служат строки:
CMP WORD PTR [EBP+14H], 0 JE FINISH
По получении сообщения WM_MENUSELECT в младшем слове параметра WPARAM может содержаться либо идентификатор пункта меню, либо номер заголовrf выпадающего меню. Это ключевой момент. Нам важно это знать, так как строка заголовка выпадающего меню и строка пункта меню получаются по-разному. Определить, что выбрано, можно по старшему слову WPARAM. Мы используем для этого константу MF_POPUP:
TEST WORD PTR [ЕВР+12Н], MF_POPUP.
Обратите внимание, как удобна и как кстати здесь команда SETNE.
Далее, для получения строки-названия используется функция GetMenuItemInfo. Третьим параметром этой функции как раз и может быть либо нуль, либо единица. Если нуль, то второй параметр - это идентификатор пункта меню, если единица, то второй параметр - номер заголовка выпадающего меню. Четвертым параметром является указатель на структуру, которая и будет заполняться в результате выполнения функции. Некоторые поля этой структуры должны быть, однако, заполнены заранее. Обращаем внимание на поле dwTypeData, которое должно содержать указатель на буфер, получающий необходимую нам строку. При этом поле cch должно содержать длину этого буфера. Но для того чтобы поля dwTypeData и cch трактовались функцией именно как указатель на буфер и его длину, поля fMask и fType должны быть правильно заполнены (смотри программу). Наконец, поле cbSize должно содержать длину всей структуры.
После получения нужной информации, т.е. строки-названия пункта меню при помощи функции SendMessage мы посылаем сообщение WM_SETTEXT, которое дает команду установить заголовок окна.
На следующем шаге мы поговорим о горячих клавишах.