Лекция на тему Сообщения и их обработка
Работа добавлена на сайт bukvasha.net: 2014-12-17Поможем написать учебную работу
Если у вас возникли сложности с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой - мы готовы помочь.
от 25%
договор
Сообщения и их обработка
Посылка сообщений
Мы это сделаем на примере нажатия на клавишу. Когда вы нажимаете на клавишу, генерируется аппаратное прерывание. Клавиатурный драйвер Windows обрабатывает это прерывание и помещает соответствующее сообщение в общую очередь сообщений Windows.
При этом указывается, какое окно должно получить это сообщение. Затем Windows извлекает из своей очереди это сообщение и помещает его в очередь сообщений приложения, содержащего окно–адресат. Вслед за этим уже само приложение выбирает из очереди поступившее сообщение и передает его соответствующей оконной функции.
Этот процесс называется посылкой (post) сообщений, так как посылка сообщения напоминает посылку письма: посылающий сообщение указывает адресата, отправляет сообщение и больше о нем не беспокоится. Отправитель не знает, когда точно его сообщение получит адресат. Процесс посылки может показаться излишне сложным, однако для этого существует несколько причин:
Во–первых, аппаратные прерывания надо обрабатывать со всей возможной скоростью. Поэтому при приеме аппаратного прерывания драйвер не тратит время на передачу сообщения в очередь приложения, а ставит его в очередь сообщений Windows. Аппаратные прерывания являются асинхронными по отношению к выполняющемуся приложению, а обработка сообщений обязательно должна быть синхронной. Поэтому механизм посылки сообщений нельзя смешивать с аппаратными прерываниями.
А во–вторых, накопление событий в очереди приложения помогает уменьшить количество переключений между приложениями, так как Windows обычно дает приложению полностью обработать события из его очереди и лишь после этого переключается на другие приложения. Кроме того, некоторые события могут группироваться в одно во время нахождения в очереди.
Рисунок SEQ Рисунок \* ARABIC 1 Маршрутизация сообщений в Windows 3.x
Извлечение сообщений из очереди приложения и направление их соответствующим окнам осуществляет функция WinMain. Этот процесс выполняется в несколько приемов:
сообщение выбирается из очереди с помощью функции GetMessage или PeekMessage
затем сообщение транслируется с помощью функции TranslateMessage[0] (одно сообщение может порождать последовательность других или заменяться, как, например, происходит для сообщений клавиатуры WM_KEYDOWN). Часто трансляция состоит из вызова более чем одной функции, сюда могут добавляться специальные средства трансляции акселераторов и немодальных диалогов (об этом позже).
И только после этого оно направляется окну с помощью функции DispatchMessage (это называется диспетчеризацией)
Для выполнения этих операций существуют специальные функции. Эти функции образуют цикл обработки сообщений, так как после завершения обработки одного сообщения приложение должно приготовиться к обработке следующего. Цикл заканчивается только при завершении работы приложения.
MSG msg;
while (GetMessage(&msg, NULL, NULL, NULL)) {
TranslateMessage(&msg);
DispatchMessage(&msg);}
Это самый простой вид цикла обработки сообщений. В реальных приложениях он более сложный. Все три функции, вызываемые здесь, принадлежат Windows. Назначение их должно быть понятно. Требуется добавить несколько замечаний о функции GetMessage. Эта функция имеет следующие аргументы:
BOOL GetMessage(lpMsg, hWnd, uMsgFilterMin, uMsgFilterMax);
lpMsg указывает на структуру MSG, в которую будет записано полученное сообщение. Если очередь сообщений пуста, то GetMessage передает управление оболочке, так что та может начать обработку сообщений другого приложения.
Какие же данные передаются сообщением?
typedef struct tagMSG {
HWND hwnd; // хендл окна-получателя
UINT message; // номер сообщения WM_...
WPARAM wParam; // параметр сообщения
LPARAM lParam; // параметр сообщения
DWORD time; // время поступления сообщения
POINT pt; // координаты сообщения (для сообщений мыши)
} MSG;
Поле message структуры MSG задает номер сообщения, посланного системой. Интерпретация параметров сообщения wParam и lParam зависит от самого сообщения. Для этого надо смотреть описание конкретного сообщения и обрабатывать параметры соответствующим образом. Так как в системе определено огромное количество разных сообщений, то для простоты использования применяются символические имена сообщений, задаваемыми с помощью #define в заголовочном файле. В качестве примера можно привести сообщения WM_CREATE, WM_PAINT, WM_QUIT.
hWnd указывает хендл окна, сообщения для которого будут выбираться из очереди. Если hWnd равен NULL, то будут выбираться сообщения для всех окон данного приложения, а если hWnd указывает реальное окно, то из очереди будут выбираться все сообщения, направленные этому окну или его потомкам (дочерним или используемым окнами, или их потомкам, в том числе отдаленным).
uMsgFilterMin и uMsgFilterMax обычно установлены в NULL. Вообще они задают фильтр для сообщений. GetMessage выбирает из очереди сообщения, номера (имена) которых лежат в интервале от uMsgFilterMin до uMsgFilterMax. Нулевые значения исключают фильтрацию.
Функция GetMessage возвращает во всех случаях, кроме одного, ненулевое значение, указывающее, что цикл надо продолжать. Только в одном случае эта функция возвратит 0 — если она извлечет из очереди сообщение WM_QUIT. Это сообщение посылается только при окончании работы приложения.
После завершения цикла надо сделать совсем немногое — освободить память от тех объектов, которые создавались во время работы приложения (если они еще существуют). Некоторые объекты, которые уничтожаются автоматически, можно не освобождать — это сделает Windows. Таков, например, зарегистрированный нами класс окон.
И остается еще одно дело: так как WinMain возвращает результат, то мы должны вернуть какое–либо значение. В Windows принято, что возвращаемое значение является параметром wParam сообщения WM_QUIT, завершившего цикл обработки сообщений. Таким образом мы пишем:
return msg.wParam;
Посылка и передача сообщений
Ранее, в разделе “ REF _Ref422032812 \* MERGEFORMAT Ошибка! Источник ссылки не найден.”, мы рассматривали метод передачи сообщений, называемый посылкой сообщений (post message), и их обработки — извлечения из очереди в цикле обработки сообщений, трансляции и последующей передачи оконной процедуре. Источниками таких сообщений могут быть как компоненты системы, например, клавиатурный драйвер, так и само приложение. Для посылки сообщения в API предусмотрена функция
BOOL PostMessage(hWnd, uMsg, wParam, lParam);
Эта функция ставит сообщение в очередь. Возвращаемое значение TRUE указывает, что сообщение поставлено в очередь, FALSE — возникла ошибка (например, ошибочно указанный адресат или очередь сообщений переполнилась). Позже сообщение будет извлечено из очереди вызовом функции GetMessage или PeekMessage в цикле обработки сообщений.
Однако механизм посылки сообщений не всегда удобен, так как не позволяет получить результат обработки сообщения, или дождаться его завершения. Точнее, позволяет, но очень громоздким способом — надо вводить специальные ответные сообщения и дожидаться их получения.
Вообще говоря, процесс посылки и обработки посланных сообщений часто называют асинхронным способом обработки сообщений, так как сама посылка сообщения и его обработка никак между собой не связаны по времени.
Для решения этих задач вводится альтернативный механизм, называемый передачей сообщений (send message). При этом сообщение в очередь не попадает, а направляется непосредственно оконной функции[1]. Приблизительно его можно рассматривать как непосредственный вызов процедуры обработки сообщений. Для передачи сообщения используется функция
LONG SendMessage(hWnd, uMsg, wParam, lParam);
Она вызывает оконную процедуру указанного окна, получает результат обработки сообщения и возвращает управление в вызвавшую процедуру после обработки указанного сообщения. Возвращаемое этой функцией значение совпадает с результатом, выполнения оконной функции. Вы можете воспользоваться ею для передачи тех или иных сообщений даже до организации цикла обработки сообщений, так как очередь сообщений при этом не используется (кроме особых случаев в Win32 API — см. ниже).
Многие функции API используют SendMessage для передачи сообщений окну. Например, функция CreateWindow, создающая окно, в процессе создания передает ему сообщение WM_CREATE (и не только одно это сообщение), а результат, возвращенный обработчиком сообщения, используется функцией CreateWindow для продолжения создания окна.
Процесс передачи сообщений с помощью функции SendMessage называется синхронной обработкой сообщений, так как процесс передачи и обработки сообщений жестко упорядочен во времени.
То, что сообщения, переданные с помощью функции SendMessage, минуют цикл обработки сообщений, накладывает ограничения на применение этой функции. Дело в том, что цикл обработки сообщений выполняет над некоторыми сообщениями определенные операции. Так, например, нельзя передавать сообщение WM_QUIT — оно обязательно должно пройти через очередь сообщений, так как используется для завершения цикла обработки сообщений. Другие сообщения (например, клавиатуры) транслируются в цикле и их тоже надо посылать, а не передавать.
Иногда сообщения передаются не какому–либо конкретному окну и не какому–либо приложению (потоку), а всем приложениям, запущенным в системе. Для этого используются широковещательные сообщения (broadcast message), которые передаются всем главным окнам всех приложений. Для передачи такого сообщения необходимо указать в качестве хендла окна–получателя специальный символ HWND_BROADCAST (равный -1), например:
SendMessage(HWND_BROADCAST, WM_DDE_INITIATE, (WPARAM)hwndDDEClient, 0L);
В этом случае сообщение WM_DDE_INITIATE будет передано главным окнам всех приложений, так что все работающие DDE–сервера смогут на него ответить.
Существенная особенность широковещательных сообщений — то, что они будут обрабатываться сразу большим количеством окон. То есть получить результат от обработчика этого сообщения обычным путем нельзя, так как остается неопределенность, результат какого обработчика возвращать. В частном случае (Win32 API) можно получить ответы от всех обработчиков с помощью функции SendMessageCallback. В общем случае при необходимости получения ответа от обработчика тот должен передать ответное сообщение (именно так сделано при установлении DDE–разговора).
Особенности посылки сообщений в Windows API
Помимо рассмотренных функций в Windows API существует еще две функции, посылающие сообщения. С одной из них — PostQuitMessage мы уже встречались в примере 1A. Эта функция посылает сообщение WM_QUIT, которое служит для завершения цикла обработки сообщений.
BOOL PostQuitMessage(wParam);
Сообщение WM_QUIT интересно еще и тем, что оно не посылается никакому окну — хендл окна–получателя равен NULL. В принципе у вас есть возможность самому посылать такие сообщения своему приложению, однако для этого обычная функция PostMessage не подходит. Вместо нее предусмотрена другая функция
BOOL PostAppMessage(hTask, uMsg, wParam, lParam);
Эта функция направляет сообщение приложению, указанному хендлом задачи hTask[2]. Для получения этого хендла можно воспользоваться функциями GetCurrentTask или GetWindowTask.
При использовании функций, посылающих сообщения, необходимо учитывать ограничения на размер очереди сообщений. По умолчанию очередь способна удерживать только 8 сообщений. При необходимости увеличить размер очереди надо воспользоваться функцией
BOOL SetMessageQueue(cMaxMsg);
где параметр cMaxMsg задает количество сообщений, которые могут находиться в очереди. Это число не может быть больше 120. Функция уничтожает старую очередь, вместе со всеми сообщениями, которые могут в ней оказаться, и создает новую, пустую, указанного размера. Если надо увеличить размер очереди, то это лучше делать в самом начале, до создания окон и организации цикла обработки сообщений.
Особенности посылки и передачи сообщений в Win32 API
В Win32 в связи с появлением многопотоковых приложений изменился процесс маршрутизации сообщений в системе. Теперь очередь сообщений принадлежит не приложению (процессу), а потоку, создавшему ее. Поток, не использующий очередей и, соответственно, не имеющий цикла обработки сообщений, называется рабочим потоком (worker thread), а поток, работающий с окнами, создающий свою очередь и цикл обработки сообщений называется интерфейсным потоком (interface thread). В одном приложении Win32 может оказаться несколько рабочих потоков, несколько интерфейсных потоков и, соответственно, несколько очередей одновременно.
Еще одно отличие связано с тем, что размер очереди больше не фиксирован — при необходимости очередь динамически увеличивается. Благодаря этому функция SetMessageQueue больше не применяется.
Возможность применения нескольких интерфейсных потоков в одном процессе приводит дополнительно к отказу от функции PostAppMessage — так как остается неясно, какому потоку надо послать сообщение, и к добавлению функции:
BOOL PostThreadMessage(dwThreadID, uMsg, wParam, lParam);
которая посылает сообщение конкретному потоку. При этом в очередь соответствующего потока помещается сообщение, имеющее нулевой хендл окна–получателя. Идентификатор потока dwThreadID можно получить посредством функций:
DWORD GetWindowThreadProcessId(hWnd, lpdwProcessID);
DWORD GetCurrentThreadId(void);
Так как один процесс может иметь несколько потоков, каждый со своей очередью и своими окнами, и сообщения могут передаваться (SendMessage) окнам любого потока и даже любого процесса, то для обеспечения необходимого уровня защиты было принято решение, что бы сообщения, направленные окну, обрабатывались только тем потоком, который это окно создал. Если сообщение передается окну, созданному тем–же потоком, то функция SendMessage просто вызывает оконную процедуру для обработки сообщения.
А если окно создано другим потоком, то процесс передачи сообщения приводит к сложному взаимодействию потока–отправителя и потока–получателя. Передающий поток помещает сообщение в очередь принимающего потока со специальным флагом переданное сообщение (QS_SENDMESSAGE) и приостанавливается до получения ответа. Принимающий поток, закончив обработку текущего сообщения, извлекает из очереди первыми сообщения с флагом переданное, обрабатывает их, возвращает результат и после этого возобновляет работу передавшего сообщение потока.
Однако в жизни ситуация существенно усложняется: часто поток, обрабатывающий переданное сообщение, передает пославшему потоку какое–либо «встречное» сообщение. Так, например, при начале DDE–разговора DDE–клиент передает с помощью функции SendMessage широковещательное сообщение WM_DDE_INITIATE, а DDE–сервер отвечает на это сообщение передачей сообщения WM_DDE_ACK, если он поддерживает требуемый DDE–разговор. То есть обработчик сообщения WM_DDE_INITIATE сервера передает с помощью той–же функции SendMessage сообщение WM_DDE_ACK окну клиента, которое в данный момент ждет конца обработки переданного им сообщения WM_DDE_INITIATE[3]. При этом было бы возможным зависание обеих потоков, так как поток клиента остановлен до конца обработки переданного им WM_DDE_INITIATE, а поток сервера будет ждать, пока остановленный поток клиента не ответит на встречное сообщение WM_DDE_ACK, которое он передает в качестве подтверждения.
Что бы избежать подобных неприятностей функция SendMessage может обрабатывать сообщения, переданные данному потоку другими потоками, находясь в ожидании ответа от обработчика переданного сообщения.
Осторожно! Зависание потоков при обмене сообщениями все–таки возможно. Такая ситуация легко может случиться при использовании MDI: в Win32 для каждого открытого дочернего MDI–окна автоматически создается свой собственный поток. В этом случае при использовании функции SetFocus для передачи фокуса от одного дочернего MDI–окна другому дочернему MDI–окну (естественно принадлежащему другому потоку) происходит обмен сообщениями WM_SETFOCUS и WM_KILLFOCUS, что приводит к остановке обеих потоков. Эта особенность платформы официально описана Microsoft.
Кроме того возможно зависание потока, передавшего сообщение, если поток–получатель завис сам или занят очень продолжительными операциями. Так как продолжительная остановка передающего сообщение потока может быть нежелательна, то в Win32 API описаны специальные функции для работы с межпотоковыми сообщениями: SendMessageTimeout, SendMessageCallback, SendNotifyMessage и ReplyMessage.
Внимание! Функции SendMessageTimeout, SendMessageCallback и SendNotifyMessage не реализованы для платформы Win32s а, кроме того, в документации (правда не всегда) встречается странное упоминание о том, что в случае Windows–95 параметр wParam является 16ти битовым (!), что может существенно ограничить применение этих функций для передачи многих сообщений.
Посылка сообщений
Мы это сделаем на примере нажатия на клавишу. Когда вы нажимаете на клавишу, генерируется аппаратное прерывание. Клавиатурный драйвер Windows обрабатывает это прерывание и помещает соответствующее сообщение в общую очередь сообщений Windows.
При этом указывается, какое окно должно получить это сообщение. Затем Windows извлекает из своей очереди это сообщение и помещает его в очередь сообщений приложения, содержащего окно–адресат. Вслед за этим уже само приложение выбирает из очереди поступившее сообщение и передает его соответствующей оконной функции.
Этот процесс называется посылкой (post) сообщений, так как посылка сообщения напоминает посылку письма: посылающий сообщение указывает адресата, отправляет сообщение и больше о нем не беспокоится. Отправитель не знает, когда точно его сообщение получит адресат. Процесс посылки может показаться излишне сложным, однако для этого существует несколько причин:
Во–первых, аппаратные прерывания надо обрабатывать со всей возможной скоростью. Поэтому при приеме аппаратного прерывания драйвер не тратит время на передачу сообщения в очередь приложения, а ставит его в очередь сообщений Windows. Аппаратные прерывания являются асинхронными по отношению к выполняющемуся приложению, а обработка сообщений обязательно должна быть синхронной. Поэтому механизм посылки сообщений нельзя смешивать с аппаратными прерываниями.
А во–вторых, накопление событий в очереди приложения помогает уменьшить количество переключений между приложениями, так как Windows обычно дает приложению полностью обработать события из его очереди и лишь после этого переключается на другие приложения. Кроме того, некоторые события могут группироваться в одно во время нахождения в очереди.
Рисунок SEQ Рисунок \* ARABIC 1 Маршрутизация сообщений в Windows 3.x
Извлечение сообщений из очереди приложения и направление их соответствующим окнам осуществляет функция WinMain. Этот процесс выполняется в несколько приемов:
сообщение выбирается из очереди с помощью функции GetMessage или PeekMessage
затем сообщение транслируется с помощью функции TranslateMessage[0] (одно сообщение может порождать последовательность других или заменяться, как, например, происходит для сообщений клавиатуры WM_KEYDOWN). Часто трансляция состоит из вызова более чем одной функции, сюда могут добавляться специальные средства трансляции акселераторов и немодальных диалогов (об этом позже).
И только после этого оно направляется окну с помощью функции DispatchMessage (это называется диспетчеризацией)
Для выполнения этих операций существуют специальные функции. Эти функции образуют цикл обработки сообщений, так как после завершения обработки одного сообщения приложение должно приготовиться к обработке следующего. Цикл заканчивается только при завершении работы приложения.
MSG msg;
while (GetMessage(&msg, NULL, NULL, NULL)) {
TranslateMessage(&msg);
DispatchMessage(&msg);}
Это самый простой вид цикла обработки сообщений. В реальных приложениях он более сложный. Все три функции, вызываемые здесь, принадлежат Windows. Назначение их должно быть понятно. Требуется добавить несколько замечаний о функции GetMessage. Эта функция имеет следующие аргументы:
BOOL GetMessage(lpMsg, hWnd, uMsgFilterMin, uMsgFilterMax);
lpMsg указывает на структуру MSG, в которую будет записано полученное сообщение. Если очередь сообщений пуста, то GetMessage передает управление оболочке, так что та может начать обработку сообщений другого приложения.
Какие же данные передаются сообщением?
typedef struct tagMSG {
HWND hwnd; // хендл окна-получателя
UINT message; // номер сообщения WM_...
WPARAM wParam; // параметр сообщения
LPARAM lParam; // параметр сообщения
DWORD time; // время поступления сообщения
POINT pt; // координаты сообщения (для сообщений мыши)
} MSG;
Поле message структуры MSG задает номер сообщения, посланного системой. Интерпретация параметров сообщения wParam и lParam зависит от самого сообщения. Для этого надо смотреть описание конкретного сообщения и обрабатывать параметры соответствующим образом. Так как в системе определено огромное количество разных сообщений, то для простоты использования применяются символические имена сообщений, задаваемыми с помощью #define в заголовочном файле. В качестве примера можно привести сообщения WM_CREATE, WM_PAINT, WM_QUIT.
hWnd указывает хендл окна, сообщения для которого будут выбираться из очереди. Если hWnd равен NULL, то будут выбираться сообщения для всех окон данного приложения, а если hWnd указывает реальное окно, то из очереди будут выбираться все сообщения, направленные этому окну или его потомкам (дочерним или используемым окнами, или их потомкам, в том числе отдаленным).
uMsgFilterMin и uMsgFilterMax обычно установлены в NULL. Вообще они задают фильтр для сообщений. GetMessage выбирает из очереди сообщения, номера (имена) которых лежат в интервале от uMsgFilterMin до uMsgFilterMax. Нулевые значения исключают фильтрацию.
Функция GetMessage возвращает во всех случаях, кроме одного, ненулевое значение, указывающее, что цикл надо продолжать. Только в одном случае эта функция возвратит 0 — если она извлечет из очереди сообщение WM_QUIT. Это сообщение посылается только при окончании работы приложения.
После завершения цикла надо сделать совсем немногое — освободить память от тех объектов, которые создавались во время работы приложения (если они еще существуют). Некоторые объекты, которые уничтожаются автоматически, можно не освобождать — это сделает Windows. Таков, например, зарегистрированный нами класс окон.
И остается еще одно дело: так как WinMain возвращает результат, то мы должны вернуть какое–либо значение. В Windows принято, что возвращаемое значение является параметром wParam сообщения WM_QUIT, завершившего цикл обработки сообщений. Таким образом мы пишем:
return msg.wParam;
Посылка и передача сообщений
Ранее, в разделе “ REF _Ref422032812 \* MERGEFORMAT Ошибка! Источник ссылки не найден.”, мы рассматривали метод передачи сообщений, называемый посылкой сообщений (post message), и их обработки — извлечения из очереди в цикле обработки сообщений, трансляции и последующей передачи оконной процедуре. Источниками таких сообщений могут быть как компоненты системы, например, клавиатурный драйвер, так и само приложение. Для посылки сообщения в API предусмотрена функция
BOOL PostMessage(hWnd, uMsg, wParam, lParam);
Эта функция ставит сообщение в очередь. Возвращаемое значение TRUE указывает, что сообщение поставлено в очередь, FALSE — возникла ошибка (например, ошибочно указанный адресат или очередь сообщений переполнилась). Позже сообщение будет извлечено из очереди вызовом функции GetMessage или PeekMessage в цикле обработки сообщений.
Однако механизм посылки сообщений не всегда удобен, так как не позволяет получить результат обработки сообщения, или дождаться его завершения. Точнее, позволяет, но очень громоздким способом — надо вводить специальные ответные сообщения и дожидаться их получения.
Вообще говоря, процесс посылки и обработки посланных сообщений часто называют асинхронным способом обработки сообщений, так как сама посылка сообщения и его обработка никак между собой не связаны по времени.
Для решения этих задач вводится альтернативный механизм, называемый передачей сообщений (send message). При этом сообщение в очередь не попадает, а направляется непосредственно оконной функции[1]. Приблизительно его можно рассматривать как непосредственный вызов процедуры обработки сообщений. Для передачи сообщения используется функция
LONG SendMessage(hWnd, uMsg, wParam, lParam);
Она вызывает оконную процедуру указанного окна, получает результат обработки сообщения и возвращает управление в вызвавшую процедуру после обработки указанного сообщения. Возвращаемое этой функцией значение совпадает с результатом, выполнения оконной функции. Вы можете воспользоваться ею для передачи тех или иных сообщений даже до организации цикла обработки сообщений, так как очередь сообщений при этом не используется (кроме особых случаев в Win32 API — см. ниже).
Многие функции API используют SendMessage для передачи сообщений окну. Например, функция CreateWindow, создающая окно, в процессе создания передает ему сообщение WM_CREATE (и не только одно это сообщение), а результат, возвращенный обработчиком сообщения, используется функцией CreateWindow для продолжения создания окна.
Процесс передачи сообщений с помощью функции SendMessage называется синхронной обработкой сообщений, так как процесс передачи и обработки сообщений жестко упорядочен во времени.
То, что сообщения, переданные с помощью функции SendMessage, минуют цикл обработки сообщений, накладывает ограничения на применение этой функции. Дело в том, что цикл обработки сообщений выполняет над некоторыми сообщениями определенные операции. Так, например, нельзя передавать сообщение WM_QUIT — оно обязательно должно пройти через очередь сообщений, так как используется для завершения цикла обработки сообщений. Другие сообщения (например, клавиатуры) транслируются в цикле и их тоже надо посылать, а не передавать.
Иногда сообщения передаются не какому–либо конкретному окну и не какому–либо приложению (потоку), а всем приложениям, запущенным в системе. Для этого используются широковещательные сообщения (broadcast message), которые передаются всем главным окнам всех приложений. Для передачи такого сообщения необходимо указать в качестве хендла окна–получателя специальный символ HWND_BROADCAST (равный -1), например:
SendMessage(HWND_BROADCAST, WM_DDE_INITIATE, (WPARAM)hwndDDEClient, 0L);
В этом случае сообщение WM_DDE_INITIATE будет передано главным окнам всех приложений, так что все работающие DDE–сервера смогут на него ответить.
Существенная особенность широковещательных сообщений — то, что они будут обрабатываться сразу большим количеством окон. То есть получить результат от обработчика этого сообщения обычным путем нельзя, так как остается неопределенность, результат какого обработчика возвращать. В частном случае (Win32 API) можно получить ответы от всех обработчиков с помощью функции SendMessageCallback. В общем случае при необходимости получения ответа от обработчика тот должен передать ответное сообщение (именно так сделано при установлении DDE–разговора).
Особенности посылки сообщений в Windows API
Помимо рассмотренных функций в Windows API существует еще две функции, посылающие сообщения. С одной из них — PostQuitMessage мы уже встречались в примере 1A. Эта функция посылает сообщение WM_QUIT, которое служит для завершения цикла обработки сообщений.
BOOL PostQuitMessage(wParam);
Сообщение WM_QUIT интересно еще и тем, что оно не посылается никакому окну — хендл окна–получателя равен NULL. В принципе у вас есть возможность самому посылать такие сообщения своему приложению, однако для этого обычная функция PostMessage не подходит. Вместо нее предусмотрена другая функция
BOOL PostAppMessage(hTask, uMsg, wParam, lParam);
Эта функция направляет сообщение приложению, указанному хендлом задачи hTask[2]. Для получения этого хендла можно воспользоваться функциями GetCurrentTask или GetWindowTask.
При использовании функций, посылающих сообщения, необходимо учитывать ограничения на размер очереди сообщений. По умолчанию очередь способна удерживать только 8 сообщений. При необходимости увеличить размер очереди надо воспользоваться функцией
BOOL SetMessageQueue(cMaxMsg);
где параметр cMaxMsg задает количество сообщений, которые могут находиться в очереди. Это число не может быть больше 120. Функция уничтожает старую очередь, вместе со всеми сообщениями, которые могут в ней оказаться, и создает новую, пустую, указанного размера. Если надо увеличить размер очереди, то это лучше делать в самом начале, до создания окон и организации цикла обработки сообщений.
Особенности посылки и передачи сообщений в Win32 API
В Win32 в связи с появлением многопотоковых приложений изменился процесс маршрутизации сообщений в системе. Теперь очередь сообщений принадлежит не приложению (процессу), а потоку, создавшему ее. Поток, не использующий очередей и, соответственно, не имеющий цикла обработки сообщений, называется рабочим потоком (worker thread), а поток, работающий с окнами, создающий свою очередь и цикл обработки сообщений называется интерфейсным потоком (interface thread). В одном приложении Win32 может оказаться несколько рабочих потоков, несколько интерфейсных потоков и, соответственно, несколько очередей одновременно.
Еще одно отличие связано с тем, что размер очереди больше не фиксирован — при необходимости очередь динамически увеличивается. Благодаря этому функция SetMessageQueue больше не применяется.
Возможность применения нескольких интерфейсных потоков в одном процессе приводит дополнительно к отказу от функции PostAppMessage — так как остается неясно, какому потоку надо послать сообщение, и к добавлению функции:
BOOL PostThreadMessage(dwThreadID, uMsg, wParam, lParam);
которая посылает сообщение конкретному потоку. При этом в очередь соответствующего потока помещается сообщение, имеющее нулевой хендл окна–получателя. Идентификатор потока dwThreadID можно получить посредством функций:
DWORD GetWindowThreadProcessId(hWnd, lpdwProcessID);
DWORD GetCurrentThreadId(void);
Так как один процесс может иметь несколько потоков, каждый со своей очередью и своими окнами, и сообщения могут передаваться (SendMessage) окнам любого потока и даже любого процесса, то для обеспечения необходимого уровня защиты было принято решение, что бы сообщения, направленные окну, обрабатывались только тем потоком, который это окно создал. Если сообщение передается окну, созданному тем–же потоком, то функция SendMessage просто вызывает оконную процедуру для обработки сообщения.
А если окно создано другим потоком, то процесс передачи сообщения приводит к сложному взаимодействию потока–отправителя и потока–получателя. Передающий поток помещает сообщение в очередь принимающего потока со специальным флагом переданное сообщение (QS_SENDMESSAGE) и приостанавливается до получения ответа. Принимающий поток, закончив обработку текущего сообщения, извлекает из очереди первыми сообщения с флагом переданное, обрабатывает их, возвращает результат и после этого возобновляет работу передавшего сообщение потока.
Однако в жизни ситуация существенно усложняется: часто поток, обрабатывающий переданное сообщение, передает пославшему потоку какое–либо «встречное» сообщение. Так, например, при начале DDE–разговора DDE–клиент передает с помощью функции SendMessage широковещательное сообщение WM_DDE_INITIATE, а DDE–сервер отвечает на это сообщение передачей сообщения WM_DDE_ACK, если он поддерживает требуемый DDE–разговор. То есть обработчик сообщения WM_DDE_INITIATE сервера передает с помощью той–же функции SendMessage сообщение WM_DDE_ACK окну клиента, которое в данный момент ждет конца обработки переданного им сообщения WM_DDE_INITIATE[3]. При этом было бы возможным зависание обеих потоков, так как поток клиента остановлен до конца обработки переданного им WM_DDE_INITIATE, а поток сервера будет ждать, пока остановленный поток клиента не ответит на встречное сообщение WM_DDE_ACK, которое он передает в качестве подтверждения.
Что бы избежать подобных неприятностей функция SendMessage может обрабатывать сообщения, переданные данному потоку другими потоками, находясь в ожидании ответа от обработчика переданного сообщения.
Осторожно! Зависание потоков при обмене сообщениями все–таки возможно. Такая ситуация легко может случиться при использовании MDI: в Win32 для каждого открытого дочернего MDI–окна автоматически создается свой собственный поток. В этом случае при использовании функции SetFocus для передачи фокуса от одного дочернего MDI–окна другому дочернему MDI–окну (естественно принадлежащему другому потоку) происходит обмен сообщениями WM_SETFOCUS и WM_KILLFOCUS, что приводит к остановке обеих потоков. Эта особенность платформы официально описана Microsoft.
Кроме того возможно зависание потока, передавшего сообщение, если поток–получатель завис сам или занят очень продолжительными операциями. Так как продолжительная остановка передающего сообщение потока может быть нежелательна, то в Win32 API описаны специальные функции для работы с межпотоковыми сообщениями: SendMessageTimeout, SendMessageCallback, SendNotifyMessage и ReplyMessage.
Внимание! Функции SendMessageTimeout, SendMessageCallback и SendNotifyMessage не реализованы для платформы Win32s а, кроме того, в документации (правда не всегда) встречается странное упоминание о том, что в случае Windows–95 параметр wParam является 16ти битовым (!), что может существенно ограничить применение этих функций для передачи многих сообщений.
LRESULT SendMessageTimeout(
hWnd, uMsg, wParam, lParam, fuFlags, uTimeout, lpdwResult);
Дополнительные параметры этой функции: lpdwResult — указывает переменную, в которую будет помещен результат обработки сообщения, uTimeout — максимальное время, в миллисекундах, в течении которого надо ожидать результата, а fuFlags — определяет, как функция будет ожидать ответа:
флаг SMTO_ABORTIFHUNG говорит о том, что если поток–получатель завис (то есть уже более 5 секунд не обрабатывает никаких сообщений), то не ждать результата, а сразу вернуть управление.
флаг SMTO_BLOCK исключает обработку «встречных» сообщений во время ожидания ответа. С этим флагом надо быть очень осторожным!
флаг SMTO_NORMAL используется тогда, когда ни SMTO_BLOCK ни SMTO_ABOPRTIFHUNG не указаны.
Возвращаемый функцией результат равен TRUE, если сообщение обработано или FALSE, если отведенное для ожидания время истекло или поток–получатель завис.
Если функция SendMessageTimeout применяется для передачи сообщения окну, созданному тем–же самым потоком, то она работает как обычная функция SendMessage — время ожидания не ограничено и проверка на зависание не выполняется.
Вторая функция SendMessageCallback передает сообщение указанному окну и возвращает управление немедленно после этого, не дожидаясь результата обработки сообщения. Но когда сообщение будет обработано, будет вызвана указанная вами функция, обрабатывающая полученный результат.
BOOL SendMessageCallback(hWnd, uMsg, wParam, lParam, lpfnResultCallback, dwData);
void CALLBACK ResultCallback(hWnd, uMsg, dwData, lResult) {...}
Параметр lpfnResultCallback является указателем на функцию–обработчик результата, а dwData — некоторые дополнительные данные, которые вы хотите передать в вашу функцию–обработчик. Прототип этой функции приведен в следующей строке.
Уточнение: реально функция–обработчик результата будет вызвана не немедленно после обработки сообщения получателем, а результат обработки будет помещен в очередь обработанных сообщений, откуда он будет извлечен тогда, когда поток, передавший сообщение, обратиться к очереди сообщений для получения следующего (то есть при вызове GetMessage, PeekMessage, WaitMessage или одну из функций SendMessage...).
Функцию SendMessageCallback можно с успехом сочетать с широковещательными сообщениями — тогда у вас появляется возможность анализировать результаты обработки этого сообщения всеми главными окнами приложений.
Если функция SendMessageCallback используется для передачи сообщения окну, созданному тем–же потоком, то получение результата и вызов функции–обработчика результата осуществится немедленно, до возвращения из функции SendMessageCallback.
Отдельно стоит рассмотреть вопросы оптимизации обработки межпотоковых сообщений. Дело в том, что Windows NT может работать на многопроцессорных системах, реально выполняя несколько потоков в одно и то же время. В этом случае может быть целесообразно сократить время, в течении которого один поток просто ожидает результат обработки другого потока. Для этого предназначены две дополнительные функции ReplyMessage и SendNotifyMessage.
Функция ReplyMessage используется обработчиком межпотокового сообщения для того, что бы досрочно сообщить вызвавшему потоку об окончании обработки сообщения. Так, например, возможна ситуация, когда поток–получатель сообщения выясняет, что сообщение будет успешно обработано и даже известно, какой результат надо будет возвратить в самом начале обработки, но до полного завершения необходимо выполнить еще некоторые дополнительные действия. В этом случае будет удобно сообщить требуемый результат сразу после его получения с помощью функции ReplyMessage, после чего продолжить обработку сообщения. Поток, передавший сообщение сможет продолжить свою работу не дожидаясь полного завершения обработки сообщения.
Функция SendNotifyMessage появилась на свет в связи с тем, что многие сообщения являются просто информационными. То есть они сообщают окну, что с ним сделано то–то и то–то, но результат обработки этого сообщения не нужен. Часто такие сообщения должны передаваться в синхронном режиме, например сообщение WM_DESTROY должно быть обработано во время выполнения функции DestroyWindow и до обработки сообщения WM_NCDESTROY, но то как оно будет обработано и каков результат его обработки — не важно. В качестве обратного примера можно привести обработку сообщения WM_CREATE, так как результат обработки нужен для дальнейшего создания окна или отказа от его создания.
Для синхронной передачи информационных сообщений удобно применять функцию SendNotifyMessage, которая передает сообщение как обычная функция SendMessage, но не дожидается результата обработки этого сообщения. До определенной степени эффект от выполнения функции SendNotifyMessage похож на эффект от функции PostMessage, но переданное таким способом сообщение будет обработано заведомо то того, как будет обработано любое посланное обычным способом сообщение.
Подробнее о циклах обработки сообщений
Цикл обработки сообщений, в том простейшем виде, который применен в примере 1A, применяется редко. В реальных приложениях этот цикл усложняется. Обычно при этом преследуют следующие задачи:
включить дополнительные средства трансляции сообщений, обычно применяемые акселераторами и немодальными диалогами;
при выполнении каких–либо продолжительных действий дать возможность обрабатывать поступающие сообщения (например, в процессе печати документа);
включить обработку состояний простоя (когда в очереди нет сообщений) для выполнения каких–либо действий в фоновом режиме.
При рассмотрении двух последних задач следует иметь в виду, что аналогичного эффекта можно добиться применением многопотоковых приложений. Более того, во многих случаях многопотоковый вариант оказывается предпочтительным. При принятии решения необходимо учитывать специфику разрабатываемого приложения и его область применения. Если отказ от устаревшей 16ти разрядной платформы Windows 3.x не вызывает возражений, а также речь идет о разработке нового приложения, а не переносе старого на новую платформу, то стоит серьезно проработать вопрос с реализацией многопотокового приложения.
Дополнительная обработка сообщений
Первая задача — дополнительная обработка сообщений — обычно сводится к выполнению дополнительных действий после извлечения сообщения из очереди и трансляцией этого сообщения. Для применения акселераторов обычно используют функцию TranslateAccelerator или TranslateMDISysAccel, для немодальных диалогов — IsDialogMessage. Часто функции, выполнившие специфичную трансляцию сообщения, заодно передают их оконной процедуре, так что последующая трансляция и вызов DispatchMessage становятся не нужны. Например:
HWND hwndDlg = ...; // хендл окна немодального диалога
MSG msg;
while (GetMessage(&msg, NULL, NULL, NULL)) {
if (!IsWindow(hwndDlg) || !IsDialogMessage(hwndDlg, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);}}
В этом примере проверяется наличие окна немодального диалога и, если оно существует, то выполняется специальная трансляция сообщений с помощью функции IsDialogMessage. Сообщения, обработанные этой функцией, в дальнейшей трансляции не участвуют
Продолжительные операции
Вторая задача — дать возможность нормальной работы интерфейса при выполнении продолжительных операций. Эта задача обычно решается путем встраивания вспомогательных циклов обработки сообщений в процессе выполнения продолжительных операций. Единственное, что при этом требуется — что бы цикл завершался не по получению WM_QUIT, а по завершению обработки последнего сообщения в очереди, что бы приложение могло продолжить свою операцию. Для этого применяют функцию PeekMessage вместо GetMessage.
BOOL PeekMessage(lpMsg, hWnd, uFilterFirst, uFilterLast, fuRemove);
Эта функция осуществляет выборку сообщений из очереди, однако, если очередь пуста, функция PeekMessage просто возвращает FALSE, а не ждет поступления сообщения. Ее параметры аналогичны параметрам функции GetMessage, кроме дополнительного fuRemove. Этот параметр может быть комбинацией флага PM_NOYIELD и одного из флагов PM_REMOVE или PM_NOREMOVE.
Флаг PM_NOYIELD говорит функции о том, что при отсутствии сообщений в очереди нельзя передавать управление другим приложениям. Обычно PeekMessage при отсутствии сообщений в очереди передает управление другим приложениям и, только если их очереди тоже пусты, возвращает FALSE. Таким образом цикл, построенный на PeekMessage без флага PM_NOYIELD, не исключает возможность нормальной работы других приложений.
Флаг PM_NOREMOVE явно указывает, что нужно только проверить наличие сообщений в очереди. Если сообщение есть, возвращается информация об этом сообщении (в структуре MSG), а сообщение остается в очереди. Флаг PM_REMOVE указывает на необходимость извлечения сообщений из очереди.
MSG msg;
// продолжительные операции (вычисления, печать ...)
while (PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);}
// дальнейшие продолжительные операции
Такой цикл сначала обработает все сообщения, накопившиеся в очереди, а затем завершиться. При использовании таких циклов надо иметь в виду следующее:
Вы можете ограничить работу интерфейса только каким–либо одним окном, может быть панелью диалога (и всеми дочерними окнами данного окна). Тогда необходимо указать вторым параметром функции PeekMessage хендл этого окна, а в цикле предусмотреть необходимую трансляцию для работы этого окна (панели диалога).
Вы можете разрешить нормальную работу всего приложения. Тогда вы должны предусмотреть в этом цикле такую–же трансляцию, как и в цикле обработки сообщений в функции WinMain (это удобно сделать, выделив всю необходимую трансляцию в отдельную процедуру) и, дополнительно, предусмотреть возможность получения WM_QUIT в таком цикле. При извлечении WM_QUIT надо либо принять меры к немедленному завершению работы приложения, либо завершить продолжительные операции и послать WM_QUIT снова. Например, так:
MSG msg;
BOOL fStop = FALSE;
// продолжительные операции (вычисления, печать ...)
while (PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
fStop = TRUE;
PostQuitMessage(msg.wParam); // так завершится цикл в WinMain
break; // а так — этот}
MyTranslation(&msg); // будем считать, что сюда мы убрали всю
// трансляцию и вызов DispatchMessage}
if (!fStop) {
// дальнейшие продолжительные операции}
Обработка состояний простоя
Третья задача — выполнение действий в состоянии простоя, очень близка к предыдущей. Часто все продолжительные операции выполняют именно в состоянии простоя. Отличие заключается в том, как именно организуется выполнение таких действий.
В предыдущем варианте фрагмент кода, выполняющий продолжительные операции, просто содержит дополнительные циклы обработки сообщений. То есть вы практически разрабатываете некоторую программу, выполняющую продолжительные вычисления, а затем не изменяя алгоритма, вставляете в него дополнительные циклы обработки сообщений. При этом небольшие сложности возникают только с обработкой сообщений.
Рассматриваемый здесь вариант — обработка состояний простоя позволяет выполнить те–же операции несколько другим способом. В цикле обработки сообщений можно легко определять моменты, когда все сообщения в очереди уже обработаны и, если это так, вызывать обработку специального сообщения (или процедуры) обслуживающего состояние простоя. Сложности будут связаны с тем, что сама по себе обработка состояния простоя не должна быть продолжительной — так как она временно останавливает цикл обработки сообщений. Если вы собираетесь организовать выполнение продолжительных операций таким способом, то вы должны разделить их на небольшие, быстро выполняемые фрагменты, которые будут последовательно вызываться при обработке состояний простоя.
Для реализации циклов с обработкой состояний простоя обычно применяют одну из двух функций: WaitMessage или GetMessage. Так как с WaitMessage мы еще не встречались, то сначала рассмотрим вариант с ней:
void WaitMessage(void);
Эта функция просто ожидает поступление в очередь нового сообщения. Причем, даже если в очереди уже есть сообщения, она все–равно будет ожидать нового, поэтому ее вызывают только когда очередь сообщений пуста.
for (;;) {
while (!PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) {
// ... выполнить операции во время простоя
WaitMessage(); // дождаться следующего сообщения}
if (msg.message == WM_QUIT) break; // прервать цикл по WM_QUIT
// нормальная обработка сообщения
TranslateMessage(&msg);
DispatchMessage(&msg);}
Такой цикл применяют обычно непосредственно в функции WinMain, вместо обычного. Надо отметить, что вызов одной функции GetMessage заменился на целый цикл из PeekMessage, необходимых операций и WaitMessage, а также проверку сообщения WM_QUIT. Причем вложенный цикл выполнятся однократно для каждого состояния простоя — после возврата из функции WaitMessage в очереди будет присутствовать сообщение, PeekMessage вернет TRUE и вложенный цикл завершиться.
В принципе, вместо WaitMessage можно использовать GetMessage, примерно так:
for (;;) {
if (!PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) {
// ... выполнить операции во время простоя
if (!GetMessage(&msg, NULL, NULL, NULL)) break; // WM_QUIT (!)}
if (msg.message == WM_QUIT) break; // прервать цикл по WM_QUIT
...}
На чем остановиться?
При выборе между двумя разными способами организации продолжительных операций надо учитывать следующие соображения:
Вариант с обработкой состояний простоя существенно более трудоемкий, однако имеет определенные достоинства, связанные с тем, что может быть организовано выполнение нескольких видов продолжительных операций одновременно. Например, можно в состоянии простоя организовать одновременную печать документа на принтере, выполнение сложного математического расчета и, скажем, оптимизацию использования памяти задачей. До определенной степени такая организация приложения позволяет имитировать многопотоковые задачи в рамках одного потока. Это может быть оправдано при разработке приложений, работающих в среде Windows 3.x, не поддерживающей многопотоковые задачи. Для приложений Win32 API обычно проще воспользоваться дополнительными потоками.
Как правило трудоемкость реализации существенно зависит от первоначально поставленной перед разработчиком задачи. Если задача первоначально поставлена так, что выполнение продолжительных операций сразу ориентировано на обработку состояний простоя, то это практически не потребует дополнительных затрат времени и сил. Но переделка уже реализованных в виде одного непрерывного процесса продолжительных операций займет изрядное время и не всегда может быть сделана без кардинальной переделки задачи.
Тщательный анализ создаваемого приложения часто приводит к упрощению поставленной задачи. Так, например, возможность организовать печать документа в фоновом режиме (при обработке состояний простоя или в отдельном потоке), невольно порождает желание воспользоваться этой возможностью “на всю катушку” — разрешить одновременное редактирование документа, его печать, а также печать еще нескольких других документов разом. Дух захватывает от открывающихся возможностей.
Однако задумайтесь над такими вопросами:
если документ и печатается, и редактируется одновременно, то что надо печатать — то, что было в документе на момент начала печати, то что в нем уже есть с учетом сделанных исправлений, или то, что будет, когда исправления закончатся?
как вы себе представляете одновременную печать двух документов на одном принтере?
каковы будут требования к компьютеру, что бы он мог без существенных задержек в работе редактора выполнять печать нескольких документов одновременно?
Конечно, во всех случаях можно найти выходы из создавшегося положения, например: делать копию документа, которая будет отправлена в печать, что позволит продолжить редактирование, использовать разные принтеры для печати разных документов, либо система сама организует спулинг для печати нескольких документов на одном принтере и так далее.
Вопрос в другом — как часто эти возможности будут использоваться и оправдают ли себя существенно возросшие требования к ресурсам, если эти возможности будут использоваться крайне редко?
Вот хороший пример — редактор Notepad крайне прост, возможности его бедны, но он занимает мало ресурсов, быстро загружается сам и быстро считывает текстовые файлы. Редактор WinWord 97 обладает неизмеримо большими возможностями, но и неизмеримо большими требованиями к ресурсам системы, долго загружается сам и достаточно долго (по сравнению с Notepad) загружает редактируемый текст. Теперь вопрос — если вам надо подправить пару строк в autoexec.bat, вы воспользуетесь редактором Notepad или WinWord?
Разрабатывая новое приложение вы должны очень хорошо представлять себе его область применения и круг задач, которые оно должно будет решать. Сент–Экзюпери говорил, что “совершенство достигается не тогда, когда уже нечего добавить, а тогда, когда нечего отнять”. Перегружая задачу разными, может быть и удобными, но редко используемыми функциями, вы рискуете сделать его неудобным при выполнении часто выполняемых функций и, в конечном итоге, бесполезным — так как трудно представить себе более бесполезную вещь, чем та, которой никто не хочет пользоваться.
hWnd, uMsg, wParam, lParam, fuFlags, uTimeout, lpdwResult);
Дополнительные параметры этой функции: lpdwResult — указывает переменную, в которую будет помещен результат обработки сообщения, uTimeout — максимальное время, в миллисекундах, в течении которого надо ожидать результата, а fuFlags — определяет, как функция будет ожидать ответа:
флаг SMTO_ABORTIFHUNG говорит о том, что если поток–получатель завис (то есть уже более 5 секунд не обрабатывает никаких сообщений), то не ждать результата, а сразу вернуть управление.
флаг SMTO_BLOCK исключает обработку «встречных» сообщений во время ожидания ответа. С этим флагом надо быть очень осторожным!
флаг SMTO_NORMAL используется тогда, когда ни SMTO_BLOCK ни SMTO_ABOPRTIFHUNG не указаны.
Возвращаемый функцией результат равен TRUE, если сообщение обработано или FALSE, если отведенное для ожидания время истекло или поток–получатель завис.
Если функция SendMessageTimeout применяется для передачи сообщения окну, созданному тем–же самым потоком, то она работает как обычная функция SendMessage — время ожидания не ограничено и проверка на зависание не выполняется.
Вторая функция SendMessageCallback передает сообщение указанному окну и возвращает управление немедленно после этого, не дожидаясь результата обработки сообщения. Но когда сообщение будет обработано, будет вызвана указанная вами функция, обрабатывающая полученный результат.
BOOL SendMessageCallback(hWnd, uMsg, wParam, lParam, lpfnResultCallback, dwData);
void CALLBACK ResultCallback(hWnd, uMsg, dwData, lResult) {...}
Параметр lpfnResultCallback является указателем на функцию–обработчик результата, а dwData — некоторые дополнительные данные, которые вы хотите передать в вашу функцию–обработчик. Прототип этой функции приведен в следующей строке.
Уточнение: реально функция–обработчик результата будет вызвана не немедленно после обработки сообщения получателем, а результат обработки будет помещен в очередь обработанных сообщений, откуда он будет извлечен тогда, когда поток, передавший сообщение, обратиться к очереди сообщений для получения следующего (то есть при вызове GetMessage, PeekMessage, WaitMessage или одну из функций SendMessage...).
Функцию SendMessageCallback можно с успехом сочетать с широковещательными сообщениями — тогда у вас появляется возможность анализировать результаты обработки этого сообщения всеми главными окнами приложений.
Если функция SendMessageCallback используется для передачи сообщения окну, созданному тем–же потоком, то получение результата и вызов функции–обработчика результата осуществится немедленно, до возвращения из функции SendMessageCallback.
Отдельно стоит рассмотреть вопросы оптимизации обработки межпотоковых сообщений. Дело в том, что Windows NT может работать на многопроцессорных системах, реально выполняя несколько потоков в одно и то же время. В этом случае может быть целесообразно сократить время, в течении которого один поток просто ожидает результат обработки другого потока. Для этого предназначены две дополнительные функции ReplyMessage и SendNotifyMessage.
Функция ReplyMessage используется обработчиком межпотокового сообщения для того, что бы досрочно сообщить вызвавшему потоку об окончании обработки сообщения. Так, например, возможна ситуация, когда поток–получатель сообщения выясняет, что сообщение будет успешно обработано и даже известно, какой результат надо будет возвратить в самом начале обработки, но до полного завершения необходимо выполнить еще некоторые дополнительные действия. В этом случае будет удобно сообщить требуемый результат сразу после его получения с помощью функции ReplyMessage, после чего продолжить обработку сообщения. Поток, передавший сообщение сможет продолжить свою работу не дожидаясь полного завершения обработки сообщения.
Функция SendNotifyMessage появилась на свет в связи с тем, что многие сообщения являются просто информационными. То есть они сообщают окну, что с ним сделано то–то и то–то, но результат обработки этого сообщения не нужен. Часто такие сообщения должны передаваться в синхронном режиме, например сообщение WM_DESTROY должно быть обработано во время выполнения функции DestroyWindow и до обработки сообщения WM_NCDESTROY, но то как оно будет обработано и каков результат его обработки — не важно. В качестве обратного примера можно привести обработку сообщения WM_CREATE, так как результат обработки нужен для дальнейшего создания окна или отказа от его создания.
Для синхронной передачи информационных сообщений удобно применять функцию SendNotifyMessage, которая передает сообщение как обычная функция SendMessage, но не дожидается результата обработки этого сообщения. До определенной степени эффект от выполнения функции SendNotifyMessage похож на эффект от функции PostMessage, но переданное таким способом сообщение будет обработано заведомо то того, как будет обработано любое посланное обычным способом сообщение.
Подробнее о циклах обработки сообщений
Цикл обработки сообщений, в том простейшем виде, который применен в примере 1A, применяется редко. В реальных приложениях этот цикл усложняется. Обычно при этом преследуют следующие задачи:
включить дополнительные средства трансляции сообщений, обычно применяемые акселераторами и немодальными диалогами;
при выполнении каких–либо продолжительных действий дать возможность обрабатывать поступающие сообщения (например, в процессе печати документа);
включить обработку состояний простоя (когда в очереди нет сообщений) для выполнения каких–либо действий в фоновом режиме.
При рассмотрении двух последних задач следует иметь в виду, что аналогичного эффекта можно добиться применением многопотоковых приложений. Более того, во многих случаях многопотоковый вариант оказывается предпочтительным. При принятии решения необходимо учитывать специфику разрабатываемого приложения и его область применения. Если отказ от устаревшей 16ти разрядной платформы Windows 3.x не вызывает возражений, а также речь идет о разработке нового приложения, а не переносе старого на новую платформу, то стоит серьезно проработать вопрос с реализацией многопотокового приложения.
Дополнительная обработка сообщений
Первая задача — дополнительная обработка сообщений — обычно сводится к выполнению дополнительных действий после извлечения сообщения из очереди и трансляцией этого сообщения. Для применения акселераторов обычно используют функцию TranslateAccelerator или TranslateMDISysAccel, для немодальных диалогов — IsDialogMessage. Часто функции, выполнившие специфичную трансляцию сообщения, заодно передают их оконной процедуре, так что последующая трансляция и вызов DispatchMessage становятся не нужны. Например:
HWND hwndDlg = ...; // хендл окна немодального диалога
MSG msg;
while (GetMessage(&msg, NULL, NULL, NULL)) {
if (!IsWindow(hwndDlg) || !IsDialogMessage(hwndDlg, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);}}
В этом примере проверяется наличие окна немодального диалога и, если оно существует, то выполняется специальная трансляция сообщений с помощью функции IsDialogMessage. Сообщения, обработанные этой функцией, в дальнейшей трансляции не участвуют
Продолжительные операции
Вторая задача — дать возможность нормальной работы интерфейса при выполнении продолжительных операций. Эта задача обычно решается путем встраивания вспомогательных циклов обработки сообщений в процессе выполнения продолжительных операций. Единственное, что при этом требуется — что бы цикл завершался не по получению WM_QUIT, а по завершению обработки последнего сообщения в очереди, что бы приложение могло продолжить свою операцию. Для этого применяют функцию PeekMessage вместо GetMessage.
BOOL PeekMessage(lpMsg, hWnd, uFilterFirst, uFilterLast, fuRemove);
Эта функция осуществляет выборку сообщений из очереди, однако, если очередь пуста, функция PeekMessage просто возвращает FALSE, а не ждет поступления сообщения. Ее параметры аналогичны параметрам функции GetMessage, кроме дополнительного fuRemove. Этот параметр может быть комбинацией флага PM_NOYIELD и одного из флагов PM_REMOVE или PM_NOREMOVE.
Флаг PM_NOYIELD говорит функции о том, что при отсутствии сообщений в очереди нельзя передавать управление другим приложениям. Обычно PeekMessage при отсутствии сообщений в очереди передает управление другим приложениям и, только если их очереди тоже пусты, возвращает FALSE. Таким образом цикл, построенный на PeekMessage без флага PM_NOYIELD, не исключает возможность нормальной работы других приложений.
Флаг PM_NOREMOVE явно указывает, что нужно только проверить наличие сообщений в очереди. Если сообщение есть, возвращается информация об этом сообщении (в структуре MSG), а сообщение остается в очереди. Флаг PM_REMOVE указывает на необходимость извлечения сообщений из очереди.
MSG msg;
// продолжительные операции (вычисления, печать ...)
while (PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);}
// дальнейшие продолжительные операции
Такой цикл сначала обработает все сообщения, накопившиеся в очереди, а затем завершиться. При использовании таких циклов надо иметь в виду следующее:
Вы можете ограничить работу интерфейса только каким–либо одним окном, может быть панелью диалога (и всеми дочерними окнами данного окна). Тогда необходимо указать вторым параметром функции PeekMessage хендл этого окна, а в цикле предусмотреть необходимую трансляцию для работы этого окна (панели диалога).
Вы можете разрешить нормальную работу всего приложения. Тогда вы должны предусмотреть в этом цикле такую–же трансляцию, как и в цикле обработки сообщений в функции WinMain (это удобно сделать, выделив всю необходимую трансляцию в отдельную процедуру) и, дополнительно, предусмотреть возможность получения WM_QUIT в таком цикле. При извлечении WM_QUIT надо либо принять меры к немедленному завершению работы приложения, либо завершить продолжительные операции и послать WM_QUIT снова. Например, так:
MSG msg;
BOOL fStop = FALSE;
// продолжительные операции (вычисления, печать ...)
while (PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
fStop = TRUE;
PostQuitMessage(msg.wParam); // так завершится цикл в WinMain
break; // а так — этот}
MyTranslation(&msg); // будем считать, что сюда мы убрали всю
// трансляцию и вызов DispatchMessage}
if (!fStop) {
// дальнейшие продолжительные операции}
Обработка состояний простоя
Третья задача — выполнение действий в состоянии простоя, очень близка к предыдущей. Часто все продолжительные операции выполняют именно в состоянии простоя. Отличие заключается в том, как именно организуется выполнение таких действий.
В предыдущем варианте фрагмент кода, выполняющий продолжительные операции, просто содержит дополнительные циклы обработки сообщений. То есть вы практически разрабатываете некоторую программу, выполняющую продолжительные вычисления, а затем не изменяя алгоритма, вставляете в него дополнительные циклы обработки сообщений. При этом небольшие сложности возникают только с обработкой сообщений.
Рассматриваемый здесь вариант — обработка состояний простоя позволяет выполнить те–же операции несколько другим способом. В цикле обработки сообщений можно легко определять моменты, когда все сообщения в очереди уже обработаны и, если это так, вызывать обработку специального сообщения (или процедуры) обслуживающего состояние простоя. Сложности будут связаны с тем, что сама по себе обработка состояния простоя не должна быть продолжительной — так как она временно останавливает цикл обработки сообщений. Если вы собираетесь организовать выполнение продолжительных операций таким способом, то вы должны разделить их на небольшие, быстро выполняемые фрагменты, которые будут последовательно вызываться при обработке состояний простоя.
Для реализации циклов с обработкой состояний простоя обычно применяют одну из двух функций: WaitMessage или GetMessage. Так как с WaitMessage мы еще не встречались, то сначала рассмотрим вариант с ней:
void WaitMessage(void);
Эта функция просто ожидает поступление в очередь нового сообщения. Причем, даже если в очереди уже есть сообщения, она все–равно будет ожидать нового, поэтому ее вызывают только когда очередь сообщений пуста.
for (;;) {
while (!PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) {
// ... выполнить операции во время простоя
WaitMessage(); // дождаться следующего сообщения}
if (msg.message == WM_QUIT) break; // прервать цикл по WM_QUIT
// нормальная обработка сообщения
TranslateMessage(&msg);
DispatchMessage(&msg);}
Такой цикл применяют обычно непосредственно в функции WinMain, вместо обычного. Надо отметить, что вызов одной функции GetMessage заменился на целый цикл из PeekMessage, необходимых операций и WaitMessage, а также проверку сообщения WM_QUIT. Причем вложенный цикл выполнятся однократно для каждого состояния простоя — после возврата из функции WaitMessage в очереди будет присутствовать сообщение, PeekMessage вернет TRUE и вложенный цикл завершиться.
В принципе, вместо WaitMessage можно использовать GetMessage, примерно так:
for (;;) {
if (!PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE)) {
// ... выполнить операции во время простоя
if (!GetMessage(&msg, NULL, NULL, NULL)) break; // WM_QUIT (!)}
if (msg.message == WM_QUIT) break; // прервать цикл по WM_QUIT
...}
На чем остановиться?
При выборе между двумя разными способами организации продолжительных операций надо учитывать следующие соображения:
Вариант с обработкой состояний простоя существенно более трудоемкий, однако имеет определенные достоинства, связанные с тем, что может быть организовано выполнение нескольких видов продолжительных операций одновременно. Например, можно в состоянии простоя организовать одновременную печать документа на принтере, выполнение сложного математического расчета и, скажем, оптимизацию использования памяти задачей. До определенной степени такая организация приложения позволяет имитировать многопотоковые задачи в рамках одного потока. Это может быть оправдано при разработке приложений, работающих в среде Windows 3.x, не поддерживающей многопотоковые задачи. Для приложений Win32 API обычно проще воспользоваться дополнительными потоками.
Как правило трудоемкость реализации существенно зависит от первоначально поставленной перед разработчиком задачи. Если задача первоначально поставлена так, что выполнение продолжительных операций сразу ориентировано на обработку состояний простоя, то это практически не потребует дополнительных затрат времени и сил. Но переделка уже реализованных в виде одного непрерывного процесса продолжительных операций займет изрядное время и не всегда может быть сделана без кардинальной переделки задачи.
Тщательный анализ создаваемого приложения часто приводит к упрощению поставленной задачи. Так, например, возможность организовать печать документа в фоновом режиме (при обработке состояний простоя или в отдельном потоке), невольно порождает желание воспользоваться этой возможностью “на всю катушку” — разрешить одновременное редактирование документа, его печать, а также печать еще нескольких других документов разом. Дух захватывает от открывающихся возможностей.
Однако задумайтесь над такими вопросами:
если документ и печатается, и редактируется одновременно, то что надо печатать — то, что было в документе на момент начала печати, то что в нем уже есть с учетом сделанных исправлений, или то, что будет, когда исправления закончатся?
как вы себе представляете одновременную печать двух документов на одном принтере?
каковы будут требования к компьютеру, что бы он мог без существенных задержек в работе редактора выполнять печать нескольких документов одновременно?
Конечно, во всех случаях можно найти выходы из создавшегося положения, например: делать копию документа, которая будет отправлена в печать, что позволит продолжить редактирование, использовать разные принтеры для печати разных документов, либо система сама организует спулинг для печати нескольких документов на одном принтере и так далее.
Вопрос в другом — как часто эти возможности будут использоваться и оправдают ли себя существенно возросшие требования к ресурсам, если эти возможности будут использоваться крайне редко?
Вот хороший пример — редактор Notepad крайне прост, возможности его бедны, но он занимает мало ресурсов, быстро загружается сам и быстро считывает текстовые файлы. Редактор WinWord 97 обладает неизмеримо большими возможностями, но и неизмеримо большими требованиями к ресурсам системы, долго загружается сам и достаточно долго (по сравнению с Notepad) загружает редактируемый текст. Теперь вопрос — если вам надо подправить пару строк в autoexec.bat, вы воспользуетесь редактором Notepad или WinWord?
Разрабатывая новое приложение вы должны очень хорошо представлять себе его область применения и круг задач, которые оно должно будет решать. Сент–Экзюпери говорил, что “совершенство достигается не тогда, когда уже нечего добавить, а тогда, когда нечего отнять”. Перегружая задачу разными, может быть и удобными, но редко используемыми функциями, вы рискуете сделать его неудобным при выполнении часто выполняемых функций и, в конечном итоге, бесполезным — так как трудно представить себе более бесполезную вещь, чем та, которой никто не хочет пользоваться.
[0] В некоторых руководствах в простейших примерах обходятся без трансляции вообще. Однако это является не совсем корректным, так как функция TranslateMessage распознает комбинацию клавиш Alt+Space как команду нажатия на кнопку системного меню. Конечно без нее приложение будет работать, но не в полной мере реализует стандартный клавиатурный интерфейс.
[1] В случае Win32 процесс более передачи сообщений сложный и переданное сообщение может оказаться в очереди, однако обрабатываться такое сообщение не так, как обычные сообщения, находящиеся в той–же очереди. Подробнее — см. ниже.
[2] Хендл задачи hTask является самостоятельным понятием, он не совпадает с хендлом копии приложения hInstance. Подробнее о хендле задачи смотри в разделе “ REF _Ref416858548 \* MERGEFORMAT Ошибка! Источник ссылки не найден.”.
[3] В случае 16ти битовой платформы Windows таких проблем не возникает, так как использование функции SendMessage подобно простому вызову оконной процедуры.