Обработка сообщений. Циклы обработки сообщений
Если и существует некоторая особенность, отличающая программирование в Windows от других областей программирования, то это сообщения. Сообщения являются тем средством, с помощью которого операционная система может дать знать приложению, что что-то произошло, например пользователь нажал клавишу на клавиатуре или щелкнул кнопкой мыши, или передвинул мышь, или подготовил принтер к выводу информации. Окно, а каждый информационный элемент на экране есть своего рода окно, также может посылать сообщения другому окну и, как правило, большинство окон реагирует на полученное сообщение тем, что пересылает его дальше, третьему окну, слегка видоизменив. Значительную помощь в организации работы с сообщениями оказывает MFC, скрывая от программиста многие подробности процесса.
Хотя операционная система и использует целые числа для идентификации событий, в тексте программы мы будем иметь дело с символьными идентификаторами. Огромное количество директив #define связывает символьные идентификаторы с соответствующими числами и позволяет программистам в разговоре между собой в кругу посвященных манипулировать словечками вроде WM_PAINT и WM_SIZE. Префикс WM означает Window Message (сообщение Windows). Фрагмент перечня сообщений представлен в листинге 3.1.
#define WM_SETFOCUS 0x0007
#define WM_KILLFOCUS 0x0008
#define WM_ENABLE 0x000A
#define WM_SETREDRAW 0x000B
#define WM_SETTEXT 0x000C
#define WM_GETTEXT 0x000D
#define WM_GETTEXTLENGTH 0x000E
#define WM_PAINT 0x000F
#define WM_CLOSE 0x0010
#define WM_QUERYENDSESSION 0x0011
#define WM_QUIT 0x0012
#define WM_QUERYOPEN 0x0013
#define WM_ERASEBKGND 0x0014
#define WM_SYSCOLORCHANGE 0x0015
#define WM_ENDSESSION 0x0016
Сообщению известно, для какого окна оно предназначено. Оно может иметь до двух параметров. Часто в эти два параметра упаковывается несколько совершенно различных величин.
Обработка разных сообщений выполняется разными компонентами операционной системы и приложения. Например, когда пользователь передвигает мышь по полю окна, формируется сообщение WM_MOUSEMOVE, которое передается окну, а окно, в свою очередь, передает это сообщение операционной системе. И уже последняя перерисовывает указатель мыши в новом месте. Когда пользователь щелкает левой кнопкой мыши на экранной кнопке, кнопка, которая также есть особый вид окна, получает сообщение WM_LBUTTONDOWN. В процессе обработки этого сообщения кнопка часто формирует новое сообщение для окна, в котором она находится, причем это сообщение гласит: "Ой, на мне щелкнули!".
Библиотека MFC позволяет программистам в подавляющем большинстве случаев полностью отстраниться от сообщений нижнего уровня, таких как WM_MOUSEMOVE и WM_LBUTTONOOWN. Программист может полностью сосредоточиться на сообщениях более высокого уровня, которые гласят что-нибудь вроде "Выбран третий элемент такого-то списка" или "Произошел щелчок на кнопке Move". Поступают такого рода сообщения в те программы, которые пишет программист, и в компоненты операционной системы точно так же, как и сообщения нижнего уровня. Единственная разница в том, что MFC берет на себя значительную часть работы по обработке сообщений низкого уровня и позволяет заметно облегчить распределение сообщений между разными классами объектов, на уровне которых и будет производиться их обработка.
Циклы обработки сообщений
Сердцем любой Windows-программы является цикл обработки сообщений (Message Loop), который практически всегда находится в функции WinMain(). Эта функция в Windows-приложениях играет ту же роль, что и функция Main () в DOS-приложениях,— ее вызывает операционная система сразу же после загрузки приложения в память. Текст типичной функции WinMain() Представлен в листинге 3.2.
Листинг 3.2.Tипичнaя функцияWinMain()
int АРIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR IpCmdLine,
int nCmdShow) зн
MSG msg;
if( !InitAppIication( hInstance))
return (FALSE);
if( !Initlnstance( hInstance,nCmdShow))
return (FALSE);
while( GetMessage( &msg, NULL, 0, 0)) зн
TranslateMessage( &msg);
DispatchMessage( &msg):
зн return (msg.wParam);зн
В С-программах для Windows, похожих на эту, функция InitApplication() вызывает RegisterWindow(), a InitInstance() — CreateWindow(). Затем наступает очередь цикла обработки сообщений. Он представляет собой типичную циклическую конструкцию С на базе оператора whi1е, внутри которой вызывается функция GetMessage(). Эта функция API заполняет msg кодом сообщения, которое операционная система распределила для этого приложения, и почти всегда возвращает TRUE. Таким образом, цикл повторяется снова и снова до тех пор, пока работает приложение. Единственный вариант, при котором GetMessage() возвращает FALSE, — получение сообщения WM_QUIT.
При работе с сообщениями, поступающими с клавиатуры, некоторую часть предварительной обработки берет на себя функция API TranslateMessage(). Ее назначение состоит в следующем. Прикладной части программы нет дела до сообщений наподобие "Нажата клавиша <А>" и «Отпущена клавиша <А>". Прикладную часть интересует только то, какую литеру (символ) ввел пользователь, т.е. ее вполне удовлетворит сообщение "Введен символ А". Вот это преобразование — нескольких сообщений о деталях процесса в одно сообщение о его сути — и выполняет функция TranslateMessage(). Она перехватывает сообщения WM_KEYDOWN и WM_KEYUP и вместо них посылает сообщение WM_CHAR. Если пользоваться библиотекой MFC, то такие мелочи, как ввод символа А, проходят, как правило, мимо вас. Пользователь вводит текст в текстовое поле или в другой элемент управления, и забота программиста— извлечь введенный текст из этого объекта после того, как пользователь щелкнет на ОК.
Функция API DispatchMessage() вызывает, в свою очередь, функцию WndProc() того окна, для которого предназначено сообщение. Типичная функция WndProc() в С-программе для Windows представляет собой огромный оператор switch с отдельными case для каждого сообщения, которое приложение намеревается самостоятельно обрабатывать. Текст ее приведен в листинге 3.3.
Листинг 3.3. Типичная функция WndProc()
LONG APIENTRY MainWndProc( HWND hwnd, // Дескриптоо окна.
UINT msg, //Тип сообщения.
UINT wParam, //' Дополнительная информация.
LONG IParam) //Дополнительная информация.
зн
switch(msg) зн
case WM_MOUSEMOVE:
// Обработка перемещения мыши.
break;
case WM_LBUTTONDOWN:
// Обработка щелчка левой кнопки мыши.
break;
case WM_ RBUTTONDOWN:
// Обработка щелчка правой кнопки мыши.
break;
case WM_PAINT :
/,/ Перерисовать окно. break;
case WM_DESTROY : // Сообщение: окно будет уничтожено.
PostQuitMessage( 0);
return 0;
break;
defauIt:
return (DefWindowProc( hwnd, msg, wParam, IParam));зн
return (0);зн
Вы, конечно, можете себе представить, какой длины достигает подобная функция в более или менее порядочном приложении. MFC решает проблему следующим образом—информация о сообщениях, которые должны обрабатываться, расположена поближе к функциям. которые и должны выполнять обработку. Таким образом, отпадает необходимость в огромных операторах switch, в которых сосредоточено распределение сообщений.
Карты сообщений
Использование карты сообщений (Message maps) лежит в основе подхода, который реализуется в MFC для программирования Windows-приложений. Суть его состоит в том, что от разработчика требуется только написать функции обработки сообщений и включить в свой класс карту сообщений, которая фактически скажет: "Я буду обрабатывать такое-то сообщение". После этого главная программа будет отвечать за то, чтобы сообщение было передано именно той функции, которая будет его обрабатывать.
Карта сообщений состоит из двух частей: одна— в файле заголовка для класса .h, а другая — в соответствующем файле реализации .срр. Они, как правило, формируются мастерами, хотя в некоторых случаях вы можете сделать это (или частично отредактировать их) и самостоятельно. В листинге 3.4 представлена часть текста файла заголовка одного из классов простого приложения ShowString.
Листинг 3.4. Карта сообщений из файла ShowString h
//2зн AFX_MSG(CShowStringApp)
arx_msg void OnAppAbout();
// ВНИМАНИЕ!! Здесь ClassWizard будет добавлять и
// удалять функции-члены.
//НЕ РЕДАКТИРУЙТЕ текст в этих блоках!
//2знAFX_MSG
DECLARE_MESSAGE_MAP()
Карта сообщений для приложения Employee класса CEmployeeView:
- В файле EmployeeView.h
//знзнAFX_MSG(CEmployeeView)
afx_msg void OnRecordAdd();
afx_msg void OnRecordDelete();
afx_msg void OnSortId();
afx_msg void OnSortName();
afx_msg void OnSortRate();
afx_msg void OnSortDepartment();
afx_msg void OnFilterDepartment();
afx_msg void OnFilterId();
afx_msg void OnFilterName();
afx_msg void OnFilterRate();
afx_msg void OnFileSaveAs();
afx_msg void OnRecordFind();
//знзнAFX_MSG
DECLARE_MESSAGE_MAP()
Здесь объявляется функция OnAppAbout(). Специальным образом оформленный комментарий позволяет ClassWizard определить, какие именно сообщения перехватываются этим классом. DECLARE_MESSAGE_MAP — это макрос, расширяемый препроцессором компилятора Visual C++, в котором объявляются переменные и функции, принимающие участие в этом фокусе с перехватом сообщений.
Карта сообщений в файле . срр, как показано в листинге 3.5, также достаточно проста.
BEGIN_MESSAGE_MAP(CShowStringApp, CwinApp)
//знзнAFX_MSG_MAP(CshowStringApp)
ON_COMMAND(ID_APP_ABOUT, OnAppAbout)
//знзнAFX_MSG_MAP
// Стандартные команды для файловых документов.
ON_COMMAND(ID_FILE_NEW, CWinApp::OnFileNew)
ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
// Стандартные команды настройки принтера.
ON_COMMAND(ID_FILE_PRINT_SETUP, CWinApp::OnFilePrintSetup)
END_MESSAGE_MAP()
Карта сообщений для приложения Employee класса CEmployeeView:
- в файле EmployeeView.cpp
BEGIN_MESSAGE_MAP(CEmployeeView, CRecordView)
//знзнAFX_MSG_MAP(CEmployeeView)
ON_COMMAND(ID_RECORD_ADD, OnRecordAdd)
ON_COMMAND(ID_RECORD_DELETE, OnRecordDelete)
ON_COMMAND(ID_SORT_ID, OnSortId)
ON_COMMAND(ID_SORT_NAME, OnSortName)
ON_COMMAND(ID_SORT_RATE, OnSortRate)
ON_COMMAND(ID_SORT_DEPARTMENT, OnSortDepartment)
ON_COMMAND(ID_FILTER_DEPARTMENT, OnFilterDepartment)
ON_COMMAND(ID_FILTER_ID, OnFilterId)
ON_COMMAND(ID_FILTER_NAME, OnFilterName)
ON_COMMAND(ID_FILTER_RATE, OnFilterRate)
ON_COMMAND(ID_FILE_SAVE_AS, OnFileSaveAs)
ON_COMMAND(ID_RECORD_FIND, OnRecordFind)
//знзнAFX_MSG_MAP
END_MESSAGE_MAP()
Некоторые макросы карты сообщений
Макросы BEGIN_MESSAGE_MAP и END_MESSAGE_MAP, так же как DECLARE_MESSAGE_MAP в файле заголовка, объявляют члены (переменные и функции), которые программа должна использовать для того, чтобы разобраться в картах всех объектов системы. Существует довольно большой набор макросов, используемых для работы с картой сообщений. Некоторые из них перечислены ниже.
•DECLARE_MESSAGE_MAP. Используется в файле заголовка для того, чтобы объявить, что в файл собственно текста программы будет включена карта сообщений.
•BEGIN_MESSAGE_MAP. Отмечает начало карты сообщений в тексте программы.
•END_MESSAGE_MAP. Отмечает конец карты сообщений в тексте программы.
•ON_COMMAND. Используется для того, чтобы перенаправить обработку некоторой команды функции-члену класса.
- ON_CONTROL. Используется, для того, чтобы перенаправить обработку кода извещения от элемента управления, введенного программистом, функции-члену класса.
•ON_MESSAGE. Используется для того, чтобы перенаправить обработку некоторого сообщения, введенного программистом, функции-члену класса.
•ON_UPDATE_COMMAND_UI. Используется для того, чтобы перенаправить обновление, связанное с заданной командой, функции-члену класса.
•ON_NOTIFY. Используется для того, чтобы перенаправить функции-члену класса обработку заданного кода извещения, который сопровождается дополнительными данными от элемента управления.
Таким образом, если вы включаете в программу некоторый компонент карты сообщений и, когда такое сообщение возникает, происходит следующее. Функции, вызванные скрытым от вас циклом обработки сообщений, решают на основании этой таблицы, какой из объектов и какая из функций-членов этого объекта будет обрабатывать сообщение.
Что происходит с картой сообщений
Компоненты, которые добавлены в карту сообщений файла заголовка, могут быть истолкованы следующим образом: "Существует функция OnAppAbout(), которая не имеет параметров". Компонент, который добавлен в собственно текст программы (файл .срр). означает: "Когда придет сообщение от команды ID_APP_ABOUT, вызовите OnAppAbout()".
Каждое приложение имеет объект, который является наследником класса CWinApp, и имеет функцию-член Run(). Эта функция обращается к CWinThread:: Run(), которая значительно длиннее, чем WinMain(), но имеет точно такой же цикл обработки сообщений — вызов GetMessage(), вызов TranslateMessage() и вызов DispatchMessage(). Почти все объекты-окна используют тот же самый класс окна, и ту же самую функцию WndProc(), но теперь названную AfxWndProc(). Функция WndProc(), как вы уже видели, знает дескриптор окна hWnd, для которого предназначено сообщение. Библиотека MFC, в свою очередь, содержит нечто, называемое картой дескрипторов (handle map), — таблицу дескрипторов окон и указателей объектов. Таким образом, главная программа может, используя всю эту информацию, найти указатель на объект cWnd*. Далее она вызывает WindowProc()— виртуальную функцию этого объекта. Кнопки или окна представления, естественно, имеют разные реализации этой функции, но волшебные свойства полиморфизма приводят к тому, что вызывается именно та реализация, которая нужна.
Функция WindowProc() вызывает OnWndMsg() — функцию C++, которая собственно и обрабатывает сообщения. Во-первых, она проверяет, что же это было — сообщение, команда или код извещения. Предположим, поступило сообщение. Тогда функция просматривает карту сообщений для своего класса, используя члены класса (переменные и функции), которые были установлены макросами BEGIN_MESSAGE_MAP, END_MESSAGE_MAP и DECLARE_MESSAGE_MAP. Помимо всего прочего, эти макросы организуют доступ к компонентам карты сообщений базового класса. Это означает, что если класс является производным от CView, но не перехватывает сообщений, которые обычно перехватываются базовым классом, то сообщение будет перехвачено функцией класса CView.