Меню с иконками на Ассемблере
Сегодня разберем кастомизацию такого элемента интерфейса, как выпадающее меню. Без меню обходится мало какое современное приложение, но вот работа с меню стандартными средствами обычно ограничивается установкой флага чекбокса, затенением неактивных пунктов или отрисовкой субменю. Попытка разнообразить меню, например, своими иконками, приводит к очень печальному результату. Ситуацию особо не спасают ни собственные битмапы, ни подгрузка изображений из списка ImageList. Очень странно, что на протяжении многих лет разработчики Windows так и не сделали инструментов "из коробки", чтобы можно было легко и комфортно работать с менюшками. К счастью, в системе есть "потайной ход", с помощью которого можно кастомизировать меню так, как вам захочется. Для своих проектов я постарался сделать как можно более универсальный инструмент для работы с меню и сейчас я с вами им поделюсь.Начнем создание собственных элементов интерфейса с того, что для каждого настраиваемого меню резервируется структура с данными следующего формата:
Code (Assembler) : Убрать нумерацию
- struct MYMENU
- hMenu dd ?
- itemWidth dd ?
- itemHeight dd ?
- hasIcons db ?
- ends
Code (Assembler) : Убрать нумерацию
- menus dd Menu1
- dd Menu2
- dd ?
- Menu1 MYMENU
- Menu2 MYMENU
Если вы хотите использовать значения размеров меню, соответствующие стандартным, то для этих целей описываются две константы с дефолтными значениями, определенными опытным путем. Еще одна константа используется в качестве идентификатора заголовков. Естественно, вы можете заменить их на ваши значения:
Code (Assembler) : Убрать нумерацию
- DEFAULT_ITEM_WIDTH = 80
- DEFAULT_ITEM_HEIGHT = 18
- DEFAULT_HEADER_ID = 9999
Code (Assembler) : Убрать нумерацию
- ICONS_COUNT = 100
- hIcons rd ICONS_COUNT*2
На этом можно заканчивать с теорией и переходить непосредственно к программированию. Начнем с создания меню. Обычно это делается на этапе инициализации окна. Создадим два меню с разными характеристиками.
Code (Assembler) : Убрать нумерацию
- ; Очистить память под массив иконок
- invoke RtlZeroMemory,hIcons,ICONS_COUNT*2*4
- ; EDI -> указатель на массив иконок
- mov edi,hIcons
- ; Создать первое меню
- invoke CreatePopupMenu
- ; Сохранить хэндл меню в структуру
- mov [Menu1.hMenu],eax
- ; Установить ширину меню 120 пикселов
- mov [Menu1.itemWidth],120
- ; В меню используются иконки
- mov [Menu1.hasIcons],TRUE
- mov ebx,eax
- ; Добавить заголовок меню
- invoke AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW+MF_GRAYED,\
- DEFAULT_HEADER_ID,szStr10
- ; Сохранить ID пункта меню в массив с иконками
- mov eax,IDM_MENU11
- stosd
- ; Загрузить иконку из ресурсов
- invoke GetModuleHandle,0
- invoke LoadIcon,eax,1
- ; Сохранить хэндл иконки в массив
- stosd
- ; Добавить пункт в меню
- invoke AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW+MF_GRAYED,\
- IDM_MENU11,szStr11
- ; Сохранить ID пункта меню в массив с иконками
- mov eax,IDM_MENU12
- stosd
- ; Загрузить иконку из ресурсов
- invoke GetModuleHandle,0
- invoke LoadIcon,eax,2
- stosd
- ; Добавить пункт в меню
- invoke AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW,IDM_MENU12,szStr12
- ...
- ; и так далее
- ...
- ; Создать второе меню
- invoke CreatePopupMenu
- mov [Menu2.hMenu],eax
- ; Ширина меню 60 пикселов, высота дефолтная
- mov [Menu2.itemWidth],60
- mov ebx,eax
- invoke AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW,IDM_MENU21,szStr21
- invoke AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW+MF_CHECKED,\
- IDM_MENU22,szStr22
- invoke AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW,IDM_MENU23,szStr23
- invoke AppendMenu,ebx,MF_SEPARATOR,NULL,NULL
- ; Создать субменю, которое не кастомизируется
- invoke CreatePopupMenu
- mov edi,eax
- invoke AppendMenu, edi,MF_STRING,IDM_MENU21,szStr21
- ; Добавить пункты меню
- invoke AppendMenu,ebx,MFT_OWNERDRAW+MF_POPUP,edi,szStr24
- invoke AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW,IDM_MENU25,szStr25
Переходим к обработчикам сообщений. Начнем с обработчика WM_MEASUREITEM, в котором будем возвращать размеры пункта меню в соответствии со значениями, которые мы задали при инициализации. Здесь нам надо определить, к какому меню принадлежит запрашиваемый пункт. Я так и не нашел способов сделать это одним движением, приходится перебирать все меню из массива по очереди и запрашивать информацию о пункте с нужным индексом. Если ошибки при этом не случилось, значит пункт принадлежит этому меню. Естественно, при такой реализации одинаковых индексов в разных меню быть не должно. Исключения составляют заголовки, они распознаются по индексу, заданному константой DEFAULT_HEADER_ID.
Code (Assembler) : Убрать нумерацию
- cmp [msg],WM_MEASUREITEM
- je .wmmeasure
- ...
- ...
- ...
- .wmmeasure:
- ; Отрисовываем пункт меню?
- mov ebx,[lparam]
- cmp [ebx+MEASUREITEMSTRUCT.CtlType],ODT_MENU
- jne .processed
- ; Найти, какому меню принадлежит пункт
- mov [mii.cbSize],sizeof.MENUITEMINFO
- mov esi,menus
- .loop_measure:
- lodsd
- or eax,eax
- jz .processed
- ; Пункт принадлежит этому меню?
- mov edi,eax
- invoke GetMenuItemInfo,[edi+MYMENU.hMenu],\
- [ebx+MEASUREITEMSTRUCT.itemID],FALSE,mii
- or eax,eax
- ; Ошибка, пункт меню не найден, проверяем следующий
- jz .loop_measure
- ; Ширина пункта меню
- mov eax,[edi+MYMENU.itemWidth]
- or eax,eax
- jnz @f
- mov eax,DEFAULT_ITEM_WIDTH
- @@:
- mov [ebx+MEASUREITEMSTRUCT.itemWidth],eax
- ; Высота пункта меню
- mov eax,[edi+MYMENU.itemHeight]
- or eax,eax
- jnz @f
- mov eax,DEFAULT_ITEM_HEIGHT
- @@:
- ; Для заголовков меню дополнительные поля
- cmp [ebx+MEASUREITEMSTRUCT.itemID],DEFAULT_HEADER_ID
- jne @f
- add eax,4
- @@:
- mov [ebx+MEASUREITEMSTRUCT.itemHeight],eax
- jmp .processed
В обработчике сообщения WM_DRAWITEM действий гораздо больше. Здесь надо предусмотреть загрузку и отрисовку иконки, которая соответствует пункту меню, надо обработать флаг взведенного чекбокса и отрисовать соответствующую иконку, надо обработать фон для заблокированного, обычного и активного пункта меню, а также установить отдельный цвет и шрифт для заголовков.
Code (Assembler) : Убрать нумерацию
- cmp [msg],WM_DRAWITEM
- je .wmdraw
- ...
- ...
- ...
- .wmdraw:
- ; Отрисовываем пункт меню?
- mov ebx,[lparam]
- cmp [ebx+DRAWITEMSTRUCT.CtlType],ODT_MENU
- jne .processed
- ; Это заголовок меню?
- cmp [ebx+DRAWITEMSTRUCT.itemID],DEFAULT_HEADER_ID
- jne .simple_text
- invoke GetSysColor,COLOR_INACTIVECAPTIONTEXT
- invoke SetTextColor,[ebx+DRAWITEMSTRUCT.hDC],eax
- invoke GetSysColor,COLOR_INACTIVECAPTION
- mov esi,eax
- invoke SetBkColor,[ebx+DRAWITEMSTRUCT.hDC],esi
- invoke CreateSolidBrush,esi
- mov [hBrush],eax
- invoke SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
- invoke CreatePen,PS_INSIDEFRAME,0,esi
- mov [hPen],eax
- invoke SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
- jmp .draw_menu_row
- .simple_text:
- test [ebx+DRAWITEMSTRUCT.itemState],ODS_SELECTED
- jnz .selected_text
- .normal_text:
- invoke GetSysColor,COLOR_MENUTEXT
- ; Пункт меню неактивен?
- test [ebx+DRAWITEMSTRUCT.itemState],ODS_GRAYED+ODS_DISABLED
- jz @f
- invoke GetSysColor,COLOR_GRAYTEXT
- @@:
- invoke SetTextColor,[ebx+DRAWITEMSTRUCT.hDC],eax
- invoke GetSysColor,COLOR_MENU
- mov esi,eax
- invoke SetBkColor,[ebx+DRAWITEMSTRUCT.hDC],esi
- invoke CreateSolidBrush,esi
- mov [hBrush],eax
- invoke SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
- invoke CreatePen,PS_INSIDEFRAME,0,esi
- mov [hPen],eax
- invoke SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
- jmp .draw_menu_row
- .selected_text:
- invoke GetSysColor,COLOR_HIGHLIGHTTEXT
- ; Пункт меню неактивен?
- test [ebx+DRAWITEMSTRUCT.itemState],ODS_GRAYED+ODS_DISABLED
- jz @f
- invoke GetSysColor,COLOR_GRAYTEXT
- @@:
- invoke SetTextColor,[ebx+DRAWITEMSTRUCT.hDC],eax
- invoke GetSysColor,COLOR_HIGHLIGHT
- ; Пункт меню неактивен?
- test [ebx+DRAWITEMSTRUCT.itemState],ODS_GRAYED+ODS_DISABLED
- jz @f
- invoke GetSysColor,COLOR_MENU
- @@:
- mov esi,eax
- invoke SetBkColor,[ebx+DRAWITEMSTRUCT.hDC],esi
- invoke CreateSolidBrush,esi
- mov [hBrush],eax
- invoke SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
- invoke CreatePen,PS_INSIDEFRAME,0,esi
- mov [hPen],eax
- invoke SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
- .draw_menu_row:
- ; Это заголовок меню?
- cmp [ebx+DRAWITEMSTRUCT.itemID],DEFAULT_HEADER_ID
- jne .draw_text
- .draw_header:
- sub [ebx+DRAWITEMSTRUCT.rcItem.bottom],2
- ; Прямоугольник с заливкой
- invoke Rectangle,[ebx+DRAWITEMSTRUCT.hDC],\
- [ebx+DRAWITEMSTRUCT.rcItem.left],\
- [ebx+DRAWITEMSTRUCT.rcItem.top],\
- [ebx+DRAWITEMSTRUCT.rcItem.right],\
- [ebx+DRAWITEMSTRUCT.rcItem.bottom]
- invoke DeleteObject,[hBrush]
- invoke DeleteObject,[hPen]
- sub [ebx+DRAWITEMSTRUCT.rcItem.left],1
- ; Жирный шрифт
- invoke GetStockObject,ANSI_VAR_FONT
- invoke GetObject,eax,sizeof.LOGFONT,logFont
- mov [logFont.lfWeight],FW_BLACK
- invoke CreateFontIndirect,logFont
- mov [hFont],eax
- invoke SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
- mov [hOldFont],eax
- add [ebx+DRAWITEMSTRUCT.rcItem.left],2
- invoke lstrlen,[ebx+DRAWITEMSTRUCT.itemData]
- lea ecx,[ebx+DRAWITEMSTRUCT.rcItem]
- invoke DrawText,[ebx+DRAWITEMSTRUCT.hDC],\
- [ebx+DRAWITEMSTRUCT.itemData],\
- eax,ecx,DT_SINGLELINE+DT_VCENTER+DT_CENTER
- invoke DeleteObject,[hFont]
- invoke SelectObject,[ebx+DRAWITEMSTRUCT.hDC],[hOldFont]
- jmp .processed
- .draw_text:
- ; Прямоугольник с заливкой
- invoke Rectangle,[ebx+DRAWITEMSTRUCT.hDC],\
- [ebx+DRAWITEMSTRUCT.rcItem.left],\
- [ebx+DRAWITEMSTRUCT.rcItem.top],\
- [ebx+DRAWITEMSTRUCT.rcItem.right],\
- [ebx+DRAWITEMSTRUCT.rcItem.bottom]
- invoke DeleteObject,[hBrush]
- invoke DeleteObject,[hPen]
- add [ebx+DRAWITEMSTRUCT.rcItem.left],1
- ; Найти, какому меню принадлежит пункт
- mov [mii.cbSize],sizeof.MENUITEMINFO
- mov [mii.fMask],MIIM_STATE
- mov esi,menus
- .loop_draw:
- lodsd
- or eax,eax
- jz .no_icon_padding
- ; Пункт принадлежит этому меню?
- mov edi,eax
- invoke GetMenuItemInfo,[edi+MYMENU.hMenu],\
- [ebx+DRAWITEMSTRUCT.itemID],FALSE,mii
- or eax,eax
- jnz .chk_state
- invoke GetLastError
- cmp eax,ERROR_MENU_ITEM_NOT_FOUND
- je .loop_draw
- jmp .no_icon_padding
- .chk_state:
- ; Это чекбокс и он установлен?
- test [mii.fState],ODS_CHECKED
- jz .chk_icon
- mov ecx,[edi+MYMENU.itemHeight]
- or ecx,ecx
- jnz @f
- mov ecx,DEFAULT_ITEM_HEIGHT
- @@:
- sub ecx,16
- shr ecx,1
- push ecx
- add [ebx+DRAWITEMSTRUCT.rcItem.top],ecx
- invoke GetModuleHandle,0
- invoke LoadIcon,eax,77
- sub [ebx+DRAWITEMSTRUCT.rcItem.right],18
- invoke DrawIconEx,[ebx+DRAWITEMSTRUCT.hDC],\
- [ebx+DRAWITEMSTRUCT.rcItem.right],\
- [ebx+DRAWITEMSTRUCT.rcItem.top],\
- eax,16,16,NULL,NULL,DI_NORMAL+DI_COMPAT
- pop ecx
- sub [ebx+DRAWITEMSTRUCT.rcItem.top],ecx
- .chk_icon:
- ; Используется ли в меню иконка
- cmp [edi+MYMENU.hasIcons],TRUE
- jne .no_icon_padding
- ; Найти иконку, которая связана с пунктом меню
- mov esi,hIcons
- .find_icon:
- ; ID пункта меню
- lodsd
- or eax,eax
- jz .no_icon_found
- cmp eax,[ebx+DRAWITEMSTRUCT.itemID]
- je @f
- ; Хэндл иконки
- lodsd
- jmp .find_icon
- @@:
- ; Хэндл иконки
- lodsd
- mov ecx,[edi+MYMENU.itemHeight]
- or ecx,ecx
- jnz @f
- mov ecx,DEFAULT_ITEM_HEIGHT
- @@:
- sub ecx,16
- shr ecx,1
- push ecx
- add [ebx+DRAWITEMSTRUCT.rcItem.top],ecx
- invoke DrawIconEx,[ebx+DRAWITEMSTRUCT.hDC],\
- [ebx+DRAWITEMSTRUCT.rcItem.left],\
- [ebx+DRAWITEMSTRUCT.rcItem.top],\
- eax,16,16,NULL,NULL,DI_NORMAL+DI_COMPAT
- pop ecx
- sub [ebx+DRAWITEMSTRUCT.rcItem.top],ecx
- .no_icon_found:
- add [ebx+DRAWITEMSTRUCT.rcItem.left],18
- .no_icon_padding:
- add [ebx+DRAWITEMSTRUCT.rcItem.left],2
- invoke lstrlen,[ebx+DRAWITEMSTRUCT.itemData]
- lea ecx,[ebx+DRAWITEMSTRUCT.rcItem]
- invoke DrawText,[ebx+DRAWITEMSTRUCT.hDC],\
- [ebx+DRAWITEMSTRUCT.itemData],\
- eax,ecx,DT_SINGLELINE+DT_VCENTER+DT_LEFT
- jmp .processed
Для отрисовки иконки перебирается массив иконок, если обнаруживается соответствующий пункт меню, то к нему подгружается его иконка и, в случае необходимости, значок чекбокса. Рамка и фон пункта меню отрисовывается в соответствии с тем, является ли текущий пункт меню активным или нет. Если при создании меню было указано, что иконки используются, то текст в любом случае выводится с отступом от левого края, зарезервированном под изображение иконки, даже если самой иконки для этого пункта меню не было найдено. Для заголовков меню иконки не загружаются.
Важное замечание. Строки заголовков и всех пунктов меню должны постоянно присутствовать в памяти. То есть нельзя, например, динамически сгенерировать меню из нескольких пунктов, используя один и тот же буфер для каждой строки. Для обычного меню такое запросто прокатывает, а для кастомного - нет.
Пример кастомизированного меню
В приложении пример программы, реализующей оба варианта вывода контекстного меню.
Просмотров: 3208 | Комментариев: 3
Внимание! Статья опубликована больше года назад, информация могла устареть!
Комментарии
Отзывы посетителей сайта о статье
mihail
(25.01.2022 в 23:18):
Спасибо! Отличный материал!
ManHunter
(15.12.2021 в 10:59):
Выплыла ситуация, когда при определении принадлежности пункта меню после вызова GetMenuItemInfo появлялась не ошибка ERROR_MENU_ITEM_NOT_FOUND, а совершенно другое. Так что ограничился проверкой результата из GetMenuItemInfo, этого достаточно. Статью и исходники обновил.
ManHunter
(30.05.2018 в 14:58):
Давно хотел добавить сюда заголовок меню, наконец сделал. Статья и код доработаны, архив с примером перезалил.
Добавить комментарий
Заполните форму для добавления комментария