воскресенье, 14 августа 2011 г.

Забытые элементы управления: функция MenuHelp

Это перевод The forgotten common controls: The MenuHelp function. Автор: Реймонд Чен.

Функция 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" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.