Шаг 41.
Примеры программ с ресурсами. Динамическое меню

    На этом шаге мы рассмотрим создание и использование динамического меню.

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

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


Рис.1. Внешний вид приложения


    Ресурсный файл menu2.rc.
//Файл 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
}


    Заголовочный файл для приложения, содержащий определения констант, внешних процедур и структур (его имя pr41_1.asm).
;Константы.
;Сообщение приходит при закрытии окна.
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
Текст этого модуля, а также исходный файл ресурсов можно взять здесь.


    Основной файл приложения, содержит подключение файла pr41_1.asm (его имя pr41_2.asm).
.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, которое дает команду установить заголовок окна.

    На следующем шаге мы поговорим о горячих клавишах.




Предыдущий шаг Содержание Следующий шаг