Блуждания по лабиринту маршрутизации сообщений и команд в MFC

Автор: Поль ДиЛашиа (Paul DiLascia)
Перевод: Александр Шаргин
Источник: Microsoft Systems Journal

Версия текста: 1.0.1

Если вы хоть раз писали программу с использованием Visual C++ и MFC, вам должны быть знакомы сообщения и команды. Вы знаете, что MFC использует нечто под названием "карты сообщений", чтобы доставить сообщения Windows к вашим виртуальным функциям. Но быть с чем-то знакомым - ещё не значит понимать это. Как всё это работает? А что, если вам захочется сделать что-нибудь необычное?

Получается примерно так. Вы запускаете AppWizard, который создаёт для вас каркас приложения. Вы заполняете пробелы в нём. Пишете код для обработки OnFileNew и OnFileOpen. Вставляете собственную функцию OnDraw. Добавляете новые пункты в меню, и даже может быть соответствующие им кнопки на панель инструментов. С помощью ClassWizard соединить это всё вместе не составляет труда. Вы добавляете обработчики сообщений: WM_CLOSE, чтобы прибрать за собой, когда приложение будет завершаться, WM_SIZE, чтобы передвигать все дочерние окна, когда представление изменит свой размер. Вы уже почти летаете. Приложение растёт не по дням, а по часам. Сохранение работает, представление выводит множество окон, и все они корректно отрисовываются. У вас уже есть целая куча диалоговых окошек и стильный битмап в окне About. Жизнь прекрасна. Вы правильно сделали, переключившись на C++, не так ли?

А потом что-то случается.

Это что-то всегда случается. Возможно, это немодальный диалог или панель управления, и вам хочется использовать ON_UPDATE_COMMAND_UI, чтобы включать и выключать на них кнопки. Но это не работает. Или вам нужно обработать команду, идентификатор которой хранится в переменной, а не описан с помощью #define. А может быть, вы создали специальное дочернее окно и поместили в его карту сообщений несколько макросов ON_COMMAND. Но, что интересно, пункты меню остаются серыми. Вы запускаете отладчик и начинаете внимательно следить, как команды путешествуют по запутанному лабиринту. Вот Лямба вызывает Блямбу, Блямба вызывает Рюмбу, Рюмба вызывает Хрюмбу. А куда сообщение делось потом? Пуф! Сгинуло в чёрной дыре.

И что делать, если вы хотите направить команды к новым объектам? С чего начинать? Какую из множества виртуальных функций перегружать, чтобы изменить поведение MFC?

Маршрутизация команд в MFC - запутанная тема, даже для предполагаемого эксперта вроде меня. Когда я впервые попытался проследить путь простой команды ID_APP_EXIT, у меня получилась картина, похожая на рисунок 1. Судя по количеству вопросов, которые мне присылают, я не одинок.


Рисунок 1. Где Минотавр?

Поэтому позвольте мне быть вашим проводником в лабиринте маршрутизации сообщений и команд в MFC. Я покажу вам, как MFC обрабатывает команды отлично от остальных сообщений, почему пункты в меню иногда загадочным образом отключаются, как и когда обновляются объекты пользовательского интерфейса и как решить те проблемы, о которых я только что говорил. И это ещё не всё. Хотите отбросить архитектуру документ/представление ради чего-то другого? Я расскажу, как это сделать. Я даже покажу вам несколько хитростей, которые поразят ваших друзей. Не забудьте компас и золотую нить, мы отправляемся в путь.

Как безумие сделать забавой

У меня есть друг, который любит повторять, столкнувшись с серьёзной проблемой: "Как можно съесть слона? По кусочку за раз". Уж не знаю, зачем кому-то может понадобиться есть слона, но, полагаю, это не легче, чем разобраться в исходных кодах MFC. Чтобы объяснить все премудрости обработки команд и сообщений, я поделю объяснение на удобоваримые кусочки. В самом первом приближении, нам нужно рассмотреть два шага:

  1. Получить сообщение.
  2. Обработать его.

Просто, правда? Первый шаг подразумевает не только получение сообщения, но и его преобразование. Что до второго шага, MFC затрачивает столько усилий на обработку сообщений WM_COMMAND, что будет разумно рассмотреть их отдельно от всех остальных сообщений Windows. Кроме того, в MFC появилось новое понятие: объекты пользовательского интерфейса. Помните эти маленькие объекты класса CCmdUI, которые появляются всякий раз, когда нужно выключить пункт меню или кнопку на панели инструментов? Те самые, из обработчиков UPDATE_COMMAND_UI? Таким образом, новое деление слона на части выглядит так:

  1. Получить сообщение.
  2. Обработать его.
  3. Обработать объекты пользовательского интерфейса.

В последующих разделах я рассмотрю эти темы подробно. Я покажу вам алгоритм работы, объясню, как MFC реализует различное поведение для разных классов, а затем и как изменить стандартное поведение MFC.

Цикл сообщений

В былые времена, когда любой программист мог считать себя хакером, а C++ ещё не покинул исследовательскую лабораторию, все писали в своих программах функцию WinMain, содержащую примерно такой цикл:

 MSG msg;
 while (GetMessage(&msg, NULL, 0, 0)) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);  // send to window proc
 }

Этот цикл сообщений - сердце любого Windows-приложения. MSG - это структура, содержащая HWND, идентификатор сообщения, WPARAM и LPARAM, плюс ещё пару вещей. Вы получаете её, а затем отправляете по назначению. Просто и прямолинейно. Кроме разве что этого TranslateMessage в середине - что это? О, оказывается, это просто функция для преобразования сообщений WM_KEYDOWN и WM_KEYUP в WM_CHAR. Вот и всё. Что вы говорите? Вам нужны горячие клавиши? Вы хотите, чтобы Ctrl+X и Ctrl+V вызывали удаление и вставку? В этом случае вам потребуется TranslateAccelerator.

 MSG msg;
 HWND hwnd =       // your main window
 HACCEL hAccel =   // load from resource file
  
 while (GetMessage(&msg, NULL, 0, 0)) {
    if (!TranslateAccelerator(hwnd, hAccel, &msg)) {
       // Not an accelerator, so dispatch as normal.
       TranslateMessage(&msg);
       DispatchMessage(&msg);
    }
 }

TranslateAccelerator - это волшебная функция, которая добавляет горячие клавиши к вашему приложению. Она видит пролетающий мимо Ctrl+X, подсматривает в таблицу горячих клавиш и преобразует его в WM_COMMAND. В отличие от TranslateMessage она отправляет WM_COMMAND с идентификатором ID_EDIT_CUT прямо в оконную процедуру. Вашей программе кажется, что пользователь выбрал Edit->Cut из меню. TranslateMessage возвращает TRUE, указывая, что сообщение было преобразовано и отправлено, так что повторная отправка не требуется.

В Windows есть самые разные волшебные функции для преобразования сообщений: вы не знаете, что они делают и как, а просто используете их. Вы используете IsDialogMessage в немодальных диалогах, чтобы заработал Tab и быстрый доступ с помощью Ctrl. Вы могли подумать, что они должны работать и так - ведь работают же они в модальных диалогах, - ан нет. Ещё есть TranslateMDISysAccel для горячих клавиш в MDI-приложениях, таких как Ctrl+F6 для перехода к следующему окну и Shift+F5 для расположения окон каскадом. Да, чуть не забыл: если вам нужно выполнять фоновую обработку, лучше используйте PeekMessage вместо GetMessage.

К тому времени как вы разберётесь со всем волшебством, ваш цикл сообщений станет гораздо более сложным, чем версия программы "Hello, world!" для Windows.

Но не нужно волноваться. Прошло ещё несколько лет, и вот уже Бьярна Страуструпа знает весь мир, все кому не лень пишут программы для Windows, и даже у Микрософт появился свой компилятор C++. Жизнь стала настолько проще, что простым нажатием на кнопку можно генерировать новые приложения десятками. Циклы сообщений исчезли.

Но за всем этим объектным блеском скрываются старые добрые методы. Просто вы этого не замечаете. В недрах функции CWinApp::Run скрывается прародителель-всех-циклов-сообщений. MFC скрывает волшебство, вернее преподносит его в более удобном виде.

Чтобы понять, как это всё работает, задумайтесь на минуту и представьте себе ситуацию, которая могла случиться в былые времена. Вы только что закончили своё Супер Приложение, содержащее цикл сообщений в WinMain. Оно уже отлажено, начищено до блеска и ожидает только упаковки и отправки в магазин. Неожиданно приходит приказ свыше: вам нужно добавить функцию XYZ, вызывающую немодальное диалоговое окно. Вы спешите к клавиатуре. Тук-тук-тук и... диалог появляется. Правда, клавиша Tab не работает. Опс! Изрядно поломав голову, вы обнаруживаете, что забыли прибегнуть к волшебству: вам нужно было вызвать IsDialogMessage в цикле сообщений!

Если вы считаете, что так быть не должно, вы заслужили главный приз. Жизнь должна быть проще! Объекты должны сами реализовывать своё поведение! И если при этом нужно вызывать какие-то непонятные функции, это не должно происходить в WinMain. Ведь это всё равно что оперировать плечо, чтобы вылечить почку.

MFC исправляет ситуацию, позволяя окнам осуществлять собственное преобразование сообщений. В MFC сам диалог, а не функция WinMain, вызывает IsDialogMessage. Как же это работает? Чтобы разобраться, начнём с MFC-версии цикла сообщений. Он начинается в функции CWinApp::Run.

 int CWinApp::Run()
 {
 •
 •
 •
    for (;;) {
       while (!::PeekMessage(&m_msgCur,...)) {
          if (!OnIdle(...))    // do some idle work
             break;
       }
       // I have a message, or else no idle work to do: // pump it
       if (!PumpMessage())
          break;
    }
    return ExitInstance();
 }

Если в очереди нет сообщений, MFC вызывает функцию OnIdle, которую можно переопределить, чтобы делать что-нибудь полезное - например, искать пары простых чисел, отличающихся ровно на два - в фоновом режиме. Только не забудьте после этого вызвать CWinApp::OnIdle, иначе у вас будут неприятности. Если же в очереди есть новые сообщения, или фоновых задач больше не осталось, CWinApp вызывает PumpMessage, в которой и выполняется обычная процедура Get/Translate/Dispatch, как в старые добрые времена:

 BOOL CWinApp::PumpMessage()
 {
 •
 •
 •
    if (!::GetMessage(&m_msgCur,...)) {
       return FALSE;
    }
    if (!PreTranslateMessage(&m_msgCur)) {
       ::TranslateMessage(&m_msgCur);
       ::DispatchMessage(&m_msgCur);
    }
    return TRUE;
 }

Знакомо, правда? Всё, кроме PreTranslateMessage. Это новая виртуальная функция. Базовая реализация в классе CWinApp просматривает всю иерархию окон от окна, которому адресовано сообщение, через всех его родителей и прародителей и до окна самого верхнего уровня, вызывая для каждого из них функцию CWnd::PreTranslateMessage.

 BOOL CWinApp::PreTranslateMessage(MSG* pMsg)
 {
    for (pWnd = /* window that sent msg */; pWnd; pWnd=pWnd->getParent())
       if (pWnd->PreTranslateMessage(pMsg))
          return TRUE;
      
    if (pMainWnd = /* main frame and it's not one of the parents */)
       if (pMainWnd->PreTranslateMessage(pMsg))
          return TRUE;
 
    return FALSE;  // not handled 
 }

Именно так: и у CWinApp, и у CWnd есть своя функция PreTranslateMessage. Теперь окна могут преобразовывать сообщения. И если окно это делает, оно возвращает TRUE, которое передаётся вверх по стеку до самого конца. Сообщение обработано. Следующее, пожалуйста. Этот процесс показан на рисунке 2.


Рисунок 2. Цикл сообщений

Эта схема преобразования сообщений действительно удобна, потому что теперь проблема с диалогами решается очень элегантно. Взгляните, как в MFC реализована функция PreTranslateMessage для диалогов.

 BOOL CDialog::PreTranslateMessage(MSG* pMsg)
 {
  if (pMsg->message >= WM_KEYFIRST && // for performance
      pMsg->message <= WM_KEYLAST) 
 
    // maybe translate dialog key
    return ::IsDialogMessage(m_hWnd, pMsg); 
 
  return FALSE;
 }

Преобразовывать сообщения диалога в классе самого диалога гораздо логичнее, чем преобразовывать их в классе CWinApp. Ведь CWinApp ничего не знает о нашем диалоге. Конечно, вызывать ::IsDialogMessage требуется только для немодальных диалогов, но PreTranslateMessage и не вызывается для модальных диалогов, потому что когда вы вызываете DoModal, Windows запускает ещё один цикл сообщений и не возвращает управление, пока диалог не будет закрыт. [После написания статьи в архитектуру MFC были внесены некоторые изменения. Теперь PreTranslateMessage вызывается и для модальных диалогов - прим. перев.] Замечательно, что теперь вам никогда не придётся беспокоиться о ::IsDialogMessage! Вы даже можете забыть о её существовании: любой диалог, который вы пишете, унаследует всё необходимое от класса CDialog, даже если вы добавляете его в спешке в последнюю минуту. Если вы никогда не программировали без MFC, вы, вероятно, даже не слышали об IsDialogMessage. И это здорово.

Точно также, поскольку меню располагаются в главном окне, вполне логично поручить ему обработку горячих клавиш. Именно так и сделано. Когда вы создаёте главное окно в функции InitInstance, оно загружает таблицу горячих клавиш с идентификатором, описывающим шаблон документа (обычно это IDR_MAINFRAME или IDR_MYDOCTYPE). CFrameWnd сохраняет таблицу в переменной m_hAccel, которую CFrameWnd::PreTranslateMessage передаёт функции ::TranslateAccelerator, словно по волшебству преобразуя ваши горячие клавиши. Кстати, когда я писал эту статью, я обнаружил интересный факт: оказывается, сначала CFrameWnd пытается получить таблицу горячих клавиш у функции CDocument::GetDefaultAccelerator. Другими словами, если вам требуются специфичные для документа горячие клавиши, всё что вам нужно - переопределить эту функцию. Чего только не узнаешь, когда начнёшь копать поглубже!

На рисунке 3 показано, как в различных классах MFC реализована функция PreTranslateMessage. Стандартной обработки оказывается достаточно в 99.9% случаев, так что вы можете почти забыть о существовании PreTranslateMessage.

И ещё одно замечание. Всё, что было сказано о классе CWinApp, в 32-битных версиях MFC справедливо и в отношении класса CWinThread. В частности, в нём есть функции CWinThread::Run, CWinThread::OnIdle, CWinThread::PumpMessage и CWinThread::PreTranslateMessage. Я бы мог сказать об этом раньше, но не хотел пугать вас слишком рано. В многозадачном мире каждый поток имеет свой собственный цикл сообщений, а приложение - лишь частный случай потока. Поэтому большая часть функций переместилась из CWinApp в CWinThread, от которого CWinApp теперь порождается.

Одна оконная процедура на всю MFC

Большинство сообщений никогда не преобразуется. PreTranslateMessage возвращает FALSE, и CWinApp::PumpMessage вызывает функцию ::DispatchMessage, которая чем-то напоминает магический портал. Сообщение исчезает в нём, а затем появляется в вашей оконной процедуре (см. рисунок 4).


Рисунок 4. Маршрутизация сообщений в Windows

Вы, конечно, знакомы с оконной процедурой. Это та самая функция с гигантским оператором switch внутри, которую вы писали в былые времена. Теперь её пишут гораздо реже. В MFC оконных процедур нет. Вернее есть, но запрятаны очень глубоко. Их заменила универсальная оконная процедура - AfxWndProc. Как одна процедура может обслужить все окна? Очень просто. Она не делает ничего специфического.

 LRESULT
 AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
 {
 •
 • // minor details omitted
 •
    CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
    return AfxCallWndProc(pWnd, hWnd, nMsg, wParam,
                          lParam);
 }

CWnd::FromHandlePermanent просматривает карту и выбирает CWnd, связанный с HWND. Затем управление передаётся в AfxCallWndProc, аналог ::CallWindowProc в MFC.

 LRESULT AFXAPI 
 AfxCallWndProc(CWnd* pWnd,HWND hWnd, 
               UINT nMsg, 
               WPARAM wParam,
               LPARAM lParam)
 {
 •
 • // Save message params in a   //  state variable
   LRESULT lResult = pWnd->
WindowProc(nMsg, wParam, lParam);
 
 •
 •
 •
    return lResult;
 }

AfxCallWndProc вызывает WindowProc. Вместо того, чтобы передать HWND в оконную процедуру в стиле С, MFC вызывает виртуальную функцию. Таким образом, всё, что произошло до этого, было нужно только чтобы перейти от С к С++. Но это действительно замечательное достижение - потому что WindowProc виртуальная.

CWnd::WindowProc, что и подразумевает её имя, - эквивалент оконной процедуры в С++. В ней обрабатываются сообщения. А поскольку она виртуальная, разные оконные классы могут реализовывать её по-разному. Благодаря полиморфизму AfxWndProc работает со всеми окнами.

Как правило, сообщения не обрабатывают в CWnd::WindowProc, но нужно понимать, что никто не мешает вам поступать именно так. WindowProc - это первый этап, первая функция класса CWnd, в которую попадает сообщение. Если вы переносите программу, написанную на С, в MFC, вы можете просто скопировать всю вашу огромную оконную процедуру в CMainFrame::WindowProc, заменить "hwnd" на "m_hWnd", кое-что подправить, и всё должно заработать. Только никому не говорите.

Раз динь-динь, два динь-динь

Все знают, что сообщения нужно обрабатывать через карты сообщений. Где они используются? В CWnd::WindowProc. Первым делом эта функция проверяет, не пришло ли WM_COMMAND или WM_NOTIFY. Если это так, сообщение поворачивает и устремляется в другом направлении (мы проследим за ним позже). Все остальные сообщения остаются на главной дороге и направляются в соответствии с картой сообщений окна. CWnd::WindowProc действует как переключатель, направляющий сообщения в соответствующие обработчики.


Рисунок 5. Карты сообщений

Карты сообщений - это таблицы, которые связывают идентификаторы сообщений WM_XXX с виртуальными функциями С++ (см. рисунок 5). WM_SIZE направляется в OnSize. WM_CLOSE попадает в OnClose. И так далее. Детали процесса скрыты знакомыми макросами DECLARE_MESSAGE_MAP, BEGIN_MESSAGE_MAP, END_MESSAGE_MAP, а так же всеми этими ON_WM_ВСЁЧТОУГОДНО. Если вы заглянете за покровы, вы не найдёте ничего удивительного - просто некоторое количество дремучего кода. DECLARE_MESSAGE_MAP объявляет несколько переменных и, что самое главное, функцию GetMessageMap. Вот что препроцессор получает на входе:

 class CMyFrame : public CFrameWnd {
    DECLARE_MESSAGE_MAP()
 };
 
 BEGIN_MESSAGE_MAP(CMyFrame, CFrameWnd)
 
 •
 • // entries
 •
 END_MESSAGE_MAP()

А вот что получится в результате:

 class CMyFrame : public CFrameWnd {
 private: 
    static const AFX_MSGMAP_ENTRY _messageEntries[];
 protected:
    static AFX_DATA const AFX_MSGMAP messageMap; 
    virtual const AFX_MSGMAP* GetMessageMap() const;
 };
 
 // BEGIN_MESSAGE_MAP
 const AFX_MSGMAP* CMyFrame::GetMessageMap() const 
 { return &CMyFrame::messageMap; }
 
 const AFX_MSGMAP CMyFrame::messageMap = { 
    &CFrameWnd::messageMap,   // base class's message map
    &CMyFrame::_messageEntries[0] // this class's entries
 }; 
 
 const AFX_MSGMAP_ENTRY CMyFrame::_messageEntries[] = { 
 •
 • // entries// END_MESSAGE_MAP:
    {0, 0, 0, 0, 0, 0 } 
 };

Структура AFX_MSGMAP содержит всего две переменных: указатель на карту сообщений базового класса и указатель на собственные записи.

struct AFX_MSGMAP {
    const AFX_MSGMAP* pBaseMap;
    const AFX_MSGMAP_ENTRY* lpEntries;
 };

Указатель на карту базового класса позволяет переходить по цепочке наследования, реализуя наследование карт сообщений. Производные классы автоматически наследуют все сообщения, которые обрабатывает базовый класс. Если вы используете MFC в виде DLL, то pBaseMap на самом деле указывает на функцию, возвращающую указатель на карту базового класса, но это уже тонкости реализации. Сами записи выглядят так:

 struct AFX_MSGMAP_ENTRY {
    UINT nMessage;   // windows message
    UINT nCode;      // control code or WM_NOTIFY code 
    UINT nID;        // control ID (or 0 for windows 
                     // messages)
    UINT nLastID;    // used for entries specifying a 
                     // range of control id's
    UINT nSig;       // signature type (action) or 
                     // pointer to message #
    AFX_PMSG pfn;    // routine to call (or special 
                     // value)
 };

Каждая запись связывает сообщение Windows, включая идентификатор элемента управления и код уведомления (такой как EN_CHANGED или CBM_DROPDOWN), с функцией-членом класса, порождённого от CCmdTarget (AFX_PMSG). Поля nCode и nID появились в 32-битной MFC. Они поддерживают ON_NOTIFY и ON_COMMAND_RANGE. Каждый макрос ON_WM_ВСЁЧТОУГОДНО заполняет структуру для сообщения WM_ВСЁЧТОУГОДНО. Например, ON_WM_CREATE раскрывается так:

 { WM_CREATE, 0, 0, 0, AfxSig_is, 
    (AFX_PMSG)(AFX_PMSGW)(int (CWnd::*)(LPCREATESTRUCT))OnCreate },

На первый взгляд, это напоминает китайские иероглифы. С WM_CREATE всё понятно. Это идентификатор сообщения, по которому CWnd::WindowProc узнаёт, что данную запись нужно использовать в случае прихода WM_CREATE. Нули вставлены, потому что идентификаторы команд и идентификатор дочернего окна не используются для данного сообщения. Ужасное тройное приведение типа нужно, дабы удостовериться, что ваша функция OnCreate имеет корректную сигнатуру. Не во все макросы вставлены конкретные имена функций. ON_MESSAGE(msg, mbrfn) раскрывается так:

 { msg, 0, 0, 0, AfxSig_lwl, 
    (AFX_PMSG)(AFX_PMSGW)(LRESULT (CWnd::*)(WPARAM, LPARAM))mbrfn },

С макросом ON_MESSAGE вы можете использовать любую функцию, но она должна принимать в качестве параметров WPARAM и LPARAM и возвращать LRESULT. Если вы попытаетесь задать функцию другого вида, С++ будет ругаться.

Осталось установить назначение этих забавных символов AfxSig_xxx. Чтобы понять, зачем они нужны, давайте на минуту остановимся и задумаемся, откуда CWnd узнает, какие аргументы передать нашему обработчику. В AFX_MSGMAP_ENTRY каждая функция объявлена как AFX_PMSG, то есть указатель на функцию класса, произведённого от CCmdTarget, которая не принимает параметров и возвращает void.

typedef void (CCmdTarget::*AFX_PMSG)(void);

Так как же вызывающий код узнает, какие аргументы передать обработчику? Здесь-то и требуются коды AfxSig (сигнатуры). Вот как WindowProc вызывает вашу функцию, после того как найдена нужная запись в карте сообщений.

 LRESULT CWnd::WindowProc(UINT nMsg, WPARAM wParam, LPARAM lParam)
 {
 •
 •
 •

 const AFX_MSGMAP_ENTRY* lpEntry = // (entry for this 
                                   //  message)
 
 union MessageMapFunctions mmf; // described below ptr 
 mmf.pfn = lpEntry->pfn;        // to your virtual function 
 
 switch (lpEntry->nSig) {
 case AfxSig_is:
   return (this->*mmf.pfn_is)((LPTSTR)lParam);
 
 case AfxSig_lwl:
   return (this->*mmf.pfn_lwl)(wParam, lParam);
 •
 • // etc
 •
 }

AfxSig_is означает, что функция получает строку (string - s) и возвращает int (i). Afx_lwl означает, что она принимает WORD и LONG, а возвращает LONG (lwl). И так далее. В файле AFXMSG_.H описана целая туча различных сигнатур - 55, если быть точным.

 enum AfxSig {
    AfxSig_end = 0, // [marks end of message map]
 
    AfxSig_bD,      // BOOL (CDC*)
    AfxSig_bb,      // BOOL (BOOL)
    AfxSig_bWww,    // BOOL (CWnd*, UINT, UINT)
    AfxSig_hDWw,    // HBRUSH (CDC*, CWnd*, UINT)
    AfxSig_iwWw,    // int (UINT, CWnd*, UINT)
    AfxSig_iWww,    // int (CWnd*, UINT, UINT)
    AfxSig_is,      // int (LPTSTR)
    AfxSig_lwl,     // LRESULT (WPARAM, LPARAM)
 •
 •
 •
 };

Теперь идея должна быть ясна. Этот "union MessageMapFunctions mmf", который вы видели в WindowProc - просто хитрый способ привести функцию к нужному типу:

 union MessageMapFunctions 
 {
    AFX_PMSG pfn;   // generic member function pointer
 
    // specific type safe variants
    BOOL    (CWnd::*pfn_bD)(CDC*);
    BOOL    (CWnd::*pfn_bb)(BOOL);
    BOOL    (CWnd::*pfn_bWww)(CWnd*, UINT, UINT);
    HBRUSH  (CWnd::*pfn_hDWw)(CDC*, CWnd*, UINT);
    int     (CWnd::*pfn_iwWw)(UINT, CWnd*, UINT);
    int     (CWnd::*pfn_iWww)(CWnd*, UINT, UINT);
    int     (CWnd::*pfn_is)(LPTSTR);
    LRESULT (CWnd::*pfn_lwl)(WPARAM, LPARAM);
    •
    • // etc, for each AfxSig code
    •
 };

В объединение входит только одна настоящая функция (pfn), но в зависимости от того, как обратиться к объединению, она превращается в pfn_is - функцию, принимающую строку и возвращающую int, или в pfn_lwl - функцию, принимающую word и long и возвращающую long, и так далее. То, что снаружи выглядит очень симпатично, может дурно смотреться изнутри. К счастью, вы можете и не смотреть. Я показал вам всё, чтобы вы не подумали, что я хочу что-то от вас скрыть, а также чтобы вы порадовались, что вам не придётся писать подобный код самостоятельно.

А теперь я покажу, как использовать всё это, если у вас имеются наклонности мазохиста. Допустим, вы хотите задать пару своих собственных сообщений. WM_RUN_FOR_CONGRESS и WM_KICK_THE_BUMS_OUT. Первое получает указатель на CCongressionalDistrict, а второе - указатель на CListOfBums. Самый простой путь обработать такие сообщения - использовать макрос ON_MESSAGE и обыкновенное приведение типа:

 BEGN_MESSAGE_MAP(...)
    ON_MESSAGE(WM_RUN_FOR_CONGRESS, OnRunForCongress)
 
 •
 •
 •
 END_MESSAGE_MAP()
 
 LRESULT OnRunForCongress(WPARAM wp, LPARAM, lp)
 {
    CCongressionalDistrict* pCd = (CCongressionalDistrict*)lp;
 •
 •
 •
 }

Но допустим, что вы пишете библиотеку. Возможно даже расширение для MFC. Вы не хотите, чтобы программист задумывался, что же хранится в LPARAM. Или вы просто очень привередливы, и не хотите использовать ON_MESSAGE с приведением типа. Вы хотите сделать макросы ON_WM_RUN_FOR_CONGRESS and ON_WM_KICK_THE_BUMS_OUT, которые можно было бы вставить в карту сообщений. Нет проблем!

Но какой же код сигнатуры использовать? Конечно AfxSig_is. Функцию, принимающую строку и возвращающую число. Потому что когда дело доходит до размещения параметра в стеке, указатель на строку ничем не отличается от указателя на CCongressionalDistrict или указателя на любой другой тип. Один адрес, четыре байта (если используются дальние указатели или Win32). Посмотрите внимательнее на макрос ON_WM_CREATE - в нём тоже используется AfxSig_is! Поэтому ваш макрос будет выглядеть так:

 #define ON_WM_RUN_FOR_CONGRESS() \
    { WM_RUN_FOR_CONGRESS, 0, 0, 0, AfxSig_is, (AFX_PMSG)(AFX_PMSGW) \
       (int (CWnd::*)(CCongressionalDistrict*))OnRunForCongress },
 
 BEGIN_MESSAGE_MAP(...)
    ON_WM_RUN_FOR_CONGRESS()
 •
 •
 •
 END_MESSAGE_MAP()
 
 // Returns int to agree with AfxSig_is.
 int OnRunForCongress(CCongressionalDistrict* pCd)
 {
    pCd->RunForHouse();
    pCd->RunForSenate();
    return 0;   // mission accomplished
 }

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

Как, нет обработчика?

Если в карте сообщений не обнаружилось нужной записи, WindowProc вызывает CWnd::DefWindowProc, которая, как вы уже догадываетесь, является аналогом стандартной оконной процедуры ::DefWindowProc в C++. Она вызывает предыдущую оконную процедуру, ту, которая обслуживала окно до того, как её подменила MFC. Часто это как раз и бывает ::DefWindowProc. Или стандартная процедура поля ввода или дочернего окна MDI. Как бы то ни было, это конец: сообщение обработано, управление возвращается назад, назад, в AfxWndProc, через портал и из функции ::DispatchMessage. Готово. Конец. Следующее сообщение, пожалуйста.

Другой вариант обработки сообщения - в одном из стандартных обработчиков CWnd::OnВсёЧтоУгодно. Когда вы обрабатываете сообщение, вы как правило вызываете стандартный обработчик после выполнения специфических для вашего приложения действий.

 CMyWnd::OnFooMumbleBletch()
 {
 •
 •
 •
    CWnd::OnFooMumbleBletch();
 }

Стандартные обработчики выглядят так.

 // (From AFXWIN2.INL)
 inline void CWnd::OnSize(UINT, int, int) { Default(); }
 inline void CWnd::OnSetFocus(CWnd*)      { Default(); }
 inline BOOL CWnd::OnNcActivate(BOOL)     
  // return (BOOL)Default(); }
 •
 • // etc

Функция Default() очень похожа на DefWindowProc; разница в том, что ей не нужно передавать аргументы (msg, wParam и lParam), потому что они берутся из структуры состояния, в которую их записала AfxCallWndProc. Помните?

Если вам это интересно, половину слона вы уже проглотили.

Злобный WM_COMMAND

Я уже говорил, что CWnd::WindowProc направляет сообщения WM_COMMAND в другом направлении. Пора рассмотреть и этот путь.

WM_COMMAND - чрезвычайно загруженное сообщение. Windows посылает его, когда пользователь выбрал команду с помощью меню или горячей клавиши. WPARAM показывает, как именно. Но элементы управления тоже используют WM_COMMAND, чтобы отправлять уведомления. Например, кнопка посылает BN_CLICKED, а поле ввода - EN_CHANGED. Когда это происходит, HWND элемента управления и его идентификатор, а также код уведомления упаковываются в WPARAM и LPARAM, не оставляя места для дополнительной информации. Как именно они упаковываются, зависит от используемой вами платформы - 16-разрядная Windows или Win32.

Такая перегрузка WM_COMMAND имеет одно приятное преимущество: нажатие на кнопку выглядит для вашего приложения так же, как и вызов команды из меню. Хотя на самом деле это совершенно разные события. Команда означает "сделай что-то", а уведомление - "что-то случилось". В более современных версиях Windows, использующих обновлённый набор элементов управления, появилось новое сообщение WM_NOTIFY, введённое, чтобы разделить эти события. WM_NOTIFY - это обобщённая форма уведомления от элементов управления. Вместо запихивания всего, что только можно, в параметры сообщения, в LPARAM размещается указатель на структуру NMHDR.

 struct NMHDR {
    HWND hwndFrom;     // control that sent notification
    UINT idFrom;       // ID of control
    UINT code;         // notification code
 };

NMHDR используется как базовый заголовок, к которому элементы управления могут добавлять дополнительную информацию. Например, всплывающая подсказка передаёт следующую структуру:

 struct TOOLTIPTEXT {  // In C++, you can derive from NMHDR
     NMHDR hdr;        // standard header
     LPSTR lpszText;   // tip text or LPSTR_CALLBACK
     char szText[80];  // tip text
     HINSTANCE hinst;
     UINT uFlags;
 };

Детали структур TOOLTIPTEXT и NMHDR для нас несущественны. Главное, что существует два типа событий: команды меню и уведомления от элементов управления. WM_NOTIFY - это всегда уведомление, тогда как WM_COMMAND может быть и тем, и другим. CWnd::WindowProc обрабатывает оба события специальным образом, как показано на рисунке 6.


Рисунок 6. Поток сообщений WM_COMMAND и WM_NOTIFY

 LRESULT CWnd::WindowProc(UINT msg, WPARAM wp, LPARAM lp)
 {
    // special case for commands
    if (msg = = WM_COMMAND)  {
       if (OnCommand(wp, lp))
          return 1L; // command handled
       else
          return DefWindowProc(msg, wp, lp);
    }
 
    // special case for notifies
    if (msg = = WM_NOTIFY) {
       LRESULT lResult = 0;
       NMHDR* pNMHDR = (NMHDR*)lp;
       if (pNMHDR->hwndFrom != NULL && 
                             OnNotify(wp, lp, &lResult))
          return lResult; // command handled
       else
          return DefWindowProc(msg, wp, lp);
    }
 •
 •
 •
 }

Если WindowProc - первая остановка на пути обработки сообщения, то OnCommand - первая остановка для всех сообщений WM_COMMAND. Если вы не можете или не хотите использовать карты сообщений для обработки команд, вы можете перегрузить OnCommand. Это приходится делать, если код команды или элемента управления хранится в переменной, а не определён с помощью #define.

Для функции OnCommand можно подыскать множество удобных применений, но истинные причины обработки команд и уведомлений отдельно от остальных сообщений носят более фундаментальный характер: это нужно, чтобы их могли обрабатывать неоконные объекты, а также чтобы элементы управления могли обработать собственные уведомления.

Дети знают лучше

Сколько раз я говорил об этом? Объекты должны сами реализовывать своё поведение! Это относится и к элементам управления. Они посылают уведомления, когда происходит что-то интересное, например, когда пользователь отредактировал текст в поле ввода или щёлкнул на кнопке комбинированного списка. Уведомления полезны, но часто гораздо удобнее обработать уведомление в самом элементе управления.

Допустим, в вашем приложении по танцевальной хореографии есть комбинированный список, который вы заполняете танцевальными фигурами на лету, когда пользователь раскроет список. Обычный способ сделать это под Windows - добавить в функцию диалога обработчик уведомления CBN_DROPDOWN, заполняющий список. А если мы захотим вставить список в другой диалог? Он не является повторно используемым! Если, конечно, не считать повторным использованием кода методику "скопировать-и-вставить".

Зачем такие сложности, когда можно сделать полностью самодостаточный список, обрабатывающий своё собственное CBN_DROPDOWN? Всё, что для этого нужно - это переопределить OnChildNotify. MFC вызывает эту виртуальную функцию всякий раз, когда родительское окно получает уведомление от дочернего.

 BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam)
 {
 •
 •
 •
 // if WM_COMMAND is really a child notification:
 if (pChild->OnChildNotify(message, wParam, lParam, pLResult))
       return TRUE;
 •
 •
 •
 }

Точно так же работает и функция OnNotify. OnCommand и OnNotify в первую очередь дают дочерним окнам возможность обрабатывать собственные уведомления. Так что комбинированный список можно написать так:

 BOOL CMamboCombo::OnChildNotify(UINT msg, WPARAM wp, LPARAM lp,LRESULT* pResult)
 {
    if (msg= =WM_COMMAND) {
       int nCode = // extract notification code, depends 
                   // on Win version 
       if (nCode= =CBN_DROPDOWN) {
          // fill combo box
          return FALSE;  // Pass to parent dialog
       }
    }
    return CComboBox::OnChildNotify(msg, wp, lp, pResult);
 }

Теперь CMamboCombo полностью самодостаточен. Возвращать ли из обработчика TRUE или FALSE - зависит от вас. Вероятно, лучше возвращать FALSE, чтобы и диалог мог узнать о том, что кнопка комбинированного списка была нажата. Решать вам.

Подобно PreTranslateMessage, OnChildNotify призвана исправить неудачную архитектуру Windows. MFC использует его и для других сообщений. Так, она направляет сообщения пользовательского рисования (WM_COMPAREITEM, WM_MEASUREITEM, WM_DRAWITEM и WM_DELETEITEM) обратно в элементы управления, чтобы они могли отрисовывать себя сами. Между прочим, эта единственная причина, по которой в OnChildNotify передаётся LRESULT*. WM_COMPAREITEM - одно из немногих сообщений, для которых возвращаемое значение играет важную роль: положительное или отрицательное число, а также ноль указывают на результат сравнения.

Прародитель всех получателей

О детях хватит. Другая важная причина обрабатывать команды отдельно от остальных сообщений - позволить неоконным объектам перехватывать их. Вы конечно знаете, что документы могут обрабатывать команды - достаточно вставить ON_COMMAND в карту сообщений документа. И вам, возможно, даже не приходило в голову, что Windows не предусматривает такой возможности! Её предоставляет архитектура документ/представление в MFC.

Откуда же у документа вообще появились карты сообщений? Дело в том, что CDocument порождён от CCmdTarget, предка всех классов, которые могут иметь карты сообщений и, следовательно, обрабатывать их. CDocument, CWnd, CWinApp (или CWinThread) и CDocTemplate - все эти классы порождены от CCmdTarget, чья единственная задача - обеспечить общую базу для обработки команд.

 CObject
   CCmdTarget
     CWnd
     CWinThread       // (Win32 only)
       CWinApp
     CDocTemplate
     CDocument

А сердцем класса CCmdTarget является функция CCmdTarget::OnCmdMsg, следующая важная остановка на пути следования команд. Если дочернее окно не обработало уведомление, а также если сообщение было не уведомлением, а командой, OnCommand вызывает OnCmdMsg. Примерно так же ведёт себя и OnNotify.

CCmdTarget::OnCmdMsg - тоже виртуальная функция. Она является аналогом WindowProc у получателей команд, отправляя команды и уведомления в нужные обработчики. WindowProc - функция класса CWnd, отправляющая сообщения, а OnCmdMsg - функция класса CCmdTarget, отправляющая команды и уведомления. OnCmdMsg использует из карты сообщений только записи, построенные с помощью макросов, перечисленных на рисунке 7. Она ищет в карте сообщений запись, чьи ID и nCode (в случае уведомления) соответствуют параметрам WM_COMMAND или WM_NOTIFY. Если такая запись найдена, вызывается соответствующий обработчик. Я не буду докучать вам деталями - здесь происходят все те же трюки с AfxSig, что и в WindowProc.

Но подождите. Нам всё ещё не понятно, как документы обрабатывают команды! WindowProc вызывает OnCommand, которая вызывает OnCmdMsg, но объект, в котором это происходит - всё равно окно, получившее команду! Так как же документы получают команды? Дело в том, что я пока рассказал вам только о CCmdTarget::OnCmdMsg. Не забывайте, что эта функция виртуальная, и другие классы могут выполнять в ней совсем другие операции. В частности, CFrameWnd переопределяет OnCmdMsg, чтобы переправлять команды представлениям и объекту самого приложения.

 BOOL CFrameWnd::OnCmdMsg(...)
 {
    if (pActiveView->OnCmdMsg(...))
       return TRUE;      // handled by view
 
    if (CWnd::OnCmdMsg(...))
       return TRUE;      // handled by me
 
    if (pApp->OnCmdMsg(...))
       return TRUE;      // handled by app
 
    return FALSE;        // not handled
 }

Рисунок 8 показывает последовательность действий. Угадайте, что делает функция CView::OnCmdMsg? Совершенно верно, она вызывает OnCmdMsg, принадлежащую документу. Как мы видим, команды автоматически направляются не во все имеющиеся в программе объекты CCmdTarget, а только в активное представление, документ, главное окно и приложение. И только потому, что так реализован класс CFrameWnd.


Рисунок 8. Маршрутизация команд в архитектуре документ/представление

До сих пор вся маршрутизация, о которой я рассказывал, происходила в пределах одного окна (и его дочерних окон в случае с OnChildNotify). Пока не вмешивается CFrameWnd, ни одно сообщение не направляется за пределы окна. Это объясняет одну из загадок, упомянутых в начале: почему иногда при добавлении дочернего окна пункты меню остаются серыми, даже если в карту сообщений этого окна вставлены макросы ON_COMMAND? Ответ: потому что команды в дочернее окно автоматически не направляются. Они направляются только в активное представление, документ, главное окно и приложение. Если вы хотите, чтобы произвольное дочернее окно CGizmoWnd тоже обрабатывало команды, вы должны направить их туда вручную. Не бойтесь, это просто.

 BOOL CMyView::OnCmdMsg(...)
 {
    if (CView::OnCmdMsg(...))
       return TRUE;   // handled by view/doc
 
    return m_wndGizmo.OnCmdMsg(...); // pass to gizmo
 }

Можно вызвать CGizmoWnd::OnCmdMsg и до вызова OnCmdMsg базового класса. В любом случае, вы должны сами вызывать OnCmdMsg для любого объекта класса, порождённого от CCmdTarget, которому вы хотите направлять сообщения. Если вас удивляет, почему MFC не делает этого за вас, подумайте, насколько долго, да и ни к чему, было бы вызывать OnCmdMsg для всех используемых в программе объектов CCmdTarget. Архитектура документ/представление - это всего лишь модель, поддерживаемая MFC. Никто не мешает вам создать свою собственную модель, но чтобы она заработала, придётся чуть-чуть поработать руками.

У описанной системы всё-таки есть один недостаток: новые объекты никак не могут сами подключить себя к механизму маршрутизации сообщений. Приложение, главное окно или любое другое окно - кто-то должен явно вызывать OnCmdMsg для новых объектов. Было бы неплохо иметь возможность регистрировать новых получателей команд в приложении (скажем, с помощью CWinApp::RegisterCmdTarget), чтобы новые объекты могли получать команды, не вовлекая в этот процесс своих родителей. В этом случае ваше CGizmoWnd смогло бы получать команды без необходимости править CMainFrame::OnCmdMsg. Надеюсь, что какой-нибудь доброжелатель из Редмонда прочтёт эту статью и добавит такую возможность в следующую версию MFC.

Ой-ой, GUI

Мы уже почти закончили. Если вы ещё не заметили, мы заглотили две части слона: получение/преобразование сообщений и их обработку. Остался один последний раздел - объекты пользовательского интерфейса. В очередной раз начну с урока истории.

Много лет назад, вскоре после изобретения колеса, но незадолго до появления высококачественного телевидения, гоминиды испытывали трудности с инициализацией меню в приложениях для Windows. Вместо сбрасывания камня на голову ничего не подозревающего пользователя, они хотели отключать пункты меню, соответствующие недоступным командам. Главный вопрос состоял в том, когда включать и отключать пункты меню: каждый раз, когда состояние программы изменяется, или по требованию, когда меню требовалось отрисовать.

Неандертальцы выбрали первый вариант, а кроманьонцы - второй. Это объясняет не только причину исчезновения неандертальцев, но и причину возникновения сообщения WM_INITMENUPOPUP. Windows посылает это сообщение перед тем как нарисовать меню. В ответ вы можете включать и отключать пункты меню, расставлять галочки и переключатели, изменять текст - словом, делать всё, что угодно. Это было значительным техническим достижением, но код по-прежнему располагался в огромном switch'е вместе с остальными обработчиками. Это означало, что состояние программы должно было быть доступным из оконной процедуры. К счастью, глобальные переменные уже были изобретены.

А затем появились C++ и MFC, и наши друзья из Редмонда сказали нам: "Нет, нет, нет. Так не пойдёт! Каждый пункт меню - это маленький объект пользовательского интерфейса! И каждый, кто хочет, должен иметь возможность включить или выключить его." Вот так появились объекты CCmdUI. Любой получатель сообщений может использовать их. Благодаря этому тот объект, который больше всех "знает" о том, каково должно быть состояние определённого пункта меню, может включать и выключать его, устанавливать или снимать галочку и т. п. Если команда File->Save включается только когда документ изменился, включение и выключение этой команды следует возложить на документ. Если представление - лучшее место, чтобы отключить команду Window->Split, делайте это в представлении. И если объект класса CBeanCounter знает, сколько бобов нужно нарисовать на панели, ему вполне можно поручить рисование.

MFC реализует эту удобную возможность, используя уже готовый механизм маршрутизации команд. Уж если магистраль для команд построена и функционирует, послать по ней во все концы специальное сообщение совсем несложно. Это сообщение - CN_UPDATE_COMMAND_UI. Оно попадает во все функции OnCmdMsg, как и WM_COMMAND. Но вместо WPARAM и LPARAM MFC подготавливает нечто под названием CCmdUI и передаёт вам указатель на него. Макрос ON_UPDATE_COMMAND_UI добавляет в карту сообщений обработчик CN_UPDATE_COMMAND_UI, и - вуаля! - объект пользовательского интерфейса пожаловал в гости. Понимание механизма усложняют только два нюанса. Во-первых, существует несколько разновидностей объектов CCmdUI для работы с пунктами меню, элементами управления, кнопками на панели инструментов и панелями строки состояния. Во-вторых, сообщения CN_UPDATE_COMMAND_UI могут быть отправлены в нескольких местах.

Я начну с первого нюанса, так как он проще. Базовый класс CCmdUI используется для работы с пунктами меню и элементами управления. Он содержит указатель на меню или на окно, а также набор виртуальных функций для изменения состояния связанного с ним объекта.

 class CCmdUI {
    CMenu* m_pMenu;      // if a menu
    CWnd*  m_pOther;     // if a window
 •
 •
 •
 public:
    virtual void Enable(BOOL bOn = TRUE);
    virtual void SetCheck(int nCheck = 1);
    virtual void SetRadio(BOOL bOn = TRUE);
    virtual void SetText(LPCTSTR lpszText);
 
    void DoUpdate(CCmdTarget* pTarget, 
                 BOOL bDisableIfNoHndler);
 };

CCmdUI::SetText вызывает ModifyMenu или SetWindowText (в зависимости от того, какой указатель установлен). Точно также CCmdUI::Enable использует EnableMenuItem или EnableWindow. Производные классы CStatusCmdUI и CToolCmdUI используются для панелей строки состояния и для кнопок на панели инструментов соответственно. CStatusCmdUI::SetText вызывает SetPaneText, а СCToolCmdUI::SetText вообще ничего не делает, поскольку на кнопках панели инструментов рисуются только картинки. Теперь идея должна быть ясна. MFC использует разные классы для работы с различными объектами пользовательского интерфейса. Полиморфизм скрывает от вас эти различия.

Что касается отправки CN_UPDATE_COMMAND_UI, то, если вы подумали о WM_INITMENUPOPUP, вы на правильном пути. Когда CFrameWnd получает WM_INITMENUPOPUP, он создаёт объект класса CCmdUI, инициализирует его последовательно для каждого пункта меню, а затем вызывает DoUpdate. Детали этого процесса довольно утомительны, но базовая идея выглядит примерно так:

 void CFrameWnd::OnInitMenuPopup(CMenu* pMenu, UINT, BOOL bSysMenu)
 {
 •
 • // Reader's Digest version
 •
    CCmdUI ui;
    ui.m_nIndexMax = pMenu->GetMenuItemCount();
    for (ui.m_nIndex = 0; ui.m_nIndex < ui.m_nIndexMax; ui.m_nIndex++) {
       ui.m_nID = pMenu->GetMenuItemID(ui.m_nIndex);
       ui.DoUpdate(this, m_bAutoMenuEnable);
    }
 }

Я опустил несколько деталей, чтобы сосредоточиться на CCmdUI::DoUpdate. Это та самая функция, которая посылает сообщение CN_UPDATE_COMMAND_UI.

 void CCmdUI::DoUpdate(CCmdTarget* pTarget, BOOL bDisableIfNoHndler)
 {
 •
 •
 •
    pTarget->OnCmdMsg(m_nID, CN_UPDATE_COMMAND_UI, this, NULL)
 •
 •
 •
 }

Первый параметр этой функции, pTarget, задаёт получателя команд, чья функция OnCmdMsg и будет вызвана, и чья карта сообщения будет использована для отправки сообщения. Обычно это главное окно, а значит сообщение попадёт также в активный вид, документ и приложение.

Второй параметр указывает, нужно ли отключать объект, если для него нет обработчика. CFrameWnd передаёт в качестве этого параметра m_bAutoMenuEnable, который по умолчанию равен TRUE. Вы, вероятно, заметили, что MFC автоматически отключает пункты меню, для которых нет обработчиков. Так, если вы добавите пункт File->Barf в главное меню, но не напишете обработчик команды ID_FILE_BARF, команда будет выключена. Теперь вы знаете, почему.

Откуда DoUpdate знает, есть обработчик команды или нет? Это одна из тех деталей, которые я опустил. DoUpdate передаёт функции OnCmdMsg код CN_COMMAND, как если бы она собиралась выполнить команду, но кроме этого она передаёт небольшую структуру AFX_CMDHANDLERINFO. Присутствие этой структуры сообщает OnCmdMsg, что нужно не выполнять команду, а указать функцию, которая отвечает за её выполнение.

 struct AFX_CMDHANDLERINFO
 {
    CCmdTarget* pTarget;             // command target
    void (CCmdTarget::*pmf)(void);   // message map 
                                     // function
 };
 
 BOOL CCmdTarget::OnCmdMsg(UINT nID, int nCode, void* pExtra,
                       AFX_CMDHANDLERINFO* pHandlerInfo)
 {
 •
 •
 •
    if (pHandlerInfo != NULL)
    {
       // just fill in the information, don't do the 
       // command (actually happens in 
       // CCmdTarget::DispatchCmdMsg)
       pHandlerInfo->pTarget = this;
       pHandlerInfo->pmf = mmf.pfn;
       return TRUE;
    }
 •
 •
 •
 }

Если OnCmdMsg вернёт FALSE, команду никто не обрабатывает. Так что если когда-нибудь вам потребуется узнать, обрабатывается ли заданная команда или уведомление, и каким объектом, просто создаёте на стеке структуру AFX_CMDHANDLERINFO и передаёте её в OnCmdMsg (вместе с кодом команды и уведомления, разумеется). Если вам вернут TRUE, команду кто-то обрабатывает, и вы можете заглянуть в AFX_CMDHANDLERINFO, чтобы узнать, кто именно.

Идея, стоящая за автоматическим отключением, состоит в том, что при активизации различных представлений или главных окон команды, не имеющие обработчиков, будут автоматически отключаться. Это удобная возможность, так как для реализации этого важного случая управления меню вам не придётся написать ни строчки кода. Тем не менее, иногда может понадобиться продемонстрировать интерфейс программы до того, как что-либо реализовано. В этом случае автоматическое отключение будет вам мешать. Самый лучший способ избавиться от него - это написать общий обработчик-заглушку OnNotImplemented, а затем подключить к нему все нереализованные команды. А если вы ленивы или не хотите забивать голову подключением каждой команды к OnNotImplemented, просто установите m_bAutoMenuEnable=FALSE в конструкторе главного окна.

Вот и всё по поводу меню. А что на счёт панелей инструментов и строк состояния? Когда ваше приложение или поток просто болтается, ничего не делая, функция CWinThread::OnIdle отправляет главному окну и всем его потомкам WM_IDLEUPDATECMDUI. Это специфичное для MFC сообщение - сигнал об обновлении для каждого элемента. CToolBar и CStatusBar перехватывают его и обновляют свои кнопки и панели. Если вас интересуют детали, обратитесь к исходным текстам.

Есть ещё одна функция, о которой вам полезно знать: CWnd::UpdateDialogControls. Эта функция обновляет элементы управления окна (обычно, диалога), посылая каждому из них CN_UPDATE_COMMAND_UI.

 void 
 CWnd::UpdateDialogControls(CCmdTarget*
   pTarget, BOOL bDisableIfNoHndler)
 {
 •
 •
 •
    CCmdUI ui;
    for (pCtrl = /* each child
            control in "this" */) {
     ui.m_pOther = pCtrl;
     // it's a window, not a menu
     ui.m_nID=pCtrl->GetDlgCtrlID();
     ui.DoUpdate(pTarget,
                 bDisableIfNoHndler);
    }
 }

Эта функция позволит вам использовать механизм ON_UPDATE_COMMAND_UI в диалогах. Вы можете написать для диалога карту сообщений и обработчики, как вы делаете это для представления или главного окна, а затем вызвать UpdateDialogControls, чтобы обновить кнопки. В качестве pTarget следует передать получателя команд, чья карта сообщений содержит нужные обработчики. Обычно это сам диалог или главное окно. UpdateDialogControls необходимо вызывать всякий раз, когда вы хотите произвести обновление. В немодальных диалогах можно делать это, когда главное окно получает WM_IDLEUPDATECMDUI, но это не будет работать для модальных диалогов, поскольку во время работы модального диалога ваш цикл сообщений приостановлен. Поэтому для модальных диалогов обычно проще вызывать UpdateDialogControls, когда происходит нечто, влияющее на состояние элементов управления (что-то вроде улучшенной версии старого неандертальского подхода). Можно также вызывать её, когда родительское окно получает WM_ENTERIDLE, которое Windows посылает, когда диалог переходит в режим ожидания.

Заключение

Уф! На сегодня теории достаточно. На рисунке 9 показана полная схема маршрутизации сообщений и команд. По сравнению с рисунком 1 это явный прогресс.


Рисунок 9. Исправленный рисунок 1

Ну, вот и всё. Надеюсь, что сегодня вы поняли, на что это похоже - съесть слона. Если что-то осталось для вас непонятным, почитайте Технические заметки №6 "Карты сообщений" и №21 "Маршрутизация сообщений и команд". Если же вы поняли всё до конца, можете смело назвать себя гуру в маршрутизации сообщений.


Статья "Блуждания по лабиринту маршрутизации сообщений и команд в MFC" Поля ДиЛашия была опубликована в Microsoft Systems Journal в июле 1995 года, copyright Miller Freeman Inc. и размещена здесь с разрешения. Все права защищены. За дополнительной информацией об MSDN Magazine (бывшем Microsoft Systems Journal) обратитесь по адресу http://msdn.microsoft.com/msdnmag.