SDT (SoftWare Development Techniques) International 1999 / 01
Dieter Crispien
Новое крутое меню – часть 1.
Меню в окне – это веянье моды. Каждая новая версия Microsoft Office и Internet Explorer – это новшества, которые так и просятся к использованию в Ваших приложениях. Как программисты, мы все хотели бы поскорее предоставить их для наших клиентов, при этом сохранив своё время, а собственные усилия – сведя к минимуму. К сожалению, так легко бывает не всегда. Но, это введение в программирование новейших форм меню – упростит Вашу задачу.
Рис. 1: Меню.
1. Новый режим GUI (графического интерфейса) для меню.
Этот рисунок меню, изображённый выше, показывает нам новые возможности. И в этой статье мы ответим на следующие вопросы:
- Как сделать значок в пункте меню.
- Как в пункте меню изобразить «галочку».
- Как в меню установить свои шрифт, цвет, тип линии и т.д.
2. Начинаем использовать VO 2.
Как же сделать пункт меню, обладающим одной из этих новых особенностей? Как всегда, Вы найдете эту информацию на Вашем установочном CD-ROM-е, в разделе Platform SDK Microsoft Developers Network (MSDN). В ходе рассмотрения этого документа вы найдете ссылки на структуру, известную как MENUITEMINFO. В CA-Visual Objects библиотека «Win32 API Library» содержит копию этой структуры внутри модуля «WinUser». Выглядит это примерно так:
STRUCTURE _winMENUITEMINFO
MEMBER cbSize AS DWORD
MEMBER fMask AS DWORD
MEMBER fType AS DWORD
MEMBER fState AS DWORD
MEMBER wID AS DWORD
MEMBER hSubMenu AS PTR
MEMBER hbmpChecked AS PTR
MEMBER hbmpUnchecked AS PTR
MEMBER dwItemData AS DWORD
MEMBER dwTypeData AS PSZ
MEMBER cch AS DWORD
MEMBER hbmpItem AS PTR // Win98+, Win2K+
Если в этой структуре присвоить члену fMask параметр «MIIM_TYPE», то это будет означать, что мы хотим получить или задать значение для члена fType, а «MIIM_DATA» – для dwTypeData. Для отображения Bitmap (значка), мы будем использовать fType, чтобы сообщить Windows, что вместо строки нам нужно отобразить Bitmap, и указатель на него – мы присвоим члену dwTypeDate.
Примечание переводчика:так же, в примере используется параметр «MIIM_SUBMENU», который извлекает или устанавливает hSubMenu, т.е. – подменю.
Перед тем, как идти дальше, ответьте себе на вопрос: «Почему структура MENUITEMINFO в нижеследующем методе: AddBitmapMenu() определена не как «AS», а как «IS»? (Подсказка, для тех, кто забыл – прочтите ещё раз мою статью в SDT от 2/98 и 3/98).
METHOD AddBitmapMenu( ) CLASS StandardShellWindow
LOCAL sMii IS _winMENUITEMINFO
LOCAL hMenubar AS PTR
LOCAL hMenuPopup AS PTR
LOCAL oBitmap AS Bitmap
hMenubar := GetMenu( SELF:Handle( ) ) // get menu handle
hMenuPopup := CreatePopupMenu( ) // create new menu
oBitmap := Flag_DEM{ } // a bitmap resource
sMii.cbsize := _SIZEOF( _winMENUITEMINFO )
sMii.fMask := _OR( MIIM_ID, MIIM_TYPE, MIIM_DATA )
sMii.fType := MFT_Bitmap
sMii.wID := 4
sMii.dwTypeData := PSZ( _CAST, oBitmap:Handle( ) ) // insert
InsertMenuItem( hMenuPopup, 4, TRUE, @sMii ) // into the empty Popup
// menu a bitmap Item
sMii.fMask := _OR( MIIM_TYPE, MIIM_DATA, MIIM_SUBMENU )
//we want to define a new submenu
sMii.fType := MFT_STRING // of the type string
sMii.hSubMenu := hMenuPopup // the bitmap Popupmenu created above
sMii.dwTypeData := String2Psz("Bitmap-Test")
sMii.cch := 64 // somewhat dirty ...
// new Item in (horizontal) Menu bar
InsertMenuItem( hMenubar, GetMenuItemCount( hMenubar ), TRUE, @sMii )
RETURN NIL
Рис. 2: Значок в пункте меню.
На рисунке 2 Вы можете увидеть, что в меню нам удалось вывести значок, но нужно сделать еще некоторые улучшения:
- Пункт меню (MenuItem) всё ещё не реагирует на щелчок «мышки».
- А может ли MenuItem реагировать на клавиатуру?
- Размеры MenuItem по-прежнему не задаются.
- А как мы можем получить, или создать значки для меню?
Примечание: Если вы используете точечные рисунки (bitmap), высота которых превышает значение, возвращённое GetSystemMetrics( SM_YMENU ), то вы можете столкнуться с ошибкой при минимизации или максимизации окна. Чтобы этого избежать, вы должны правильно обработать сообщение WM_MEASUREITEM.
Как показано на рисунке 3, для bitmap-элементов меню также используется система корректировки и по ширине. Мы увидим, на примере OwnerDraw-меню, как решить этот вопрос во второй части этой статьи в следующем SDT (от 2/99).
Рис. 3: Bitmap-меню с несколькими колонками.
Возможно, эти примеры не самые удачные, поскольку новые пункты меню содержат лишь один Bitmap и совсем без текста. Но, те из Вас, кто был внимательным, наверняка обратил внимание, что мы определяли только вывод Bitmap, а не текста, и поэтому – результат на самом деле ожидаем. Если вы хотите узнать, как в пунктах меню размещать значки (bitmap) и текстовые элементы рядом друг с другом, читайте часть 2 этой статьи (SDT 2/99).
Так же, многие из Вас заметили, что мы использовали API-функцию InsertMenuItem(), вместо стандартного метода InsertItem() для класса меню. Это связано с тем, что InsertItem() не использует нужную нам структуру MENUITEMINFO.
3. Подбор и рисование значков (bitmap) в меню.
Bitmap для MenuItem может быть получен двумя способами:
- Загружен в память c помощью LoadBitmap() (см. пример в AEF: GetBitmaps()).
- Непосредственно создан во время выполнения программы с помощью GDI-функций (см. пример в AEF: CreateLineBitmaps()).
Примечание переводчика. В качестве значка (точечного рисунка) могут использоваться:
- Ранее созданный Bitmap-класс.
- Bitmap, полученный и из указанного исполняемого файла, динамически подключаемой библиотеки (DLL) или файла значка. (См. API-функцию ExtractIcon()).
- Также, в качестве значка может использоваться соответствующим образом конвертированное (с помощью API-функций) не-Bitmap изображение.
Внимание! Для старых ОС (Win98/ME, Win2K) нужно использовать только Bitmap 16x16 (макс. 20х16), 256 цветов. В более новых – возможны варианты.
Везде и далее, я позволяю себе вольность использовать разговорный термин «значок», «картинка» и т.п. Вы должны себе чётко представлять, что при выводе изображения в меню используются именно bitmap-ресурсы (типа, .bmp-файлов). BMP – это лишь одна из разновидностей форматов хранения точечных (растровых) изображений.
В следующем примере мы выполним такие действия:
1. CreateCompatibleDC().
Эта API-функция создаёт контекст устройства в памяти (DC), совместимый с заданным устройством.
2. CreateCompatibleBitmap().
Она создает точечный рисунок, совместимый с устройством, которое связано с заданным контекстом устройства.
3. SelectObject().
Примечание переводчика: Функция SelectObject выбирает объект в заданный контекст устройства (DC). Новый объект заменяет предыдущий объект того же самого типа.
4. С помощью функций Ellipse() и LineTo() создадим простые изображения.
А кусочек кода, который я приведу ниже, покажет простой пример, в котором нарисуются разные линии, по аналогии с меню на рисунке 4, взятым из Word 97.
Рис. 4: Bitmap-меню типов линий (из Word 97)
METHOD CreateLineBitmaps( ) CLASS StandardShellWindow
// taken from Platform SDK of the MSDN CDs and adapted to VO
// a few preparations are omitted here
LOCAL hDCDesktop AS PTR
LOCAL hDCMem AS PTR
LOCAL hOldBrush AS PTR
LOCAL I AS WORD
LOCAL hNewPen, hOldPen AS PTR
LOCAL hOldBM AS PTR
hDCDesktop := GetDC( hDesktop)
hDCMem := CreateCompatibleDC( hDC ) // 1. Step
// create brush with the menu background color and select it for the memory DC
hOldBrush := SelectObject( hDCMem, CreateSolidBrush( dwColorMenu ) )
FOR I := 1 UPTO 5
// create bitmaps for the compatible DC created in step 1 and draw the bitmaps
aBMP[ I ] := CreateCompatibleBitmap( hDCDesktop, CX_LineBMP, CY_LineBMP ) // 2. Step
hOldBM := SelectObject( hDCMem, aBMP[ I ] ) // 3. Step
// Вставка от переводчика:
***
// PS_SOLID 0 Перо является сплошным.
// PS_DASH 1 Перо является штриховым.
// PS_DOT 2 Перо является пунктирным.
// PS_DASHDOT 3 Перо имеет чередующиеся штрихи и точки.
// PS_DASHDOTDOT 4 Перо имеет чередующиеся штрихи и двойные точки.
// Похоже, что в оригинале примера пропущены строки:
hNewPen := CreatePen( I - 1, 1, DWORD( _CAST, RGB( 0, 0, 0 ) ) )
hOldPen := SelectObject( hDCMem, hNewPen )
***
// preparations for step 4 are omitted here
LineTo( hDCMem, CX_LineBMP, CY_LineBMP / 2 ) // 4. Step
// remove pen, select the old pen and old bitmap
DeleteObject( SelectObject( hDCMem, hOldPen ) )
// Вставка от переводчика:
***
// Использование cтроки DeleteObject( SelectObject( hDCMem, hOldPen ) ) заменяетдве:
// SelectObject( hDCMem, hOldPen )
// DeleteObject( hNewPen )
***
SelectObject( hDCMem, hOldBM )
NEXT I
// remove used Brush and reuse the old Brush
DeleteObject( SelectObject( hDCMem, hOldBrush ) )
// remove the DC from memory and release the desktop DC
DeleteDC( hDCMem )
ReleaseDC( hDesktop, hDCDesktop )
RETURN NIL
Полный исходный код этого примера есть на диске. В дальнейшем, это растровое меню можно сделать, например, в виде разных стрелок или диаграмм.
Перед тем, как использовать новые MenuItems, мы должны спросить себя: «Где и как я могу определить, что пользователь выбрал именно этот MenuItem?». Как и для всех остальных пунктов меню, обработчик события MenuCommand отвечает за обработку событий. Он получает oEvent-объект, который содержит ItemID, присвоенный в MENUITEMINFO-структуре и wID:
METHOD MenuCommand( oEvt ) CLASS ToDoExplorer
LOCAL wID AS WORD
wID := oEvt:ItemID
DO CASE
CASE oEvt:ItemID == ...
// SELF:OnIDExecute() // own method call
OTHERWISE
SUPER:MenuCommand( oEvt )
END CASE
RETURN NIL
Я думаю, что теперь у Вас появилось много весёлой работы с растровым меню. А в следующей статье – мы покажем вам, как реализовать OwnerDraw-меню. К этому времени мы будем полностью «Office97-совместимыми», так как затронем сами основы меню.
Литература:
SDT (SoftWare Development Techniques) International 1999 / 02
Dieter Crispien
Новое крутое меню – часть 2.
В части 1 (см. SDT 1/99), я показал, как создаются пункты меню, содержащие не текст, а bitmap-ы. В этом выпуске я хочу показать, как можно одновременно отображать и текст и растровые изображения в меню. Это одна из новаций, которая сделала Office97 таким узнаваемым.
1. Ничто не делается без OwnerDraw.
Естественно, если мы хотим отойти от стандарта – Microsoft позволяет нам это сделать. Подробности можно найти в библиотеке MSDN по ключевому слову MFT_OWNERDRAW. Для того чтобы присвоить этот атрибут пункту меню (MenuItem), мы создадим свой подкласс (subclass) MenuItem, который и будем использовать в окне, меню которого уже создали.
METHOD Init( hMenuPopup, nID, lHasBitmap ) CLASS CoolMenuItem
LOCAL sMii IS _winMenuItemInfo
LOCAL pMyItem AS MyItem
Default( @lHasBitmap, FALSE )
SELF:lHasBitmap := lHasBitmap
IF hMenuPopup == NULL_PTR
// Error
ELSE
GetMenuItemInfo( hMenuPopup, nID, FALSE, @sMii )
SELF:dwMenuID := nID
SELF:dwStatus := sMii.fstate
SELF:dwType := sMii.fType
IF SELF:lHasBitmap
pMyItem := MemAlloc( _SIZEOF( MyItem ) )
MemClear( pMyItem, _SIZEOF( MyItem ) )
// change the item to an owner-drawn item and save the address of the
// item structure as item data
sMii.fMask := MIIM_TYPE //_Or(MIIM_TYPE,MIIM_DATA)
sMii.fType := MFT_OWNERDRAW
SELF:dwType := sMii.fType
sMii.dwItemdata := DWORD( _CAST, pMyItem )
SetMenuItemInfo( hMenuPopup, nID, FALSE, @sMii )
ENDIF
ENDIF
RETURN SELF
Когда мы пытаемся «рисовать» вручную, мы «вступаем в игру» с некоторым опозданием, и поэтому для работы нам крайне важным становится сохранение информации в созданной нами (user-defined) структуре pMyItem. Это делается с помощью члена dwItemData (см. выше). Для вышеуказанного метода Init, важно, чтобы он вызывался в конце фазы инициализации окна. Поэтому мы перехватим следующее сообщение в Dispatch() окна, владеющее нашим меню:
METHOD Dispatch( oEvent ) CLASS dwCustomer
LOCAL dwMsg AS DWORD
dwMsg := oEvent:Message
DO CASE
CASE ( dwMsg == WM_PARENTNOTIFY )
IF LoWord( oEvent:wParam ) = WM_CREATE
SUPER:Dispatch( oEvent )
SELF:ChangeCoolMenu( oEvent )
RETURN 0L
ENDIF
// any extensions go here
И в методе ChangeCoolMenu(), мы заменим позиции меню на наши собственные CoolMenuItems:
METHOD ChangeCoolMenu( oEvent ) CLASS dwCustomer
IF SELF:Menu != NULL_OBJECT .AND. IsMethod( SELF:Menu, #ChangeCoolMenu )
SELF:Menu:ChangeCoolMenu( oEvent, SELF )
ENDIF
RETURN 0L
Наш новый класс CoolMenu (наследник стандартного класса «Menu») настраивает желаемый вид MenuItems:
METHOD ChangeCoolMenu( oEvent, oWin ) CLASS CoolMenu
LOCAL sMii IS _winMenuItemInfo
LOCAL hMenubar AS PTR
LOCAL hMenuPopup AS PTR
LOCAL oItem AS CoolMenuItem
LOCAL nID AS DWORD
hMenubar := SELF:Handle()
DO CASE
CASE IsInstanceOf( oWin, #DataWindow )
GetMenuItemInfo( hMenubar, IDM_StandardShellMenu_File_ID, FALSE, @sMii )
hMenuPopup := sMii.hSubMenu
FOR nID := IDM_StandardShellMenu_File_Open_ID;
UPTO IDM_StandardShellMenu_File_Exit_ID
oItem := CoolMenuItem{ hMenuPopup, nID, TRUE }
AAdd( SELF:aCoolItem, oItem )
NEXT nID
Для всех пунктов меню, для которых мы установили MFT_OWNERDRAW-ы, мы должны среагировать на сообщения WM_MEASUREITEM и WM_DRAWITEM. Сброс-установку OwnerDraw-атрибута можно сделать при обработке сообщения WM_INITMENUPOPUP.
2. Установка связи между значками инструментальной панели (ToolbarButtons) и нашими MenuItems.
Стандартный редактор меню соединяет значки панели инструментов с MenuItems. Эти значки берутся из bitmap-ленты (bitmap ribbon), в качестве которой выступает или встроенная стандартная лента VO или наша самостоятельно нарисованная. Однако, для наших пунктов меню нам нужны значки в виде ImageList. Поэтому мы поступим просто: мы переделаем под себя существующую ленту ToolBar в ImageList:
// extract from an extended ChangeCoolMenu-Method of the CoolMenu-Class:
oTB := oWin:Toolbar
oImageList := ImageList{ 255, { 20, 16 } }
oImageList:Add( oTB:Bitmap )
SELF:oImages := oImageList
Тулбар (Toolbar) VO может содержать до 255 картинок с разрешением 20х16 (см. метод AppendItem() в CAVO25.Hlp). А библиотека MSDN подсказывает, что мы даже можем одновременно добавлять и использовать несколько ImageList. Количество картинок можно определить из общей длины bitmap-ленты тулбара, просто разделив её длину на размер одного изображения, как описано в файле помощи о ImageList:Init().
Примечание переводчика: что-то я там этого (в описании ImageList:Init()) – не увидел. Возможно, это было описано в «старой» версии help-а. Но, косвенно это «читается» в помощи по методу ToolBar:AppendItem().
Еще одно дополнение в методе ChangeCoolMenu() для класса CoolMenu – это добавление индекса для каждого пункта меню с соответствующим изображением:
nBID := AScan( oTB:aTipsText, { |x| x[ 2 ] = nID } )
nButtonID := oTB:aTipsText[ nBID, 1 ]
IF nButtonID > 0
AAdd( SELF:aToolbarId, { nID, nButtonID } )
oItem:nButtonIndex := nButtonID
ELSE
oItem:nButtonIndex := -1
ENDIF
oItem:dwMenuID := nID
Так как в VO уже есть массив aTipsText – мы используем его для доступа к информации. После чего, наш CoolMenu-объект будет содержать всю нужную информацию для рисования значков.
3. Задание размера пунктов меню (Measuring MenuItems).
Перед рисованием пунктов меню (MenuItems), мы должны задать их размеры. Окно-владелец получает сообщение WM_MEASUREITEM, которое обрабатывается в обработчике событий, где мы и заполняем структуру с информацией, необходимой для отрисовки MenuItems. DrawText() (с параметром DT_CALCRECT) – важнейшая функция, используемая при обработке события WM_MEASUREITEM. Она производит вычисление размера текста. Кроме того, нам нужно дополнить текст пробелами по краям (CXTEXTMARGIN), изображение значка по ширине (oButtonSize:Width), и сделать некоторый зазор между значком и текстом меню (CXGAP). В любом случае, нам нужно получить системные размеры с помощью GetSystemMetrics(). (SM_CYMENU – нам даст высоту меню. А SM_CXMENUCHECK – укажет ширину стандартного значка «галочка», который мы заменим на наш значок, и, следовательно, его ширину нужно вычесть из нашей общей ширины).
Рис. 1: Размеры пункта меню.
Рисунок 1, его я взял из статьи Paul DiLascia [2], наглядно показывает нужную информацию.
Эта информация хранится в структуре _winMEASUREITEMSTRUCT, которая определена в VO. Для того чтобы правильно заполнить эту структуру для рисуемого меню, при обработке сообщения WM_MEASUREITEM, мы должны проверить, равен ли член структуры CtlType значению ODT_MENU. Дело в том, что сепараторы (разделители в меню) требуют специальной обработки, так как они используют только половину высоты стандартного пункта меню.
Примечание переводчика: вообще-то здесь какое-то «словоблудие». Действительно, проверять ODT_MENU нужно, но означает оно лишь признак OwnerDrawTypeMenu. А делаем это лишь для того, чтобы убедиться, что перерисовываем именно меню, а не что-то другое (см. ODT_BUTTON, ODT_COMBOBOX, ODT_LISTBOX и т.д.). Ну, и разделители бывают разные, хотя их высота обычно меньше, чем у других пунктов меню…
4. Рисование пункта меню.
После кропотливой подготовки, наконец-то можно преступить к рисованию пункта меню. Как только Windows получит размеры нашего пункта, что мы сделали при обработке WM_ MEASUREITEM, он пошлёт нашему окну сообщение WM_DRAWITEM: по очереди, по одному для каждого элемента. И с информацией из _winDRAWITEMSTRUT, можно сделать всё, что угодно. Отображение цвета и тип 3D-края (raised или pressed) определяются ItemState (ODS_GRAYED, ODS_SELECTED или ODS_CHECKED). Также, важно учесть, надо ли рисовать какие-то значки. Может, надо нарисовать «галочку»? Вообще, значок не имеет значения, он просто ставится на свою позицию, как представлено на рис. 1.
5. Великая боль OwnerDraw-программирования.
Windows чрезвычайно жёстко относится к программистам, которые решили взять на себя труд написать OwnerDraw-программу. Не то, чтобы оно не работает. Но, нам придётся наново изобрести обработку подчеркнутых букв. Подчеркивания есть, но нам потребуется дополнительная обработка события, которое будет реагировать на WM_MENUCHAR. В этом примере, этот вопрос не решён. Но, он может быть раскрыт в следующем или в последующем выпуске SDT. Кстати, работа ускорителей (акселераторов, «горячих» клавиш) на самом деле не зависит от атрибута OwnerDraw, а это значит, что они работают в обычном режиме.
6. Чистоплотность.
Напоминаю, мы определили наши собственные структуры для расширения стандартной структуры MENUITEMINFO. А это значит, что при закрытии окна, мы должны не забыть освободить эти указатели.
7. Цвета и шрифты.
Часто в коммерческих приложениях забывают о системных настройках цветов и шрифтов. Пол DiLascia в своей статье [2] рекомендует использовать функции GetSysColor() и GetSystemMetrics() в том случае, если пользователь вдруг захочет выбрать свои цвета и шрифты. Кроме того, стоит добавить обработку сообщений WM_SYSCOLORCHANGE и WM_SETTINGCHANGE. Самые правильная реакция – это всё уничтожить и построить меню с нуля. В этой таблице перечислены только наиболее важные для меня параметры для GetSystemMetrics() и GetSysColor():
COLOR_MENU
|
Menu color
|
COLOR_MENUTEXT
|
Text color in Menu-Item
|
COLOR_HIGHLIGHT
|
Background color for selected Menu-Item
|
COLOR_HIGHLIGHTTEXT
|
Text color for selected Menu-Item
|
SM_CYMENU
|
Height of a Menu-Item
|
SM_CXMENUCHECK
|
Width of a Menu Checkmark
|
8. Последнее замечание.
Множество дополнительных проблем, связанных с созданием OwnerDraw-меню приоткрыл Paul DiLascia [2], но всё это выходит за пределы этой ознакомительной статьи.
Если, по прочтению этой статьи кто-то начал считать, что Office97 и Office2000 работают с OwnerDraw-меню – это неверно. Для этой цели Microsoft создала нечто новое: CommandBars. Объектная модель для этих COM-объектов описана в VBAOff8.HLP. К сожалению, эти объекты могут быть использованы только в самом MS-Office и его приложениях.
Пройдя через все неприятности написания этого VO-примера, я лёгкостью понимаю, почему VO-программисты стараются держаться подальше от разработки собственного меню и ждут, когда Microsoft включить CommandBars в ComCtl32.dll. Естественно, никаких гарантий этому нет.
Тем не менее, я решился сделать с ещё один пример с CustomDraw и написал TreeListView для Cayman, который будет представлен в следующем выпуске SDT, с версиями для VO и ClassMate.
Литература:
- MSDN Library, see under Platform SDK/User Interface Services/Windows User Interface/Resources/Menus/Using Menus/Creating Owner-Draw Menu Items.
- «Give Your Applications the Hot New Interface Look with Cool Menu Buttons» by Paul DiLascia, MS System Journal 1/98.
An excellent article, well worth reading, concerning the troubles of ownerdraw programming. How much the emotions can be involved, is showed by the following short quotations:
«Too much Windows programming isn´t healthy, you know»;
«... a waste of precious neurons»;
«... until my fingers were in danger of carpal collapse».
Dieter Crispien Software Entwicklung Vertrieb (dcSE)
Frauenlobstr, 24
80337 München
Falls Sie uns noch nicht als Lieferant aufgeführt haben, benötigen Sie sicher unsere USt-ID.
Diese lautet: DE 221 513 168.
Inhaltlich verantwortlich gemäß § 6 MDStV: Dieter Crispien
Продолжение следует…
09.07.2012 г. Карандаш.
P.S.: К большому сожалению, у меня нет оригинальных примеров к этой статье. Но, если кто-либо ими поделится, то я с удовольствием выложу их для всеобщего обозрения. И напишу ему хвалебную Оду.
Комментарии
День добрый
Да, конечно.
День добрый
Отличный сайт у вас, побольше бы таких.