Глава 5.
Поговорим о сообщениях...
Хороша, плоха ли весть. —
Докладай мне все как есть!
Леонид Филатов
После того как мы познакомились со структурой классов библиотеки и рассмотрели минимальную программу на ее основе, поддерживающую однооконный интерфейс, необходимо разобраться в вопросах взаимодействия всех обязательных и вспомогательных объектов между собой.
Обработка сообщений в библиотеке MFC
В литературе по языку C++ иногда проскальзывает такая терминология — методы класса называются сообщениями. И в этом есть некоторый смысл. Благодаря свойству инкапсуляции "общение" с некоторыми компонентами класса осуществляется только посредством определенных методов. Мы не будем приписывать слову "сообщение" еще одно значение, достаточно того, что "класс" и "объект" перегружены настолько, что иногда трудно сформулировать нормальное предложение, описывающее ту или иную операцию. Однако с учетом этого обстоятельства становится очевидным, что использование библиотеки классов MFC должно было оказать влияние на структуру программ для Windows, скрыв в классах многочисленные операторы switch или "взломщики" сообщений. Давайте для начата рассмотрим, каким образом реализуется обязательный цикл обработки сообщений при использовании библиотеки MFC.
При рассмотрении наших первых программ мы только коснулись такого важного вопроса, как обработка сообщений. И дело тут, как вы уже догадываетесь, в том, что библиотека MFC опять взяла на себя решение этой важнейшей задачи. Ниже представлена функция Run из класса CWinThread. который, как вы помните, отвечает за работу с потоками:
int CWinThread::Run()
{
// для отслеживания состояния времени ожидания
BOOL bldle = TRUE;
LONG HdleCount = 0;
// получает и распределяет сообщения,
// пока не получено сообщение WM_QUIT for (;;)
{
// фаза!: проверка возможности работы в период ожидания
while(bldle && !::PeekMessage(&m_msgCur, NULL,
NULL, NULL, PM_NOREMOVE))
{
// вызов Onldle, пока установлена bIdle
if (!Onldle (UdleCount++) )
bldle = FALSE; // присвоить состояние "нет ожидания"
}
// фаза2: подкачка сообщений, пока возможно
do
{
// подкачка сообщения и выход по WM_QUIT
if (!PumpMessage())
return Exitlnstance ();
// сброс состояния "нет ожидания" после подкачки
// "нормального" сообщения
if (IsIdleMessage(&m_msgCur))
{
bIdle = TRUE; HdleCount = 0;
}
}
while(::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
}
}
Хотя показанный цикл внешне и отличается от стандартного, практически по всем компонентам они совпадают. Сообщения выбираются из очереди при помощи функции API :: Peek Message, а работу по поддержке клавиатуры, выбору оконной процедуры и передачи в нee сообщений выполняет недокументированная функция PumpMessage из класса CWinThread (вы можете в этом убедиться, если посмотрите ее исходный текст в файле ..\MFC\SRC \THRDCORE.CPP).
Основное отличие заключается в наличии специального механизма, позволяющего перехватить у системы циклы фоновой обработки путем вызова переопределяемой функции Onldle класса вашего приложения. Такая возможность появляется, когда приложение принимает сообщения с помощью ::PeekMessage. Если очередь сообщений пуста, то функция возвращает FALSE. Отметим, что цикл обработки сообщений MFC требует специальной обработки сообщения WM_QUIT, поскольку PeekMessage не реагирует на это сообщение так, как это делает функция ::GetMessage. Эту работу выполняет упоминавшаяся функция PumpMessage.
И еще несколько слов в заключение вводной части. Представленный цикл включает в себя две фазы: первая предназначена для внутреннего употребления библиотекой MFC, а во время второй вы получаете возможность выполнять любые задачи. Более того, если очередь приложения остается пустой "достаточно длительное время" (довольно неточная формулировка, но, к сожалению, привести конкретные цифры не представляется возможным по вполне понятным причинам), то вы можете получить дополнительные вызовы OnIdle в течение одного цикла фоновой обработки. При этом параметр HdleCount позволяет отслеживать номер вызова. Однако следует иметь в виду, что при поступлении очередного сообщения HdleCount сбрасывается в нуль.
Конечно, не всякая программа в системе Windows требует фоновой обработки, и можно просто игнорировать эту возможность. Тем более, что реализация функции OnIdle в классе CWinApp обновляет объекты интерфейса пользователя и очищает внутренние структуры данных, т. е. как обычно выполняет за вас всю черновую работу.
Рассмотренный цикл не демонстрирует, каким образом происходит собственно обработка сообщений. Неужели необходимо переопределять оконную процедуру? Для ответа на этот вопрос следует рассмотреть, как организована обработка сообщений в библиотеке классов MFC. Но перед этим рекомендую обратиться к приложению 2, чтобы узнать, о каких сообщениях вообще идет речь.
В отличие от рассмотренной классификации сообщений, в библиотеке MFC принята несколько иная. Все возможные сообщения разделены на три основные категории:
В первую категорию входят те сообщения, которые начинаются с префикса WM_, за исключением WM_COMMAND. Все сообщения этой категории предназначены для обработки окнами и представлениями (view) и часто содержат параметры, которые определяют алгоритм обработки того или иного сообщения. Сюда входят, например, аппаратные сообщения, сообщения обслуживания окна и т. д.
Вторая категория включает извещения (notification messages) от элементов управления и других дочерних окон, направляемые своим "родителям". Например, элемент управления LISTVIEW посылает своему родительскому окну сообщение WM_COMMAND, содержащее код извещения LVN_SETDISPINFO, когда требуется обновить информацию об элементах списка. Оконная процедура отвечает на полученное извещение заполнением структуры LV_DISPINFO и передачей ее обратно в элемент управления. Механизм передачи извещений аналогичен другим WМ_-сообщениям, с одним единственным исключением. Извещение BN_CLICKED, посылаемое элементом управления BUTTON, когда пользователь щелкает по нему, трактуется как командное и передается аналогично другим командам.
Сообщения этих двух категорий предназначены для объектов классов, образованных от базового класса CWnd, т. е. имеющих дескриптор HWND окна Windows. Их обработка осуществляется в соответствующей оконной процедуре.
Наконец, третья категория охватывает все сообщения WM_COMMAND. называемые командами (или командными сообщениями), от объектов интерфейса пользователя, которые включают меню, кнопки панелей инструментов и командные клавиши (accelerator keys). Обработка команд осуществляется способом, отличающимся от обработки других сообщений, и может производиться множеством объектов, включающим документы, шаблоны документов и сам объект "приложение".
Независимо от способа разделения сообщений по тем или иным категориям, они существуют для того, чтобы их обрабатывали, т. е. при получении сообщения некоторая часть программы должна выполнить определенную последовательность действий. При рассмотрении оконной процедуры говорилось, что поступающие в нее сообщения обрабатываются индивидуально. Там же мы упомянули о существовании специального типа функций, каждая из которых отвечает за обработку одного и только одного сообщения. Аналогичный подход реализован и в библиотеке классов MFC, где для обработки каждого отдельного сообщения используется специальная функция-обработчик. Например, можно иметь одну функцию-обработчик для сообщения WM_PAINT, другую — для сообщения WM_KEYUP и третью — для сообщения WM_LBUTTONDOWN. Все функции-обработчики являются функциями какого-либо класса, и для них используются названия: функция-член обработчик сообщения, функция-обработчик сообщения или просто обработчик сообщения. Принципы работы обработчиков сообщений не изменяются от того, к какой категории относится сообщение, однако механизм их вызова различен. Но прежде чем рассматривать механизмы посылки и получения сообщений, необходимо понять, каким образом сопоставляются сообщение и соответствующий ему обработчик.
Библиотека классов MFC предоставляет альтернативу многочисленным операторам switch, используемым в традиционных Windows-программах для обработки сообщений, посылаемых окну. Взаимосвязь сообщений и их обработчиков может быть определена таким образом, что при поступлении сообщения в оконную процедуру соответствующий обработчик вызывается автоматически. Реализация такой взаимосвязи основана на понятии Карты (или Таблицы) сообщений (message map).
Карта сообщений представляет, собой механизм пересылки сообщений и команд Windows в окна, документы, представления и другие объекты приложения, реализованного на базе MFC. Такие карты преобразуют сообщения Windows, извещения элементов управления, а также команды меню, кнопок панелей инструментов, командных клавиш клавиатуры в функции соответствующих классов, которые их обрабатывают. Эта особенность карты сообщений реализована по аналогии с виртуальными функциями C++, но имеет дополнительные преимущества, не доступные для них. Каждый класс, который может получить сообщение, должен иметь свою карту сообщений для того, чтобы соответствующим образом обрабатывать сообщения. При этом следует иметь в виду, что карта сообщений должна определяться вне какой-либо функции или объявления класса. Она также не может размещаться внутри С-блока.
Для определения карты сообщений используются три макроса: BEGIN_ MESSAGE_MAP, END_MESSAGE_MAP и DECLARE_MESSAGE_MAP.
Макрос DECLARE_MESSAGE_MAP располагается в конце объявления класса, использующего карту сообщений:
class CTheClass : public CBaseClass
{
//объявления компонентов класса
protected:
//{{AFX_MSG_MAP(TheClass)
afx_msg void OnPaint();
//}}AFX_MSG_MAP,
DECLARE_MESSAGE_MAP()
};
Он используется для объявления трех специальных компонентов класса, о которых нужно знать, но которые практически никогда непосредственно не используются.
Структура карты сообщений достаточно проста и представляет собой набор макросов, заключенных в специальные "операторные скобки":
BEGIN_MESSAGE_MAP(CTheClass, CBaseClass))
// (AFX_MSG_MAP!CTheClass)
ON_WM_CREATE ()
ON_WM_DESTROY()
ON_COMMAND(ID_CHAR_BOLD, OnCharBold)
ON_UPDATE_COMMAND_UI(ID_CHAR__BOLD, OnUpdateCharBold)
// {{AFX_MSG_MAP
ON NOTIFY(FN_GETFORMAT, ID_VIEW_TORMATBAR, OnGetCharFormat) ON_NOTIFY(FN_SETFORMAT, ID_VIEW_FORMATBAR, OnSetCharFormat)
END_MESSAGE_MAP()
Примечание
При использовании специальных средств автоматической генерации программного кода — AppWizard и ClassWizard — все три описанных макроса, а также специальные "операторные скобки" для ClassWizard (//{{AFX_MSG_ MAP(CTheClass) и //{(AFX_MSG_MAP) формируются автоматически при создании оконного класса.
Как видите, начинается карта с выполнения макроса:
BEGIN_MESSAGE_MAP(CTheClass, CBaseClass)
который имеет следующие параметры: CTheClass — задает имя класса, владельца карты сообщений; CBaseClass — определяет имя базового класса.
Заканчивается карта сообщений вызовом макроса END_MESSAGE_MAP, имеющего еще более простой формат:
END_MESSAGE_MAP()
Между этими двумя вызовами располагаются специальные макросы, называемые компонентами карты сообщений. Вот они-то, собственно, и позволяют сопоставить сообщение с конкретным обработчиком.
Для каждого стандартного сообщения Windows определен свой макрос в форме:
ON_WM_XXX // XXX - имя сообщения, например, ON__WM_PAINT.
Имена обработчиков определяются при распаковке параметров каждого сообщения Windows на основе простого соглашения. Имя всегда начинается с префикса "On", за которым следует имя соответствующего сообщения Windows (без префикса WM_), записанное строчными буквами. Например, для сообщения WM_ PAINT в классе CWnd определен обработчик
afx msg void OnPaint () ;
а для сообщения WM_LBUTTONUP — обработчик
afx_msg void OnLButtonUp(
UINT nFlags,
CPoint point);
За формирование параметров, своих для каждого конкретного сообщения (например, nFlags и point — для сообщения WM_LBUTTONUP), отвечает соответствующий макрос. Описание всех обработчиков стандартных сообщений Windows можно найти в файле <afxwin.h> в объявлении класса CWnd.
Командные сообщения Windows от меню, командных клавиш и кнопок панелей инструментов обрабатываются макросом:
ON_COKMAND(id, nemberFn)
Этот макрос в качестве параметров использует идентификатор команды id и произвольное имя обработчика команды memberFn. Прототип обработчика должен быть описан в соответствующем классе и иметь вид:
afx__msg void memberFn();
Макрос ON_COMMAND__EX, определяющий расширенную форму обработчика, является надмножеством над функциональными возможностями макроса ON_COMMAND. Такие обработчики команд принимают единственный параметр типа UINT, содержащий идентификатор команды, и возвращают значение типа BOOL: если возвращается FALSE, то команда передается для обработки другому объекту-"получателю" команды, в противном случае обработка завершается.
Команды обновления выполняются посредством того же механизма, что и для "обычных" команд, только при этом используется макрос
ON_QPDATE_COMMAND_UI(id, memberFn)
Обработчики таких команд имеют вид:
afx_msg void memberFn (CCmdUI *pCmdUI);
В обоих случаях обработчик команды — memberFn — будет вызван, только если в оконную процедуру поступит сообщение, которое в параметре wParam содержит идентификатор команды, совпадающий с id.
Следующая группа макросов позволяет сопоставить с соответствующим обработчиком команды, посылаемые элементами управления и другими дочерними окнами своим родителям.
Для обработки извещений (notification message) от элементов управления применяется макрос
ON__CONTSOL (wNotifyCode, id, memberFn)
Этот макрос в качестве параметров использует код извещения (notification code) wNotifyCode от элемента управления, идентификатор элемента управления id и произвольное имя обработчика команды memberFn, прототип которой должен быть описан в соответствующем классе и иметь вид:
afx_msg void memberFn();
Так же, как и для обработчиков команд, для каждого извещения необходимо использовать один и только один макрос ON_CONTROL. Обработчик memberFn вызывается только в том случае, если код извещения wNotify от элемента управления (например, от BN_CLICKED) совпадает с кодом извещения, определенным в компоненте карты сообщений, а значение параметра id совпадает с идентификатором элемента управления.
Помимо рассмотренного макроса, общего для всех элементов управления и дочерних окон, для кнопок (BUTTON), элементов редактирования (EDIT), списка (LISTBOX) и комбинированного списка (СОМВОВОХ) используются специальные макросы, например
ON_BN_LICKED (id, memberFn),
ON_EN_SETFOCUS(id, memberFn),
ON_LBN_DBLCLK(id, memberFn)
и многие другие. Полный список макросов этой группы можно найти в документации или в файле <afxmsg.h>. Их принцип действия полностью совпадает с принципом действия макроса ON_CONTROL.
С появлением в системах Windows 95 и Windows NT 4.0 более сложных дополнительных элементов управления (common controls) имеющихся макросов оказалось недостаточно, т. к. для этих элементов вместе с сообщением зачастую должны быть посланы и некоторые данные. В связи с этим общие элементы управления Windows используют более мощное сообщение WM_NOTIFY, а в версиях Visual C++, начиная с 4.0, имеют прямую поддержку для этого сообщения на базе макроса:
ON_NOTIFY(wNotifyCode, id, memberFn)
Как легко видеть, параметры этого макроса полностью аналогичны параметрам уже рассмотренного макроса ON_CONTROL. Единственное, но существенное, отличие заключается в параметрах обработчика memberFn.
afx_msg void memberFn(
NMHDR *pNotifyStruct,
LRESULT *result);
В качестве параметров обработчика используются: pNotifyStruct — указатель на структуру типа NMHDR или большую, включающую ее в себя: result — указатель на переменную, куда должно быть записано возвращаемое значение.
Ниже приведен список извещений, общих для всех дополнительных элементов управления:
NM_CLICK
Пользователь нажал левую кнопку мыши в элементе управления
NM_DBLCLK
Пользователь дважды щелкнул левой кнопкой мыши в элементе управления
NM_RCLICK
Пользователь нажал правую кнопку мыши в элементе управления
NM_RDBLCLK
Пользователь дважды щелкнул правой кнопкой мыши в элементе управления
NM_RETURN
Пользователь нажал клавишу <Enter>, когда элемент управления имел фокус ввода
NM_SETFOCUS
Элемент управления получил фокус ввода NM_KILLFOCUS Элемент управления потерял фокус ввода
NM_OUTOFMEMORY
Элемент управления не может завершить какую-либо операцию из-за нехватки памяти
Если необходимо использовать один обработчик для нескольких команд или извещений от элементов управления, то можно воспользоваться соответствующими макросами
ON_COMMAND_RANGE(idFirst, idLast, memberFn) ON_CONTROL_RANGE(wNotifyCode, idFirst, idLast, memberFn) ON_NOTIFY_RANGE(wNotifyCode, idFirst, idLast, memberFn)
Эти макросы в качестве параметров использует два идентификатора idFirst и idLast, которые составляют непрерывный ряд. Обработчик memberFn будет вызываться всякий раз при поступлении в оконную процедуру сообщения, содержащего в параметре wParam идентификатор команды, границы которого определяются параметрами idFirst и idLast. Прототип обработчика должен иметь вид:
afx_msg void memberFn(UINT nID);
для первого и
afx_msg void memberFn(
UINT nID,
NMHDR *pNotifyStruct,
LRESULT *result);
для двух последних макросов.
Аналогичный макрос существует и для команд обновления
ON_UPDATE_COMMAND_UI_RANGE(idFirst, idLast, memberFn)
Обработчик, как и для макроса ON_UPDATE_COMMAND_UI, имеет единственный параметр — указатель на объект CCmdUI.
afx_msg void memberFn(CCmdUI *pCmdUI);
Для извещений по аналогии с макросом ON_COMMAND_EX определены также макросы:
ON_NOTIFY_EX(nCode, id, memberFn)
ON_NOTIFY__EX_RANGE(wNotifyCode, idFirst, idLast, memberFn)
Команды, определяемые пользователем, включаются в карту сообщений при помощи макроса
ON_MESSAGE(WM_NAMEMSG, OnNameMsg)
В качестве параметров этот макрос использует номер — WM_NAMEMSG и имя обработчика сообщения — OnNameMsg. Очевидно, что необходимо определить как номер сообщения, так и его функцию обработки:
#define WM_NAMEMSG (WMJJSER + 100)
afx_msg LRESULT OnNameMsg(
WPARAM wParam,
LPARAM IParam );
Понятно, что в этом случае всю ответственность по использованию параметров wParam и IParam берет на себя программист.
Сообщения, определяемые пользователем, включаются в карту при помощи макроса:
ON_REGISTERED_MESSAGE(nMessageVariable, memberFn)
В качестве параметров этот макрос использует идентификатор зарегистрированного сообщения — nMessageVariable и имя обработчика — memberFn. Обработчик должен иметь следующий прототип
afx_msg LRESULT meraberFn(
WPARAM wParam,
LPARAM IParam);
Для получения уникального идентификатора сообщения необходимо вызвать функцию Windows API "RegisterWindow Message, описанную в этой главе ниже.
Теперь, познакомившись с общей структурой карты сообщений и ее компонентами, можно переходить к вопросу о том, в какой последовательности и кем осуществляется поиск обработчика сообщения или команды. Если еще раз внимательно посмотреть на иерархию классов библиотеки MFC, приведенную в главе 1, то можно заметить, что все классы, которые могут получать сообщения, являются производными от класса CCmdTarget.
Подобно тому, как класс CObject является базовым для практически всех классов библиотеки MFC, класс CCmdTarget выступает в качестве базового для всех классов, которые могут работать с сообщениями. Из всех компонентов этого класса для нас интерес представляет один:
virtual BOOL CCmdTarget: :OnCmdMsg (
UINT nID,
int nCode,
void* pExtra,
AFX_CMDHANDLERINFO *pHandler!nfo)
Функция устанавливает маршрут и распределяет командные сообщения. Если посмотреть поставляемые исходные тексты, то видно, что это единственная функция (не считая оконной процедуры по умолчанию), которая вызывается в оконной процедуре. Это означает, что именно она занимается поиском соответствующих обработчиков сообщений.
В качестве параметров функция принимает: пЮ — идентификатор команды; nCode— код извещения; pExtra — определяется значением nCode; pHandlerlnfo — обычно равен NULL, в противном случае функция сама заполняет элементы pTargetw pmf структуры pHandlerlnfo.
Если функция нашла адресата, который обработает сообщение, то она возвращает TRUE, в противном случае FALSE.
Таким образом, основным назначением OnCmdMsg является распределение команды по другим объектам. В принципе, эту функцию можно переопределить, однако при этом не следует забывать, что на вас перекладывается задача указания следующего получателя команды, чтобы не прервалась цепочка передачи сообщений.
Чтобы продемонстрировать практические аспекты обработки сообщений Windows, вернемся немного назад, в предыдущую главу, и покажем описанный механизм "в действии" на примере сообщения WM_CREATE.
Как вы помните, при создании главного окна приложения Styles необходимо было создать и обработчик сообщения WM_CREATE, в котором формировались дочерние и всплывающие окна, а также устанавливались их параметры. Последовательность действий при этом следующая:
// Файл MainFrm.h
class CMainFrame : public CFrameWnd
{
...
// Generated message map functions
protected:
// {{AFX__MSG (CMainFrame)
afx_msg int OnCreate(LPCREATESTRUCT IpCreateStruct);
//}}AFX_MSG DECLARE_MESSAGE_MAP()
};
// Файл MainFrm.cpp
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
//{{AFX_MSG_MAP(CMainFrame)
ON_WM_CREATE()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
Рис. 5.1. Создаем обработчик сообщения WM_CREATE
Примечание
Комментарии на английском языке я оставил без перевода, чтобы лишний раз подчеркнуть, что они добавлены не мной, а мастером ClassWizard.
Теперь, после этого небольшого практического отступления от теоретических положений, займемся рассмотрением достаточно важного вопроса — каким образом осуществляется поиск обработчика команды.
Когда Run— функция класса CwinThread — получает сообщение, она направляет его в соответствующее окно, являющееся получателем, в котором сообщение будет обрабатываться (вами или самой библиотекой — сейчас неважно). Несколько иначе все выглядит для команд. Поскольку они формируются в результате взаимодействия пользователя с программой, то обычно команда начинает свой путь к получателю от главного окна приложения. Напомним, что каждый объект, способный получать сообщения, располагает своей картой сообщений. Что же происходит, когда какой-либо объект получает сообщение? А все очень просто. Имея свою карту сообщений, получатель ищет в ней поступившую команду (или сообщение) и в случае успеха запускает соответствующий обработчик. Сразу же возникает вопрос: "А если не найдет?" Ну что ж, в этом случае поиск переносится в карту сообщений базовых классов. А если и там нет ассоциированного обработчика, то главное окно передает эту команду следующему кандидату на обработку. Для его определения в библиотеке MFC определен стандартный маршрут, по которому передается команда. Этот процесс продолжается до тех пор, пока команда либо будет выполнена, либо будет отправлена оконной процедуре, действующей по умолчанию.
Порядок, в котором адресаты передают команды, "зашит" в библиотеке классов MFC и представляет собой следующую последовательность.
• активное окно класса CMDIChildWnd;
• сам объект класса CMDIFrameWnd;
• приложение-объект класса CWinApp или производного от него.
• активное представление (view);
• текущий фрейм документа;
• приложение-объект класса CWinApp или производного от него.
• текущее представление;
• документ, присоединенный к представлению.
• текущий документ;
• шаблон документа, присоединенный к нему.
• текущий блок диалога;
• окно, которое владеет этим диалогом;
• приложение-объект класса CWinApp или производного от него.
Очевидно, что представленные здесь переходы взаимозависимы. Например, если команду получает документ, присоединенный к текущему представлению (view), то следующим объектом, которому будет передана команда, является текущий документ, а затем его шаблон.
Примечание
Пусть вас не смущает, что многие перечисленные объекты мы еще не рассматривали и они вам могут быть незнакомы. Каждый из них мы рассмотрим подробно, а пока познакомьтесь с общими принципами, просто приняв их на веру.
Чтобы сказанное стало более понятным, рассмотрим небольшой пример. Пользователь работает с MDI-приложением и хочет выделить весь текущий документ (для реализации этого действия в объекте класса документа приложения предусмотрен специальный обработчик). Он выбирает пункт меню Edit/Select All (Правка/Выделить все), с которым ассоциирована команда ID_EDIT_SELECT_ALL. Первым эту команду получает главное окно приложения, которое дает возможность обработать его текущему активному дочернему окну MDI. До того, как дочернее окно просмотрит свою карту сообщений, оно дает возможность обработать команду своему представлению (view). Представление проверяет свою карту сообщений и, поскольку не находит там обработчика, направляет команду в ассоциированный с ним документ. Теперь уже документ ищет обработчик в своей карте сообщений, находит его и начинает обрабатывать команду. Если бы мы не реализовали обработчик в нашем объекте документа, то команда была бы направлена в шаблон, и в случае неудачи поиск продолжился в объекте класса CMDIFratneWnd, а затем в объекте класса приложения. Наглядно описанная последовательность действий представлена на рис. 5.2. Для команды, совершающей этот или любой другой маршрут, вызывается метод OnCmdMsg следующего объекта.
Рис. 5.2. Пример обработки команды ID_EDIT_SELECT_ALL
В отличие от команд, стандартные сообщения Windows не используют какого-либо маршрута для поиска обработчика, т. к. они посылаются непосредственно в оконную процедуру того объекта, который должен соответствующим образом отреагировать на них. Но и в этом случае каждый оконный класс, образованный непосредственно или косвенно из класса CWnd, имеет собственную карту сообщений, которая устанавливает связи между сообщениями и соответствующими обработчиками. Если соответствующий обработчик не найден в карте сообщений некоторого класса, то он ищется в карте сообщений базового для него класса. При этом более эффективным является использование механизма виртуальных функций, определяемых для стандартных сообщений Windows в классе CWnd.
Команды обновления и класс CCmdill
Когда пользователь раскрывает меню, каждый его элемент "должен знать", как он будет отображаться — доступным, отмеченным или нет. Получатель команды предоставляет ему эту информацию, реализуя обработчик сообщения ON_UPDATE_COMMAND_UI, внутри которого используется специальный класс CCmdUI, определяющий или элемент меню, или кнопку панели инструментов, или любой другой объект интерфейса пользователя, способный формировать команды. Этот обработчик вызывается до вывода элементов интерфейса пользователя на экран (или их перерисовки), что позволяет изменить их внешний вид. Маршрут поиска обработчика совпадает с тем, который действует для "обычных" командных сообщений (рис. 5.3).
Рис. 5.3. Пример обработки команды обновления для ID_EDIT_SELECT_ALL
Рассмотрим возможности, заложенные в классе CCmdUI для обновления элементов интерфейса пользователя, поскольку многими из них нам придется активно пользоваться буквально в следующей главе. Начнем с элементов данных класса.
UINT CCmdUI::m_nID
Идентификатор объекта интерфейса пользователя.
UINT CCmdUI::m_nlndex
Индекс объекта интерфейса пользователя.
CMenu CCmdUI::m_pMenu
Указатель на меню, представляемое объектом CCmdUI. Если представляется не меню, то этот параметр равен NULL.
CMenu CCmdUI: :m_pSubMenu
Указатель на имеющееся подменю, представляемое объектом CCmdUI. Если представляется не меню, то этот параметр равен NULL. Если подменю является раскрывающимся, то параметр m_nID содержит идентификатор его первого элемента.
CWnd* CCmdUI::m_pOther
Указатель на объект окна, который послал извещение (notification), например, панель управления или строка состояния. Если извещение пришло от меню или не от оконного объекта, то этот параметр равен NULL.
Кроме представленных компонентов, у класса имеются 5 методов.
virtual void CCmdUI::Enable (BOOL bOn = TRUE)
Блокирует (bOn равно FALSE) или разблокирует (bОn равно TRUE) элемент интерфейса пользователя, ассоциированный с этой командой.
virtual void CCmdUI::SetCheck (int nCheck = 1)
Устанавливает элемент интерфейса пользователя в одно из возможных состояний в зависимости от передаваемого параметра: 0 — не помечено, 1 — помечено и 2 — неопределенное состояние. Последнее состояние применимо только к кнопкам панелей инструментов.
virtual void CCmdUI::SetRadio (BOOL bOn = TRUE)
Блокирует (bОn равно FALSE) или разблокирует (bОn равно TRUE) группу переключателей.
virtual void CCmdUI::SetText (LPCTSTR IpszText)
Устанавливает текст для этого элемента интерфейса пользователя.
void CCmdUI::ContinueRouting ()
Позволяет направить команду на обработку дальше по маршруту. Эта функция действует совместно с обработчиком команды ON_COMMAND_EX, который возвращает FALSE.
Как я уже сказал, практические аспекты использования перечисленных здесь возможностей будут подробно рассмотрены в следующих главах при обсуждении вопросов, связанных с организацией интерфейса пользователя. В заключение общего обзора принципов работы с сообщениями, заложенными в библиотеке классов MFC, и несколько забегая вперед, рассмотрим основные функции, которые для этого предназначены.
Функции для работы с сообщениями
Понимание работы с сообщениями крайне важно для написания правильных и эффективных программ для Windows. Поэтому рассмотрим те основные функции, которые вы можете (и должны) широко использовать.
static const MSG* CWnd::GetCurrentMessage ()
Возвращает указатель на структуру MSG, которая содержит информацию о текущем сообщении. Может быть вызвана только из обработчика, заданного макросом ON_MESSAGE.
LRESULT CWnd::Default ()
Служит для вызова оконной процедуры, выполняемой по умолчанию.
virtual BOOL CWnd::PreTranslateMessage (MSG *pMsg)
Используется классом CWinApp для фильтрации оконных сообщений до того, как они будут распределены функциями Windows ::TranslateMessage и ::DispatchMessage.
LRESULT CWnd::SendMessage (
UINT msg,
WPARАМ wPnr, = 0,
LPARAM IPrm = 0)
Посылает сообщение в объект CWnd, непосредственно вызывая оконную процедуру, и не выходит из нее, пока та не обработает сообщение.
B00L CWnd::PostMessage (
UINT msg,
WPARAM wPrm = 0,
LPARAM iPrm = 0)
Помещает сообщение в очередь приложения и заканчивает работу без ожидания его обработки. Из очереди сообщения можно извлечь функциями Windows ::GetMessage или ::PeekMessage.
B00L CWnd::SendNotifyMessage (
UINT msg,
WPARAM wPrm = 0,
LPARAM IPrm = 0)
Посылает в окно определенное извещение. Если окно было создано текущим потоком, то функция для обработки извещения вызывает оконную процедуру и не завершается, пока оно не будет обработано. Если же окно было создано другим потоком, то, послав извещение в оконную процедуру, функция завершает свою работу.
UINT ::RegisterWindowMessage (LPCTSTR IpString)
Позволяет получить идентификатор (в диапазоне от ОхСООО до OxFFFF) нового уникального сообщения, который может затем использоваться при вызовах функций SendMessage или PostMessage. В качестве параметра IpString она использует указатель на строку, определяющую имя сообщения. Эта функция должна использоваться для предотвращения возможных конфликтов в случае, когда нужно связать между собой два (или больше) приложения. Зарегистрированное сообщение действует до окончания сеанса работы Windows. Обработчик сообщения ассоциируется с ним посредством специального макроса ОМ_ REGISTERED_MESSAGE, например, как показано ниже:
class CUserWnd : public CParentWnd
{
...
//{{AFX_MSG_MAP(CUserWnd)
afx_msg LRESULT OnSample(WPARAM wParam, LPARAM IParam);
//{{AFX_MSG_MAP DECLARE_MESSAGE_MAP()
};
UINT WM_SAMPLE = RegisterWindowMessage("MESSAGE_SAMPLE"); BEGIN_MESSAGE_MAP(CUserWnd, CParentWnd)
//{{AFX_MSG_MAP{CUserWnd)
ON_REGISTERED_MESSAGE(WM_SAMPLE, OnSample)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
Наиболее интересными из перечисленных являются две функции: SendMessage и PostMessage (рис. 5.4).
Рис. 5.4. Различие между посылкой сообщений функциями SendMessage и PostMessage
Основная цель этих функций — послать сообщение в окно своего или некоторого другого приложения. Но на этом их совпадения заканчиваются. Дело в том, что функция SendMessage явно вызывает процедуру того окна, которое ассоциировано с вызывающим ее объектом и не выходит из процедуры, пока та не обработает сообщение. Другими словами, выполнение программы не будет продолжено до тех пор, пока сообщение не будет обработано. В отличие от нее функция PostMessage не вызывает явно оконную процедуру, а посылает сообщение в очередь сообщений приложения, откуда оно выбирается с помощью функции GetMessage. Выполнение самой функции PostMessage при этом завершается, и работа приложения может быть продолжена.
Но от теории, пусть даже и очень важной, вернемся к практике — пора развить наше приложение, добавив в него механизмы "общения с пользователем".