Новые книги

Колонки и статьи Алексея Федорчука, печатавшиеся в журнале LinuxFormat на протяжении 2006-2013 годов, собранные в хронологическом порядке. Они посвящёны UNIX, Linux и другим UNIX-подобным системам, их приложениям, а также идеологическим вопросам Свободного и Открытого Программного Обеспечения (FOSS). Публикуются в авторской редакции.
Книга будет интересна представителям любых стартапов, руководителям среднего звена, которые решили стать независимыми консультантами и экспертами. Рекламным агентствам она поможет в оптимизации внутренних процессов и воспитании проектных менеджеров в штате. Сотрудникам HR-отделов, рекрутерам и охотникам за головами книга пригодится при поиске редких специалистов с ценными навыками. Фрилансерам книга расширит кругозор, представляя репортаж с другой стороны баррикад.

Глава 27. Модель аппаратного ввода и локальное состояние ввода

ГЛАВА 27 Модель аппаратного ввода и локальное состояние ввода

В этой главе мы рассмотрим модель аппаратного ввода. В частности, я расскажу, как события от клавиатуры и мыши попадают в систему и пересылаются соответствую щим оконным процедурам. Создавая модель ввода, Microsoft стремилась главным об разом к тому, чтобы ни один поток не мог нарушить работу других потоков. Вот при мер из 1б-разрядной Windows' задача (так в этой системе назывались выполняемые программы), зависшая в бесконечном цикле, приводила к тому, что зависали и осталь ные задачи — их дальнейшее выполнение становилось невозможным. Пользователю ничего не оставалось, как только перезагрузить компьютер А все потому, что опера ционная система слишком много разрешала отдельно взятой задаче. Отказоустойчи вые операционные системы вроде Windows 2000 и Windows 98 не дают зависшему потоку блокировать другим потокам прием аппаратного ввода

Поток необработанного ввода

Общая схема модели аппаратного ввода в системе показана на рис. 27-1. При запуске система создает себе особый поток необработанного ввода (raw input thread, RIT) и системную очередь аппаратного ввода (system hardware input queue, SHIQ). RIT и SHIQ — это фундамент, на котором построена вся модель аппаратного ввода.

Обычно RIT бездействует, ожидая появления какого-нибудь элемента в SHIQ Ког да пользователь нажимает и отпускает клавишу на клавиатуре или кнопку мыши, либо перемещает мышь, соответствующий драйвер устройства добавляет аппаратное собы тие в SHIQ Тогда RIT пробуждается, извлекает этот элемент из SHIQ, преобразует его в сообщение (WM_KEY*, WM_?BUTTON* или WM_MOUSEMOVE) и ставит в конец оче реди виртуального ввода (virtualized input queue, VIQ) нужного потока. Далее RIT воз вращается в начало цикла и ждет появления следующего элемента в SHIQ RIT никог да не перестает реагировать на события аппаратного ввода — весь его код написан самой Microsoft и очень тщательно протестирован.

Как же RIT узнает, в чью очередь надо пересылать сообщения аппаратного ввода? Ну, с сообщениями от мыши все ясно: RIT просто выясняет, в каком окне находится ее курсор, и, вызвав GetWindowThreadProcessId, определяет поток, создавший это окно. Поток с данным идентификатором и получит сообщение от мыши.

В случае сообщений от клавиатуры все происходит несколько иначе. В любой момент с RIT "связан" лишь какой-то один поток, называемый активным (foreground thrcad). Именно сму принадлежит окно, с которым работает пользователь в данное время.

rihter27-1.jpg

Рис. 27-1. Модель аппаратного ввода

Когда пользователь входит в систему, процесс Windows Explorer порождает поток, который создает панель задач и рабочий стол. Этот поток привязывается к RIT. Если Вы запустите Calculator, то его поток, создавший окно, немедленно подключится к RIT После этого поток, принадлежащий Explorer, отключается от RIT, так как единовре менно с RIT может быть связан только один поток. При нажатии клавиши в SHIQ появится соответствующий элемент. Это приведет к тому, что RIT пробудится, преоб разует событие аппаратного ввода в сообщение от клавиатуры и поместит его в VIQ потока Calculator.

Каким образом различные потоки подключаются к RIT? Если при создании про цесса его поток создает окно, последнее автоматически появляется на переднем пла не (становится активным), и этот поток присоединяется к RIT. Кроме того, RIT отве чает за обработку особых комбинаций клавиш- Alt+Tab, AIr+Esc и Ctrl+Alt+Del. Посколь ку эти комбинации клавиш RIT обрабатывает самостоятельно, пользователи могут в любой момент активизировать соответствующие окна с клавиатуры; ни одно прило жение не в состоянии перехватить упомянутые комбинации клавиш Как только поль зователь нажимает одну из таких комбинаций клавиш, RIT активизирует выбранное окно, и в результате его поток подключается к RIT. Кстати, в Windows есть функции, позволяющие программно активизировать окно, присоединив сго поток к ШТ. Мы обсудим их несколько позже.

На рис 27-1 видно, как работает механизм защиты потоков друг от друга. Посы лая сообщение в окно B1 или B2, RIT помещает его в очередь виртуального ввода потока В. Обрабатывая это сообщение, поток — при синхронизации на каком-либо объекте ядра — может войти в бесконечный цикл или попасть в ситуацию взаимной блокировки. Если так и случится, он все равно останется присоединенным к RIT, и сообщения будут поступать именно в его очередь виртуального ввода

Однако пользователь, заметив, что ни окно B1, ни окно B2 не реагируют на его действия, может переключиться, например, в окно А1 нажатием клавиш Alt+Tab По скольку RIT сам обрабатывает комбинацию клавиш Alt+Tab, переключение пройдет без всяких проблем. После активизации окна A1 к RIT будет подключен поток А. Теперь пользователь может спокойно работать с окном A1, даже несмотря на то что поток В и оба его окна зависли.

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

Независимая обработка ввода потоками, предотвращающая неблагоприятное воздей ствие одного потока на другой, — это лишь часть того, чтo обеспечивает отказоус тойчивость модели аппаратного ввода По этого недостаточно для надежной изоля ции потоков друг от друга, и поэтому система поддерживает дополнительную кон цепцию — локальное состояние ввода (local input btate).

Каждый поток обладает собственным состоянием ввода, сведения о котором хра нятся в структуре THREADINFO (глава 26) В информацию об этом состоянии вклю чаются данные об очереди виртуального ввода потока и группа переменных. Послед ние содержат управляющую информацию о состоянии ввода.

Для клавиатуры поддерживаются следующие.сведения

  • какое окно находится в фокусе клавиатуры;
  • какое окно активно в данный момент;
  • какие клавиши нажаты;
  • состояние курсора ввода ;

Для мыши учитывается такая информация

  • каким окном захвачена мышь;
  • какова форма курсора мыши;
  • видим ли этот курсор;

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

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

Как Вы уже знаете, ввод с клавиатуры направляется потоком необработанного ввода (RTT) в очередь виртуального ввода какого-либо потока, но только не в окно RIT по мещает события от клавиатуры в очередь потока безотносительно конкретному окну. Когда поток вызывает GetMessage, событие от клавиатуры извлекается из очереди и перенаправляется окну (созданному потоком), на котором в данный момент сосре доточен фокус ввода(рис 27-2)

Чтобы направить клавиатурный ввод в другое окно, нужно указать, в очередь ка кого потока RIT должен помещагь события от клавиатуры, а также "сообщить" пере менным состояния ввода потока, какое окно будет находиться в фокусе Одним вызо вом SetFocus эти задачи не решить Если в данный момент ввод от RIT получает поток 1, то вызов SetFocus с передачей описателей окон А, В или С приведет к смене фокуса.

Окно, теряющее фокус, убирает используемый для обозначения фокуса прямоуголь ник или гасит курсор ввода, а окно, получающее фокус, рисует такой прямоугольник или показывает курсор ввода.

rihter27-2.jpg

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

Предположим, однако, что поток 1 по-прежнему получает ввод от RIT и вызывает SetFocus, передавая ей описатель окна E. В этом случае система не дает функции что либо сделать, так как окно, на которое Вы хотите перевести фокус, не использует очередь виртуального ввода, подключенную в данный момент к RIT. Когда поток 1 выполнит этот вызов, на экране не произойдет ни смены фокуса, ни каких-либо из менений

Возьмем другую ситуацию: поток 1 подключен к RIT, а поток 2 вызывает SetFocus, передавая ей описатель окна E. На этот раз значения переменных локального состо яния ввода потока 2 изменяются так, что - когда RIT в следующий раз направит со бытия от клавиатуры этому потоку — ввод с клавиатуры получит окно E. Этот вызов не заставит RIT направить клавиатурный ввод в очередь виртуального ввода потока 2

Так как фокус теперь сосредоточен на окне F, потока 2, оно получает сообщение WM_SETFOCUS Если окно E — кнопка, на нем появляется прямоугольник, обознача ющий фокус, и в результате на экране могут появиться два окна с такими прямоуголь никами (окна А и E). Сами понимаете, это вряд ли кому понравится. Поэтому вызы вать SetFocus следует с большой осторожностью — чтобы не создавать подобных си туаций. Вызов SetFocus безопасен, только если Ваш поток подключен к RIT

Кстати, если Вы переведете фокус на окно, которое, получив сообщение WM_SET FOCUS, показывает курсор ввода, не исключено одновременное появление на экране нескольких окон с таким курсором. Это тоже вряд ли кого обрадует.

Когда фокус переводится с одного окна на другое обычным способом (например, щелчком окна), теряющее фокус окно получает сообщение WM_KILLFOCUS. Если окно, получающее фокус, принадлежит другому потоку, переменные локального со стояния ввода потока, который владеет окном, теряющим фокус, обновляются так, чтобы показать: окон в фокусе нет. И вызов GetFocus возвращает при этом NULL, зас тавляя поток считать, что окон в фокусе нет.

Функция SetActiveWindow активизирует в системе окно верхнего уровня и перево дит на него фокус:

HWND SetActiveWindow(HWND hwnd);

Как и SetFocus, эта функция ничего не делает, если поток вызывает ее с описате лем окна, созданного другим потоком.

Функцию SetActiveWindow дополняет GetActiveWindow:

HWND GetActiveWindow();

Она работает так же, как и GetFocus, но возвращает описатель активного окна, указанного в переменных локального состояния ввода вызывающего потока. Так что, если активное окно принадлежит другому потоку, функция возвращает NULL.

Есть и другие функции, влияющие на порядок размещения окон, их статус (актив но или неактивно) и фокус:

BOOL BringWindowToTop(HWND hwnd);

BOOL SetWindowPos( HWND hwnd, HWND hwndInsertAfter, int x, int у, int cx, int су, UINT fuFlags);

Обе эти функции работают одинаково (фактически BringWmdowToTop вызывает SetWindowPos, передавая ей HWND_TOP во втором параметре). Когда поток, вызыва ющий любую из этих функций, не связан с RIT, они ничего не делают. В ином случае (когда поток связан с RIT) система активизирует указанное окно Обратите внимание, что здесь не имеет значения, принадлежит ли это окно вызвавшему потоку. Окно ста новится активным, а к RIT подключается тот поток, который создал данное окно, Кроме того, значения переменных локального состояния ввода обоих потоков обнов ляются так, чтобы отразить эти изменения.

Иногда потоку нужно вывести свое окно на передний план. Например, Вы запла нировали какую-то встречу, используя Microsoft Outlook. Где-то за полчаса до назна ченного времени Outlook выводит на экран диалоговое окно с напоминанием о встре че. Если поток Outlook не связан с RIT, это диалоговое окно появится под другими окнами, и Вы его не увидите. Поэтому нужен какой-то способ, который позволил бы привлекать внимание к определенному окну, даже если в данный момент пользова тель работает с окном другого приложения.

Вот функция, которая выводит окно на передний план и подключает его поток к RIT:

BOOL SetForegroundWindow(HWND hwnd);

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

HWND GetForegroundWindow();

Она возвращает описатель окна, находящегося сейчас на переднем плане.

В болсс ранних версиях Windows функция SetForegroundWindow срабатывала все гда. То есть поток, вызвавший ее, всегда мог перевести указанное окно на передний план (даже если оно было создано другим потоком). Однако разработчики стали зло употреблять этой функцией и нагромождать окна друг на друга. Представьте, я пишу журнальную статью, и вдруг выскакивает окно с сообщением о завершении печати. Если бы я не смотрел на экран, то начал бы вводить текст не в свой документ, а в это окно. Еще больше раздражает, когда пытаешься выбрать команду в меню, а на экране появляется какое-то окно и закрывает меню.

Чтобы прекратить всю эту неразбериху, Microsoft сделала SetForegroundWindow чуть поумнее. В частности, эта функция срабатывает, только если вызывающий поток уже подключен к RIT или если поток, связанный с RIT R данный момент, не получал ввода на протяжении определенного периода (который задается функцией System ParametersInfo и значением SPI_SETFOREGROUNDLOCKTIMEOUT). Кроме того, SetFore groundWindow терпит неудачу, когда активно какое-нибудь меню

Если SetForegroundWindow нс удается переместить окно па передний план, то его кнопка на панели задач начинает мигать. Заметив это, пользователь будет в курсе, что окно требует его внимания Чтобы выяснить, в чем дело, пользователю придется ак тивизировать это окно вручную. Управлять режимом мигания окна позволяет функ ция SystemParameterslnfo со значением SPI_SETFOREGROUNDFLASHCOUNT.

Из-за такого поведения SetForegroundWindow в систему встроено несколько новых функций. Первая из них, AllowSetForegroundWindow, разрешает потоку указанного процесса успешно вызвать SetForegroundWindow, но только если и вызывающий ее поток может успешно вызвать SetForegroundWindow Чтобы любой процесс мог выво дить окно «поверх» остальных окон, открытых Вашим потоком, передайте в парамет ре dwProcessId значение ASFW_ANY (определенное как -1):

BOOL AllowSetForegroundWindow(DWORD dwProcessId);

Кроме того, можно полностью заблокировать работу SetForegroundWindow, вызвав LockSetForegroundWindow:

BOOL LockSetForegroundWindow(UINT uLockCode);

В параметре uLockCode она принимает либо LSFW_LOCK, либо LSFW_UNLOCK. Данная функция вызывается системой, когда на экране активно какое-нибудь систем ное меню, — чтобы никакое окно не могло его закрыть. (Поскольку меню Start не явля ется встроенным, то при его открытии Windows Explorcr сам вызывает эти функции.)

Система автоматически снимает блокировку с функции SetForegroundWindow, когда пользователь нажимает клавишу Alt или активизирует какое-либо окно. Так что при ложение не может навечно заблокировать SetForegroundWindow.

Другой аспект управления клавиатурой и локальным состоянием ввода связан с массивом синхронного состояния клавиш (synchronous key state array). Этот массив включается в переменные локального состояния ввода каждого потока. В то же время массив асинхронного состояния клавиш (asynchronous key state array) — только один, и он разделяется всеми потоками. Эти массивы отражают состояние всех клавиш на данный момент, и функция GetAsyncKeyState позволяет определить, нажата ли сейчас заданная клавиша:

SHORT GetAsyncKeyState(int nVirtKey);

Параметр nVirtKey задает код виртуальной клавиши, состояние которой нужно проверить. Старший бит результата определяет, нажата в данный момент клавиша (1) или нет (0). Я часто пользовался этой функцией, определяя при обработке сообще ния, отпустил ли пользователь основную (обычно левую) кнопку мыши. Передав зна чение VK_LBUTTON, я ждал, когда обнулится старший бит Заметьте, что GetAsyncKey State всегда возвращает 0 (не нажата), если ее вызывает другой поток, а не тот, кото рый создал окно, находящееся сейчас в фокусе ввода.

Функция GetKeyState отличается от GetAsyncKeyState тем, что возвращает состоя ние клавиатуры на момент, когда из очереди потока извлечено последнее сообщение от клавиатуры:

SHORT GetKeyState(int nVirtKey);

Эту функцию можно вызвать в любой момент; для нее невяжно, какое окно в фокусе.

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

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

Один из аспектов управления курсором мыши заключается в его отображении или гашении, Если поток вьзывает ShowCursor(FAI.SE), то система скрывает курсор, когда он оказывается на любом окне, созданном этим потоком, и показывает курсор вся кий раз, когда он попадает в окно, созданное другим потоком.

Другой аспект управления курсором мыши — возможность ограничить его пере мещение каким-либо прямоугольным участком. Для этого надо вызвать функцию ClipCursor:

BOOL ClipCursor(CONST RECT *ргс);

Она ограничивает перемещение курсора мыши прямоугольником, на который указывает параметр prc. И опять система разрешает потоку ограничить перемещение курсоря заданным прямоугольником. Но, когда возникает событие асинхронной ак тивизации, т. e. когда пользователь переключается в окно другого приложения, нажи мает клавиши Ctrl+Esc или же поток вызывает SetForegroundWindow, система снимает ограничения на передвижение курсора, позволяя свободно перемещать его по экрану.

И здесь мы подошли к концепции захвата, мыши (mouse capture). «Захватывая» мышь (вызовом SetCapture), окно требует, чтобы все связанные с мышью сообщения RIT отправлял в очередь виртуального ввода вызывающего потока, а из нее — устано вившему захват окну до тех пор, пока программа не вызовет ReleaseCapture.

Как и в предыдущих случаях, это тоже снижает отказоустойчивость системы, но без компромиссов, увы, не обойтись. Вызывая SetCapture, поток заставляет RIT поме щать все сообщения от мыши в свою очередь виртуального ввода. При этом SetCapture соответственно настраивает переменные локального состояния ввода данного потока.

Обычно приложение вызывает SetCapture, когда пользователь нажимает кнопку мыши. Но поток может вызвать эту функцию, даже если нажатия кнопки мыши не было. Если SetCapture вызывается при нажатой кнопке, захват действует для всей си стемы Как только система определяет, что все кнопки мыши отпущены, RIT переста ет направлять сообщения от мыши исключительно в очередь виртуального ввода дан ного потока. Вместо этого он передает сообщения в очередь ввода, связанную с ок ном, «поверх» которого курсор находится в данный момент. И это нормальное пове дение системы, когда захват мыши не установлен

Однако для вызвавшего SetCapture потока ничего не меняется. Всякий раз, когда курсор оказывается на любом из окон, созданных установившим захват потоком, со общения от мыши направляются в окно, применительно к которому этот захват и установлен. Иначе говоря, когда пользователь отпускает всс кнопки мыши, захват осуществляется на уровне лишь данного потока, а нс всей системы

Если пользователь попытается активизировать окно, созданное другим потоком, система автоматически отправит установившему захват потоку сообщения о нажатии и отжатии кнопок мыши. Затем она изменит переменные локального состояния вво да потока, чтобы отразить тот факт, что поток более не работает в режиме захвата. Словом, Microsoft считает, что захват мыши чаще всего применяется для выполнения таких операций, как щелчок и перетаскивание экранного объекта.

Последняя переменная локального состояния ввода, связанная с мышью, относится к форме курсора. Всякий раз, когда поток вызывает SetCursor для изменения формы курсора, переменные локального состояния ввода соответствующим образом обнов ляются То есть переменные локального состояния ввода всегда запоминают послед нюю форму курсора, установленную потоком

Допустим, пользователь перемещает курсор мыши на окно Вашей программы, окно получает сообщение WM_SETCURSOR, и Вы вызываете SetCursor, чтобы преоб разовать курсор в «несочные часы». Вызвав SetCursor, программа начинает выполнять какую-то длительную операцию (Бесконечный цикл — лучший пример длительной операции Шутка ) Далее пользователь перемещает курсор из окна Вашей программы в окно другого приложения, и это окно может изменить форму курсора.

Для такого изменения переменные локального состояния ввода не нужны Но пе реведем курсор обратно в то окно, поток которого по-прежнсму занят обработкой. Системе "хочется" послать окну сообщения WM_SETCURSOR, но процедура этого окна не может выбрать их из очереди, так кяк его поток продолжает свою операцию. Тогда система определяет, какая форма была у курсора в прошлый раз (информация об этом содержится в переменных локального состояния ввода данного потока), и автомати чески восстанавливает ее (в нашем примере — "песочные часы"). Теперь пользовате лю четко видно, что в этом окне работа еще не закончена и придется подождать.

Подключение к очередям виртуального ввода и переменным локального состояния ввода

Как Вы уже убедились, отказоустойчивость модели ввода достигается благодаря тому, что у каждого потока имеются собственные переменные локального состояния вво да, а подключение потока к RIT и отключение от него происходит по мере необхо димости. Иногда нужно, чтобы два потока (или более) разделяли один набор пере менных локального состояния ввода или одну очередь виртуального ввода

Вы можете заставить два и более потока совместно использовать одну и ту же очередь виртуального ввода и переменные локального состояния ввода с помощью функции AttachTbreadInput:

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

Параметр idAttach задаст идентификатор потока, чьи переменные локального со стояния ввода и очередь виртуального ввода Вам больше не нужны, а паряметр idAtta chedTo — идентификатор потока, чьи переменные локального состояния ввода и вир туальная очередь ввода должны совместно использоваться потоками И, наконец, па раметр fAttach должен быть или TRUE, чтобы инициировать совместное использова ние одной очереди, или FALSE — тогда каждый поток будет вновь использовать свои переменные состояния ввода и очередь А чтобы одну очередь (и переменные состо яния ввода) разделяли более двух потоков, вызовите АttасЬТhrеаdInput соответствую щее число раз.

Вернемся к одному из предыдущих примеров и допустим, что поток А вызывает AttachThreadlnput, передавая в первом параметре свой идентификатор, во втором — идентификатор потока В и в последнем — TRUE:

AttachThreadInput(idThreadA, idThreadB, TRUE);

Теперь любое событие аппаратного ввода, адресованное окну A1, B1 или B2, будет добавлено в конец очереди виртуального ввода потока В. Аналогичная очередь пото ка А больше не получит новых событий, если только Вы не разъедините очереди, повторно вызвав AttachThreadInput с передачей FALSE в параметре fAttach

rihter27-3.jpg

Рис. 27-3. Аппаратные сообщения для окон А1, В1 и B2 помещаются в очередь виртуального ввода потока В

Потоки, присоединенные к одной очереди виртуального ввода (VIQ), сохраняют индивидуальные очереди сообщений (синхронных, асинхронных и ответных), а так же флаги пробуждения. Однако Вы серьезно снизите надежность системы, если зас тавите все потоки использовать одну очередь сообщений. Если какой-нибудь поток зависнет при обработке нажатия клавиши, другие потоки не получат никакого ввода. Поэтому использования AttachThreadInput следует по возможности избегать.

Система неявно соединяет очереди виртуального ввода двух потоков, если какой то из них устанавливает ловушку регистрации (journal record hook) или ловушку вос произведения (journal playback hook). Когда ловушка снимается, система восстанав ливает схему организации очереди ввода, существовавшую до установки ловушки.

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

Есть еще один случай, когда система неявно вызывает AttachThreadlnput. Допустим, приложение создает два потока. Первый открывает на экране диалоговое окно Затем второй поток вызывает CreateWindow, указывая стиль WS_CHILD и передавая описа

тель этого диалогового окна, чтобы оно стало "родителем" дочернего окна. Тогда система сама вызывает АttасhТhrеаdIпрut, чтобы поток (которому принадлежит дочер нее окно) использовал ту же очередь ввода, что и поток, создавший исходное диало говое окно Это приводит к синхронизации ввода во всех дочерних окнах исходного диалогового окна.

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

Эта программа, «27 LISLab.exe» (см. листинг на рис 27-4), — своего рода лаборатория, в которой Вы сможете поэкспериментировать с локальным состоянием ввода. Файлы исходного кода и ресурсов этой программы находятся в каталоге 27-LISLab на ком пакт-диске, прилагаемом к книге.

В качестве подопытных кроликов нам понадобятся два потока Один поток есть в нашей LISLab, a вторым будет Notepad Если на момент запуска LISLab программа Notepad не выполняется, LISLab сама запустит эту программу. После инициализации LISLab Вы увидите следующее диалоговое окно.

rihter27-4.jpg

В левом верхнем углу окна — раздел Windows, его поля обновляются дважды в секунду, т. e дважды в секунду диалоговое окно получает сообщение WM_TIMER и в ответ вызывает функции GefFocus, GetActiveWtndow, GetForegroundWindow, GetCapture и GetClipCursor Первые четыре функции возвращают описатели окна (считываемые из переменных локального состояния ввода моего потока), через которые я могу определить класс и заголовок окна и вывести эту информацию на экран

Если я активизирую другое приложение (тот же Notepad), названия полей Focus и Activc меняются на (No Window), ц поля Foreground — на [Notepad] Umitled - Notepad Обратите внимание, что активизация Notepad заставляет LISLab считать, что ни ак тивных, ни находящихся в фокусе окон нет

Теперь поэкспериментируем со сменой фокуса. Выберем SetFocus в списке Func tion — в правом верхнем углу диалогового окна. Затем в поле Delay введем время (в секундах), в течение которого LISLab будет ждать, прсжде чем вызвать SetFocus B дан ном случае, видимо, лучше установить пулевое время задержки Позже я объясню, как используется поле Delay.

Выберем окно (описатель которого мы хотим передать функции SetFocus) в спис ке Notepad Windows And Self, расположенном в левой части диалогового окна Для эксперимента укажем [Notepad] Untitled - Notepad. Теперь все готово к вызову SetFocus.

Щелкните кнопку Delay и понаблюдайте, что произойдет в разделе Windows Ничего. Система отказалась менять фокус.

Если Вы действительно хотите перевести фокус на Notepad, щелкните кнопку Attach To Notepad, что заставит LISLab вызвать:

AttachThreadinput(GetWindowThreadProcessId(g_hwnrtNotepad, NULL), GetCurrentThreadId(), TRUE);

В результате этого вызова поток LISLab станет использовать ту же очередь вирту ального ввода, что и Notepad Кроме того, поток LISLab «разделит» переменные локаль ного состояния ввода Notepad

Если после щелчка кнопки Attach To Notepad Вы щелкнете окно Notepad, диало говое окно LISLab примет следующий вид.

rihter27-5.jpg

Теперь, когда очереди ввода соединены друг с другом, LISLab способна отслежи вать изменения фокуса, происходящие в Notepad R приведенном выше диалоговом окне показано, что в данный момент фокус установлен на поле ввода А если мы от кроем в Notepad диалоговое окно File Opеn, то LISLab, продолжая следить за Notepad, покажет нам, какое окно Notepad получило фокус ввода, какое окно активно и т. д.

Теперь можно вернуться в LISLab, щелкнуть кнопку Delay и вновь попытаться зас тавить SetFocus перевести фокус на Notepad На этот раз всс пройдет успешно, пото му что очереди ввода соединены

Если хотите, поэкспериментируйте с SelActiweWindow, SetForegroundWtndow, Bring WindowToTop и SetWindowPos, выбирая нужную функцию в списке Function Попробуй те вызывать их и когда очереди соединены, и когда они разъединены; при этом обра щайте внимяние на различия в поведении функций

А сейчас я поясню, зачем предусмотрена задержка. Она заставляет LISLab вызывать указанную функцию только по истечении заданного числа секунд. Для иллюстрации возьмем такой пример. Но прежде отключите LISLab от Notcpad, щелкнув кнопку Detach From Notepad Затем в списке Notepad Windows And Self выберите —>This Dialog Box<—, a в списке Function — SerFocus и установите задержку на 10 секунд. На конец "нажмите" кнопку Delay и быстро щелкните окно Notepad, чтобы оно стало ак тивным Вы должны активизировать Notepad до того, как истекут заданные 10 секунд.

Пока идет отсчет времени задержки, справа от счетчика высвечивается слово Pending lIo истечении 10 секунд слово Pending меняется на Executed, и появляется

результат вызова функции Если Вы внимательно следите за работой программы, то увидите, что фокус теперь установлен на окно списка Function. Но ввод с клавиатуры по-прежнему направляется в Notepad. Таким образом, и поток LISLab, и поток Note pad — оба считают, что в фокусе находится одно из их окон Но на самом деле RIT остается связанным с потоком Notepad.

И последнее замечание в этой связи: SetFocus и SetActiveWindow возвращают опи сатель окна, которое изначально находилось в фокусе или было активным Инфор мация об этом окне отображается в поле PrevWnd. Кроме того, непосредственно пе ред вызовом SetForegroundWindow программа обращается к GetForegroundWindow, что бы получить описатель окна, которое располагалось "поверх" остальных окон. Эта информация также отображается в поле PrevWnd.

Далее поэкспериментируем с курсором мыши. Всякий раз, когда курсор проходит над диалоговым окном LISLab (но нс над каким-либо из его дочерних окон), он изоб ражается в виде вертикальной стрелки. По мере поступления диалоговому окну сооб щения от мыши добавляются в список Mouse Messages Received. Таким образом, Вы всегда в курсе того, когда диалоговое окно получает сообщения от мыши. Сдвинув курсор за пределы основного окна или поместив его на одно из дочерних окон, Вы увидите, что сообщения больше не вносятся в список.

Теперь переместите курсор в правую часть диалогового окна, установив его над текстом Click Right Mouse Button To Set Capture, а затем нажмите и удерживайте пра вую кнопку мыши. После этого LISLab вызовет функцию SctCapture, передав ей опи сатель своего диалогового окна. Заметьте: факт захвата мыши программой LISLab от разится в разделе Windows

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

Теперь мы можем увидеть другой эффект. Отпустите правую кнопку и следите, что произойдет. Окно, в свое время захватившее мышь, показывает, что LISLab по-прежне му считает мышь захваченной. Но сдвиньте курсор за пределы диалогового окна LISLab, и он больше нс останется вертикальной стрелкой, а сообщения от мыши пере станут появляться в списке Mouse Messages Received. Установив же курсор на какое-либо из дочерних окон LISLab, Вы сразу увидите: захват по-прежнему действует, потому что все эти окна используют один набор переменных локального состояния ввода.

Закончив эксперименты, отключите режим захвата одним из двух способов:

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

Какой бы способ Вы ни выбрали, обратите внимание на поле Capture в разделе Win dows — теперь оно отражает тот факт, что больше ни одно окно не захватывает мышь.

И еще два эксперимента, связанных с мышью в одном мы ограничим поле пере мещения курсора заданным прямоугольником, а в другом — изменим «видимость» курсора. Если Вы щелкнете кнопку «Top, Left», программа LISLab выполнит такой код:

RECT rc;

...

SetRect(&rc, 0, 0, GetSystemMetrics(SM_CXSCREEN) / 2, GetSystemMetrics(SM_CYSCREEN) / 2);

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

Windows 98 щелчка заголовка окна другого приложения и последующего пере мещения этого окна;

Windows 2000 щелчка заголовка окна другого приложения (последующего переме щения этого окна не требуется),

Windows 2000 активизации Task Manager нажатием клавиш Ctrl+Shifl+Еsc и после дующей его отмены.

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

Щелчок кнопки Hide или Show вызывает выполнение соответственно.

ShowCursor(FALSE);

или

ShowCursor(TRUE);

Когда курсор скрыт, его не видно при перемещении над диалоговым окном LISLab. Но как только курсор оказывается за пределами окна, он снова видим. Для нейтрали зации действия кнопки Hide используйте кнопку Show. Заметьте, что скрытие курсо ра носит кумулятивный характер пять раз щелкнув кнопку Hide, придется столько же раз щелкнуть кнопку Show, прежде чем курсор станет видимым.

И последний эксперимент — с кнопкой Infinite Loop При ее щелчке выполняется код:

SetCursor(LoadCursor(NULL, IDC_NO));

for (,,)

Первая строка меняет форму курсора на перечеркнутый круг, а вторая выполняет бесконечный цикл. После щелчка кнопки Infinite Loop программа перестает реагиро вать на какой-либо ввод. Перемещая курсор в пределах диалогового окна LISLab, Вы увидите, что курсор остается перечеркнутым кругом. Если же Вы сместите его в дру гое окно, он получит форму, заданную в текущем окне.

Если теперь вернуть курсор в диалоговое окно LlSLab, система обнаружит, что программа не отвечает, и автоматически восстановит прежнюю форму курсора — перечеркнутый круг. Так что вошедший в бесконечный цикл поток хоть и не вызывает положительных эмоций, но пользоваться другими окнами не мешает.

Заметьте: если Вы переместите на окно зависшей программы LISLab другое окно, а потом уберете его, система отправит LISLab сообщение WM_PAINT и обнаружит, что данный поток не отвечает. Из этой ситуации система выходит элементарно: перери совывает окно нереагирующего приложения. Конечно, перерисовать окно правиль но она нс в состоянии, так как ей не известно, что именно делало приложение, и поэтому она просто затирает окно цветом фона и перерисовывает рамку его окна.

Проблема теперь в том, что на экране есть ни на что не отвечающее окно. Как от него избавиться? Под управлением Windows 98 нужно сначала нажать клавиши Ctrl+Alt+Del, чтобы на экране появилось окно Close Program.

rihter27-6.jpg

B Windows 2000 можно либо щелкнуть правой кнопкой мыши кнопку приложе ния на панели задач, либо открыть окно Task Manager

rihter27-7.jpg

Затем следует выбрать из списка название программы, которую нужно завершить (в данном случае — Local Input State Lab), и щелкнуть кнопку End Task Система попы тается завершить LISLab «по-хорошему» (послав сообщение WM_CLOSE), но обнару жит, что приложение не отвечает Это заставит ее вывести одно из окон первое — в Windows 08 второе — в Windows 2000

rihter27-8.jpg

Если Вы выберете кнопку End Task (в Windows 98) или End Now (в Windows 2000), система завершит LISLab принудительно Кнопка Cancel сообщит системе, что Вы передумали завершать приложение Так что щелкните кнопку End Task или End Now, чтобы удалить LISLab из системы

Общий смысл этих экспериментов — продемонстрировать отказоустойчивость системы Ни одно приложение практически не способно привести систему в такое состояние, когда работа с другими приложениями станет невозможной Кроме того, и Windows 98, и Windows 2000 автоматически освобождают все ресурсы, выделявши еся поnjкам завершенного процесса, — утечки памяти не происходит!

LISLab

 

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

Эта программа, «27 LISWatch.exe» (см. листинг на рис. 27-5), полезная утилита, ко торая отслеживает следующие окна: активное, находящееся в фокусе и захватившее мышь. Файлы исходного кода и ресурсов этой программы находятся в каталоге 27 LISWatch на компакт-диске, прилагаемом к книге. После запуска LISWatch открывает диалоговое окно, показанное ниже.

rihter27-9.jpg

Получив сообщение WM_INITDIALOG, это окно вызывает SetTimer, чтобы создать таймер, срабатывающий два раза в секунду. А когда от таймера поступает сообщение WM_TIMER, обновляется содержимое диалогового окна — сведения об активном окне, окне в фокусе и окне, захватившем мышь В нижней части диалогового окна также сообщается, какое окно находится сейчас на переднем плане. Поэкспериментируйте с этой утилитой, просто щелкая окна, созданные различными приложениями. При этом Вы заметите, что LISWatch корректно сообщает информацию об окнах незави симо от того, какой поток создал то или иное окно

Самая интересная часть кода этой программы выполняется при приеме сообще ния. WM_TIMER, поэтому, когда я буду давать пояснения, посматривайте на исходный код функции Dlg_ОпТimеr. И пока считайте, что глобальная переменная g_dwThread IdAttachTo равна 0. Ее назначение я объясню позже.

Так как у каждого потока свой набор переменных локалыюго состояния ввода, Dlg_OnTimer сначала вызывает GetForegroundWindow для определения окна, с которым имеетдело пользователь. GetForegroundWindow всегда возвращаетдействичелышй описатель окна независимо от того, какой поток создал это окно или вызвал эту функцию

Полученный таким образом описатель передается в GetWindowThreadProcessId, чтобы выяснить, какой поток связан с RIT. Далее LISWatch, вызвав AttachThreadInput, подключяется к переменным локального состояния ввода потока, связанного с RIT. После этого поток LISWatch может вызывать GetFocus, GetActiveWindow и GetCapture — любая и,з них возвращает действительный описатель окна. Вспомогательная функция CalcWndText формирует строки с именами классов и заголовками окон Потом эти строки показываются в диалоговом окне LISWatch И, наконец, перед самым возвра том управления Dlg_OnTimer сновя вызывае: AttachThreadInput, на этот раз передавая FALSE в последнем параметре и тем самым отключая два потока друг от друга

Ну нот, общее представление о программе LISWatch Вы получили. Однако она поддерживает и другую функциональность, о которой я хочу сейчас рассказать. Пос ле запуска LISWatch отслеживает активизацию окон во всех частях системы. На это и указывает надпись System-wide в верхней части диалогового окна. Но LISWatch может наблюдать за локальным состоянием ввода и только в одном потоке. Если Вы выбе рете именно этот режим, LISWatch будет сообщать Вам о том, что "думает" поток о своем состоянии ввода.

Чтобы LISWatch контролировала состояние ввода лишь одного потока, щелкните окно LISWatch левой кнопкой мыши и, не отпуская кнопку, переместите курсор мыши в окно, созданное другим потоком, а затем отпустите кнопку. Как только Вы это сде лаете, LISWarch присвоит глобальной переменной g_dwThreadIdAttachTo идентифика тор выбранного потока, значение которого заменит надпись System-wide в верхней части окна LISWatch. При ненулевом значении этой переменной функция Dlg_OnTimer ведет себя несколько иначе: теперь она подключает LISWatch к переменным локаль ного состояния ввода выбранного Вами потока.

Проделаем один эксперимент Зяпустите Calculator и выберите его окно в LISWatch. Активизировав окно Calculator, Вы увидите следующую информацию.

rihter27-10.jpg

В моей системе идентификатор потока Calculator получил значение 0x000004ec. В данном случае LISWatch настроена на наблюдение зa локальным состоянием ввода этого потока если я щелкну любую кнопку или флажок в Calculator, LISWatch момен тально отреагирует на это, сообщив о соответствующей смене фокуса

Однако, если Вы теперь активизируете окно, созданное другим приложением (у меня — Notepad), окно LISWatch будет выглядеть так, как показано ниже

rihter27-11.jpg

Как видите, поток Calculator считает, что нет ни одного окна, которое находилось бы в фокусе, было бы активно или захватило бы мышь

Чтобы по-настоящему разобраться в концепции локального состояния ввода, за пустите несколько копий LISWatch и настройге их на мониторинг отдельных пото ков После этого щелкайте в их окнах и следите, что происходит с локальным состо янием ввода каждою из потоков

LISWatch