Операционная система Microsoft Windows 3.1 для программиста
1.4. Обработка сообщений
Откуда берутся сообщения?
Большинство сообщений создают драйверы устройств ввода/вывода, таких, как клавиатура, мышь или таймер. Драйверы создают сообщения при поступлении аппаратных прерываний. Например, когда вы нажимаете и затем отпускаете клавишу, драйвер обрабатывает прерывания от клавиатуры и создает несколько сообщений. Аналогично сообщения создаются при перемещении мыши или в том случае, когда вы нажимаете кнопки на корпусе мыши. Можно сказать, что драйверы устройств ввода/вывода транслируют аппаратные прерывания в сообщения.
Куда направляются сообщения, созданные драйверами?
Прежде всего сообщения попадают в системную очередь сообщений Windows. Системная очередь сообщений одна. Далее из нее сообщения распределяются в очереди сообщений приложений. Для каждого приложения создается своя очередь сообщений.
Очередь сообщения приложений может пополняться не только из системной очереди. Любое приложение может послать сообщение любому другому сообщению, в том числе и само себе.
Основная работа, которую должно выполнять приложение, заключается в обслуживании собственной очереди сообщений. Обычно приложение в цикле опрашивает свою очередь сообщений. Обнаружив сообщение, приложение с помощью специальной функции из программного интерфейса Windows распределяет его нужной функции окна, которая и выполняет обработку сообщения.
Когда вы посмотрите на исходный текст нашего первого приложения, обрабатывающего сообщения, вас может удивить тот факт, что функция WinMain не выполняет никакой работы, имеющей отношение к поведению приложения. Внутри этой функции находятся инициализирующий фрагмент и цикл обработки сообщений (Message Loop). Вся основная работа выполняется в функции окна, которой передаются.
Грубо говоря, приложения Windows похожи на загружаемые драйверы MS-DOS. Эти драйверы также состоят из инициализирующего фрагмента и функции, обрабатывающей команды, выдаваемые драйверу. Приложение Windows после инициализации переходит в состояние постоянного опроса собственной очереди сообщений. Как только происходит какое-либо событие, имеющее отношение к приложению, в очереди приложения появляется сообщение и приложение начинает его обрабатывать. После обработки приложение вновь возвращается к опросу собственной очереди сообщений. Иногда функция окна может получать сообщения непосредственно, минуя очередь приложения.
Так как Windows — мультизадачная операционная система, ее разработчики должны были предусмотреть механизм совместного использования несколькими параллельно работающими приложениями таких ресурсов, как мышь и клавиатура. Так как все сообщения, создаваемые драйверами мыши и клавиатуры, попадают в системную очередь сообщений, должен существовать способ распределения этих сообщений между различными приложениями.
В Windows существует понятие фокуса ввода (input focus), помогающее в распределении сообщений. Фокус ввода — это атрибут, который в любой момент времени может относиться только к одному окну. Если окно имеет фокус ввода, все сообщения от клавиатуры распределяются сначала в очередь сообщений приложения, создавшего окно, а затем — функции окна, владеющего фокусом ввода. Нажимая определенные клавиши, вы можете перемещать фокус ввода от одного окна к другому. Например, если вы работаете с диалоговой панелью, содержащей несколько окон, предназначенных для ввода текста, с помощью клавиши вы можете переключать фокус с одного такого окна на другое и вводить текст в различные окна диалоговой панели. Существуют также комбинации клавиш, с помощью которых вы можете переключиться на другое приложение. При этом фокус ввода будет отдан другому окну, принадлежащему другому приложению.
Сообщения от драйвера мыши всегда передаются функции того окна, над которым находится курсор мыши. При необходимости приложение может выполнить операцию захвата (capturing) мыши. В этом случае все сообщения от мыши будут поступать в очередь приложения, захватившего мышь, вне зависимости от положения курсора мыши.
Простейший цикл обработки сообщений состоит из вызовов двух функций — GetMessage и DispatchMessage.
Функция GetMessage предназначена для выборки сообщения из очереди приложения. Сообщение выбирается из очереди и записывается в область данных, принадлежащую приложению.
Функция DispatchMessage предназначена для распределения выбранного из очереди сообщения нужной функции окна. Так как приложение обычно создает много окон и эти окна используют различные функции окна, необходимо распределить сообщение именно тому окну, для которого оно предназначено. Поэтому приложение должно распределить сообщение после его выборки из очереди приложения, в котором находятся сообщения для всех окон. Windows оказывает приложению существенную помощь в решении этой задачи — для распределения приложению достаточно вызвать функцию DispatchMessage.
Вот как выглядит простейший вариант цикла обработки сообщений:
Завершение цикла обработки сообщений происходит при выборке из очереди специального сообщения, в ответ на которое функция GetMessage возвращает нулевое значение. Тип MSG описан в файле windows.h.
Процесс обработки сообщений схематически показан на рис. 1.9.
Рис. 1.9. Процесс обработки сообщений
На этом рисунке слева показаны ресурсы и подсистемы Windows, справа — приложения. Сообщения поступают от драйверов таких устройств, как клавиатура, мышь, таймер, и попадают в системную очередь сообщений. Из системной очереди сообщений Windows выбирает сообщения, предназначенные для приложения, и помещает их в очередь сообщения приложения.
Функция WinMain в цикле обработки сообщений с помощью функции GetMessage выбирает сообщения из очереди сообщений приложения и распределяет их функциям окон, вызывая функцию DispatchMessage.
На нашем рисунке для простоты показано только одно приложение, в котором определена только одна функция окна. Реально же существует много приложений, и каждое приложение имеет несколько функций окна. Каждое приложение вызывает в цикле обработки сообщений функцию GetMessage, выбирая сообщения из своей очереди. Затем каждое приложение распределяет выбранное сообщение одной из своих функций окна, для чего используется функция DispatchMessage.
Функция окна получает сообщения при создании окна, в процессе работы приложения, а также при разрушении окна.
Сообщение с кодом WM_CREATE передается функции окна в момент создания окна. Функция окна при обработке этого сообщения выполняет инициализирующие действия (аналогично конструктору класса в языке C++). Коды сообщений определены в файле windows.h, включаемом в исходные тексты любых приложений Windows.
В процессе работы приложения функция окна может получать сообщения с различными кодами как через очередь сообщений приложения, так и непосредственно, в обход очереди сообщений. Обработчики этих сообщений, определенные в функции окна, являются методами для работы с окном как с объектом.
При разрушении структуры данных окна (при уничтожении окна) функция окна получает сообщение с кодом WM_DESTROY. Обработчик этого сообщения действует как деструктор. Если ваша функция окна во время обработки сообщения WM_CREATE создала какие-либо структуры данных, эти структуры должны быть разрушены (а заказанная для них память возвращена операционной системе) во время обработки сообщения WM_DESTROY.
Глава 3. Обработка сообщений Windows
Как объяснялось в главе 2, Windows взаимодействует с приложениями, направляя им сообщения. Вы узнали, как строить каркас приложения Windows. Здесь каркас программы будет расширен с целью иллюстрации обработки некоторых часто используемых сообщений.
Что такое сообщения?
В Windows существует множество сообщений. Каждое из них представляется уникальным 32-разрядным целым числом. В файле Windows.h определены стандартные макроимена для всех возможных сообщений. В общем случае для идентификации сообщений всегда используются макроимена, а не числовые значения. Вот некоторые широко используемые в Windows макроимена сообщений:
Каждое сообщение имеет два параметра, которые содержат дополнительную информацию, зависящую от типа сообщения. Один из этих параметров имеет тип WPARAM, другой – LPARAM. В Windows оба этих типа представляют собой 32-разрядные целые. Для этих параметров в программах чаще всего используются имена соответственно wParam и lParam. По мере изучения различных сообщений будет объясняться и значение этих параметров для каждого конкретного сообщения.
Как отмечалось в главе 2, обработку сообщений в программе должна выполнять оконная функция. Эта функция имеет четыре параметра: дескриптор окна, которому направляется сообщение, собственно сообщение, и, наконец, wParam и lParam.
Иногда информация, содержащаяся в wParam и lParam, может состоять из двух частей, которые размещаются в двух 16-разрядных словах, составляющих каждый из этих параметров. Для обеспечения простого доступа к каждой части wParam и lParam в Windows определены два макроса – LOWORD и HIWORD. Они возвращают соответственно старшее и младшее слова длинного целого и используются так:
Обработка нажатая клавиш
Одно из широко используемых в Windows сообщений порождается при нажатии клавиши. Это сообщение называется WM_CHAR. Следует отметить, что Ваше приложение никогда не получает сигналы и коды клавиш непосредственно от клавиатуры. Вместо них при нажатии клавиши активное окно получает сообщение WM_CHAR. Чтобы показать, как этот процесс работает, мы расширим здесь каркасную программу из главы 2 так, что она сможет обрабатывать клавиатурные сообщения.
Для сообщений WM_CHAR параметр wParam содержит ASCII-код нажатой клавиши. LOWORD(lParam) содержит количество повторов, генерируемых при удерживании клавиши в нажатом положении.
HIWORD(lParam) представляет собой битовую карту со следующими значениями битов, структура которой показана в таблице 3.1.
Таблица 3.1
Битовая карта lParam
Равен 1, если клавиша отпущена, и 0, если она нажата | |
Устанавливается, если клавиша уже была нажата перед посылкой сообщения | |
Устанавливается в 1, если дополнительно нажата клавиша [Alt] | |
12¸9 | Используется операционной системой |
Устанавливается в 1, если нажата клавиша функциональной или дополнительной части клавиатуры | |
7¸0 | Код клавиши (scan-код) |
В данном случае для нас важно только значение wParam, поскольку оно содержит собственно вводимый символ. Однако заметьте, насколько подробную информацию о состоянии системы предоставляет Windows. Естественно, Вы можете использовать ту часть этой информации, которая Вас интересует.
Чтобы обработать сообщения WM_CHAR, нужно добавить его в оператор switch в функции окна. Приведем оконную функцию, которая обрабатывает ввод символов с клавиатуры и отображает эти символы на экране:
char str[80] = «»; // Буфер для строки вывода
LRESULT CALLBACK WindowFunc (HWND hwnd,
case WM_CHAR: // Обработка нажатия клавиши
hdc=GetDC(hwnd); // Для получ. контекста устр-ва
TextOut(hdc, 1, 1, » «,2); //Стереть старый код
// новый символ в выходной буфер
TextOut (hdc, 1, 1, str, strlen(str)); // Вывод
ReleaseDC (hwnd, hdc); // Освободить контекст
case WM_DESTROY: // Завершение программы
// Все сообщения, не обрабатываемые в данной
// функции, направляются на обработку
return DefWindowProc (hwnd, message,
Вас может удивить тот факт, что для такой простой задачи, как отображение введенного символа, требуется так много операторов. Объясняется это тем, что для вывода на экран Windows должен установить связь между Вашей программой и экраном. Такая связь устанавливается путем создания и получения в программе контекста устройства (Device Context, DC),что происходит при вызове функции GetDC(). В данный момент не важно, что представляет собой контекст устройства, – это будет рассматриваться в следующем разделе. Однако программа может вывести что-либо на экран только получив контекст устройства, а по окончании процесса вывода программа должна этот контекст освободить, вызвав ReleaseDC(). Одновременно в Windows может существовать конечное множество контекстов устройств, так что если программа не освобождает DC, у Windows вскоре не останется свободных ресурсов для предоставления DC и очередной вызов GetDC() вернет NULL.
Функции API GetDC() и ReleaseDC() имеют следующие прототипы:
HDC GetDCfHWND hwnd);
int ReleaseDC(HWND hwnd, HDC hdc);
GetDC() возвращает контекст устройства, ассоциированный с окном, дескриптор которого задается параметром hwnd. Тип HDC означает дескриптор контекста устройства.
ReleaseDC() возвращает ненулевое значение, если контекст устройства действительно был освобожден. Параметр hwnd задает дескриптор окна, ассоциированного с освобождаемым DC, a hdc – дескриптор освобожденного DC.
Функция API, выводящая символы на экран, называется TextOut(). Вот ее прототип:
BOOL TextOut(HDC hdc, int x, int у,
LPSTR lpstr, int nlength);
Функция TextOut() выводит на экран строку, задаваемую указателем lpstr, начиная с точки, координаты которой задаются параметрами х и у (по умолчанию эти координаты задаются в пикселах). Длина выводимой строки задается параметром nlength. Функция TextOut() возвращает ненулевое значение при успешном завершении.
В нашем примере каждый раз при получении сообщения WM_CHAR символ, введенный пользователем, преобразуется в строку длиной в один символ при помощи функции sprintf(), а затем выводится на экран при помощи TextOut(), начиная с позиции [1,1].
Переменная str определена как внешняя для того, чтобы сохранить ее значение в интервале времени между вызовами оконной функции в последующих примерах.
Левый верхний угол рабочей области окна имеет координаты [0,0]. Координаты окна всегда задаются относительно начала рабочей области, а не экрана, поэтому вводимые символы всегда будут отображаться в верхнем левом углу рабочей области окна независимо от его положения на экране.
Причина первого вызова TextOut() заключается в необходимости стереть ранее выведенный символ. Поскольку Windows использует графический режим дисплея и символы шрифта могут иметь различный размер, вывод очередного символа на экран не обязательно приведет к полному стиранию предыдущего символа. Например, если Вы выведете символ i поверх w, часть w все равно останется на экране. (Попробуйте закомментировать первый вызов TextOut() – и увидите, каков будет результат.)
Следует отметить, что никакие функции Windows API не позволяют вывод за границами окна. При попытке пересечь границы окна выводимое изображение будет автоматически усекаться.
Вы можете подумать, что вызов TextOut() для вывода единственного символа не является эффективным использованием этой функции. Однако в Windows отсутствует функция вывода одного символа. Впоследствии Вы увидите, что для большинства операций взаимодействия с пользователем используются диалоги, меню, инструментальные панели и т.п. Поэтому Windows предусматривает всего несколько функций для вывода текста в рабочую область окна.
Пример 3-1. Приведем полную программу обработки ввода символов с клавиатуры.
// Обработка сообщений WM_CHAR
LRESULT CALLBACK WindowFunc(HWND, UINT,
char szWinName[]=»МоеОкно»; // Имя класса окна
char str[80]=»»; // Буфер для строки вывода
int WINAPI WinMain(HINSTANCE hThisInst,
WNDCLASS wcl; // Определить класс окна
wcl.hInstance=hThisInst; // Дескриптор приложения
wcl.lpszClassName=szWinName; // Имя класса окна
wcl.lpfnWndProc=WindowFunc; // Функция окна
wcl.style=0; // Стиль по умолчанию
wcl.lpszMenuName=NULL; // Без меню
wcl.cbClsExtra=0; // Без дополнительной информации
(HBRUSH)GetStockObject(WHITE_BRUSH); //Белый фон
if(!RegisterClass(&wcl)) // Регистрируем класс окна
hwnd=CreateWindow(szWinName, // Создать окно
«Обработка сообщений WM_CHAR»,
WS_OVERLAPPEDWINDOW, // Стиль окна
HWND_DESKTOP, // Нет родител. окна
hThisInst,// Дескриптор приложения
NULL); // Нет дополнит. аргументов
ShowWindow (hwnd, nWinMode); // Показать окно
UpdateWindow (hwnd); // и перерисовать
while(GetMessage(&msg,NULL,0,0)) // Запустить цикл
TranslateMessage(&msg); // Разреш. исп. клавиатуры
DispatchMessage (&msg); // Вернуть управл. Windows
// Следующая функция вызывается операционной
// системой Windows и получает в качестве
// параметров сообщения из очереди сообщений
LRESULT CALLBACK WindowFunc(HWND hwnd,
case WM_CHAR: // Обработка нажатия клавиши
hdc=GetDC(hwnd); // Для получ. контекста устр-ва
TextOut(hdc, 1, 1, » «,2); //Стереть старый код
sprintf(str,»%c»,(char)wParam); //Запись символа
TextOut (hdc, 1, 1, str, strlen(str)); // Вывод
ReleaseDC (hwnd, hdc); // Освободить контекст
case WM_DESTROY: // Завершение программы
// Все сообщения, не обрабатываемые в
// данной функции, направляются на обработку
На рис. 3.1 представлено окно, создаваемое этой программой.
Приведенная программа перед выводом строки на экран получает контекст устройства. Контекст устройства освобождается после того, как вывод на экран закончен. Сейчас самое время объяснить, что представляет собой контекст устройства.
Контекст устройства– это структура данных, связывающая Ваше приложение с драйвером устройства и полностью определяющая состояние драйвера и способ вывода информации.
Перед тем, как Ваше приложение начнет вывод информации в окно, оно должно получить контекст устройства. До этого связи между программой и окном не существует. Таким образом, перед началом любого процесса вывода необходимо получить контекст устройства. Поскольку TextOut() и другие функции вывода требуют в качестве параметра дескриптор контекста устройства, это правило выполняется автоматически.