    ОКОННЫЕ СООБЩЕНИЯ И АСИНХРОННЫЙ ВВОД 

    глава 11 

    Разрабатывая Win32-cpeny, Microsoft стремилась решить целый ряд масштабных 
задач — взять хотя бы поддержку виртуальной памяти, реализацию вытесняющей 
многозадачности и защиты. Смысл поставленных целей — создание отказоустойчивой 
среды, в которой отсутствует нежелательное взаимовлияние приложений. А именно 
в этой области проявляются существенные недостатки 16-разрядной Windows. 
    Чтобы достичь этих высот, Microsoft пришлось изменить многое из того, к 
чему привыкли программисты в 16-разрядной Windows. Например, ввод с клавиатуры 
и мыши Win32-cHcreMa обрабатывает совершенно иначе, чем 16-разрядная. В итоге 
простой перенос многих приложений на платформу Win32 невозможен — приходится 
существенно перерабатывать отдельные их части. Вот о таких изменениях мы и 
поговорим в этой главе. 

    Многозадачность 

    По-моему, многозадачность — единственное по-настоящему важное отличие 
Win32 от 16разрядной Windows. Хотя последняя способна выполнять несколько 
приложений одновременно, реализованная в ней многозадачность не является 
вытесняющей: прежде чем планировщик выделит процессорное время другой 
программе, предыдущая должна сообщить об окончании своей текущей операции. В 
итоге — проблемы и для пользователей, и для программистов. 
    Для первых это означает, что управление системой может быть утрачено на 
какое-то время, продолжительность которого определяется приложением, а не 
пользователем. Если программа медленно выполняет какую-то операцию вроде 
форматирования гибкого диска, пользователь не может переключиться на другую 
задачу, продолжив форматирование в фоновом режиме. Такая ситуация 
пользователям не по вкусу: как известно, время — деньги. 
    Понимая важность этой проблемы, разработчики программ для 16-разрядной 
Windows пытаются реализовать приложения так, чтобы они выполняли свои операции 
поэтапно. Например, программа могла бы отформатировать на дискете одну дорожку 
и вернуть управление Windows. Та, получив управление, дает возможность 
пользователю переключиться в другие задачи. Если же пользователь не работает с 
другими программами, 16-разрядная Windows возвращает управление первой 
программе для форматирования следующей дорожки. 
    Что ж, такой метод работает, но значительно усложняет написание программ. 
Есть и другой вариант — его мы рассмотрим на примере все той же программы 
форматирования. С помощью функции SetTimer программа устанавливает таймер, 
который затем через сообщение WM_TIMER уведомляет ее о том, что пора выполнить 
следующую часть процесса. Но и здесь не без проблем: 

    315 

    316 .______________________________Windows для профессионалов 

    • Число таймеров для приложений в 16-разрядной Windows ограничено. Что 
делать программе, если таймер недоступен, — не давать форматировать диск, пока 
не завершится другое приложение, использующее таймер? 
    • Программа должна отслеживать, как далеко она продвинулась в выполнении 
своей операции. Форматирующей программе придется сохранять в глобальных 
переменных или в динамически выделенном блоке памяти такую информацию, как 
буквенное обозначение устройства, на котором осуществляется форматирование, 
номер только что отформатированной дорожки и т. д. 
    • Программа — вместо функции, форматирующей весь диск, — должна содержать 
функцию, форматирующую отдельные дорожки. А это значит, что функции в 
программе будут разбиты самым неестественным для программиста образом. Ведь 
при разработке алгоритма Вы не предполагаете, что начинать его исполнение 
можно хоть с середины. Представляете, как трудно было бы реализовать алгоритм, 
если бы он включал несколько вложенных циклов и требовал начинать исполнение с 
самого «нижнего» из них! 
    • Сообщения WM_TIMER посылаются через постоянные интервалы времени. 
Скажем, если таймер установлен на 1 секунду, сообщения WM_TIMER поступают 60 
раз в минуту независимо от того, исполняется программа на медленной 386-й 
машине с тактовой частотой 25 МГц или на быстром Pentium-90. Получается, даже 
на быстрой машине программа не дает никакого выигрыша в скорости работы. 
    Еще один излюбленный «16-битниками» способ заставить приложения вести себя 
более учтиво с другими программами — применение функции PeekMessage. Вызвав 
ее, приложение сообщает 16-разрядной Windows: «У меня еще есть работа, но я 
готово повременить с ней, если это нужно другому приложению». 
    Этот метод проще: ведь разработчику не надо учитывать, что исполнение 
процесса может начаться с середины. Опять же не требуется таймеров или других 
особых системных ресурсов. Но и здесь есть проблемы: во-первых, циклы 
PeekMessage должны, как вкрапления, присутствовать по всему тексту программы, 
и во-вторых, приложение должно уметь обрабатывать асинхронные события любого 
типа. Например, электронная таблица может пересчитывать значения в ячейках в 
то время, как другое приложение пытается установить с ней DDE-связь. Но 
проверить корректность работы программы во всех мыслимых ситуациях почти невозможно. 
    На практике — даже применяя методы, позволяющие сгладить «трения» между 
приложениями, — добиться плавного переключения между программами все равно не 
удается. Иногда с момента щелчка окна программы до его активизации в 
16-разрядной Windows проходит целая секунда, а то и больше. Гораздо хуже, если 
из-за какой-нибудь ошибки программа вообще не возвращает управление Windows, и 
вся система виснет по-настоящему: ни переключиться в другое приложение, ни 
сохранить на диске текущую работу. Остается перезапускать компьютер. Нет, это неприемлемо! 
    В Win32 эти (и многие другие) проблемы решаются вытесняющей 
многозадачностью. Ее реализация в Windows NT и Windows 95 дает куда больше, 
чем просто возможность одновременного выполнения приложений. Благодаря ей 
среда стала намного устойчивее к отказам, так как ни одно приложение не может 
получить контроля над всеми системными ресурсами. 

    Распределение времени с вытеснением 

    В 16-разрядной Windows только один поток управления. Иначе говоря, 
микропроцессор переключается с исполнения функций одного приложения на 
исполнение функций другого, а попутно делает что-то и для самой операционной 
системы. Всякий раз, когда пользователь 

    ____________Глава 11. Оконные сообщения и асинхронный ввод 317 

    переходит из одного приложения в другое, операционная система осуществляет 
переключение задач (task switching), т. е., сохраняя состояние регистров 
процессора перед деактивизацией текущей задачи, восстанавливает регистры для 
вновь активизируемой задачи. Заметьте, что я подчеркнул ответственность 
операционной системы за переключение задач. Поскольку в операционной системе 
один поток, то, стоит какому-нибудь участку кода войти в бесконечный цикл, и 
блок операционной системы, отвечающий за переключение задач, никогда не 
получит управление, а система, естественно, зависнет. 
    Кроме того, 16-разрядная Windows основана на концепции модулей и задач. 
Модуль — это загружаемый в память исполняемый файл. А модуль, запущенный на 
исполнение, называется в 16-разрядной Windows задачей. За некоторыми 
исключениями ресурсы (блоки памяти или окна), созданные (или выделенные) в 
период выполнения конкретной задачи, считаются принадлежащими этой задаче. 
Отдельные ресурсы — вроде значков или курсоров — фактически принадлежат 
модулю, что позволяет всем задачам этого модуля использовать их совместно. 
    В Win32 термин «модуль» подразумевает то же, что и в 16-разрядной Windows, 
но с понятием «задача» связано две новых концепции: процессы (processes) и 
потоки (threads). 
    Как уже говорилось в главе 3, процессом называется экземпляр выполняемой 
программы. Например, если запущен 1 экземпляр Notepad и 2 экземпляра программы 
Calculator, в системе исполняются 3 процесса. А термин «поток» (см. главу 4) 
описывает путь, по которому «проходит» система, исполняя код процесса. При 
запуске исполняемого файла операционная система создает и процесс, и поток. 
Например, когда пользователь вызывает Win32-пpилoжeниe, система находит 
ЕХЕ-файл программы, создает процесс и поток для ее нового экземпляра и 
указывает процессору начать исполнение потока со стартового кода стандартной 
библиотеки С; в свою очередь стартовый код вызывает Вашу функцию WinMain. При 
завершении потока (после возврата из WinMain) система уничтожает и поток, и процесс. 
    Каждый процесс имеет минимум 1 поток; система распределяет процессорное 
время между потоками, но не между процессами. Начав выполнение, поток может 
создать внутри процесса дополнительные потоки. Они выполняются до тех пор, 
пока не будут уничтожены или не завершатся сами. Количество создаваемых 
потоков ограничено лишь общим объемом системных ресурсов. 
    При выполнении потока система может отнять у него процессорное время и 
передать другому потоку. Однако процессор нельзя прервать в момент исполнения 
команды (машинной команды, а не строки исходного кода). Способность 
операционной системы прерывать поток практически в любой момент и передавать 
процессорное время ожидающему потоку называется вытесняющей многозадачностью 
(preemptive multitasking). 
    Время жизни процесса напрямую связано с его потоками. Потоки процесса тоже 
живут «собственной жизнью»: одни потоки появляются, другие приостанавливаются 
и возобновляются, третьи завершаются. Когда все потоки в процессе завершены, 
завершается и сам процесс; тогда принадлежащие ему ресурсы освобождаются, и 
система удаляет его из памяти. 
    Большинство объектов, созданных потоком, считается собственностью 
процесса, которому принадлежит данный поток. Например, выделенный потоком блок 
памяти принадлежит процессу, а не потоку. То же относится к глобальным и 
статическим переменным приложения. Все объекты GDI (перья, кисти, битовые 
карты) — тоже собственность конкретного процесса. Большая часть объектов User 
(значки, курсоры, оконные классы, меню и таблицы акселераторов) также 
принадлежит процессу, которому в свою очередь принадлежит поток, создавший или 
загрузивший эти объекты в память. Лишь два объекта User — окна и программные 
ловушки (hooks) — принадлежат потоку, а не процессу. 
    Вы должны разбираться в принадлежности объектов, чтобы понимать, какие из 
них можно сделать общими, а какие — нет. Если внутри процесса 7 потоков и один 
из них 

    318 ______________________________Windows для профессионалов 

    выделяет блок памяти, этот блок доступен любому из семи потоков. Такого 
рода «общедоступность» может вызвать проблемы, если все потоки попытаются 
одновременно и читать, и записывать в этот блок памяти. Впрочем, синхронизацию 
потоков мы подробно обсудили в главе 10. 
    Принадлежность объектов важна и потому, что \Уш32-система по завершении 
потока или процесса проводит очистку гораздо тщательнее 16-разрядной Windows. 
Если поток, завершаясь, не удалил созданное им окно, его обязательно уничтожит 
система — она не позволит окну где-нибудь «засесть» и попусту тратить 
драгоценную память и системные ресурсы. Но если поток, например, создав или 
загрузив в память курсор мыши, потом завершается, система не разрушит курсора, 
так как курсоры принадлежат процессу, а не потоку. Но завершится процесс — и 
этот курсор будет обязательно уничтожен. В 16-разрядной Windows задача 
эквивалентна одному-единственному потоку, поэтому концепция принадлежности там 
менее сложна. 

    Очереди потока и обработка сообщений 

    Большинство операций, выполняемых Win32-пpилoжeниями, инициируется 
оконными сообщениями. В 16-разрядной Windows всего один поток. Если Ваша 
программа посылает сообщение окну другой задачи, исполнение Вашей задачи 
приостанавливается, и начинается исполнение кода, обрабатывающего сообщение. 
После этого система вновь возвращается к исполнению кода Вашей задачи. Так 
было в 16-разрядной Windows. В многозадачной среде все иначе. 
    В Win32 код оконной процедуры выполняется создавшим окно потоком, и он не 
обязательно является тем же потоком, что отправил сообщение. Чтобы другой 
поток мог обработать сообщение, вызывающий поток уведомляет принимающий о 
необходимости выполнения каких-то действий. Затем вызывающий поток 
приостанавливает себя до тех пор, пока поток-приемник не обработает запрос. 
Поэтому далее мы рассмотрим методы, применяемые для синхронной (sent) и 
асинхронной (posted) посылки различных сообщений. 

    Архитектура очередей сообщений в Win32 

    Как я уже говорил, одна из главных целей Win32 — предоставить всем 
приложениям отказоустойчивую среду. Для этого каждый поток должен исполняться 
в такой среде, где он может считать себя единственным. Точнее, прочие потоки 
не должны никоим образом влиять на очередь сообщений данного потока. Кроме 
того, для каждого потока нужно смоделировать среду, позволяющую ему 
самостоятельно управлять фокусом клавиатуры (keyboard focus), активизировать 
окна, поддерживать захват мыши (mouse capture) и т. д. 
    Всякий раз при создании потока система создает структуру THREADINFO и 
связывает ее с этим потоком. Элементы этой структуры используются, чтобы 
обмануть поток — заставить его считать, будто он выполняется в среде, 
принадлежащей только ему. Кстати, THREADINFO — это внутренняя 
(недокументированная) структура, идентифицирующая очередь асинхронных 
сообщений потока (posted-message queue), очередь синхронных сообщений потока 
(sent-message queue), очередь ответных сообщений (reply-message queue), 
виртуальную очередь ввода (virtualized input queue) и флаги пробуждения (wake 
flags); она также включает ряд других переменных, используемых для записи 
локального состояния ввода данного потока. На рис. 11-1 показана связь 
структур THREADINFO с тремя потоками. 
    В оставшейся части главы мы обсудим элементы структуры THREADINFO. 

    Глава 11. Оконные сообщения и асинхронный ввод 319 
    
    THREADINFO	- MSG (асинхронное) |- MSG (асинхронное) 1 
    Указатель очереди асинхронных сообщений 
    Указатель виртуальной очереди ввода	MSG (ввод) ]- MSG (ввод) ]- MSG (ввод) ] 
    Указатель очереди синхронных сообщений	- MSG (синхронное) 1- MSG 
(синхронное) ] 
    Указатель очереди ответных сообщений	MSG (ответное) I— MSG (ответное) 1 
    Флаги пробуждения	DINFO	- MSG (асинхронное) 1- MSG (асинхронное) ) 
    Переменные локального состояния ввода 
    THREA 
    Указатель очереди асинхронных сообщений 
    Указатель виртуальной очереди ввода	MSG (ввод) 1- MSG (ввод) J- MSG (ввод) j 
    Указатель очереди синхронных сообщений	- MSG (синхронное) |- MSG 
(синхронное) \ 
    Указатель очереди ответных сообщений	MSG (ответное) |- MSG (ответное) 1 
    Флаги пробуждения	хронное) \- MSG (асинхронное);!! 
    Переменные локального состояния ввода 
    THREADINFO	- MSG (асин 
    Указатель очереди асинхронных сообщений 
    Указатель виртуальной очереди ввода	MSG (ввод) j- MSG (ввод) )- MSG (ввод) 1 
    Указатель очереди синхронных сообщений	- MSG (синхронное) 1- MSG 
(синхронное) 1 
    Указатель очереди ответных сообщений	MSG (ответное) |- MSG (ответное) 1 
    Флаги пробуждения 
    Переменные локального состояния ввода 
    
    Рис. 11-1. Три потока и соответствующие им структуры THREADINFO 


    320 _____________________________Windows для профессионалов 


    Асинхронная посылка сообщений в очередь потока 

    В Win32 только что созданный поток считается рабочим, и система старается 
как можно больше на нем сэкономить. Однако стоит этому потоку обратиться к 
одной из User- или GDI-функций, как он становится потоком пользовательского 
интерфейса. С потоками пользовательского интерфейса связаны дополнительные 
«накладные расходы»: для них, в частности, создаются структуры THREADINFO. 
Иначе говоря, система не связывает структуру THREADINFO с потоком, пока тот не 
обратится к какой-либо User- или GDI-функции. 
    Ну а когда с потоком связывается такая структура, тот получает свой набор 
очередей сообщений. Если процесс создает 10 потоков и все они вызывают функцию 
CreateWindow (User-функция), то и наборов очередей сообщений будет тоже 10. 
Сообщения ставятся в очередь асинхронных сообщений функцией PostMessage: 

    BOOL PostMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM IParam): 

    Когда поток вызывает эту функцию, система определяет, каким потоком 
создано окно, идентифицируемое параметром hWnd, и помещает сообщение в его 
очередь асинхронных сообщений.' Возврат из PostMessage происходит сразу после 
того, как сообщение поставлено в очередь; вызывающий поток «понятия не имеет», 
обработано ли оно процедурой соответствующего окна. На самом деле вполне 
вероятно, что окно даже не получит этого сообщения. Такое возможно, если 
поток, создавший это окно, завершится до того, как обработает все сообщения из 
своей очереди. 
    Сообщение можно поставить в очередь асинхронных сообщений потока и вызовом 
PostThreadMessage: 

    BOOL PostThreadMessage(DWORD idThread, UINT Msg, WPARAM wParam, LPARAM IParam); 

    В 16-разрядной Windows у каждой задачи своя очередь сообщений, так что 
приложение не обрабатывает сообщения, предназначенные другим программам. По 
умолчанию очередь позволяет хранить до 8 сообщений. Изменить ее размер можно 
вызовом SetMessageQueue. В Win32 API тоже есть такая функция, но она в общем 
не нужна, так как сообщения хранятся в виде связанного списка и ограничений на 
их количество нет. В Win32 очередь сообщений потока реализована как двусвязный 
список. Когда сообщения поступают в очередь, к концу списка добавляются 
структуры MSG. При выборке сообщения из очереди система возвращает первое 
сообщение из списка. И структура THREADINFO содержит указатель на первое 
сообщение в связанном списке, а не на саму очередь сообщений. 
    Значение, возвращаемое PostMessage, говорит о том, достаточно ли в очереди 
места для данного сообщения. В 16-разрядной Windows функция PostMessage 
возвращает FALSE, если очередь переполнена. А в Win32, как Вы уже 
догадываетесь, такая ситуация практически невозможна. 
    Нужный поток идентифицируется первым параметром, idThread. Когда сообщение 
помещено в очередь, элемент hWnd структуры MSG устанавливается как NULL. 
Применяется эта функция, когда приложение выполняет какую-то особую обработку 
в главном цикле выборки сообщений потока, — в этом случае он пишется так, 
чтобы после выборки сообщения 
    1. Какой поток создал окно, можно определить с помощью GetWindowThreadProcessId: 

    DWORD GetWindowThreadProcessId(HWND hWnd LPDWord IpdwProcessId) 

    Она возвращает уникальный общесистемный идентификатор потока» создавшего 
окно hWnd. Передав адрес переменной типа DWORD в параметре IpdwProcessId^ 
можно получить и уникальный общесистемный идентификатор процесса, которому 
принадлежит этот поток. Но обычно идентификатор процесса не нужен, и мы просто 
передаем NULL. 

    ______________Глава 11. Оконные сообщения и асинхронный ввод 321 

    функцией GetMessage (или PeekMessage) код в цикле сравнивал hWnd с NULL и, 
выполняя эту самую особую обработку, мог проверить значение элемента msg 
структуры MSG. Если поток определил, что сообщение не адресовано какому-либо 
окну, DispatchMessage не вызывается, и цикл переходит к обработке следующего сообщения. 
    Как и PostMessage, функция PostThreadMessage возвращает управление сразу 
же, как только сообщение будет поставлено в очередь потока. И по-прежднему 
вызывающий поток остается в неведении, обработано ли сообщение. 

    Функция PostThreadMessage заменила PostAppMessage из 16-разрядной Windows. 


    Посылка сообщения окну 

    Оконное сообщение можно отправить непосредственно оконной процедуре вызовом 

    SendMessage:                                        ' "" 
    LRESULT SendMessage(HWND hwnd, UINT Msg, WPARAM wPar-am, LPARAM IParam): 

    Оконная процедура обработает сообщение и только по окончании обработки 
функция SendMessage вернет управление. Благодаря тому, что в эту функцию 
заложен механизм синхронизации, ее используют гораздо чаще, чем PostMessage 
win PostThreadMessage. При переходе к исполнению следующей строки кода, поток, 
вызвавший SendMessage, знает, что сообщение полностью обработано. 
    Если поток вызывает SendMessage для посылки сообщения окну, созданному им 
же, то функция работает очень просто: вызывает оконную процедуру 
соответствующего окна как подпрограмму. Окончив обработку, оконная процедура 
возвращает функции SendMessage 32-битное значение, а та возвращает это 
значение вызвавшему ее потоку. 
    Если поток посылает сообщение окну, созданному другим потоком, операции, 
выполняемые функцией SendMessage, значительно усложняются.2 Win32 требует, 
чтобы оконное сообщение обрабатывалось потоком, создавшим окно. Поэтому, если 
вызвать SendMessage для отправки сообщения окну, созданному в другом процессе 
и, естественно, другим потоком, Ваш поток не сможет обработать это сообщение — 
ведь он не работает в адресном пространстве другого процесса, а потому не 
имеет доступа к коду и данным соответствующей оконной процедуры. И 
действительно, Ваш поток приостанавливается, пока другой поток обрабатывает 
сообщение. Таким образом, чтобы один поток мог отправить сообщение окну, 
созданному другим потоком, система должна выполнить следующие действия. 
    Во-первых, переданное сообщение присоединяется к очереди сообщений 
потока-приемника, в результате чего для этого потока устанавливается флаг 
QS_SENDMESSAGE (его мы обсудим позже). Во-вторых, если поток-приемник в данный 
момент выполняет какойто код и не ожидает сообщений (через вызов GetMessage, 
PeekMessage или WaitMessage), то переданное сообщение обработать не удастся — 
система не прервет поток для немедленной обработки сообщения. А когда 
поток-приемник ждет сообщений, система вначале проверяет, установлен ли флаг 
пробуждения QS_SENDMESSAGE, и, если да, просматривает список в 
очег>еди_синхронных сообщений, отыскивая первое из них. В очереди может 
находиться более одного такого сообщения. Скажем, несколько потоков 
одновременно послали сообщение одному и тому же окну — система просто ставит 
эти сообщения в очередь синхронных сообщений потока. 
    2. Это верно даже в том случае, если оба потока принадлежат одному процессу. 

    322 _____________________________Windows для профессионалов 

    Итак, когда поток ждет сообщений, система отыскивает в очереди синхронных 
сообщений первое и вызывает для его обработки нужную оконную процедуру. Если 
таких сообщений нет, флаг QS_SENDMESSAGE сбрасывается. Пока поток-приемник 
обрабатывает сообщения, поток, отправивший сообщение через SendMessage, 
простаивает, ожидая появления сообщения в очереди ответных сообщений. По 
окончании обработки значение, возвращенное оконной процедурой, передается 
асинхронно в очередь ответных сообщений потока-отправителя. Теперь он 
пробудится и извлечет упомянутое значение из ответного сообщения. Именно это 
значение и будет результатом вызова SendMessage. С этого момента 
поток-отправитель возобновляет работу в обычном режиме. 
    Ожидая возврата управления функцией SendMessage, поток в основном 
простаивает. Но кое-чем он может заняться: если другой поток посылает 
сообщение окну, созданному первым (ожидающим) потоком, система тут же 
обрабатывает это сообщение, не дожидаясь, когда поток вызовет GetMessage, 
PeekMessage или WaitMessage. 
    Поскольку \Уш32-подсистема обрабатывает межпоточные сообщения описанным 
выше образом, Ваш поток может зависнуть. Скажем, в потоке, обрабатывающем 
синхронное сообщение, — «жучок», из-за чего поток вошел в бесконечный цикл. 
Что же произойдет с потоком, вызвавшим SendMessage? Возобновится ли 
когда-нибудь его исполнение? Значит ли это, что ошибка в одном приложении 
«подвесит» другое? Ответ — да! 
    Защитить код программы от подобных ситуаций позволяют четыре 
\Уш32-функции, и первая из них — SendMessageTimeout. 
    LRESULT SendMessageTlmeout(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM 
IParam, UINT fuFlags, UINT uTimeout, LPDWORD IpdwResult): 
    SendMessageTimeout позволяет задавать отрезок времени, в течение которого 
Вы готовы ждать ответа от другого потока на Ваше сообщение. Первые 4 параметра 
идентичны параметрам функции SendMessage. В параметре fuFlags можно передавать 
флаги SMTO_NORMAL, SMTO_ABORTIFHUNG, SMTO_BLOCK или комбинацию 
SMTO_ABORTIFHUNG и SMTO_BLOCK. 
    Флаг SMTO_ABORTIFHUNG указывает, что SendMessageTimeout должна проверить, 
не завис ли поток-приемник3, и, если да, немедленно вернуть управление. Флаг 
SMTO_BLOCK предотвращает обработку вызывающим потоком любых синхронных 
сообщений до возврата из SendMessageTimeout. Флаг SMTO_NORMAL определен в 
WINUSER.H как 0; он используется, когда прочие флаги не годятся. 
    Я уже говорил, что ожидание потоком окончания обработки синхронного 
сообщения может быть прервано для обработки другого синхронного сообщения. 
Флаг SMTO_BLOCK предотвращает такое прерывание. Его используют, только если 
поток, ожидая конца обработки своего сообщения, не может обрабатывать 
«постороннее» синхронное сообщение. Применение этого флага иногда приводит к 
блокировке потока до конца таймаута. Например, если Ваш поток отправит 
сообщение другому, а тому нужно послать сообщение Вашему, ни один из них не 
сможет продолжить обработку, и оба зависнут.      ~'—"" 
    Параметр uTimeout определяет таймаут — время (в миллисекундах), в течение 
которого Вы готовы ждать ответного сообщения. При успешном выполнении функция 
возвращает TRUE, а результат сообщения копируется по адресу, указанному в 
параметре IpdwResult. 
    Кстати, прототип этой функции в заголовочном файле WINUSER.H неверен. В 
прототипе следовало бы определить функцию как возвращающую значение типа BOOL, 
поскольку значение типа LRESULT возвращается на самом деле через ее параметр. 
Это создает определенные проблемы, так как SendMessageTimeout вернет FALSE, 
если Вы передадите неправильный описатель окна или если закончится заданный 
период ожидания. Единствен

    3. Операционная система считает поток зависшим, если он прекращает 
обработку сообщений более чем на 5 секунд. 

    ____________Глава 11. Оконные сообщения и асинхронный ввод 323 

    ный способ узнать причину неудачного завершения функции — вызвать 
GetLastError. Последняя вернет О (ERROR_SUCCESS), если ошибка связана с 
окончанием периода ожидания. А если причина в неверном описателе, GetLastError 
даст код 1400 (ERROR_INVALID_WINDOW.HANDLE). 
    Если Вы обращаетесь к SendMessageTimeout для посылки сообщения окну, 
созданному вызывающим потоком, система просто вызывает оконную процедуру, 
помещая возвращаемое значение в IpdwResult. Тогда код, расположенный за 
вызовом SendMessageTimeout, не начнет исполняться, пока сообщение не будет 
обработано, — ведь вся обработка осуществляется одним потоком. ' 
    Теперь перейдем к рассмотрению второй функции, предназначенной для 
отправки межпоточных сообщений: 
    BOOL SendMessageCallback(HWND hwnd. UINT uMsg, WPARAM wParam, LPARAM 
IParam, SENDASYNCPROC IpResultCallBack, DWORD dwData); 
    И вновь первые 4 параметра идентичны параметрам функции SendMessage. При 
вызове Вашим потоком SendMessageCallback функция отправляет сообщение в 
очередь синхронных сообщений потока-приемника и тут же возвращает управление 
вызывающему (т. е. Вашему) потоку. Закончив обработку сообщения, 
поток-приемник асинхронно отправляет свое сообщение в очередь ответных 
сообщений Вашего потока. Позже система уведомит Ваш поток об этом, вызвав 
написанную Вами функцию; у нее должен быть следующий прототип: 

    VOID CALLBACK ResultCallBack(HWND hwnd, UINT uMsg, DWORD dwData, 
    LRESULT IResult); 

    Адрес этой функции обратного вызова передается SendMessageCallback в 
параметре IpResultCallBack. А при вызове ResultCallBack в первых двух 
параметрах передается описатель окна, закончившего обработку сообщения, а 
также код самого сообщения. Параметр dwData функции ResultCallBack всегда 
получает значение, переданное SendMessageCallback через одноименный параметр. 
(Система просто берет то, что указано там, и передает Вашей функции 
ResultCallBack.) Последний параметр функции ResultCallBack сообщает результат 
обработки сообщения, полученный от оконной процедуры. 
    Поскольку SendMessageCallback, передавая сообщение другому потоку, 
немедленно возвращает управление, ResultCallBack вызывается после обработки 
сообщения потокомприемником не сразу, а с задержкой. Сначала поток-приемник 
асинхронно ставит сообщение в очередь ответных сообщений потока-отправителя. 
Затем при первом же вызове потоком-отправителем любой из функций GetMessage, 
PeekMessage, WaitMessage или одной из функций SendMessage*, сообщение 
извлекается из очереди ответных сообщений и только потом вызывается Ваша 
функция ResultCallback. 
    Существует и другое применение SendMessageCallback. В Win32 предусмотрен 
метод, позволяющий разослать широковещательное сообщение всем перекрытым окнам 
(overlapped windows) в системе; он состоит в том, что Вы вызываете SendMessage 
и в параметре hwnd "передаёт (Гей HWND_BROADCAST (определенное как -1). Так 
рассылают широковещательные сообщения, возвращаемые значения которых Вас не 
интересуют, поскольку функция способна вернуть лишь значение LRESULT. Но, 
используя SendMessageCallback, можно получить результаты обработки 
широковещательного сообщения от каждого перекрытого окна. Ваша функция 
SendMessageCallback будет вызвана с результатом обработки сообщения в каждом 
из таких окон. 
    Если SendMessageCallback вызывается для отправки сообщения окну, 
созданному вызывающим потоком, система немедленно вызывает оконную процедуру, 
а после обработки сообщения — функцию ResultCallBack. После возврата из 
ResultCallback исполнение начинается со строки, следующей за вызовом SendMessageCallback. 

    324 _____________________________Windows для профессионалов 

    Третья функция, предназначенная для передачи межпоточных сообщений: 

    BOOL SendNotlfyMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM IParam): 

    Поместив сообщение в очередь синхронных сообщений потока-приемника, она 
немедленно возвращает управление вызывающему потоку. Так ведет себя и 
PostMessage, помните? Но два отличия SendNotifyMessage от PostMessage все же есть. 
    Во-первых, если SendNotifyMessage посылает сообщение окну, созданному 
другим потоком, приоритет данного синхронного сообщения выше приоритета 
асинхронных сообщений, находящихся в очереди того же потока. Иными словами, 
сообщения, помещаемые в очередь с помощью SendNotifyMessage, всегда 
извлекаются до выборки сообщений, отправленных через PostMessage. 
    Во-вторых, если сообщение посылается окну, созданному вызывающим потоком, 
SendNotifyMessage работает точно так же, как и SendMessage, т. е. не 
возвращает управление до окончания обработки сообщения. 
    Большинство синхронных сообщений посылаются окну для уведомления, т. е. 
чтобы сообщить ему об изменении состояния и чтобы оно как-то отреагировало на 
это, прежде чем Вы продолжите свою работу. Например, WM_ACTIVATE, WM_DESTROY, 
WM_ENABLE, WM_SIZE, WM_SETFOCUS, WM_MOVE и многие другие сообщения — просто 
уведомления, посылаемые системой окну в синхронном, а не асинхронном режиме. 
Поэтому система не приостанавливает свою работу только ради того, чтобы 
оконная процедура могла их обработать. Прямо противоположный эффект дает 
отправка сообщения WM_CREATE — тогда система ждет, когда окно закончит его 
обработку. Если возвращено значение -1, окно не создано. 
    И, наконец, четвертая функция, связанная с обработкой межпоточных сообщений: 

    BOOL ReplyMessage(LRESULT IResult); 

    Эта функция отличается от трех описанных выше. В то время как функции 
семейства Send* используются посылающим сообщения потоком для защиты себя от 
зависания, ReplyMessage вызывается потоком, принимающим оконное сообщение. 
Вызвав ее, поток как бы говорит системе, что он уже немало сделал и получил 
результат обработки сообщения и что этот результат нужно упаковать и 
асинхронно отправить в очередь ответных сообщений потокаотправителя. Последний 
сможет пробудиться, получить результат и возобновить работу. 
    Поток, вызывающий ReplyMessage, передает результат обработки сообщения 
через параметр IResult. После вызова ReplyMessage исполнение 
потока-отправителя возобновляется, а поток, занятый обработкой сообщения, 
продолжает эту обработку. Ни один из потоков не приостанавливается — оба 
работают как обычно. Когда поток, обрабатывающий сообщение, вернется из своей 
оконной процедуры, любое возвращаемое значение уже игнорируется. 
    Заметьте: ReplyMessage надо вызывать из оконной процедуры, получившей 
сообщение, но не из потока, вызвавшего одну из функций семейства Send*. 
Поэтому, чтобы написать «защищенный от зависаний» код, следует заменить все 
вызовы SendMessage вызовами одной из трех новых функций Send* и не полагаться 
на то, что оконная процедура будет вызывать именно ReplyMessage. 
    Вы должны также понимать, что вызов ReplyMessage при обработке сообщения, 
посланного этим же потоком, не влечет никаких действий. На это и указывает 
значение, возвращаемое ReplyMessage: TRUE при обработке межпоточного сообщения 
и FAESE при попытке вызова функции для обработки внутрипоточного сообщения. 
    Если Вас интересует, является обрабатываемое сообщение внутрипоточным или 
межпоточным, вызовите InSendMessage: 

    BOOL InSendMessage(VOID): 

    Имя этой функции не совсем точно соответствует тому, что она делает в 
действительности. На первый взгляд, функция должна возвращать TRUE, если поток 
обрабатывает синхронное сообщение, и FALSE — при обработке асинхронного 
сообщения. И все же это не так. 

    ______________Глава 11. Оконные сообщения и асинхронный ввод 325 

    Функция возвращает TRUE, если поток обрабатывает внутрипоточное синхронное 
сообщение, и FALSE — при обработке межпоточного сообщения (синхронного или 
асинхронного). Возвращаемые значения функций InSendMessage и ReplyMessage идентичны. 

    Пробуждение потока 

    Когда поток вызывает GetMessage или WaitMessage и никаких сообщений для 
него и созданных им окон нет, система может приостановить выполнение потока, и 
тогда он уже не получит процессорного времени. Но как только потоку будет 
отправлено синхронное или асинхронное сообщение, система установит флаг 
пробуждения, указывающий, что теперь поток должен получить процессорное время 
и обработать сообщение. Если пользователь ничего не набирает на клавиатуре и 
не трогает мышь, то обычно в таких обстоятельствах нлкакдх сообщений окнам не 
посылается. А это значит, что большинство потоков в системе не получают 
процессорного времени. 
    Во время выполнения поток может опросить состояние своих очередей вызовом GetQueueStatus: 

    DWORD GetQueueStatus(UINT fuFlags); 

    Параметр fuFlags — флаг или группа флагов, объединенных побитовой 
операцией OR; он позволяет проверить значения отдельных битов пробуждения(wake 
bits). Допустимые значения флагов и их смысл описаны в таблице: 

    Флаг               Сообщение в очереди 

    QSJCEY            WMJCEYUP, WM_KEYDOWN, WM_SYSKEYUP или WM_SYSKEYDOWN 
    QS_MOUSE         To же, что QS_MOUSEMOVE ! QS_MOUSEBUTTON 
    QS_MOUSEMOVE    WM_MOUSEMOVE 
    QS_MOUSEBUTTON   WM_?BUTTON*1 
    QS_PAINT           WM_PAINT 
    QS_POSTMESSAGE    Асинхронное сообщение (отличное от сообщения аппаратного ввода) 
    QS_SENDMESSAGE    Синхронное сообщение, посланное другим потоком (включая 
ответные сообщения) 
    QS_TIMER          WM_TIMER 
    QS_HOTKEY        WM_HOTKEY 
    QSJNPUT          To же, что QS_MOUSE ! QS_KEY 
    QS_ALLEVENTS      To же, что QSJNPUT : QS_POSTMESSAGE ! QS_TIMER ! 
    QS_PAINT ! QS_HOTKEY2 
    QS_ALLINPUT       To же, что QS_ALLEVENTS ! QS_SENDMESSAGE 

    1. Знак вопроса (?) заменяет букву L, М или R, а звездочка (») — DOWN, UP 
или DBLCLK. 
    2. Флаг QS_SENDMESSAGE не комбинируется с QS_ALLEVENTS, так как 
зарезервирован системой для внутренних целей. 
    При вызове GetQueueStatus_napSiMeip fuFlags сообщает функции, наличие 
каких типов сообщений в очереди следует проверить. Чем меньше идентификаторов 
QS_* объединено побитовой операцией OR, тем быстрее отрабатывается вызов. 
Результат сообщается в старшем слове значения, возвращаемого функцией. 
Возвращаемый набор флагов всегда представляет собой подмножество набора, 
переданного Вами функции. Например, при вызове: 

    BOOL fPaintMsgWaiting = HIWORD(GetQueueStatus(QS_TIMER)) & QS_PAINT; 

    значение fPaintMsgWaiting всегда равно FALSE независимо от наличия в 
очереди сообщения WM_PAINT, так как флаг QS_PAINT в параметре функции не указан. 

    326 _____________________________Windows для профессионалов 

    Младшее слово возвращаемого значения содержит типы сообщений, помещенных в 
очередь, но не обработанных с момента последнего вызова GetQueueStatus, 
GetMessage или PeekMessage. 
    Не все флаги пробуждения обрабатываются системой одинаково. Флаг 
QS_MOUSEMOVE устанавливается, если в очереди есть необработанное сообщение 
WM_MOUSEMOVE. Когда GetMessage или PeekMessage (с флагом PM_REMOVE) извлекают 
последнее сообщение WM_MOUSEMOVE, флаг сбрасывается и остается в таком 
состоянии, пока в очереди ввода снова не окажется сообщение WM_MOUSEMOVE. 
Флаги QS_KEY, QS_MOUSEBUTTON и QS_HOTKEY действуют при соответствующих 
сообщениях аналогичным образом. 
    Флаг QS_PAINT обрабатывается иначе. Он устанавливается, если в окне, 
созданном данным потоком, имеется недействительный, требующий перерисовки 
регион. Когда область, занятая всеми окнами, созданными одним потоком, 
становится действительной (обычно в результате вызова ValidateRect, 
ValidateRegion или BeginPaint), флаг QS_PAINT сбрасывается. Еще раз подчеркну: 
данный флаг сбрасывается, только если становятся действительными все окна, 
принадлежащие потоку. Вызов GetMessage или PeekMessage на этот флаг 
пробуждения не влияет. 
    Флаг QS_POSTMESSAGE устанавливается, когда в очереди асинхронных сообщений 
потока есть минимум одно сообщение. При этом не учитываются аппаратные 
сообщения, находящиеся в виртуальной очереди ввода потока. Этот флаг 
сбрасывается после обработки всех сообщений из очереди асинхронных сообщений. 
    Флаг QS_TIMER устанавливается после срабатывания таймера (созданного 
потоком). После того как GetMessage или PeekMessage вернет событие WM_TIMER, 
флаг сбрасывается и остается в таком состоянии, пока таймер вновь не сработает. 
    Флаг QS_SENDMESSAGE указывает, что сообщение появилось в очереди либо 
синхронных, либо ответных сообщений. Он используется внутри системы для 
идентификации и обработки межпоточных синхронных сообщений, а для синхронных 
сообщений, посылаемых потоком самому себе, не применяется. Вы можете указывать 
флаг QS_SENDMESSAGE, но необходимость в нем возникает крайне редко. Я ни разу 
не видел его ни в одном приложении. 
    Есть еще один (недокументированный) флаг статуса очереди — QS_QUIT. Он 
устанавливается при вызове потоком PostQuitMessage. Сообщение WM_QUIT при этом 
не добавляется к очереди сообщений. И учтите, что GetQueueStatus не возвращает 
состояние этого флага. 
    Когда поток вызывает GetMessage или PeekMessage, система проверяет 
состояние очередей потока и определяет, какое сообщение надо обработать (рис. 11-2): 
    1. Если флаг QS_SENDMESSAGE установлен, система отправляет сообщение 
соответствующей оконной процедуре. GetMessage и PeekMessage контролируют 
процесс обработки и не передают управление потоку сразу после того, как 
оконная процедура обработает сообщение; вместо этого обе функции ждут 
следующего сообщения. 
    2. Если в очереди асинхронных сообщений потока есть какие-то сообщения, 
GetMessage и PeekMessage заполняют переданную им структуру MSG и возвращают 
управление. Цикл выборки сообщений (расположенный в потоке) в этот момент 
обычно обращается к DispatchMessage, чтобы соответствующая^оконнаяпроцедура 
обработала сообщение. 
    3. Если установлен флаг QS_QUIT, GetMessage и PeekMessage возвращают 
сообщение WM_QUIT и сбрасывают этот флаг. 
    4. Если в виртуальной очереди ввода потока есть какие-то сообщения, 
GetMessage и PeekMessage возвращают сообщение аппаратного ввода. 
    5. Если установлен флаг QS_PAINT, GetMessage и PeekMessage возвращают 
сообщение WM_PAINT для соответствующего окна. 
    6. Если установлен флаг QS_TIMER, GetMessage и PeekMessage возвращают 
сообщение WMJTIMER. 



                 327 ??????




    328 ______________________________Windows для профессионалов 

    Хоть и трудно в это поверить, но для такого безумия есть своя причина. 
Главное, из чего исходила Microsoft, разрабатывая описанный алгоритм, — 
приложения должны слушаться пользователя, и именно его действия (с клавиатурой 
и мышью) управляют программой, порождая события аппаратного ввода. Работая с 
программой, пользователь может нажать кнопку мыши, что приводит к генерации 
последовательности определенных событий. А программа порождает отдельные 
события, отправляя асинхронные сообщения в очередь потока. 
    Так, нажатие кнопки мыши могло бы заставить окно, которое обрабатывает 
сообщение WM_LBUTTONDOWN, послать 3 асинхронных сообщения разным окнам. 
Поскольку эти 3 программных события возникают в результате аппаратного 
события, система обрабатывает их до того, как принимает новое аппаратное 
событие, инициируемое пользователем. И именно поэтому очередь асинхронных 
сообщений проверяется раньше виртуальной очереди ввода. 
    Прекрасный пример такой последовательности событий — вызов функции 
TranslateMessage, проверяющей, не было ли выбрано из очереди ввода сообщение 
WM_KEYDOWN или WM_SYSKEYDOWN. Если да, функция проверяет, можно ли информацию 
о виртуальной клавише преобразовать в символьный эквивалент. Если это 
возможно, TranslateMessage вызывает PostMessage, чтобы поместить в очередь 
асинхронных сообщений WM_CHAR или WM_SYSCHAR. При следующем вызове GetMessage 
система проверяет содержимое очереди асинхронных сообщений и, если в ней есть 
сообщение, выбирает его и возвращает потоку. Возвращается либо WM_CHAR, либо 
WM_SYSCHAR. Но вот GetMessage вызывается еще раз, и система обнаруживает, что 
очередь асинхронных сообщений пуста. Тогда она проверяет очередь ввода, где и 
находит сообщение WM_(SYS)KEYUP; именно оно и возвращается. 
    Поскольку система устроена так, а не иначе, приведенная ниже 
последовательность аппаратных событий: 

    WM_KEYDOWN 
    WM_KEYUP 

    генерирует такую последовательность сообщений для оконной процедуры (при 
этом предполагается, что информацию о виртуальной клавише можно преобразовать 
в ее символьный эквивалент): 

    WM_KEYDOWN 
    WM_CHAR 
    WM_KEYUP 

    А теперь вернемся к тому, как система решает, что за сообщение должна 
вернуть функция GetMessage или PeekMessage. Просмотрев очередь асинхронных 
сообщений, система, прежде чем перейти к проверке виртуальной очереди ввода, 
проверяет флаг QS_QUIT. Вспомните: этот флаг устанавливается, когда поток 
вызывает PostQuitMessage. Вызов PostQuitMessage дает примерно то же, что и 
вызов PostMessage, которая вставляет сообщение в конец очереди и тем самым 
заставляет обрабатывать это сообщение до проверки очереди ввода. Так почему же 
PostQuitMessage устанавливает флаг вместо того, чтобы поместить WM_QUIT в 
очередь сообщений? На то есть две причины. 
    Во-первых, в 16-разрядной Windows очередь не может состоять более чем из 8 
сообщений. Если очередь заполнена и приложение пытается включить в нее 
сообщение WM_QUIT, это сообщение потеряется, а приложение никогда не 
завершится. Но благодаря тому, что сообщение WM_QUIT обрабатывается как особый 
флаг, сообщение никуда не пропадает. Конечно, в Win32 ничего подобного нет, 
так как очереди в Win32 представляют собой связанные списки, допускающие 
динамическое разрастание. И вторая причина в том, что приложению нужно дать 
обработать все прочие посланные ему сообщения_р,о_гого, как система завершит 
его. Поэтому, если в программе есть такой фрагмент кода: 

    _Глава 11. Оконные сообщения и асинхронный ввод 329 


    case WM_CLOSE: 
    PostQuitMessage(O); 
    PostMessage(hwnd. WM_USER, 0, 0); 

    сообщение WM_USER извлекается из очереди до WM_QUIT, хотя WM_USER помещено 
в очередь после вызова PostQuitMessage. 
    А теперь о последних двух сообщениях: WM_PAINT и WM_TIMER. У WM_PAINT 
низкий приоритет, так как зарисовка экрана — операция не самая быстрая. Если 
бы это сообщение посылалось всякий раз, когда окно становится 
недействительным, работа системы замедлилась бы весьма ощутимо. Помещая 
WM_PAINT после ввода с клавиатуры, система работает гораздо быстрее. Например, 
из меню можно вызвать какую-нибудь команду, открывающую диалоговое окно, 
выбрать в нем что-то, нажать клавишу Enter — и проделать все это даже до того, 
как окно появится на экране. Достаточно быстро нажимая клавиши, Вы наверняка 
заметите, что сообщения об их нажатии извлекаются прежде, чем дело доходит до 
сообщений WM_PAINT. А когда Вы нажмете клавишу Enter, подтверждая тем самым 
значения параметров, установленных в диалоговом окне, система разрушит окно и 
сбросит флаг WM_PAINT. 
    Приоритет WM_TIMER еще ниже, чем WM_PAINT. Чтобы понять, почему, вспомните 
поставляемую с системой программу Clock. Эта программа обновляет свое окно 
всякий раз, когда получает сообщение WM_TIMER. Теперь представьте: сообщения 
WM_TIMER возвращаются до WM_PAiNT, a Clock настроила таймер так, что он 
постоянно срабатывает, не давая GetMessage вернуть сообщение WM_PAINT. 
Программе Clock тогда не удалось бы прорисовать свое окно — она только и 
занималась бы тем, что обновляла показания времени; при этом она никогда не 
получила бы сообщения WM_PAINT. 
    Функции GetMessage и PeekMessage проверяют флаги пробуждения только для 
вызывающего потока. Это значит, что потоки никогда не смогут извлечь сообщения 
из очереди, присоединенной к другому потоку, включая сообщения для потоков 
того же процесса. 

    Обмен данными через сообщения 

    Я уже говорил, что Win32 не позволяет двум программам разделять блок 
памяти, передавая его описатель из одного процесса в другой. Кроме того, я 
объяснял, почему блок нельзя разделять и за счет передачи его адреса из одного 
процесса в другой. Оба эти метода совместного использования данных срабатывают 
в 16-разрядной Windows, но не в Win32. А причина та же: у каждого процесса в 
Win32 свое адресное пространство. 
    Чтобы приложения разделяли какие-то блоки памяти, я, как Вы помните, 
советовал применять файлы, проецируемые в память (см. главу 8). Но возьмем 
ситуацию, когда один процесс готовит блок данных, предназначенный для 
использования совместно с другими приложениями. Подготовив данные, он должен 
как-то уведомить об этом другие приложения. Один из способов — применить 
объекты «событие» (об этом см. главу 10). Но можно также передать сообщение 
окну другого процесса. Этот способ мы сейчас и рассмотрим. 
    В некоторых оконных сообщениях адрес блока памяти задается в параметре 
IParam. Например, сообщение WM_SETTEXT использует IParam как указатель на 
строку (с нулевым символом в конце), содержащую новый текст для окна. 
Рассмотрим такой вызов: 
    SendMessage(FindWindow(NULL, "Calculator"), WM_SETTEXT, 0, (LPARAM) "A 
Test Caption"): 
    Вроде бы все достаточно безобидно: определяется описатель окна приложения 
Calculator, и делается попытка изменить его заголовок на A Test Caption. Но 
приглядимся к тому, что тут происходит. 

    330 _____________________________Windows для профессионалов 

    В IParam передается адрес строки (с новым заголовком), расположенной в 
адресном пространстве Вашего процесса. Получив это сообщение, оконная 
процедура программы Calculator берет IParam и пытается манипулировать чем-то, 
что, «по ее мнению», является указателем на строку с новым заголовком. 
    Но адрес в IParam указывает на строку в адресном пространстве Вашего 
процесса, а не программы Calculator. Вот Вам и долгожданная неприятность — 
нарушение доступа к памяти. Но если Вы все же выполните показанную ранее 
строку, все будет работать нормально. Что за наваждение? 
    А дело в том, что система отслеживает сообщения WM_SETTEXT и обрабатывает 
их не так, как большинство других сообщений. При вызове SendMessage внутренний 
код функции проверяет, не пытаетесь ли Вы послать сообщение WM_SETTEXT. Если 
это так, функция копирует строку из Вашего адресного пространства в блок 
памяти и делает его доступным другим процессам. Затем сообщение посылается 
потоку другого процесса. Когда потокприемник готов к обработке WM_SETTEXT, он 
определяет адрес общего блока памяти (содержащего новый текст окна) в адресном 
пространстве своего процесса. Параметру IParam присваивается значение именно 
этого адреса, и WM_SETTEXT направляется нужной оконной процедуре. Не слишком 
ли тут накручено, а? 
    К счастью, большинство сообщений не требует такой обработки — она 
выполняется, только если сообщение посылается другому процессу. (Заметьте: 
описанная обработка осуществляется и для любого сообщения, параметры wParam 
или IParam которого содержат указатель на какую-либо структуру данных.) 
    А вот другой случай, когда от системы требуется особая обработка, — 
сообщение WM_GETTEXT. Допустим, Ваша программа содержит код: 

    char szBuf[200]; 
    SendMessage(FindWindow(NULL, "Calculator"), WM_GETTEXT, 
    sizeof(szBuf), (LPARAM) szBuf): 

    WM_GETTEXT требует, чтобы оконная процедура программы Calculator поместила 
в буфер, на который указывает szBuf, заголовок своего окна. Когда Вы посылаете 
это сообщение окну другого процесса, система должна на самом деле послать 2 
сообщения. Сначала — WM_GETTEXTLENGTH окну. Оконная процедура возвращает число 
символов в строке заголовка. Это значение система использует при выделении 
блока памяти, разделяемого двумя процессами. 
    Выделив блок памяти, система посылает для его заполнения сообщение 
WIDGETTEXT. Затем переключается обратно на процесс, первым вызвавший функцию 
SendMessage, копирует данные из общего блока памяти в буфер, на который 
указывает szBuf, и заставляет SendMessage вернуть управление. 
    Что ж, все хорошо, пока Вы посылаете сообщения, известные системе. А если 
мы определим собственное (WM_USER + х) сообщение, собираясь отправить его окну 
другого процесса? Система не «поймет», что нам нужно выделить общий блок 
памяти и скорректировать пересылаемые значения указателей. Но выход есть — это 
сообщение WM_COPYDATA: 

    COPYDATASTRUCT cds; 
    SendMessage(hwndReceiver, WM_COPYDATA, (WPARAM) hwndSender, (LPARAM) &cds): 
    Здесь COPYDATASTRUCT — структура, определенная в WINUSER.H: 
    typedef struct tagCOPYDATASTRUCT { 
    DWORD dwData; 
    DWORD cbData: 
    PVOID IpData: 
    } COPYDATASTRUCT: 


    ______________Глава 11. Оконные сообщения и асинхронный ввод 331 

    Чтобы переслать данные окну другого процесса, нужно сначала 
инициализировать эту структуру. Элемент dwData резервируется для использования 
в Вашей программе. В него разрешается записывать любое 32-битное значение. 
Например, передавая в другой процесс данные, в этом элементе можно указывать 
тип данных. 
    Элемент cbData задает число байтов, пересылаемых в другой процесс, a 
IpData указывает на первый байт данных. Адрес, идентифицируемый элементом 
IpData, находится, конечно же, в адресном пространстве отправителя. 
    Когда SendMessage «видит», что Вы посылаете сообщение WM_COPYDATA, она 
выделяет блок памяти размером cbData байтов и копирует данные из адресного 
пространства Вашей программы в этот блок. Затем отправляет сообщение 
окну-приемнику (destination window). При обработке этого сообщения принимающей 
оконной процедурой параметр IParam указывает на структуру COPYDATASTRUCT, 
находящуюся в адресном пространстве процесса-приемника. Элемент IpData этой 
структуры указывает на скопированный блок памяти, причем адрес изменяется так, 
чтобы отразить размещение блока в адресном пространстве процесса-приемника. 
    Вам следует помнить три важных вещи, связанных с сообщением WM_COPYDATA. 
Во-первых, отправляйте его всегда синхронно; никогда не пытайтесь делать этого 
асинхронно. Последнее просто невозможно: как только принимающая оконная 
процедура обработает сообщение, система должна освободить блок памяти, занятый 
скопированными данными. При передаче WM_COPYDATA как асинхронного сообщения 
появится неопределенность в том, когда оно будет обработано, и система не 
сможет освободить упомянутый блок памяти. 
    Во-вторых, на создание системой копии данных в адресном пространстве 
другого процесса неизбежно уходит какое-то время. Значит, пока SendMessage не 
вернет управление, нельзя допускать изменения содержимого блока памяти 
каким-либо другим потоком. 
    В-третьих, сообщение WM_COPYDATA работает при пересылке данных как из 
\Уш32-процесса в Winl6-nporpaMMy, так и наоборот. И, по-видимому, это лучший 
способ связи между 32- и 16-разрядными программами. 

    Программа-пример CopyData 

    Программа CopyData (COPYDATA.EXE) (см. листинг на рис. 11-3) демонстрирует 
использование сообщения WM_COPYDATA при пересылке блока данных из одной 
программы в другую. Файлы исходного кода и ресурсов для этой программы 
находятся в каталоге COPYDATA на компакт-диске, прилагаемом к книге. Чтобы 
увидеть приложение CopyData в действии, запустите минимум 2 его копии; при 
этом каждая копия открывает такое диалоговое окно: 
    Datal: )вмп1асмД!ЯЕ1send Data1 to other windows Data2: jsome more test 
data     Send DataZ to other windows 
    Если Вы хотите посмотреть, как данные копируются из одного приложения в 
другое, то сначала измените содержимое полей ввода Datal и Data2. Затем 
щелкните одну из двух кнопок Send Data* To Other Windows (переслать данные* в 
другие окна), и программа отправит данные всем выполняемым экземплярам 
CopyData. Последние включат новые данные в свои поля ввода. 

    332 _____________________________Windows для профессионалов I 

    А теперь обсудим принцип работы программы. Щелчок одной из двух кнопок 
приводит к: 
    1. Инициализации элемента dwData структуры COPYDATASTRUCT нулевым 
значением (если «нажата» кнопка Send Datal To Other Windows) или единицей 
(если «нажата» кнопка Send Data2 To Other Windows). 
    2. Подсчету длины текстовой строки (в символах) из соответствующего поля 
ввода с добавлением единицы, чтобы учесть нулевой символ в конце. Полученное 
число символов преобразуется в количество байтов умножением на sizeof(TCHAR), 
и результат записывается в элемент cbData структуры COPYDATASTRUCT. 
    3. Вызову HeapAlhc, чтобы выделить блок памяти, достаточный для хранения 
строки из поля ввода с учетом концевого нулевого символа. Адрес этого блока 
записывается в элемент IpData все той же структуры. 
    4. Копированию текста из поля ввода в выделенный блок памяти. 
    Теперь все готово для пересылки в другие окна. Чтобы определить, каким 
окнам посылать сообщение WM_COPYDATA, программа: 
    1. Получает описатель первого окна, принадлежащего «двойнику» данного 
экземпляра CopyData. 
    2. Получает текст строки заголовка CopyData. 
    3. Просматривает все окна «двойника», сравнивая их заголовки со строкой 
заголовка данного экземпляра CopyData. Если заголовки совпадают, в текущее 
окно «двойника» посылается WM_COPYDATA. Так как в этом цикле я не делаю особых 
проверок, то экземпляр CopyData, вызывающий SendMessage, отправит сообщение 
WM_COPYDATA сам себе. (Это, кстати, яркий пример того, что в роли отправителя 
и получателя данного сообщения может выступать один и тот же поток.) 
    4. Проверив все окна, CopyData вызывает HeapFree, чтобы освободить память, 
отведенную под строку из поля ввода. 
    Вот так данные и пересылаются из одного приложения в другое посредством 
оконных сообщений. 

    COPYDATA.C 
    /,**,*...,**..,,.*********,*.**,***..***...*».......*...,.*..**..*.,,*...*..... 
    Модуль: CopyData.С 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    .,.***********.*.**.*,...***........,.**..***,.***************.*.*.***......../ 
    
    «include ".,\CmnHdr.H"           /* см. приложение В */ 
    ftinclude <windows,h> 
    ftinclude <windowsx.h> 
    ftinclude <tchar.h> 
    ffinclude "Resource.H" 
    
    / I Microsoft не включила распаковщик для WM_COPYDATA в 
    // WINDOWSX.H. Поэтому я написал его здесь... 

    см. след. стр. 

    Рис. 11-3. Листинг программы CopyData 


    _____________Глава 11. Оконные сообщения и асинхронный ввод 333 


    РИС. 11-3  (продолжение) 
    
    /* BOOL Cls_OnCopyData(HWND hwnd, HWND hwndFrom, 
    PCOPYDATASTRUCT cds) */ 
    
    «define HANDLE_WM_COPYDATA(hwnd, wParam, IParam, fn) \ 
    ((fn)((hwnd), (HWND)(wParam), \ 
    (PCOPYDATASTRUCT)lParam), OL) 
    
    #define FORWARD_WM_COPYDATA(hwnd, hwndFrom, cds, fn) \ 
    (BOOL)(UINT)(DWORD)(fn)((hwnd), WM_COPYDATA, \ 
    (WPARAM)(hwndFrom), (LPARAM)(cds)) 
    
    ///////////////////////////////////////

    BOOL Dlg_OnCopyData(HWND hwnd, HWND hwndFrom, PCOPYDATASTRUCT cds) { 
    Edit_SetText( 
    GetDlgItem(hwnd, cds->dwData ? IDC_DATA2 : IDC_DATA1), 
    cds->lpData); 
    return(TRUE); 
    BOOL Dlg_Or]lniL[)ialog (HWND nwnd, HWND hwnaFocus. LPARAM IParaiii) { 
    
    // присваиваем значок диалоговаму окну 
    chSETDLGICONS(hwnd, IDI_COPYDATA, IDI_COPYDATA); 
    
    // инициализируем поле ввода тестовыми данными 
    Edit_SetText(GetDlgItem(hwnd, IDC_DATA1), __TEXT("Some test data")); 
    Edit_SetText.(GetDlgItem(hwnd. IDC_DATA2), __TEXT("Some more test data")): 
    return(TRUE); 
    i 
    
    void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtI, UINT codeNotil-y) { 
    HWND hwndEdit, hwndSibling; 
    COPYDATASTRUCT cds; 
    TCHAR szCaption[100], szCaptionSibling[100]; 
    switch (id) { 
    case IDC_COPYDATA1: 
    case IDC_COPYDATA2; 
    
    if (codeNotify != BN_CLICKED) 
    break: 
    hwndEdit = GetDlgItem(hwnd, 
    (id == IDC_COPYDATA1) ? IDC_DATA1 : IDC_DATA?)' 


    334 ______________________________Windows для профессионалов 


    РИС. 11-3   (продолжение) 

    II готовим содержимое COPYDATASTRUCT; 
    
    // 0 = ID_DATA1, 1 = ID_DATA2 
    cds.dwData = (DWORD) ((id == IDC_COPYDATA1) ? 0 : 1); 
    
    // получаем длину пересылаемого блока данных 
    cds.cbData = (Edit_GetTextLength(hwndEdit) + 1) * sizeof(TCHAR); 
    
    // выделяем блок памяти для хранения строки 
    cds.lpData = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cds.cbData); 
    
    // копируем текст из поля ввода в выделенный блок 
    Edit_GetText(hwndEdit, cds.lpData, cds.cbData): 
    
    // ищем первое перекрытое окно 
    hwndSibling = GetFirstSibling(hwnd); 
    
    // получаем заголовок нашего окна 
    GetWindowText(hwnd, szCaption, chDIMOF(szCaption)); 
    while (IsWindow(hwndSibling)) { 
    
    // получаем заголовок очередного окна, которому 
    // можно было бы переслать данные 
    GetWindowText(hwndSibling, szCaptionSibling, 
    chDIMOF(szCaptionSibling)); 
    if (_tcscmp(szCaption, szCaptionSibling) == 0) { 
    // Если заголовок окна совпадает с нашим, посылаем 
    // ему данные. Здесь возможна посылка сообщения 
    // самим себе. Это нормально и демонстрирует, что 
    // посылка данных с использованием WM_COPYDATA 
    // самому себе вполне допустима, 
    FORWARD_WM_COPYDATA(hwndSibling, hwnd, &cds, SendMessage); 
    } 
    
    // получаем описатель следующего перекрытого окна 
    hwndSibling = GetNextSibling(hwndSibling); 
    
    // освобождаем буфер данных 
    HeapFree(GetProcessHeap(), 0, cds.lpData); 
    break; 
    case IDCANCEL: 
    EndDialog(hwnd, id); 
    break; 
    i 
    i. 

    см. след. стр. 


    ______________Глава 11. Оконные сообщения и асинхронный ввод 335 


    РИС. 11-3   (продолжение) 

    BOOL CALLBACK Dlg_Proc (HWND hwnd. UINT uMsg. WPARAM wParam, LPARAM 
IParam) { 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); 
    chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); 
    chHANDLE_DLGMSG(hwnd, WM_COPYDATA, Dlg_OnCopyData); 
    return(FALSE); 
    \, 
    ( 
    int WINAPI _tWinMain (HINSTANCE hinstExe. 
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCmdShow) { 
    chWARNIFUNICODEUNDERWIN95(); 
    DialogBox(hinstExe. MAKEINTRESOURCE(IDD_COPYDATA), NULL, Dlg_Proc); 
    return(O); 
    ////////////////////////// конец файла ///////////////////////////////// 


    Разупорядоченный ввод 

    Используемый в 16-разрядной Windows упорядоченный ввод (serialized input), 
означает, что система обрабатывает события от клавиатуры или мыши в том 
порядке, в каком они инициируются действиями пользователя. События извлекаются 
из системной очереди по мере того, как их запрашивают приложения. Допустим, на 
клавиатуре нажаты сочетания клавиш: 
    АВС, Alt+Tab и XYZ. Тогда в системную очередь сообщений ставится 7 
аппаратных событий.4 Приложение, в фокусе которого находится клавиатура, 
выбирает сообщения АВС из системной очереди и выводит в свое окно 
соответствующие символы. Теперь предположим, что в данной программе есть 
ошибка, из-за которой программа при каждом вводе буквы С попадает в 
бесконечный цикл. В этот момент зависает и вся система. Поэтому Alt+Tab и XYZ 
никогда не будут выбраны из системной очереди. А если пользователь попытается 
мышью активизировать другое приложение, то событие, связанное с мышью, будет 
добавлено в конец системной очереди — за последним событием от клавиатуры. 
Значит, и это событие никогда не будет выбрано из системной очереди. Остается 
одно — перезагрузить Мишину. 
    Microsoft в свое время пыталась улучшить эту ситуацию. В 16-разрядной 
Windows была введена поддержка комбинации клавиш Ctrl+Alt+Del, к которой 
пользователь мог прибегнуть, если приложение переставало реагировать на его 
команды. При нажатии этих клавиш система определяла, какое приложение в 
настоящий момент активно, и делала попытку убрать его из памяти. Лично я давно 
убедился, что обычно 16-разрядной Windows не удается корректно восстановиться 
после зависания приложения, и компьютер все равно приходится перезагружать. 
    4. На самом деле при этом в очереди появляется более 7 событий. Каждое 
нажатие клавиши, например, генерирует события WM_KEYDOWN и WM_KEYUP. Говоря в 
данном случае о 7 событиях, я просто хочу упростить рассмотрение этой темы. 

    336 ______________________________Windows для профессионалов 

    Решение проблемы — разупорядоченный ввод (deserialized input). В этом 
случае аппаратные события не обязательно обрабатываются в порядке поступления. 
Знаю, Вы наверняка сейчас подумали: «Значит ли это, что, если я набрал на 
клавиатуре буквы ЛВС, поток получит что-нибудь вроде CAB?» Нет, конечно нет. 
Но если Вы нажали АВС, Alt+Tab и XYZ, то поток, который активизируется после 
нажатия клавиш Alt+Tab, вполне может обработать XYZ .раньше, чем первый поток 
закончит обработку АВС. 
    В Win32 ввод рассматривается на уровне потока, а не всей системы, как в 
16-разрядной Windows. И поток принимает аппаратные события в том порядке, в 
каком пользователь их формирует. Вот так и разупорядочивается ввод. 

    Как достигается разупорядочивание 

    При запуске система создает себе особый поток необработанного ввода (raw 
input thread, RIT). Когда на клавиатуре нажимают и отпускают клавишу или 
кнопку мыши или перемещают мышь, соответствующий драйвер устройства добавляет 
аппаратное событие к очереди RIT. В результате поток RIT пробуждается, 
проверяет первое событие в очереди, транслирует его в соответствующее 
сообщение WM_KEY*, WM_?BUTTON* или WM_MOUSEMOVE, а затем отправляет событие в 
виртуальную очередь ввода нужного потока. 
    На рис. 11-1 было показано, что у каждого потока есть своя очередь 
асинхронных сообщений и своя виртуальная очередь ввода. Когда поток создает 
окно, система помещает все предназначенные этому окну сообщения в очередь 
асинхронных сообщений потока, которому принадлежит окно. 
    Рассмотрим такой сценарий (рис. 11-4): процесс создает потоки А и В. Далее 
поток А создает окно А, а поток В — два окна: В и С. Если какой-то поток 
отправит асинхронное сообщение окну А, оно будет помещено в очередь 
асинхронных сообщений потока А. По аналогии любые сообщения, отправленные 
окнам В и С, ставятся в очередь асинхронных сообщений потока В. 

    Виртуальная                                     Виртуальная 
    -  очередь ввода^^г                            ^очередь ввода^^ 
    ПотокА   j___ Очередь асинхронных ^         Поток В   Г-—— ^sp^b 
асинхронных v 
    |    ^^   сообщений   „^^                  |    -^^.   сообщений ^^ 
    Окно А |                                 ; Окно В | 
    Окно С | 

    Рис. 11-4. Сообщения для окна А помещаются в очереди потока А; сообщения 
для окон В и С — в очереди потока В 

    При обработке аппаратного события поток RIT должен определить, в чью 
виртуальную очередь ввода его отправить. В случае сигнала от мыши RIT сначала 
определяет, поверх какого окна находится курсор мыши, а затем размещает 
событие (WM_?BUTTON* или WM_MOUSEMOVE) в виртуальной очереди ввода потока, 
создавшего данное окно. Обрабатывая сигнал от клавиатуры, RIT определяет, 
какой поток активен, т. е. с каким потоком пользователь работает в данный 
момент. Соответствующее клавиатурное сообщение помещается в виртуальную 
очередь ввода этого потока. 

    ______________Глава 11. Оконные сообщения и асинхронный ввод 337 

    Чтобы система могла переключать потоки независимо от того, занят ли поток 
в данный момент обработкой ввода, RIT — прежде чем отправить аппаратное 
событие в виртуальную очередь ввода потока — проверяет его тип. Например, RIT 
особым образом реагирует на следующие комбинации клавиш: 

    Комбинация 
    клавиш          Windows 95                    Windows NT 

    Alt-1-Tab           Активизирует другое окно.          Активизирует другое окно, 
    				подключая его поток к RIT.         подключая его поток к RIT. 
    Alt+Esc            Активизирует другое окно,          Активизирует другое окно, 
    				подключая его поток к RIT.         подключая его поток к RIT. 
    Ctrl+Esc           Открывает меню Start (Пуск)        Открывает меню Start 
    				на панели задач,                   на панели задач. 
    Ctrl+Alt+Del       Выводит системное диалоговое      Открывает диалоговое окно 
    				окно Close Program (Завершение     Windows NT Security. 
			    работы программы). 


    Разделение потоками виртуальных очередей ввода 

    Вы можете заставить два и более потоков совместно использовать одну 
виртуальную очередь ввода и переменные локального состояния ввода (о них чуть 
позже) с помощью Attach Threadlnput 

    BOOL AttachThreadInput(DWORD idAttach, DWORD idAttachTo, BOOL fAttach); 

    Функция «говорит» системе разрешить двум потокам использовать одну и ту же 
виртуальную очередь ввода (рис. 11-5). Первый параметр, idAttach, задает 
идентификатор потока, чья виртуальная очередь ввода Вам больше не нужна. 
Второй — idAttachedTo — это идентификатор потока, чью виртуальную очередь 
ввода должны разделять два потока. И, наконец, fAttach должен быть или TRUE, 
чтобы инициировать совместное использование единой очереди, или FALSE — тогда 
каждый поток будет вновь использовать свою очередь. А чтобы одну виртуальную 
очередь ввода могли разделить более двух потоков, вызовите AttachThreadInput 
соответствующее число раз. 
    Вернемся к предыдущему примеру и допустим, что поток В вызывает 
AttachThreadInput, передавая в первом параметре свой идентификатор, во втором 
— идентификатор потока Айв последнем — TRUE: 

    AttachThreadInput(idThreadB, idThreadA, TRUE); 

    Теперь любое событие аппаратного ввода, относящееся к окнам В и С, будет 
добавлено в конец виртуальной очереди ввода потока А. Аналогичная очередь 
потока В больше не получит новых событий, если только Вы не разъедините 
очереди, повторно вызвав AttachThreadInput с передачей FALSE в параметре fAttach. 
    Потоки, присоединенные к одной виртуальной очереди ввода, сохраняют 
индивидуальные очереди сообщений. Чем больше потоков Вы подключаете к одной 
виртуальной очереди ввода, тем больше система становится похожей на 
16-разрядную Windows. Ведь у той все задачи подключены к единственной очереди 
ввода — системной. И, кстати, если запустить 16-разрядные приложения под 
управлением Windows 95 или Windows NT, система создаст им единую очередь 
ввода, т. е. смоделирует для них среду 16-разрядной Windows. 
    Однако Вы серьезно снизите надежность Win32-cncTeMbi, если заставите все 
потоки использовать одну очередь сообщений. Если какое-нибудь приложение 
зависнет при обработке нажатия клавиши, другие программы не получат ввода. 
Поэтому подумайте хорошенько, прежде чем вызывать AttachThreadInput. 

    338 ______________________________Windows для профессионалов 

    Виртуальная                                      Виртуальная 
    ^^ очередь ввода ^^г                             ^ очередь ввода ^^ 
    Поток A   S-__ Очередь асинхронных ^    ^   Поток В   |—— Очередь 
асинхронных v 
    |    ~^^   сообщений   „^^                   |    •-^    сообщений   ^^ 
    Окно А |                                   Окно В | 
    Окно С | 
    ^MM^WN—M^ 

    Рис. 11-5. Аппаратные сообщения для окон А, В и С помещаются в виртуальную 
очередь ввода потока А 

    VWINDOWS/ Windows NT допускает выполнение приложений 16-разрядной Windows 
в отдельном адресном пространстве. Очереди ввода всех 16-разрядных 
приложений, исполняемых в одном адресном пространстве, подсоединены друг 
к другу. Но если два таких приложения исполняются в разных адресных 
пространствах, система не объединяет их очереди ввода. Как следствие, Windows 
NT способна надежнее исполнять 16-разрядные приложения, чем сама 16-разрядная 
Windows. Если виснет одно приложение, виснут и другие 16-разрядные приложения, 
исполняемые.? том же адресном пространстве. Если же они выполняются в разных 
адресных пространствах, зависание одного приложения не влияет на другое. 

    Система неявно соединяет виртуальные очереди ввода нескольких потоков, 
если приложение устанавливает ловушку-регистратор (journal record hook) или 
ловушку-проигрыватель (journal playback hook). Когда ловушка снимается, 
система автоматически восстанавливает схему организации очереди ввода, 
существовавшую до установки ловушки. 
    Установкой ловушки-регистратора программа сообщает, что хочет получать 
уведомления о всех аппаратных событиях, вызываемых пользователем. Приложение 
обычно записывает или регистрирует эту информацию. А в следующем сеансе работы 
приложение устанавливает ловушку-проигрыватель, в результате чего система 
игнорирует пользовательский ввод и ожидает, когда установившее ловушку 
приложение начнет воспроизводить (проигрывать) записанные ранее события. 
    Воспроизведение записанных событий моделирует повторение действий 
пользователя. В частности, программа Recorder (Запись Макрокоманд), 
поставляемая с 16-разрядной Windows, позволяет записывать события для 
последующего воспроизведения за счет установки ловушки-регистратора и 
ловушки-проигрывателя. Но Recorder не поставляется с Windows 95 и Windows NT, 
так как снижает надежность работы системы. 
    Есть еще один случай, когда система неявно вызывает AttachThreadInput. 
Допустим, приложение создает два потока. Первый открывает на экране диалоговое 
окно. Затем второй поток вызывает CreateWindow, указывая стиль WS_CHILD и 
передавая описатель диалогового окна как описатель дочернего окна. В этом 
случае система неявно вызывает AttachThreadInput, чтобы поток (которому 
принадлежит дочернее окно) использовал ту же очередь ввода, что и поток, 
создавший исходное диалоговое окно. Это приводит к синхронизации ввода во всех 
дочерних окнах исходного диалогового окна. Позднее я покажу, что окна, 
созданные разными потоками, могут выглядеть так, словно все они одновременно 

    ______________Глава 11. Оконные сообщения и асинхронный ввод 339 

    находятся в фокусе клавиатуры. Ну а чтобы не смущать пользователя, 
соедините очереди ввода вместе, и тогда лишь одно окно окажется в фокусе клавиатуры. 

    Локальное состояние ввода 

    Раньше, при разработке программ для MS-DOS, всегда исходили из того, что 
активное приложение будет единственным в системе. Как следствие, программы 
«считали», что экран, память, дисковое пространство, процессор и даже любые 
нажатия клавиш адресованы только им, — думаю, Вы поняли, что я имел в виду. 
    Но вот появилась 16-разрядная Windows, и программистам пришлось учиться 
сотрудничать друг с другом. Программы, исполняемые одновременно, должны делить 
между собой ограниченные системные ресурсы. Каждой приходится ограничивать 
поле своего вывода маленькой прямоугольной областью экрана, выделять память 
только при острой необходимости, передавать управление другим приложениям при 
нажатии некоторых клавиш, специально приостанавливать свое выполнение, чтобы и 
другие могли получить частицу процессорного времени. Если приложение 
«забывалось», захватывая слишком много ресурсов и мешая другим программам, 
пользователю оставалось либо закрыть «нахальную» программу, либо запустить ее 
в одиночестве, что, конечно, противоречило самой идее многозадачной среды. 
    Многое из сказанного верно и в отношении Win32. Разработчики по-прежнему 
проектируют свои приложения так, чтобы использовать для вывода небольшую 
прямоугольную область экрана, выделяют память только при необходимости и т. д. 
Но Win32 заставляет проявлять учтивость. Система устанавливает предельный 
размер памяти, которую приложение может «прибрать к рукам». Она следит за 
клавиатурой и дает пользователю возможность переключиться на другое 
приложение, хочет того активное приложение или нет. Она вытесняет текущее 
приложение и предоставляет время другой программе независимо от того, 
насколько текущее приложение стремится дорваться до процессора.5 
    Кто же выиграл от того, что система получила такую власть над 
приложениями? Все — и пользователи, и разработчики. Система делает все это, 
невзирая на наши попытки воспрепятствовать ей, а значит — расслабьтесь и 
перестаньте вынашивать коварные замыслы о том, как бы потеснить другие программы. 
    Такой стиль работы системы основан на концепции локального состояния ввода 
(local input state). Каждый поток обладает собственным состоянием ввода, 
сведения о котором хранятся в структур.е-1НДЕАР1М.РО. В информацию об этом 
состоянии включается данные о виртуальной очереди ввода потока и группа 
переменных. Последние содержат управляющую информацию о_састоянии ввода:   
~~~" ' 
    Клавиатура: 
    • какое окно находится в фокусе клавиатуры; 
    • какое окно активно; 
    • какие нажатые клавиши хранятся в массиве синхронного состояния клавиш 
(synchronous key state array); 
    • состояние курсора ввода. 
    5. Процессу можно присвоить высокий класс приоритета; однако это может 
привести к резкому замедлению скорости работы процессов с более низким 
приоритетом. Но даже если Вы установите высокий класс приоритета, система все 
равно даст пользователю завершить такой процесс. 

    340 ____________________________Windows для профессионалов 

    Мышь: 
    • каким окном она захвачена; 
    • какова форма ее курсора; 
    • видим ли этот курсор. 
    Так как у каждого потока свой набор переменных состояния ввода, то и 
представления об окне, находящемся в фокусе, об окне, захватившем мышь, и т. 
п. у них тоже сугубо свои. «С точки зрения» потока, клавиатурный фокус либо 
есть у одного из его окон, либо его нет ни у одного окна во всей системе. То 
же самое относится и к мыши: либо она захвачена одним из его окон, либо не 
захвачена никем. В общем, перечислять можно еще долго. Так вот, подобный 
сепаратизм приводит к некоторым последствиям — их-то мы и обсудим ниже. 

    Ввод с клавиатуры и фокус 

    Win32 и 16-разрядная Windows обрабатывают ввод с клавиатуры по-разному. 
Когда я только начинал работать с Win32, я все пытался — используя свои знания 
о 16-разрядной Windows — понять, как система обрабатывает ввод с клавиатуры. 
Но оказалось, что понимание обработки ввода с клавиатуры в 16-разрядной 
Windows лишь затрудняет понимание того, как это делается в Win32. 
    В Win32 ввод с клавиатуры направляется потоком необработанного ввода (RIT) 
в виртуальную очередь ввода какого-либо потока, но только не в окно. RIT 
помещает клавиатурные события в витруальную очередь потока безотносительно 
конкретному окну. Когда поток вызывает GetMessage, клавиатурное событие 
извлекается из очереди и связывается с окном (созданным потоком), на котором в 
данный момент сосредоточен фокус ввода (рис. 11-6). 

    Поток необработанного ввода | 
    Виртуальная очередь ввода потока 1 |            Виртуальная очередь ввода 
потока 2 I 
    Окно A j  Окно В j Окно С, ,1           Окно D j  Окно Е |  Окно F | 

    Рис. 11-6. RIT направляет пользовательский ввод с клавиатуры в виртуальную 
очередь ввода одного из потоков 

    Чтобы направить клавиатурный ввод в другое окно, нужно указать, в очередь 
какого потока RIT должен помещать клавиатурные события, а также «сообщить» 
переменным состояния ввода потока, какое окно должно быть в фокусе. Эта задача 
не решается простым вызовом SetFocus. Если в данный момент ввод от RIT 
получает поток 1, то вызов SetFocus с передачей описателей окон А, В или С 
приведет к смене фокуса. Окно, теряющее фокус, убирает используемый для 
обозначения фокуса прямоугольник или гасит курсор ввода, а окно, получающее 
фокус, рисует такой прямоугольник или показывает курсор ввода. 
    Предположим, однако, что поток 1 по-прежнему получает ввод от RIT и 
вызывает SetFocus, передавая ей описатель окна Е. В этом случае система не 
дает функции что-либо сделать, так как окно, на которое Вы хотите перевести 
фокус, не~исТюл"ьзуёт виртуальную очередь ввода, «подключенную» в данный 
момент к RIT. Когда поток 1 выполнит этот вызов, на экране не произойдет ни 
смены фокуса, ни каких-либо изменений. 

    .Глава 11. Оконные сообщения и асинхронный ввод 341 

    Возьмем другую ситуацию: поток 1 подключен к RIT, а поток 2 вызывает 
SetFocus, передавая ей описатель окна Е. На этот раз значения переменных 
локального состояния ввода потока 2 изменяются так, что — когда RIT направит 
клавиатурные события этому потоку в следующий раз — клавиатурный ввод получит 
окно Е. Этот вызов не заставит RIT направить клавиатурный ввод в виртуальную 
очередь ввода потока 2. 
    Так как фокус окна Е теперь сосредоточен на потоке 2, оно получает 
сообщение WM_SETFOCUS. Если окно Е — кнопка, на нем появляется прямоугольник, 
обозначающий фокус, и в результате на экране могут возникнуть два окна с 
такими прямоугольниками. Сами понимаете, это вряд ли кому понравится. Поэтому 
вызывайте SetFocus с большой осторожностью — чтобы не создавать подобных ситуаций. 
    Кстати, если Вы переведете фокус на окно, которое, получив сообщение 
WM_SETFOCUS, показывает курсор ввода, не исключено одновременное появление на 
экране нескольких окон с таким курсором. Это тоже вряд ли кого обрадует. 
    Когда фокус переводится с одного окна на другое обычным способом 
(например, щелчком окна), теряющее фокус окно получает сообщение WM_KILLFOCUS. 
Если окно, получающее фокус, принадлежит другому потоку, переменные локального 
состояния ввода потока, которому принадлежит окно, теряющее фокус, обновляются 
так, чтобы показать: окон в фокусе нет. И вызов GetFocus возвращает при этом 
NULL, заставляя поток «думать», что окон в фокусе нет. При переносе приложений 
с 16-разрядной Windows на Win32 это приводит к серьезным проблемам, поскольку 
обычно в них не предполагается, что GetFocus может вернуть NULL. 
    Функция SetActiveWindow активизирует в системе окно верхнего уровня: 

    HWND SetActiveWindow(HWND hwnd); 

    В 16-разрядной Windows приложение обычно использует эту функцию для 
активизации другой программы. В Win32 функция SetActiveWindow ведет себя 
аналогично SetFocus. Иначе говоря, если поток вызывает ее с описателем окна, 
созданного другим потоком, система ничего не делает. Но если окно создано тем 
же потоком, система сменяет активное окно. Функцию SetActiveWindow дополняет GetActiveWindow. 

    HWND GetActiveWlndow(HWND hwnd); 

    Она работает, как и GetFocus, но возвращает описатель активного окна, 
указанного в переменных локального состояния ввода вызывающего потока. Так 
что, если активное окно принадлежит другому потоку, функция возвращает NULL. 
    Эти функции в Win32 работают иначе, чем в 16-разрядной Windows: система, 
забирая управление у приложений, передает его пользователю. Microsoft 
полагает, что активизация окон должна происходить по команде пользователя, а 
не программы. 
    Действительно: пользователь, например, запустил длительную операцию в 
приложении А и переключился в приложение В. Выполнив операцию, приложение А 
активизирует свое окно. Захочется ли Вам, чтобы основное окно приложения А 
вдруг «всплыло» поверх окна приложения В в то время, как пользователь работает 
именно с приложением В? Ведь для него это будет полной неожиданностью. Кроме 
того, он может сразу и не заметить, что активным стало окно приложения А, и 
ввести данные не туда, куда нужно, — ничего хорошего из этого не выйдет. 
    И все-таки иногда программам нужно активизировать какие-то окна. 
Приведенные далее функции не только переводят фокус между окнами, но и 
позволяют заставить RIT перенаправить ввод в другой поток. Одна из них — 
SetForegroundWindow— в 16-разрядной Windows отсутствует: 

    BOOL SetForegr-oundWindow(HWND hwnd): 

    342 ______________________________Windows для профессионалов 

    Она активизирует окно, идентифицируемое параметром hwnd, и переводит на 
него фокус независимо от того, какой поток создал это окно. Функцию 
SetForegroundWindow дополняет GetForegroundWindow: 

    BOOL GetForegroundWindow(HWND hwnd): 

    которая возвращает описатель активного в данный момент окна. 
    А теперь несколько слов о функциях, способных изменять Z-порядок окон, их 
статус и фокус: Bring WindowTo Top и SetWindowPos. Первая имеется как в 
16-разрядной Windows, так и в Win32: 

    BOOL BringWindowToTop(HWND hwnd); 

    Если ее вызывает поток активного (foreground) процесса, Win32 активизирует 
указанное окно независимо от того, каким потоком оно было создано. Кроме того, 
Win32 перенаправляет RIT в создавший окно поток и устанавливает фокус на окно, 
регистрируя этот факт в переменных локального состояния ввода потока. Если же 
поток, вызвавший BringWindowToTop, исполняется в фоновом процессе, порядок 
расположения окон не меняется. 
    SetWindowPos позволяет переводить окно как «наверх» (во втором параметре 
передается HWND_TOP), так и на «задний план» (во втором параметре передается HWND_BOTTOM): 

    BOOL SetWindowPos(HWND hwnd, HWND hwndInsertAfter. 
    int x, int y, int ex, int cy, UINT fuFlags): 

    (Кстати, функция BringWindowToTop внутренне реализуется как вызов 
SetWindowPos со вторым параметром, равным HWND_TOP.) 
    Другой аспект управления клавиатурой и локальным состоянием ввода связан с 
массивом синхронного состояния клавиш. Переменные локального состояния ввода 
каждою потока включают в себя массив синхронного состояния клавиш, но все 
потоки совместно используют один массив асинхронного состояния клавиш. Эти 
массивы отражают состояние всех клавиш на данный момент. Функция 
GetAsyncKeyState применяется для определе ния того, нажата ли сейчас заданная клавиша: 

    SHORT GetAsyncKeyState(int nVirtKey); 

    Параметр nVirtKey задает код виртуальной клавиши, состояние которой нужно 
про верить. Старший бит результата определяет, нажата в данный момент клавиша 
(1) или не-i (0). Я часто пользовался этой функцией, определяя при обработке 
сообщения, отпустил ли пользователь основную (обычно левую) кнопку мыши. 
Передав значение VK_LBUTTON, я ждал, когда обнулится старший бит. В Win32 
функция несколько изменилась и всегда возвращает 0 (не нажата), если ее 
вызывает другой поток, а не тот, который создал окно, на ходящееся сейчас в 
фокусе ввода. 
    Функция GetKeyState отличается от GetAsyncKeyState тем, что возвращает 
состояние клавиатуры на момент, когда из очереди потока извлечено последнее 
клавиатурное сообщение: 

    SHORT GetKeyState(int nVirtKey); 

    Эту функцию можно вызвать в любой момент; ей все равно, какое окно в 
фокусе. Подробнее о массивах состояния клавиш и связанных с ними функциях см. 
в книге Рихтера (Richter) и Лока (Locke) Windows 95: A Developer's Guide, 1995 
(глава 8 «Keystroke Processing»}." 

    Управление курсором мыши 

    В концепцию локального состояния ввода входит и управление состоянием 
курсора мыши. Поскольку мышь, как и клавиатура, должна быть доступна всем 
потокам, Win32 не позволяет какому-то одному потоку монопольно распоряжаться 
курсором мыши, изменяя его форму или ограничивая область его перемещения. 
Рассмотрим, как система управляет этим курсором. 

    Глава 11. Оконные сообщения и асинхронный ввод 343 

    Один из аспектов управления курсором мыши заключается в его показе или 
гашейии. Допустим, программа 16-разрядной Windows вызывает ShowCursor (FALSE), 
скрывая курсор, но потом «забывает» вызвать ShowCursor (TRUE), и пользователь 
не может работать с мышью в другой программе. 
    Win32 такого не допускает. Система скрывает курсор, когда он оказывается 
над окном, созданным потоком, вызвавшим ShowCursor (FALSE), и показывает его 
всякий раз, когда он располагается над окном, созданным другим потоком. 
    Другой аспект управления курсором мыши — возможность ограничить его 
перемещение каким-либо прямоугольным участком. В 16-разрядной Windows это 
делают вызовом ClipCursor. 

    BOOL ClipCursor-CCONST RECT *lprc); 

    Эта функция ограничивает перемещение курсора мыши прямоугольником, на 
который указывает параметр Iprc. И опять та же проблема, когда приложение по 
идее не должно бы ограничивать перемещение курсора по экрану. В Win32 эта 
проблема решается так. Система разрешает программе ограничить перемещение 
курсора заданным прямоугольником. Затем, если возникает событие асинхронной 
активизации (asynchronous activation event), т. е. если пользователь 
переключается в окно другого приложения, нажимает клавиши Ctrl+Esc или поток 
вызывает SetForegroundWindow, система перестает ограничивать перемещение 
курсора, позволяя ему свободно перемещаться по экрану. 
    В чем же суть захвата мыши? Захватывая мышь (вызовом SetCapture), окно 
требует, чтобы все связанные с мышью сообщения RIT отправлял в виртуальную 
очередь ввода вызывающего потока, а из нее — установившему захват окну до тех 
пор, пока программа не вызовет ReleaseCapture. 
    Если приложение под управлением 16-разрядной Windows вызывает SetCapture и 
впоследствии забывает вызвать ReleaseCapture, то сообщения от мыши не будут 
направлены другим окнам. И вновь возникает ситуация, которая для Win32 
неприемлема, но исправить ее уже сложнее. При вызове программой SetCapture 
поток RIT получает указание помещать все «мышиные» сообщения в виртуальную 
очередь ввода вызвавшего потока. При этом SetCapture соответственно 
настраивает переменные локального состояния ввода данного потока. 
    Обычно приложение вызывает SetCapture, когда пользователь нажимает кнопку 
мыши. Но поток имеет право вызывать эту функцию, даже если нажатия не было. 
Если SetCapture вызывается при нажатой кнопке, захват действует для~всей 
системы, совсем как в 16-разрядной Windows. Как только система определяет, что 
все кнопки мыши отпущены, RIT перестает направлять сообщения от мыши 
исключительно в виртуальную очередь ввода данного потока. Вместо этого он 
передает сообщения в очередь ввода, связанную с окном, «поверх» которого 
находится курсор в данный момент. И это нормальное поведение 
системы7к'ог'да^ах5ат мыши не установлен. 
    Однако вызвавшему SetCapture потоку «кажется», что режим захвата мыши 
попрежнему действует. Всякий раз, когда курсор оказывается на любом из окон, 
созданных установившим захват потоком, сообщения от мыши направляются в окно, 
применительно к которому этот захват и установлен. Другими словами, когда 
пользователь отпустит все кнопки мыши, захват осуществляется на уровне лишь 
данного потока, а не всей системы. 
    Кроме того, если пользователь попытается активизировать окно, созданное 
другим потоком, система автоматически отправит установившему захват потоку 
сообщения о нажатии и отжатии кнопок мыши. Затем она изменит переменные 
локального состояния ввода потока, чтобы отразить тот факт, что поток более не 
работает в режиме захвата. Словом, Microsoft считает, что захват мыши чаще 
всего применяется для выполнения операций типа 

    344 ______________________________Windows для профессионалов 

    click-and-drag (щелкнуть-и-перетащить). Если захват мыши Вам нужен для 
других операций, боюсь, придется немало поэкспериментировать, чтобы 
разобраться, какие изменения произошли в Win32 по сравнению с 16-разрядной Windows. 
    Последняя переменная локального состояния ввода, связанная с мышью, — 
форма курсора. Всякий раз, когда поток вызывает SetCursor для изменения формы 
курсора, переменные локального состояния ввода соответствующим образом 
обновляются. Иначе говоря, переменные локального состояния ввода всегда 
запоминают последнюю форму курсора, установленную потоком. 
    Допустим, пользователь перемещает курсор мыши на окно Вашей программы, 
окно получает сообщение WM_SETCURSOR, и Вы вызываете SetCursor, чтобы 
преобразовать курсор в «песочные часы». Вызвав SetCursor, программа начинает 
выполнять какую-то длительную операцию. (Бесконечный цикл — лучший пример 
длительной операции. Шучу.) Далее пользователь уводит курсор из окна Вашей 
программы в окно другого приложения. В 16-разрядной Windows форма курсора не 
меняется, а в Windows 95 и Windows NT она может быть изменена процедурой 
другого окна. 
    Чтобы один поток изменил форму курсора, пока другой занят длительной 
операцией, переменные локального состояния ввода не нужны. Но переведем курсор 
обратно в то окно, поток которого по-прежнему занят обработкой. Системе 
«хочется» послать окну сообщения WM_SETCURSOR, но окно не может выбрать их из 
очереди, так как его поток продолжает свою операцию. Тогда система определяет, 
какая форма была у курсора в прошлый раз (информация об этом содержится в 
переменных локального состояния ввода данного потока), и автоматически 
восстанавливает ее (в нашем примере — «песочные часы»). Теперь пользователю 
четко видно, что в этом окне работа еще не закончена и придется подождать. 

    Программа-пример LISLab 

    Программа LISLab (Local Input State Laboratory, LISLAB.EXE) (см. листинг 
на рис. 11-7) — своеобразная лаборатория, в которой Вы сможете 
поэкспериментировать с локальным со стоянием ввода. Файлы исходного кода и 
ресурсов для этой программы находятся в каталоге LISLAB на компакт-диске, 
прилагаемом к книге. 
    Запустите приложение Notepad, потом — LISLab, и Вы увидите такое 
диалоговое окно: 
    В левом верхнем углу окна — раздел Windows (окна); его поля обновляются 
раз в секунду, т. е. каждую секунду диалоговое окно получает сообщение 
WM_TIMER и в ответ 

    ____________Глава 11. Оконные сообщения и асинхронный ввод 345 

    вызывает GetFocus, GetActiveWindow, GetForegroundWindow, GetCapture и 
GetClipCursor. Первые четыре функции возвращают описатели окна (считываемые из 
переменных локального состояния ввода потока моей программы), через которые я 
могу определить имя класса и заголовок окна и вывести эту информацию на экран. 
    Если я активизирую другое приложение (тот же Notepad), названия полей 
Focus (в фокусе) и Active (активное) меняются на (No Window) [(окна нет)], а 
поля Foreground (приоритетное) — на [Notepad] Unfilled - Notepad. Обратите 
внимание, что активизация Notepad заставляет LISLab считать, что нет ни 
активных, ни находящихся в фокусе окон. 
    Теперь поэкспериментируем со сменой фокуса. Выберем SetFocus в 
комбинированном списке Function (функция) — в правом верхнем углу диалогового 
окна. Затем в поле Delay (задержка) введем время (в секундах), в течение 
которого LISLab будет ждать, прежде чем вызвать SetFocus. В данном случае, 
видимо, лучше установить нулевое время задержки. Позже я объясню, как 
используется поле Delay. 
    Выберем QKHO (описатель которого Вы хотите передать функции SetFocus) в 
списке Notepad Windows And Self, расположенном в левой части диалогового окна. 
Для эксперимента~возьмйтё строку [Notepad] Untitled - Notepad. Теперь все 
готово к вызову SetFocus. Щелкните кнопку Delay и понаблюдайте, что произойдет 
в разделе Windows. Ничего. Система отказалась менять фокус. 
    Если Вы действительно хотите перевести фокус на Notepad, щелкните кнопку 
Attach То Notepad (подключить к Notepad), что заставит LISLab вызвать: 

    AttachThreadInput(GetWindowThreadProcessId(g_hwndNotepad, NULL), 
GetCurrentThreadIdO. TRUE); 

    В результате этого вызова поток LISLab станет использовать ту же 
виртуальную очередь ввода, что и Notepad. Кроме того, поток LISLab «разделит» 
переменные локального состояния ввода Notepad. 
    Если после щелчка кнопки Attach To Notepad Вы щелкнете окно Notepad, 
диалоговое окно LISLab примет следующий вид: 
    Теперь, когда очереди ввода соединены друг с другом, LISLab способен 
отслеживать изменения фокуса, происходящие в Notepad. В приведенном диалоговом 
окне показано, что в данный момент фокус установлен на поле ввода. А если мы 
откроем в Notepad диалоговое окно File Open, то LISLab, продолжая следить за 
Notepad, покажет нам, какое окно Notepad получило фокус ввода, какое окно 
активно и т. д. 

    346 ______________________________Windows для профессионалов 

    Теперь можно вернуться в LISLab, щелкнуть кнопку Delay и вновь попытаться 
заставить SetFocus перевести фокус на Notepad. На этот раз все пройдет 
успешно, потому что очереди ввода соединены. 
    Ну а далее, если хотите, поэкспериментируйте с SetActiveWindow, 
SetForegroundWindow, BringWindowToTop и SetWindowPos, выбирая нужную функцию в 
списке Function. Попробуйте вызывать их и когда очереди соединены, и когда они 
разъединены; при этом обращайте внимание на различия в поведении функций. 
    Теперь поясню, зачем предусмотрена задержка. (Она заставляет LISLab 
вызывать указанную функцию только по истечении заданного числа секунд.) Для 
иллюстрации возьмем такой пример. Но прежде отключите LISLab от Notepad, 
щелкнув кнопку Detach From Notepad (отключить от Notepad). Затем в списке 
Notepad Windows And Self выберите —>This Dialog Box<—, а в списке Function — 
SetFocus и установите задержку на 10 секунд. Наконец «нажмите» кнопку Delay и 
щелкните окно Notepad, чтобы оно стало активным. Вы должны активизировать 
Notepad до того, как истекут заданные 10 секунд. 
    Пока идет отсчет времени задержки, справа от счетчика секунд высвечивается 
слово Pending (ожидание). По истечении 10 секунд слово Pending меняется на 
Executed (выполнено), и появляется результат вызова функции. Если Вы 
внимательно следите за работой программы, то увидите, что фокус теперь 
установлен на окно списка Function. Но ввод с клавиатуры по-прежнему 
направляется в Notepad. Таким образом, и поток LISLab, и поток Notepad — оба 
«считают», что в фокусе находится одно из их окон. Но на самом деле RIT 
остается «подключен» к потоку Notepad. 
    И последнее замечание в этой связи: SetFocus и SetActive Window возвращают 
описатель окна, которое находилось в фокусе или было активным до вызова 
функции. Информация об этом окне отображается в поле PrevWnd (предыдущее 
окно). Кроме того, непосредствен но перед вызовом SetForegroundWindow 
программа обращается к GetForegroundWindow, чтобы получить описатель окна, 
находящегося «поверх» остальных окон. Эта информация так же отображается в 
поле PrevWnd. 
    А сейчас поэкспериментируем с курсором мыши. Всякий раз, когда курсор 
проходи! над диалоговым окном LISLab (но не над каким-либо из его дочерних 
окон), он изобража ется в виде вертикальной стрелки. По мере поступления 
диалоговому окну сообщения от мыши добавляются к списку Mouse Messages 
Received (сообщения, полученные от мыши). Таким образом, Вы всегда в курсе 
того, когда диалоговое окно получает сообщения от мыши. Сдвинув курсор за 
пределы основного окна или поместив его на одно из дочерних окон, Вы увидите, 
что сообщения больше не включаются в список. 
    Теперь переместите курсор в правую часть диалогового окна, установив его 
над тек стом Click Right Mouse Button To Set Capture (щелкните правой кнопкой, 
чтобы установить захват), а затем нажмите и удерживайте правую кнопку мыши. 
После этого программа LISLab вызовет функцию SetCapture, передав ей описатель 
своего диалогового окна. Заметьте: факт захвата мыши программой LISLab 
отразится в разделе Windows. 
    Далее, не отпуская правой кнопки, проведите курсор над дочерними окнами 
LISLab и понаблюдайте за сообщениями от мыши, добавляемыми к списку. Если 
курсор выведен за пределы диалогового окна LISLab, программа по-прежнему 
получает сообщения от мыши. Курсор сохраняет форму вертикальной стрелки 
независимо от того, над каким участком экрана Вы его перемещаете. Именно так 
работает захват мыши в 16-разрядной Windows. 
    Теперь можно понаблюдать одно из отличий Win32-cHcreMbi от 16-разрядной 
Windows. Отпустите правую кнопку и следите, что произойдет. Окно, в свое время 
захватившее мышь, показывает, что программа по-прежнему считает мышь 
захваченной. Но сдвиньте курсор за пределы диалогового окна LISLab, и он 
больше не останется вертикальной стрелкой, а сообщения от мыши перестанут 
появляться в списке Mouse Messages Received. Установив же курсор на какое-либо 
из дочерних окон LISLab, Вы сразу увидите: захват 

    ______________Глава 11. Оконные сообщения и асинхронный ввод 347 

    по-прежнему действует, потому что все эти окна используют один набор 
переменных локального состояния ввода. Такое поведение кардинально отличается 
от поведения 16разрядной Windows. 
    Закончив эксперименты, отключите режим захвата одним из способов: 
    • двойным щелчком правой кнопки мыши в любом месте диалогового окна LISLab 
(чтобы программа вызвала функцию ReleaseCapture); 
    • щелчком окна, созданного любым другим потоком (отличным от потока 
LISLab). В этом случае система автоматически передаст диалоговому окну LISLab 
сообщение о нажатии и отпускании кнопки мыши. 
    Какой бы способ Вы ни выбрали, обратите внимание на поле Capture (захват) 
в разделе Windows — теперь оно отражает тот факт, что больше ни одно окно не 
захватывает мышь. И еще два эксперимента, связанных с мышью: один — ограничить 
поле перемещения курсора заданным прямоугольником, другой — изменить 
«видимость» курсора. Если Вы щелкнете кнопку Top, Left (верхняя левая четверть 
экрана), программа LISLab исполнит код: 

    RECT гс: 
    SetRect(&rc, О, О, 
    GetSystemMetrics(SM_CXSCREEN) / 2, 
    GetSystemMetrics(SM_CYSCREEN) / 2); 

    Это ограничит поле перемещения курсора верхней левой четвертью экрана. 
Переключившись в окно другого приложения нажатием клавиш Alt+Tab, Вы заметите, 
что ограничение по-прежнему действует. Но система снимет его автоматически в 
результате одного из таких действий: 

    Windows 95     Щелчка заголовка окна другого приложения и последующего 
перемещения этого окна. 
    Windows NT    Щелчка заголовка окна другого приложения (последующего 
перемещения этого окна не требуется.) 
    Windows NT    Активизации и последующей отмены Task Manager. 

    Для снятия ограничения на перемещение курсора можно также щелкнуть кнопку 
Remove (удалить) в диалоговом окне LISLab (если эта кнопка в пределах текущего 
поля перемещения курсора). 
    Щелчок кнопки Hide (скрыть) или Show (показать) вызывает исполнение кода: 

    ShowCursor(FALSE); 

    или 

    ShowCursor(TRUE); 

    Когда курсор скрыт, его не видно при перемещении над диалоговым окном 
LISLab. Но как только курсор оказывается за пределами окна, он снова видим. 
Для нейтрализации действия кнопки Hide используйте кнопку Show. Заметьте, что 
скрытие курсора носит кумулятивный характер: 5 раз щелкнув кнопку Hide, 
придется столько же раз щелкнуть кнопку Show, прежде чем курсор станет видимым. 
    И последний эксперимент — с кнопкой Infinite Loop (бесконечный цикл). При 
ее «нажатии» исполняется код: 

    SetCursor(LoadCursor(NULL, IDC_NO)); 
    for (; ;) 

    348 ______________________________Windows для профессионалов 

    Первая строка меняет форму курсора на перечеркнутый круг, а вторая 
исполняет бесконечный цикл. После щелчка кнопки Infinite Loop программа 
перестает реагировать на какойлибо ввод. Переместив курсор в пределы 
диалогового окна LISLab, Вы увидите, что курсор остается перечеркнутым кругом. 
Если же сместить его в другое окно, он получит форму, заданную в текущем окне. 
    Если теперь вернуть курсор в диалоговое окно LISLab, система обнаружит, 
что программа не отвечает, и автоматическм восстановит прежнюю форму курсора — 
перечеркнутый круг. В 16-разрядной Windows приложение, вошедшее в бесконечный 
цикл, «подвешивает» не только себя, но и всю систему. А в Win32 бесконечный 
цикл — лишь незначительное неудобство для пользователя. 
    Заметьте: если Вы закроете окно зависшей программы LISLab другим окном, а 
затем уберете его, система отправит LISLab сообщение WM_PAINT и обнаружит, что 
данный поток не отвечает. Из этой затруднительной ситуации система выходит 
очень просто: перерисовывает окно нереагирующего приложения. Конечно, 
перерисовать окно правильно она не в состоянии, так как ей неизвестно, чем 
собиралось заняться приложение, и поэтому она просто затирает окно цветом фона 
и перерисовывает рамку его окна. 
    Проблема теперь в том, что на экране есть ни на что не отвечающее окно. 
Как от него избавиться? Под управлением Windows 95 нужно сначала нажать 
клавиши Ctrl+Alt+Del, чтобы на экране появилось окно Close Program (Завершение 
работы программы). 
    В Windows NT можно либо щелкнуть правой кнопкой мыши кнопку приложения на 
панели задач, либо открыть окно Task Manager: 
    Затем следует выбрать из списка название программы, которую нужно 
завершить, — в данном случае Local Input State Lab — и щелкнуть кнопку End 
Task. Система попытается завершить LISLab «по-хорошему», но обнаружит, что 
приложение не отвечает. Это заставит ее вывести окно: 

    _Глава 11. Оконные сообщения и асинхронный ввод 349 

    Если Вы выберете кнопку End Task и в этом окне, система завершит LISLab 
принудительно. Кнопка Cancel сообщит системе, что Вы передумали завершать 
приложение, а выбор кнопки Wait (только в Windows NT) отложит принудительное 
завершение; пользуйтесь этой кнопкой, только если Вы надеетесь, что в течение 
5 секунд программа снова начнет отвечать на ввод. Но нам известно, что LISLab 
не станет на что-то реагировать, поскольку бесконечный цикл никогда не 
завершится. Так что «нажмите» кнопку End Task, чтобы удалить LISLab из системы. 
    Общий смысл этих экспериментов — продемонстрировать отказоустойчивость 
системы. Ни одно приложение практически неспособно привести систему в такое 
состояние, когда работа с другими приложениями станет невозможной. Кроме того, 
и Windows 95, и Windows NT автоматически освобождают все ресурсы, выделявшиеся 
потокам завершенного процесса, — утечки памяти не происходит! 

    LISLAB.C 
    Модуль: LISLab.С Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    ttinclude ". . \Cn)nHdr, H"           /* см. приложение В */ 
    ftinclude <windows.h> 
    fflnclude <windowsx.h> 
    #include <tchar.h> 
    flinclude <string.h> 
    ftinclude <stdio.h>               // для sprintf 
    #include "Resource.H" 
    //////////////////////////
    ftdefine TIMER_DELAY (1 * 1000) // 1 секунда * 1000 миллисекунд 
    UINT  g_uTimerId = 1; 
    int   g_nEventId = 0; 
    DWORD g_dwEventTime = 0; 
    RWND  g_hwndSubject = NULL: 

    см. след. стр. 

    Рис. 11-7. Листинг программы LISLab 

    350 ______________________________Windows для профессионалов 


    РИС. 11-7  (продолжение) 

    HWND  g_hwndNotepad = NULL; 
    //////////////////////////////л.//.'////////////////////У///////,////////У///,/,1 
    void CalcWndText (HWND hwnd, LPTSTR szBuf, int nLen) { 
    TCHAR szClass[50], szCaption[50], szBufT[150]; 
    if (hwnd == (HWND) NULL) { 
    _tcscpy(szBuf. __TEXT("(no window)")); 
    return: 
    } 
    if (!IsWinaow(hwnd)) { 
    _tcscpy(szBur, __TEXT("(invalid window)")); 
    return; 
    } 
    GetClassName(hwnd, szClass, chDIMOF(szClass)); 
    GetWindowText(hwnd, szCaption, chDIMOF(szCaption)): 
    _stprintr(szBurT, __TEXT("[%s] %s"), (LPTSTR) szClass, 
    (*szCaption == 0) ? (LPTSTR) __TEXT("(no caption)") 
    : (LPTSTR) szCaption); 
    _tcsncpy(szBuf, szBufT, nLen - 1); 
    szBuf[nLen - 1] = 0; // принудительно завершаем строку нулевым символом 
    \ 
    // для уменьшения используемого размера стека, один экземпляр 
    // WALKWINDOWTREEDATA создаем как локальную переменную в 
    // WalkWindowTree() и указатель на нее передаем WalkWindowTreeRecurse 
    // данные, используемые WalkWindowTreeRecurse 
    typedef struct { 
    HWND  hwndLB;          // описатель окна списка для вывода 
    HWND  hwndParent;      // описатель родительского окна 
    int   nLevel;          // глубина рекурсии 
    int   nindex;          // индекс элемента списка 
    TCHAR szBuf[100];      // буфер вывода 
    int   iBuf;            // индекс в szBuf 
    } WALKWINDOWTREEDATA, .LPWALKWINDOWTREEDATA; 
    void WalkWindowTreeRecurse (LPWALKWINDOWTREEDATA pWWT) { 
    const int nIndexAmount = 2; 
    HWND hwndGhild; 
    pWWT->nLevel++: 

    см. след. стр. 

    _____________Глава 11. Оконные сообщения и асинхронный ввод 351 


    РИС. 11-7  (продолжение) 

    if (!IsWlndow(pWWT->hwndParent)) 
    return; 
    for (pWWT->iBuf = 0; pWWT->iBuf < pWWT->nLevel * nIndexAmount; pWWT->iBuf++) 
    pWWT->szBuf[pWWT->iBuf] = __TEXT(' •); 
    CalcWndText(pWWT->hwhdParent, &pWWT->szBuf[pWWT->iBuf]. 
    cnDIMOF(pWWT->szBuf) - pWWT->iBuf); 
    pWWT->n!ndex = ListBox_AddString(pWWT->hwndLB, pWWT->szBuf); 
    ListBox_SetItemData(pWWT->hwndLB, pWWT->nIndex, pWWT->hwndParent); 
    hwndChild = GetFirstChild(pWWT->hwndParent); 
    while (hwndChlld != NULL) { 
    pWWT->hwndParent = hwndChild; 
    WalkWindowTreeRecurse(pWWT): 
    hwndChild = GetNextSibling(hwndChiid): 
    } 
    pWWT->nLevel--: 
    void WalkWindowTree (HWNU liwndLB, HWND hwndParent) { 
    WALKWINDOWTREEDATA WWT; 
    WWT.hwndLB = hwndLB; 
    WWT.hwndParent = hwndParent; 
    WWT.nLevel = -1; 
    WalkWindowTreeRecurse(&WWT); 
    } 
    BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { 
    HWND hwndT: 
    // связываем значок с диалрговым окном 
    chSETDLGICONS(hwnd, IDI_LISLAB, IDI_LISLAB); 
    // связываем курсор в форме "стрелка вверх" с клиентской областью 
    // диалогового окна 
    SetClassLong(hwnd, GCL_HCURSOR, (LONG) LoadCursor(NULL, IDC_UPARROW))• 
    g_uTimerId = SetTimer(hwnd, g_uTimerId, TIMER_DELAY, NULL); 
    hwndT = Get01gltem(hwnd, IDC_WNDFUNC); 
    ComboBox^AddString(hwndT, __TEXT("Set Focus")): 

    iM. uieo. imp. 

    352 ____________________________Windows для профессионалов 


    РИС. 11-7  (продолжение) 

    ComboBox_AddString(hwndT, __TEXT("SetActiveWindow")); 
    ComboBox_AddSt ring(hwndT, __TEXT("SetForegroundWnd")); 
    ComboBox_AddString(hwndT, __TEXT("BringWindowToTop")); 
    ComboBox_AddString(hwndT, __TEXT("SetWindowPos-TOP")); 
    ComboBox_AddSt ring(hwndT, __TEXT("SetWindowPos-BTM")): 
    CornboBox_SetCurSel(hwndT, 0); 
    // заполняем список окон заголовками нашего окна и окон Explorer 
    
    // сначала свое диалоговое окно 
    hwndT = GetDlgItem(hwnd, IDC_WNDS); 
    ListBox_AddString(hwndT, __ТЕХТ(""> This dialog box <--")); 
    \      ListBox_SetItemData(hwndT, 0, hwnd); 
    ListBox_SetCurSel(hwndT, O): 
    // теперь окна Notepad 
    g_hwndNotepad = FindWindow(__TEXT("Notepad"), NULL); 
    WalkWindowTree(hwndT, g_hwndNotepad); 
    return(TRUE); 
    } 
    //////////////////////////////////////////////////////// 
    void Dlg_OnDestroy (HWND hwnd) { 
    if (g_uTimer!d != 0) 
    KillTi]ner(hwnd, g_uTimerId); 
    } 
    l!llilllllllillllllllllllllltllll//ll/ll/liili///liltli/l///:i /i /-• 
/.''/,'/'/ // '. 
    void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtl. UINT codeNotify) { 
    HWND hwndT; 
    RECT re; 
    switch (id) { 
    case IDCANCEL: 
    EndDialog(hwnd, D); 
    break; 
    case IDC_FUNCSTART: 
    g_dwEventTime = GetTickGountO + 10DD * 
    GetDigItemInt(hwnd. IDC_DELAY, NULL, FALSE); 
    hwndT = GetDlgItem(hwnd, IDC_WNDS); 
    g_hwndSubject = (HWND) ListBox_GetItemData(hwndT, 
    ListBox_GetCurSel(hwndT)); 
    g_nEvent!d = ComboBox_GetCurSel(GetDlgItem(hwnd, IDCJilNDFUNC)); 
    SetWindowText(GetDlgItem(hwnd, IDC_EVENTPENDING), __TEXT("Pending")); 
    break; 

    [..ч. L.-iri). стр. 


    _____________Глава 11. Оконные сообщения и асинхронный ввод 353 


    РИС. 11-7   (продолжение) 

    case IDC_THREADATTACH; 
    AttachThreadInput(GetWlndowTrireadProcessId(g_hwndNotepad. NULL), 
    GetCurrentTI-ireadIdO, TRUE); 
    break; 
    case IDC_THREADDETACH: 
    AttachThread Input(GetWindowThread Process Id(g_hwndNote pad. NULL), 
    GetCurrentThreadIdO, FALSE); 
    break; 
    case IDC_SETCLIPRECT: 
    SetRect(&rc, О, О, 
    GetSystemMetrics(SM_CXSCREEN) / 2, 
    GetSystemMetrics(SM_CYSCREEN) / 2); 
    ClipCursor(&rc); 
    break; 
    case IDC_REMOVECLIPRECT: 
    ClipCursor(NULL); 
    break; 
    case IDC_HIDECURSOR: 
    ShowCursor(FALSE); 
    break; 
    case IDC_SHOWCURSOR: 
    ShowCursor(TRUE); 
    break: 
    case IDC_INFINITELOOP: 
    SetCursor(LoadCursor(NULL, IDC_NO)); 
    for (:;) 
    break; 
    void AddStr (HWNO hwndLB, LPCTSTR szBuf) { 
    int nindex; 
    do { 
    nindex = ListBox_AddString(hwndLB, szBuf); 
    if (nindex == LB_ERR) 
    ListBox_DeleteString(hwndLB, 0); 
    } while (nindex == LB_ERR); 

    см. след. стр. 

    354 ______________________________Windows для профессионалов 


    РИС. 11-7   (продолжение) 

    ListBox_SetCurSel(hwndLB, nindex); 
    i i 
    int Dlg_OnRButtonDown (HWND hwnd, BOOL fDoubleClick, 
    int x, int y, UINT keyFlags) { 
    TCHAR szBuf[100]; 
    _stprintf(szBuf, 
    __TEXT("Capture=%-3s. Msg=RButtonDown, ") 
    __TEXT("DblClk=%-3s, x=%5d. y=%5d"), 
    (GetCaptureO == NULL) ? __TEXT("No") : __TEXT("Yes"), 
    fDoubleClick ? __TEXT("Yes") : __TEXT("No"), x, y); 
    AddStr(GetDlgItem(hwnd. IDC_MOUSEMSGS), szBuf); 
    if (!fDoubleClick) { 
    SetCapture(hwnd); 
    } else { 
    ReleaseCapture(): 
    } return(O); 
    } 
    ///////////////////////////////// 
    int Dlg_OnRButtonUp (HWND hwnd, int x, int y, UINT keyFlags) { 
    TCHAR szBuf[100]; 
    _stprintf(szBuf, __TEXT("Capture=%-3s, Msg=RButtonUp,   x=%5d, y=%5d"), 
    (GetCaptureO == NULL) ? __TEXT("No") : __TEXT("Yes"). x. y); 
    AddStr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf): 
    return(O); 
    } 
    //////////////////////////////////
    int Dlg_OnLButtonDown (HWND hwnd, BOOL fDoubleClick. 
    int x, int y, UINT keyFlags) { 
    TCHAR szBuf[100]; 
    _stprintf(szBuf, __TEXT("Capture=%-3s, Msg=LButtonDown, ") 
    __TEXT("DblClk=%-3s, x=%5d, y=%5d"), 
    (GetCaptureO == NULL) ? __TEXT("No") : __TEXT("Yes"), 
    fDoubleClick ? __TEXT("Yes") : __TEXT("No"), x, y): 

    см. след. стр. 

    _____________Глава 11. Оконные сообщения и асинхронный ввод 355 


    РИС. 11-7   (продолжение) 

    Add8tr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf); 
    return(O); 
    } 
    void Dlg_OnLButtonUp (HWND hwnd, int x, int y, UINT keyFlags) { 
    TCHAR szBuf[100]; 
    _stprintf(szBuf, __TEXT("Gapture=%-3s, Msg=LButtonUp,   x=%5d, y=%5d"), 
    (GetCaptureO == NULL) ? __TEXT("No") : „TEXT ("Yes"), x, y); 
    Add8tr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf); 
    } 
    /////////////////////////////////
    void Dlg_OnMouseMove (HWND hwnd, int x, int y, UINT keyFlags) { 
    TCHAR szBuf[100]: 
    _stprintf(szBuf, __TEXT("Capture=%-3s, Msg=MouseMove, x=%5d, y=%5d"), 
    (GetCaptureO == NULL) ? __TEXT("No") ; _„TEXT("Yes"), x, y); 
    AddStr(GetDlgItem(hwnd, IDC_MOUSEMSGS), szBuf); 
    } 
    void Dlg_OnTimer (HWNO hwnd, UINT id) { 
    TCHAR szBuf[100]; 
    RECT re; 
    HWND hwndT: 
    CalcWndText(GetFocus(), szBuf, chDIMOF(szBuf)); 
    SetWindowText(GetDlgItem(hwnd, IDC_WNDFOCUS), szBuf); 
    CalcWndText(GetCapture(), szBuf, chDIMOF(szBuf)); 
    SetWindowText(GetDlgItem(hwnd, IDC_WNDCAPTURE), szBuf): 
    CalcWndText(GetActiveWindow(), szBuf, chDIMOF(szBuf)); 
    SetWindowText(GetDlgItem(hwnd, IDC_WNDACTIVE), szBuf); 
    CalcWndText(GetForegroundWindow(), szBuf, chDIMOF(szBuf)); 
    SetWindowText(GetDlgItem(hwnd, IDC_WNDFOREGROUND), szBuf): 
    GetClipCursor(&rc): 
    _stprintf(szBuf, 
    __TEXT("left=%d, top=%d, right=%d, bottom=%d"), 
    rc.left, re.top, re.right, re.bottom); 

    см. слео. if tip. 

    356 ______________________________Windows для профессионалов 

    РИС. 11-7  (продолжение) 


    SetWindowText(GetDlgItem(hwnd, IDC_CLIPCURSOR), szBuf); 
    if ((g_dwEventTime ==0) !! (GetTickCount() < g_dwEventTime)) 
    return; 
    switch (g_nEventId) { 
    case 0: // SetFocus 
    g_hwnd8ubject = SetFocus(g_hwndSubject); 
    break; 
    case 1: // SetActiveWlndow 
    g_hwndSubject = SetActiveWindow(g_hwndSubject); 
    break; 
    case 2: // SetForegroundWindow 
    hwndT = GetForegroundWindowO; 
    SetForegroundWindow(g_hwndSubject); 
    g_hwnd3ubject = hwndT; 
    break; 
    case 3: // BringWindowToTop 
    ВringWindowToTop(g_hwndSubject); 
    break; 
    case 4: // SetWindowPos с передачей HWND_TOP 
    SetWindowPos(g_hwndSubject, HWNDJTOP, О, О, О. О, 
    SWP_NOMOVE ; SWP_NOSIZE); 
    g_hwnd3ubject = (HWND) 1; 
    break: 
    case 5; // SetWindowPos с передачей HWND_BOTTOM 
    SetWindowPos(g_hwndSubject. HWND_BOTTOM, О, О, О, О, 
    SWP_NOMOVE ; SWP_NOSIZE); 
    g_hwndSubject = (HWND) 1; 
    break: 
    \, 
    i 
    if (g_hwndSubject == (HWND) 1) { 
    SetWindowText(GetDlgItem(hwnd, IDC_PREVWND), __TEXT("Can't tell.")); 
    } else { 
    CalcWndText(g_hwndSubject, szBuf, chDIMOF(szBuf)); 
    SetWindowText(GetDlgItem(hwnd, IDC_PREVWND), szBuf); 
    } 
    g_hwnd8ubject = NULL; g_nEvent!d = 0; g_dwEventTime = 0; 
    SetWindowText(GetDlgItem(hwnd, IDC_EVENTPENDING), __TEXT("Executed")); 
    } 
    ///////////////////////////////////////////////////// 

    см. след. стр. 

    _____________Глава 11.  Оконные сообщения и асинхронный ввод 357 


    РИС. 11-7  (продолжение) 

    BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM 
IParam) { 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_0nlnit0ialog); 
    chHANDLE_DLGMSG(hwnd, WM_OESTROY, Dlg_OnDestroy); 
    GhHANDLE_DLGMSG(hwnd, WM_COMMANO, Dlg_OnCommand); 
    chHANDLE_DLQMSG(hwnd, WM_MOUSEMOVE, Dlg_OnMouseMove): 
    chHANDLE_DLGMSG(hwnd. WM_LBUTTONDOWN, 01g_OnLButtonDown); 
    chHANDLE_DLGMSG(hwnd, WM_LBUTTONDBLCLK, Dlg_OnLButtonDown); 
    chHANDLE_DLGMSG(hwnd, WM_LBUTTONUP, 01g_OnLButtonUp); 
    chHANDLE_DLGMSG(hwnd, WM_RBUTTONDOWN, Dlg_OnRButtonDown); 
    GhHANDLE_DLGHSG(hwnd, WM_RBUTTONDBLCLK, Dlg_OnRButtonOown); 
    chHANDLE_DLGMSG(hwnd, WM_RBUTTONUP, Dlg_OnRButtonUp); 
    chHANDLE_DLGMSG(hwnd, WM_TIMER, Dlg_OnTimer); 
    } return(FALSE); 
    \ / 
    ////////////// 
    int WINAPI _tWinMain (HINSTANCE hinstExe. 
    HINSTANCE hinstPrev. LPTSTR pszCmdLine, int nCmdShow) { 
    chWARNIFUNICODEUNDERWIN95(); 
    DialogBox(hinstExe, MAKEINTRESOURCE(IDD_LISLAB), NULL, Dlg_Proc); 
    return(O); 
    } 
    ////////////////////////////////// конец файла 
