Функция
MenuHelp
- это одна из самых запутывающих функций в библиотеке общих элементов управления. К счастью, вам почти никогда не придётся её использовать, и когда вы узнаете её историю - вы не захотите её использовать.Наша история начинается с 16-битных Windows. Сообщение
WM_MENUSELECT
отправляется для уведомления окна об изменениях в состоянии выделения меню, ассоциированного с окном либо по факту создания окна с меню, либо через вызов функции вроде TrackPopupMenu
. Параметры сообщения WM_MENUSELECT
в 16-битных Windows были такими:wParam | = | ID элемента меню | если выделен простой пункт меню |
= | описатель pop-up меню | если выделено pop-up меню | |
lParam | = | MAKELPARAM(флаги, описатель меню-родителя) |
Функция
MenuHelp
передавала параметры сообщения WM_MENUSELECT
вместе с таблицей, описывающей проецирование между пунктами меню и подсказками, отображаемыми в строке статуса окна. Информация предоставлялась в запутанном формате массива UINT
, который имел такой вид (псевдо определение):
type TMenuHelpPopupUINTs = packed record uiPopupStringID: UINT; hmenuPopup: HMENU; end; TMenuHelpUINTs = packed record uiMenuItemIDStringOffset: UINT; uiMenuIndexStringOffset: UINT; rgwPopups: array[0..0] of TMenuHelpPopupUINTs; end;Поле
uiMenuItemIDStringOffset
указывало значение, которое нужно добавить к ID меню, чтобы получить ID строки, которая будет показываться в строке статуса. Т.е. если у вас был такой пункт меню:
MENUITEM "&New\tCtrl+N" ,200в вашем шаблоне меню, и вы указывали смещение 1000, то функция
MenuHelp
использовала строку с идентификатором 200 + 1000 = 1200:
STRINGTABLE BEGIN 1200 "Opens a new blank document." END
uiMenuIndexStringOffset
работало аналогично для всплывающих (pop-up) меню, которые были прямыми дочерними элементами пунктов главного меню, но поскольку pop-up меню не имели ID в 16-битных Windows, то вместо ID использовался индекс элемента меню, начиная от нуля. К примеру, если у вас была такая структура меню первого уровня:
BEGIN POPUP "&File" BEGIN ... END POPUP "&View" BEGIN ... END ENDи вы задавали
uiMenuIndexStringOffset
равное 800, то строка подсказки для пункта File должна была иметь идентификатор 0 + 800 = 800, а строка для View - 1 + 800 = 801.
STRINGTABLE BEGIN 800 "Contains commands for working with the current document." 801 "Contains edit commands." ENDПоследний случай - это когда pop-up меню является ещё более вложенным. Как мы видели выше, сообщение
WM_MENUSELECT
кодировало описатель pop-up меню, вместо его ID. Этот описатель искался в массиве переменной длины TMenuHelpPopupUINTs
(заканчивающимся элементом {0, 0}). Заметьте, что второй член записи TMenuHelpPopupUINTs
имеет тип HMENU
, а не UINT
. Но в 16-битной Windows, SizeOf(HMENU) = SizeOf(UINT) = 2, а 16-битные коды (вроде сообщения WM_MENUSELECT
) имели сильную зависимость от таких совпадений.Если pop-up окно имело описатель, скажем,
HMENU($1234)
, то функция MenuHelp
искала запись TMenuHelpPopupUINTs
, в которой поле hMenuPopup
было рано HMENU($1234)
, а затем она могла использовать соответствующее uiPopupStringID
для доступа к строке-подсказке.Давайте посмотрим на всё это на практике. Вот меню и соответствующая ему таблица строк:
1 MENU BEGIN POPUP "&File" BEGIN MENUITEM "&New\tCtrl+N" ,200 MENUITEM "&Open\tCtrl+O" ,201 MENUITEM "&Save\tCtrl+S" ,202 MENUITEM "Save &As" ,203 MENUITEM "" ,-1 MENUITEM "E&xit" ,204 END POPUP "&View" BEGIN MENUITEM "&Status bar" ,240 MENUITEM "&Full screen" ,230 POPUP "Te&xt Size" BEGIN MENUITEM "&Large" ,225 MENUITEM "&Normal" ,226 MENUITEM "&Small" ,227 END END END STRINGTABLE BEGIN 800 "Contains commands for loading and saving files." 801 "Contains commands for manipulating the view." 1200 "Opens a new blank document." 1201 "Opens an existing document." 1202 "Saves the current document." 1203 "Saves the current document with a new name." 1225 "Selects large font size." 1226 "Selects normal font size." 1227 "Selects small font size." 1230 "Maximizes the window to full screen." 1240 "Shows or hides the status bar." 2006 "Specifies the relative size of text." ENDЗаметьте, что нет никаких требований по последовательной нумерации пунктов меню. Всё, что волнует функцию
MenuHelp
- чтобы разница между идентификаторами меню и строк была бы постоянной константой, фиксированным смещением.Таблица, которая подключает меню к строковой таблице, выглядит так:
var rguiHelp: array[0..5] of UINT = ( 1000, // uiMenuItemIDStringOffset 800, // uiMenuIndexStringOffset 2006, // uiPopupStringID 0, // placeholder 0, 0 // конец TMenuHelpPopupUINTs );Поскольку тут есть "внучатое" pop-up меню, мы создали запись "placeholder", в которую будет записан описатель меню в run-time:
procedure TForm1.FormCreate(Sender: TObject); var hmenuMain: HMENU; hmenuView: HMENU; hmenuText: HMENU; begin hmenuMain := GetMenu(hwnd); hmenuView := GetSubMenu(hmenuMain, 1); hmenuText := GetSubMenu(hmenuView, 2); rguiHelp[3] := UINT(hmenuText); g_hwndStatus := CreateWindow(STATUSCLASSNAME, nil, WS_CHILD or CCS_BOTTOM or SBARS_SIZEGRIP or WS_VISIBLE, 0, 0, 0, 0, Handle, HMENU(100), HINSTANCE, 0); end;Мы находим меню "Text Size" и записываем его описатель в массив
rguiHelp
, так что MenuHelp
сможет его найти. Оконная процедура должна содержать такую строку:
... case Msg of .... WM_MENUSELECT: MenuHelp(uiMsg, wParam, lParam, GetMenu(hwnd), HINSTANCE, g_hwndStatus, rguiHelp); ...Этот последний шаг, наконец, соединяет воедино все кусочки. Когда приходит сообщение
WM_MENUSELECT
, функция MenuHelp
использует выбранный элемент для поиска соответствующей строки в ресурсах (по указанному HINSTANCE), загружает ресурс и показывает его в строке статуса окна.(Я бы предложил вам использовать вышеуказанный код для вставки в 16-битную программу и её тестовый запуск, но я сомневаюсь, что кто-то примет это приглашение, потому сегодня мало людей имеют доступ к 16-битному компилятору под Windows).
Этот метод отлично работал в 16-битных Windows. Но посмотрите, что происходит при переходе к 32-битным Windows: параметры сообщения
WM_MENUSELECT
были изменены на 32-битные значения. В двух 32-битных параметрах сообщений нет места, чтобы затолкнуть 48 бит информации (два оконных описателя и 16 бит флагов). Чем-то нужно пожертвовать и в жертву был отдан описатель pop-up меню. Вместо передачи описателя передаётся индекс pop-up меню в параметрах сообщения. Это не приводит к потере данных, т.к. описатель меню можно получить передачей описателя родительского меню и индекса в функцию GetSubMenu
. Новые параметры выглядят так:LOWORD(wParam) | = | ID элемента меню | если выбран обычный пункт меню |
= | индекс pop-up меню | если выбрано подменю | |
HIWORD(wParam) | = | флаги | |
lParam | = | описатель родительского меню |
Массив UINT изменил своё значение, чтобы отражать новую раскладку параметров:
type TMenuHelpPopupUINTs = packed record uiPopupStringID: UINT; uiPopupIndex: UINT; end; TMenuHelpUINTs = packed record uiMenuItemIDStringOffset: UINT; uiMenuIndexStringOffset: UINT; rgwPopups: array[0..0] of TMenuHelpPopupUINTs; end;Преимущество в изменении значения с
HMENU
до индекса UINT
заключается в отсутствии необходимости модифицировать массив в run-time. Okей, давайте попробуем:
var rguiHelp: array[0..5] of UINT = ( 1000, // uiMenuItemIDStringOffset 800, // uiMenuIndexStringOffset 2006, // uiPopupStringID, 2, // uiPopupMenuIndex 0, 0 // конец TMenuHelpPopupUINTs ); ... g_hwndStatus := CreateWindow(STATUSCLASSNAME, nil, WS_CHILD or CCS_BOTTOM or SBARS_SIZEGRIP or WS_VISIBLE, 0, 0, 0, 0, Handle, HMENU(100), HINSTANCE, 0); ... // Добавили к WndProc case Msg of ... WM_MENUSELECT: MenuHelp(uiMsg, wParam, lParam, GetMenu(hwnd), HINSTANCE, g_hwndStatus, rguiHelp); ...Заметьте, что код получился идентичен 16-битному, за исключением того, что теперь нам не надо инициализировать массив
UINT
описателем pop-up меню.Если вы запустите Win32 программу с таким кодом, то увидите, что текст в строке статуса меняется в зависимости от того, какой пункт меню вы выбрали. Функция
MenuHelp
также знает о командах в системном меню и предоставляет подсказки и по ним.Вау, вроде это звучит как клёвая функция. Почему же тогда я сказал, что вы не захотите её использовать? Давайте посмотрим на ограничения функции
MenuHelp
.Во-первых, заметьте, что строки для меню должны находиться в том же исполняемом модуле, что и само меню (они разделяют один HINSTANCE. Более того, смещение от идентификатора меню до строки постоянно для всех пунктов меню. Эти два кусочка означают, что вы не можете построить меню по частям, одна из которых приходит от сторонней DLL - посольку вы можете использовать только один HINSTANCE и одно смещение.
Во-вторых, фиксированное смещение означает, что вы не можете иметь меню с динамически создаваемым контентом - потому что у вас не будет строк для динамически создаваемых пунктов. Что ещё хуже: если динамически добавленный пункт меню случайно будет иметь такой идентификатор, при добавлении к которому смещения будет получаться допустимый строковый идентификатор - эта случайная строка будет использоваться как подсказка к этому динамическому пункту меню! К примеру, в нашем примере выше: если бы мы создали элемент меню, чей идентификатор оказался бы равен 1000, то функция
MenuHelp
искала бы строку с идентификатором 1000 + 1000 = 2000. И если вдруг у вас по этой позиции лежит какая-то строка (не связанная с меню) - то она будет показана в строке статуса окна.Но, как я надеюсь, к этому моменту вы уже заметили фатальный изъян в функции
MenuHelp
: индекс pop-up меню. Я аккуратно спроектировал пример выше, чтобы избежать этой проблемы. Индекс pop-up меню "Text Size" равен 2 - и это единственное pop-up меню с индексом 2 (меню "File" имеет индекс 0, а меню "View" - индекс 1). В реальной жизни, конечно же, у вас нет роскоши "размешивания" меню так, чтобы никакие два под-меню не имели бы одинакового индекса. А когда они будут иметь одинаковый индекс, то строки-подсказки для них будут перепутаны - потому что функция MenuHelp
не может сказать, какой из этих нескольких "вторых pop-up меню" вы хотели использовать для строки 2006.Можно ли это исправить? Если вы попытаетесь вернуться к старому способу идентификации с
HMENU
, то вы встретитесь с несколькими проблемами: во-первых, на 64-битных Windows вы не можете приводить HMENU
к UINT
, потому что HMENU
является 64-битным значением, а UINT
- только 32-битным. Вы могли бы обойти это, расширением параметра функции MenuHelp
с массива UINT
до массива UINT_PTR
, но это не единственная проблема.Механизм, основанный на
HMENU
, поддерживает только одно окно в один момент времени, потому что глобальный массив нужно исправлять для каждого клиента. Чтобы ввести поддержку нескольких окон, вам нужно сделать копию глобального массива и редактировать эту локальную копию. Чтобы избежать создания локальной копии, вам придётся придумать какой-то способ указания pop-up окна.В итоге, вы тратите ещё больше времени на решение проблем с
HMENU
, но даже это не решает другие проблемы, указанные выше. Попытка использования функции MenuHelp
для этих проблем ведёт к созданию ещё более запутанных механизмов для выражения отношения между элементом меню и его строкой-подсказкой. В конце концов, вы придете к точке, когда общее решение является слишком сложным для поставленной задачи, и вам лучше просто придумать специальное решение для конкретной ситуации, как это делали мы, когда мы добавили поддержку строк-подсказок в наши контекстные меню Оболочки.(Единственный люди, которые используют функцию
MenuHelp
IRL, просто не используют pop-up меню, избегая, таким образом, всех этих проблем с HMENU
).
Комментариев нет:
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.