Blog. Just Blog

Меню с иконками на Ассемблере

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Образ мышления: Assembler | Автор: ManHunter
Сегодня разберем кастомизацию такого элемента интерфейса, как выпадающее меню. Без меню обходится мало какое современное приложение, но вот работа с меню стандартными средствами обычно ограничивается установкой флага чекбокса, затенением неактивных пунктов или отрисовкой субменю. Попытка разнообразить меню, например, своими иконками, приводит к очень печальному результату. Ситуацию особо не спасают ни собственные битмапы, ни подгрузка изображений из списка ImageList. Очень странно, что на протяжении многих лет разработчики Windows так и не сделали инструментов "из коробки", чтобы можно было легко и комфортно работать с менюшками. К счастью, в системе есть "потайной ход", с помощью которого можно кастомизировать меню так, как вам захочется. Для своих проектов я постарался сделать как можно более универсальный инструмент для работы с меню и сейчас я с вами им поделюсь.

Начнем создание собственных элементов интерфейса с того, что для каждого настраиваемого меню резервируется структура с данными следующего формата:
  1. struct MYMENU
  2.   hMenu      dd ?
  3.   itemWidth  dd ?
  4.   itemHeight dd ?
  5.   hasIcons   db ?
  6. ends
В это же время, все меню, которые требуют кастомизации, формируют массив структур и указателей на них. Список заканчивается нулевым DWORD'ом:
  1. menus   dd Menu1
  2.         dd Menu2
  3.         dd ?
  4.  
  5. Menu1   MYMENU
  6. Menu2   MYMENU
Описание полей структуры: hMenu - хэндл меню, itemWidth - ширина пункта меню в пикселах, itemHeight - высота пункта меню в пикселах, hasIcons - флаг, определяющий, будут или нет использоваться в меню иконки (1 - да, 0 - нет). Если иконки будут использоваться, то слева от текста пунктов меню резервируется место для них, в противном случае текст пункта будет отрисовываться прямо от края меню.

Если вы хотите использовать значения размеров меню, соответствующие стандартным, то для этих целей описываются две константы с дефолтными значениями, определенными опытным путем. Естественно, вы можете заменить их на ваши значения:
  1. DEFAULT_ITEM_WIDTH  = 80
  2. DEFAULT_ITEM_HEIGHT = 18
Для хранения иконок используется массив из пар DWORD'ов, определяющих ID пункта меню и хэндл иконки, который ему соответствует. Можно обернуть это в какую-нибудь структуру, но лично мне кажется это лишним.
  1. ICONS_COUNT = 100
  2. hIcons  rd ICONS_COUNT*2
Теперь ненадолго отвлечемся на теорию. Для того, чтобы можно было использовать собственный обработчик для кастомизации меню, его пункты должны быть добавлены с использованием флага MFT_OWNERDRAW. Перед отрисовкой такого пункта меню система отправляет окну-владельцу сообщение WM_MEASUREITEM. В lParam этого сообщения передается указатель на структуру MEASUREITEMSTRUCT, в которую, при необходимости, мы можем внести свои изменения, а именно установить ширину и высоту пунктов меню. Непосредственно при отрисовке пункта меню окну отправляется сообщение WM_DRAWITEM. В lParam сообщения передается указатель на структуру DRAWITEMSTRUCT, из которой мы можем получить все необходимые значения, например, текст пункта меню или состояние чекбокса, а также самостоятельно нарисовать пункт меню в том виде, в каком нам надо.

На этом можно заканчивать с теорией и переходить непосредственно к программированию. Начнем с создания меню. Обычно это делается на этапе инициализации окна. Создадим два меню с разными характеристиками.
  1.         ; Очистить память под массив иконок
  2.         invoke  RtlZeroMemory,hIcons,ICONS_COUNT*2*4
  3.         ; EDI -> указатель на массив иконок
  4.         mov     edi,hIcons
  5.  
  6.         ; Создать первое меню
  7.         invoke  CreatePopupMenu
  8.         ; Сохранить хэндл меню в структуру
  9.         mov     [Menu1.hMenu],eax
  10.         ; Установить ширину меню 120 пикселов
  11.         mov     [Menu1.itemWidth],120
  12.         ; В меню используются иконки
  13.         mov     [Menu1.hasIcons],TRUE
  14.         mov     ebx,eax
  15.  
  16.         ; Сохранить ID пункта меню в массив с иконками
  17.         mov     eax,IDM_MENU11
  18.         stosd
  19.         ; Загрузить иконку из ресурсов
  20.         invoke  GetModuleHandle,0
  21.         invoke  LoadIcon,eax,1
  22.         ; Сохранить хэндл иконки в массив
  23.         stosd
  24.  
  25.         ; Добавить пункт в меню
  26.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW+MF_GRAYED,\
  27.                 IDM_MENU11,szStr11
  28.  
  29.         ; Сохранить ID пункта меню в массив с иконками
  30.         mov     eax,IDM_MENU12
  31.         stosd
  32.         ; Загрузить иконку из ресурсов
  33.         invoke  GetModuleHandle,0
  34.         invoke  LoadIcon,eax,2
  35.         stosd
  36.  
  37.         ; Добавить пункт в меню
  38.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW,IDM_MENU12,szStr12
  39.         ...
  40.         ; и так далее
  41.         ...
  42.  
  43.         ; Создать второе меню
  44.         invoke  CreatePopupMenu
  45.         mov     [Menu2.hMenu],eax
  46.         ; Ширина меню 60 пикселов, высота дефолтная
  47.         mov     [Menu2.itemWidth],60
  48.         mov     ebx,eax
  49.  
  50.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW,IDM_MENU21,szStr21
  51.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW+MF_CHECKED,\
  52.                 IDM_MENU22,szStr22
  53.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW,IDM_MENU23,szStr23
  54.         invoke  AppendMenu,ebx,MF_SEPARATOR,NULL,NULL
  55.  
  56.         ; Создать субменю, которое не кастомизируется
  57.         invoke  CreatePopupMenu
  58.         mov     edi,eax
  59.         invoke  AppendMenu, edi,MF_STRING,IDM_MENU21,szStr21
  60.  
  61.         ; Добавить пункты меню
  62.         invoke  AppendMenu,ebx,MFT_OWNERDRAW+MF_POPUP,edi,szStr24
  63.         invoke  AppendMenu,ebx,MF_STRING+MFT_OWNERDRAW,IDM_MENU25,szStr25
Подобным образом в приложении создаются все менюшки, которые надо кастомизировать. Каждая описывается в своей структуре, в каждую добавляются пункты с соответствующими состояниями чекбоксов, статусом активности, иконками и текстами. Если меню не описано в массиве, то оно будет выводиться на экран штатными средствами.

Переходим к обработчикам сообщений. Начнем с обработчика WM_MEASUREITEM, в котором будем возвращать размеры пункта меню в соответствии со значениями, которые мы задали при инициализации. Здесь нам надо определить, к какому меню принадлежит запрашиваемый пункт. Я так и не нашел способов сделать это одним движением, приходится перебирать все меню из массива по очереди и запрашивать информацию о пункте с нужным индексом. Если ошибки при этом не случилось, значит пункт принадлежит этому меню. Естественно, при такой реализации одинаковых индексов в разных меню быть не должно.
  1.         cmp     [msg],WM_MEASUREITEM
  2.         je      .wmmeasure
  3.         ...
  4.         ...
  5.         ...
  6. .wmmeasure:
  7.         ; Отрисовываем пункт меню?
  8.         mov     ebx,[lparam]
  9.         cmp     [ebx+MEASUREITEMSTRUCT.CtlType],ODT_MENU
  10.         jne     .processed
  11.  
  12.         ; Найти, какому меню принадлежит пункт
  13.         mov     [mii.cbSize],sizeof.MENUITEMINFO
  14.         mov     esi,menus
  15. .loop_measure:
  16.         lodsd
  17.         or      eax,eax
  18.         jz      .processed
  19.  
  20.         ; Пункт принадлежит этому меню?
  21.         mov     edi,eax
  22.         invoke  GetMenuItemInfo,[edi+MYMENU.hMenu],\
  23.                 [ebx+MEASUREITEMSTRUCT.itemID],FALSE,mii
  24.         or      eax,eax
  25.         jnz     @f
  26.  
  27.         invoke  GetLastError
  28.         cmp     eax,ERROR_MENU_ITEM_NOT_FOUND
  29.         je      .loop_measure
  30.         jmp     .processed
  31. @@:
  32.         ; Ширина пункта меню
  33.         mov     eax,[edi+MYMENU.itemWidth]
  34.         or      eax,eax
  35.         jnz     @f
  36.         mov     eax,DEFAULT_ITEM_WIDTH
  37. @@:
  38.         mov     [ebx+MEASUREITEMSTRUCT.itemWidth],eax
  39.  
  40.         ; Высота пункта меню
  41.         mov     eax,[edi+MYMENU.itemHeight]
  42.         or      eax,eax
  43.         jnz     @f
  44.         mov     eax,DEFAULT_ITEM_HEIGHT
  45. @@:
  46.         mov     [ebx+MEASUREITEMSTRUCT.itemHeight],eax
  47.         jmp     .processed
Алгоритм работы следующий: последовательно перебираются все хэндлы меню из массива, для каждого меню проверяется, принадлежит ли пункт этому меню или нет. Если принадлежит, то из соответствующей структуры загружается или установленные, или дефолтные значения размеров пунктов меню.

В обработчике сообщения WM_DRAWITEM действий гораздо больше. Здесь надо предусмотреть загрузку и отрисовку иконки, которая соответствует пункту меню, надо обработать флаг взведенного чекбокса и отрисовать соответствующую иконку, надо обработать фон для заблокированного, обычного и активного пункта меню.
  1.         cmp     [msg],WM_DRAWITEM
  2.         je      .wmdraw
  3.         ...
  4.         ...
  5.         ...
  6. .wmdraw:
  7.         ; Отрисовываем пункт меню?
  8.         mov     ebx,[lparam]
  9.         cmp     [ebx+DRAWITEMSTRUCT.CtlType],ODT_MENU
  10.         jne     .processed
  11.  
  12.         ; Пункт меню активен?
  13.         test    [ebx+DRAWITEMSTRUCT.itemState],ODS_SELECTED
  14.         jnz     .selected_text
  15. .normal_text:
  16.         invoke  GetSysColor,COLOR_MENUTEXT
  17.         ; Пункт меню неактивен?
  18.         test    [ebx+DRAWITEMSTRUCT.itemState],ODS_GRAYED+ODS_DISABLED
  19.         jz      @f
  20.         invoke  GetSysColor,COLOR_GRAYTEXT
  21. @@:
  22.         invoke  SetTextColor,[ebx+DRAWITEMSTRUCT.hDC],eax
  23.         invoke  GetSysColor,COLOR_MENU
  24.         mov     esi,eax
  25.         invoke  SetBkColor,[ebx+DRAWITEMSTRUCT.hDC],esi
  26.  
  27.         invoke  CreateSolidBrush,esi
  28.         mov     [hBrush],eax
  29.         invoke  SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
  30.  
  31.         invoke  CreatePen,PS_INSIDEFRAME,0,esi
  32.         mov     [hPen],eax
  33.         invoke  SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
  34.  
  35.         jmp     .draw_text
  36.  
  37. .selected_text:
  38.         invoke  GetSysColor,COLOR_HIGHLIGHTTEXT
  39.         ; Пункт меню неактивен?
  40.         test    [ebx+DRAWITEMSTRUCT.itemState],ODS_GRAYED+ODS_DISABLED
  41.         jz      @f
  42.         invoke  GetSysColor,COLOR_GRAYTEXT
  43. @@:
  44.         invoke  SetTextColor,[ebx+DRAWITEMSTRUCT.hDC],eax
  45.  
  46.         invoke  GetSysColor,COLOR_HIGHLIGHT
  47.         ; Пункт меню неактивен?
  48.         test    [ebx+DRAWITEMSTRUCT.itemState],ODS_GRAYED+ODS_DISABLED
  49.         jz      @f
  50.         invoke  GetSysColor,COLOR_MENU
  51. @@:
  52.         mov     esi,eax
  53.         invoke  SetBkColor,[ebx+DRAWITEMSTRUCT.hDC],esi
  54.  
  55.         invoke  CreateSolidBrush,esi
  56.         mov     [hBrush],eax
  57.         invoke  SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
  58.  
  59.         invoke  CreatePen,PS_INSIDEFRAME,0,esi
  60.         mov     [hPen],eax
  61.         invoke  SelectObject,[ebx+DRAWITEMSTRUCT.hDC],eax
  62.  
  63. .draw_text:
  64.         ; Прямоугольник с заливкой
  65.         invoke  Rectangle,[ebx+DRAWITEMSTRUCT.hDC],\
  66.                 [ebx+DRAWITEMSTRUCT.rcItem.left],\
  67.                 [ebx+DRAWITEMSTRUCT.rcItem.top],\
  68.                 [ebx+DRAWITEMSTRUCT.rcItem.right],\
  69.                 [ebx+DRAWITEMSTRUCT.rcItem.bottom]
  70.  
  71.         invoke  DeleteObject,[hBrush]
  72.         invoke  DeleteObject,[hPen]
  73.  
  74.         add     [ebx+DRAWITEMSTRUCT.rcItem.left],1
  75.  
  76.         ; Найти, какому меню принадлежит пункт
  77.         mov     [mii.cbSize],sizeof.MENUITEMINFO
  78.         mov     [mii.fMask],MIIM_STATE
  79.         mov     esi,menus
  80. .loop_draw:
  81.         lodsd
  82.         or      eax,eax
  83.         jz      .no_icon_padding
  84.  
  85.         ; Пункт принадлежит этому меню?
  86.         mov     edi,eax
  87.         invoke  GetMenuItemInfo,[edi+MYMENU.hMenu],\
  88.                 [ebx+DRAWITEMSTRUCT.itemID],FALSE,mii
  89.         or      eax,eax
  90.         jnz     .chk_state
  91.  
  92.         invoke  GetLastError
  93.         cmp     eax,ERROR_MENU_ITEM_NOT_FOUND
  94.         je      .loop_draw
  95.         jmp     .no_icon_padding
  96.  
  97. .chk_state:
  98.         ; Это чекбокс и он установлен?
  99.         test    [mii.fState],ODS_CHECKED
  100.         jz      .chk_icon
  101.  
  102.         mov     ecx,[edi+MYMENU.itemHeight]
  103.         or      ecx,ecx
  104.         jnz     @f
  105.         mov     ecx,DEFAULT_ITEM_HEIGHT
  106. @@:
  107.         sub     ecx,16
  108.         shr     ecx,1
  109.         push    ecx
  110.         add     [ebx+DRAWITEMSTRUCT.rcItem.top],ecx
  111.  
  112.         ; Иконка чекбокса хранится в ресурсах с фиксированным индексом
  113.         invoke  GetModuleHandle,0
  114.         invoke  LoadIcon,eax,77
  115.  
  116.         sub     [ebx+DRAWITEMSTRUCT.rcItem.right],18
  117.  
  118.         invoke  DrawIconEx,[ebx+DRAWITEMSTRUCT.hDC],\
  119.                 [ebx+DRAWITEMSTRUCT.rcItem.right],\
  120.                 [ebx+DRAWITEMSTRUCT.rcItem.top],\
  121.                 eax,16,16,NULL,NULL,DI_NORMAL+DI_COMPAT
  122.         pop     ecx
  123.         sub     [ebx+DRAWITEMSTRUCT.rcItem.top],ecx
  124.  
  125. .chk_icon:
  126.         ; Используется ли в меню иконка
  127.         cmp     [edi+MYMENU.hasIcons],TRUE
  128.         jne     .no_icon_padding
  129.  
  130.         ; Найти иконку, которая связана с пунктом меню
  131.         mov     esi,hIcons
  132. .find_icon:
  133.         ; ID пункта меню
  134.         lodsd
  135.         or      eax,eax
  136.         jz      .no_icon_found
  137.  
  138.         cmp     eax,[ebx+DRAWITEMSTRUCT.itemID]
  139.         je      @f
  140.  
  141.         ; Хэндл иконки
  142.         lodsd
  143.         jmp     .find_icon
  144. @@:
  145.         ; Хэндл иконки
  146.         lodsd
  147.  
  148.         mov     ecx,[edi+MYMENU.itemHeight]
  149.         or      ecx,ecx
  150.         jnz     @f
  151.         mov     ecx,DEFAULT_ITEM_HEIGHT
  152. @@:
  153.         sub     ecx,16
  154.         shr     ecx,1
  155.         push    ecx
  156.         add     [ebx+DRAWITEMSTRUCT.rcItem.top],ecx
  157.         invoke  DrawIconEx,[ebx+DRAWITEMSTRUCT.hDC],\
  158.                 [ebx+DRAWITEMSTRUCT.rcItem.left],\
  159.                 [ebx+DRAWITEMSTRUCT.rcItem.top],\
  160.                 eax,16,16,NULL,NULL,DI_NORMAL+DI_COMPAT
  161.         pop     ecx
  162.         sub     [ebx+DRAWITEMSTRUCT.rcItem.top],ecx
  163.  
  164. .no_icon_found:
  165.         add     [ebx+DRAWITEMSTRUCT.rcItem.left],18
  166.  
  167. .no_icon_padding:
  168.         add     [ebx+DRAWITEMSTRUCT.rcItem.left],2
  169.         invoke  lstrlen,[ebx+DRAWITEMSTRUCT.itemData]
  170.         lea     ecx,[ebx+DRAWITEMSTRUCT.rcItem]
  171.         invoke  DrawText,[ebx+DRAWITEMSTRUCT.hDC],\
  172.                 [ebx+DRAWITEMSTRUCT.itemData],\
  173.                 eax,ecx,DT_SINGLELINE+DT_VCENTER+DT_LEFT
  174.  
  175.         jmp     .processed
Конечно, десятки тысяч разработчиков и дизайнеры интерфейсов MicroSoft ошибаться не могут, но на мой взгляд нет большего мудачества, чем рисовать значок чекбокса слева от пункта меню вместо иконки этого пункта. Лично я считаю, что чекбокс должен находиться справа от текста пункта меню, чтобы не забивать основную иконку. Поэтому в моей реализации пользовательской отрисовки меню иконка чекбокса выводится справа, никак не влияя на основную иконку пункта меню.

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

В приложении пример программы, реализующей оба варианта вывода контекстного меню.

Пример программы с исходным текстом (FASM)Пример программы с исходным текстом (FASM)

Menu.Demo.zip (8,933 bytes)


Поделиться ссылкой ВКонтакте Поделиться ссылкой на Facebook Поделиться ссылкой на LiveJournal Поделиться ссылкой в Мой Круг Добавить в Мой мир Добавить на ЛиРу (Liveinternet) Добавить в закладки Memori Добавить в закладки Google
Просмотров: 246 | Комментариев: 0

Метки: Assembler

Комментарии

Отзывы посетителей сайта о статье
Комментариeв нет

Добавить комментарий

Заполните форму для добавления комментария
Имя*:
Текст комментария (не более 2000 символов)*:

*Все поля обязательны для заполнения.
Комментарии, содержащие рекламу, ненормативную лексику, оскорбления и т.п., а также флуд и сообщения не по теме, будут удаляться. Нарушителям может быть заблокирован доступ к сайту.
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2017
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.24 сек. / MySQL: 2 (0.0044 сек.) / Память: 4.5 Mb
Наверх