Focus and Windows activation (part 1)
Недавно я плотно занимался созданием выпадающего окна, в котором есть кнопки, поле ввода, а также элемент управления типа "список". В одном случае окно работало независимо, как меню, в другом - привязывалось к полю ввода на родительской форме (собственное поле ввода не отображалось), текст которого служил фильтром для элементов списка. Создание выпадающих окон - очень сложная задача, много нюансов связанных с активацией окон, фокусом ввода, определением ситуаций когда окно необходимо закрыть и т.д. Эта статья - попытка доходчиво объяснить на примерах как работает активация окон и как функционирует фокус ввода.
Любой UI поток имеет состояние. Это состояние хранится где-то в недрах ОС, но доступно из пользовательского режима при помощи следующих функций:
GetGUIThreadInfo
Это, пожалуй, самая-самая функция, которая демонстрирует составляющие UI состояния потока. Возвращаемая структура
GUITHREADINFO
хранит следующие значения:typedef struct tagGUITHREADINFO { DWORD cbSize; DWORD flags; HWND hwndActive; HWND hwndFocus; HWND hwndCapture; HWND hwndMenuOwner; HWND hwndMoveSize; HWND hwndCaret; RECT rcCaret; } GUITHREADINFO, *PGUITHREADINFO;
flags
Флажки состояний потока - мигающая каретка, меню, окно в процессе изменения размера или положения и т.п.
hwndActive
Дескриптор активного окна потока.
hwndFocus
Дескриптор окна потока, которое имеет логический фокус.
hwndCapture
Дескриптор окна потока, которое захватило мышь вызовом
SetCapture
.hwndMenuOwner
Дескриптор окна потока, которое владеет текущим меню.
hwndMoveSize
Дескриптор окна потока, которое в процессе изменения размера или положения.
hwndCaret
Дескриптор окна потока, которое отображает каретку.
rcCaret
Прямоугольная область каретки.
GetActiveWindow
Возвращает значение
hwndActive
структурыGUITHREADINFO
текущего потока.GetFocus
Возвращает значение
hwndFocus
структурыGUITHREADINFO
текущего потока.GetCapture
Возвращает значение
hwndCapture
структурыGUITHREADINFO
текущего потока.
В системе может быть активно множество потоков, каждый из которых, являясь UI потоком (IsGUIThread
возвращает истину, а также позволяет явно активировать поток в качестве UI потока; эта активация выполняется автоматически в момент первого использования функций для работы с окнами), имеет вышеописанное состояние. Но пользовательский ввод с клавиатуры попадает лишь в определенное окно, дескриптор которого совпадает с hwndFocus
одного из потоков. Почему так происходит? Потому, что существует глобальное состояние рабочего стола ОС, а в него входят так называемые Foreground Thread и Foreground Window. Foreground Window - это окно верхнего уровня, которое содержит (или же само таковым является) элемент управления с физическим фокусом. Логический фокус - это состояние потока, физический фокус - состояние рабочего стола и привязка клавиатуры к потоку и окну. Foreground Thread владеет окном с физическим фокусом, в этот поток будут попадать клавиатурные сообщения. Foreground Thread не обязательно владеет Foreground Window, поскольку элемент управления может быть создать в другом потоке и размещен на окне верхнего уровня. Функция GetForegroundWindow
позволяет узнать какое окно сейчас активно с точки зрения пользовательского ввода.
Стоит заметить, что Active Window и Foreground Window - это top-level окна, без стиля WS_CHILD
, а Focus Window
может быть как top-level окном, так и любым его дочерним окном.
Тестовое приложение.
Тестовое приложение состоит из нескольких оконных классов - CTopLevelWnd
, CTracingButton
и CTracingEdit
. Обработчики интересных нам сообщений содержат трассировочный код, который отправляет полезную информацию в лог. Текст окон содержит идентификатор оконного потока, а за ним - дескриптор окна.
Трассировочная информация содержит следующие элементы:
-->
отмечает точку входа в обработчик,<--
- выхода;<->
идентифицирует операцию;- далее следует идентификатор текущего потока;
- после имя класса (если мы попали в обработчик оконного сообщения);
- в скобках указывается дескриптор окна (если это окно либо элемент управления);
- потом имя функции или метода с параметрами;
- завершает это все статус рабочего стола/потока - Foreground Window (FW), Active Window (AW) и Focused Window (F).
На все потоки установлены хуки WH_GETMESSAGE
и WH_CBT
, трассирующие интересные события.
Цикл сообщений - свой. Каждому вызову GetMessage
предшествует PeekMessage
с флагом PM_NOREMOVE
. Это сделано для того, чтобы мы видели сообщения, которые вытягивает сам поток, а также сообщения, которые вынимает оттуда ОС без нашего ведома.
Сценарий 1 - активация\деактивация окна приложения.
Рассмотрим следующий сценарий: окно приложения неактивно, окно Notepad активно. Пользователь производит щелчок мышью по текстовому полю ввода окна приложения. Окно активируется, заголовок меняет свой внешний вид, фокус ввода получает поле ввода. Далее щелчок по окну Notepad. Окно деактивируется. Трассировочный лог происходящего приведен ниже, вместе с детальным разбором всех событий.
<-> 1AC4: GetMsgProc(wParam = PM_NOREMOVE, lParam = { message = WM_LBUTTONDOWN } { FW = 0, AW = 0, F = 0 } --> 1AC4: CTracingEdit(1709F4)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 } --> 1AC4: CTopLevelWnd(B0B70)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 } <-- 1AC4: CTopLevelWnd(B0B70)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 } <-- 1AC4: CTopLevelWnd(1709F4)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 } <-> 1AC4: CBTProc(HCBT_ACTIVATE, hWnd = B0B70, AS = { hWndActive = 0, fMouse = 1 }) { FW = 0, AW = 0, F = 0 } --> 1AC4: CTopLevelWnd(B0B70)::OnActivateApp(bActive = 1, dwThreadID = 0) { FW = B0B70, AW = B0B70, F = 0 } <-- 1AC4: CTopLevelWnd(B0B70)::OnActivateApp(bActive = 1, dwThreadID = 0) { FW = B0B70, AW = B0B70, F = 0 } --> 1AC4: CTopLevelWnd(B0B70)::OnNcActivate(bActive = 1) { FW = B0B70, AW = B0B70, F = 0 } <-- 1AC4: CTopLevelWnd(B0B70)::OnNcActivate(bActive = 1) { FW = B0B70, AW = B0B70, F = 0 } --> 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 2, pWndOther = 0, bMinimized = 0) { FW = B0B70, AW = B0B70, F = 0 } <-> 1AC4: CBTProc(HCBT_SETFOCUS, hSetFocusWnd = B0B70, hKillFocusWnd = 0) { FW = B0B70, AW = B0B70, F = 0 } --> 1AC4: CTopLevelWnd(B0B70)::OnSetFocus(pOldWnd = 0) { FW = B0B70, AW = B0B70, F = B0B70 } <-- 1AC4: CTopLevelWnd(B0B70)::OnSetFocus(pOldWnd = 0) { FW = B0B70, AW = B0B70, F = B0B70 } <-- 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 2, pWndOther = 0, bMinimized = 0) { FW = B0B70, AW = B0B70, F = B0B70 } <-> 1AC4: GetMsgProc(wParam = PM_REMOVE, lParam = { message = WM_LBUTTONDOWN } { FW = B0B70, AW = B0B70, F = B0B70 } --> 1AC4: CTracingEdit(1709F4)::OnLButtonDown(nFlags = 1, point = {114, 10}) { FW = B0B70, AW = B0B70, F = B0B70 } <-> 1AC4: CBTProc(HCBT_SETFOCUS, hSetFocusWnd = 1709F4, hKillFocusWnd = B0B70) { FW = B0B70, AW = B0B70, F = B0B70 } --> 1AC4: CTopLevelWnd(B0B70)::OnKillFocus(pNewWnd = 1709F4) { FW = B0B70, AW = B0B70, F = 1709F4 } <-- 1AC4: CTopLevelWnd(B0B70)::OnKillFocus(pNewWnd = 1709F4) { FW = B0B70, AW = B0B70, F = 1709F4 } --> 1AC4: CTracingEdit(1709F4)::OnSetFocus(pOldWnd = B0B70) { FW = B0B70, AW = B0B70, F = 1709F4 } <-- 1AC4: CTracingEdit(1709F4)::OnSetFocus(pOldWnd = B0B70) { FW = B0B70, AW = B0B70, F = 1709F4 } <-- 1AC4: CTracingEdit(1709F4)::OnLButtonDown(nFlags = 1, point = {114, 10}) { FW = B0B70, AW = B0B70, F = 1709F4 } <-> 1AC4: GetMsgProc(wParam = PM_NOREMOVE, lParam = { message = WM_LBUTTONUP } { FW = B0B70, AW = B0B70, F = 1709F4 } <-> 1AC4: GetMsgProc(wParam = PM_REMOVE, lParam = { message = WM_LBUTTONUP } { FW = B0B70, AW = B0B70, F = 1709F4 } --> 1AC4: CTracingEdit(1709F4)::OnLButtonUp(nFlags = 0, point = {114, 10}) { FW = B0B70, AW = B0B70, F = 1709F4 } <-- 1AC4: CTracingEdit(1709F4)::OnLButtonUp(nFlags = 0, point = {114, 10}) { FW = B0B70, AW = B0B70, F = 1709F4 } --> 1AC4: CTopLevelWnd(B0B70)::OnNcActivate(bActive = 0) { FW = 0, AW = B0B70, F = 1709F4 } <-- 1AC4: CTopLevelWnd(B0B70)::OnNcActivate(bActive = 0) { FW = 20830, AW = B0B70, F = 1709F4 } --> 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 0, pWndOther = 0, bMinimized = 0) { FW = 20830, AW = B0B70, F = 1709F4 } <-- 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 0, pWndOther = 0, bMinimized = 0) { FW = 20830, AW = B0B70, F = 1709F4 } --> 1AC4: CTopLevelWnd(B0B70)::OnActivateApp(bActive = 0, dwThreadID = 370) { FW = 20830, AW = 0, F = 1709F4 } <-- 1AC4: CTopLevelWnd(B0B70)::OnActivateApp(bActive = 0, dwThreadID = 370) { FW = 20830, AW = 0, F = 1709F4 } --> 1AC4: CTracingEdit(1709F4)::OnKillFocus(pNewWnd = 0) { FW = 20830, AW = 0, F = 0 } <-- 1AC4: CTracingEdit(1709F4)::OnKillFocus(pNewWnd = 0) { FW = 20830, AW = 0, F = 0 }
<-> 1AC4: GetMsgProc(wParam = PM_NOREMOVE, lParam = { message = WM_LBUTTONDOWN } { FW = 0, AW = 0, F = 0 }
В очереди появилось сообщение
WM_LBUTTONDOWN
. Но ОС пока еще ничего не делает. Действия ниже ОС выполняет лишь когда сообщение изымается из очереди вызовомGetMessage
илиPeekMessage
с флагомPM_REMOVE
(а именно эти вызовы будут выше по стеку нежели выполняющийся далее код).
--> 1AC4: CTracingEdit(1709F4)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 } --> 1AC4: CTopLevelWnd(B0B70)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 } <-- 1AC4: CTopLevelWnd(B0B70)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 } <-- 1AC4: CTopLevelWnd(1709F4)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 }
Вызван обработчик сообщения
WM_MOUSEACTIVATE
классаCTracingEdit
. ОС посылает это сообщение окну, которое находилось под курсором мыши во время щелчка. Код возврата сообщает ОС что следует делать далее - активировать окно или нет, пропускать сообщение о нажатии или же удалить его из очереди. Обработчик по умолчанию посылает это же сообщение родительскому окну. Обработкик корневых окон возвращаетMA_ACTIVATE
, что приводит к активации окна и получению сообщений о щелчке.При такой схеме элемент управления может определить реакцию окна.
Стоит заметить, что сообщение синхронное и его обработчик вызывается из кода режима ядра, управление которому передается вызовом функции
GetMessage
из кода приложения. Если быть совсем точным, то процедура активации в даном сценарии происходит за один вызовGetMessage
, которая в результате вернетWM_LBUTTONDOWN
(см. далее).> Win32App.exe!CTracingEdit::OnMouseActivate(CWnd * pDesktopWnd, unsigned int nHitTest, unsigned int message) Line 115 C++ Win32App.exe!CWnd::OnWndMsg(unsigned int message, unsigned int wParam, long lParam, long * pResult) Line 2375 + 0x2f bytes C++ Win32App.exe!CWnd::WindowProc(unsigned int message, unsigned int wParam, long lParam) Line 2087 + 0x20 bytes C++ Win32App.exe!AfxCallWndProc(CWnd * pWnd, HWND__ * hWnd, unsigned int nMsg, unsigned int wParam, long lParam) Line 257 + 0x1c bytes C++ Win32App.exe!AfxWndProc(HWND__ * hWnd, unsigned int nMsg, unsigned int wParam, long lParam) Line 420 C++ user32.dll!_InternalCallWinProc@20() + 0x23 bytes user32.dll!_UserCallWinProcCheckWow@32() + 0xb3 bytes user32.dll!_DispatchClientMessage@20() + 0x4b bytes user32.dll!___fnDWORD@4() + 0x24 bytes ntdll.dll!_KiUserCallbackDispatcher@12() + 0x2e bytes user32.dll!_DispatchClientMessage@20() user32.dll!_NtUserGetMessage@16() + 0xc bytes user32.dll!_GetMessageW@16() + 0x2b bytes Win32App.exe!AfxInternalPumpMessage() Line 153 + 0x13 bytes C++ Win32App.exe!AfxPumpMessage() Line 193 C++ Win32App.exe!wWinMain(HINSTANCE__ * hInstance, HINSTANCE__ * hPrevInstance, wchar_t * lpCmdLine, int nCmdShow) Line 135 + 0x5 bytes C++
<-> 1AC4: CBTProc(HCBT_ACTIVATE, hWnd = B0B70, AS = { hWndActive = 0, fMouse = 1 }) { FW = 0, AW = 0, F = 0 }
ОС сообщает приложению, что окно
B0B70
сейчас будет активировано. Обратите внимание, что Foreground Window (FW), Active Window (AW) и Focused Window (F) - не определены (равны нулю).
--> 1AC4: CTopLevelWnd(B0B70)::OnActivateApp(bActive = 1, dwThreadID = 0) { FW = B0B70, AW = B0B70, F = 0 } <-- 1AC4: CTopLevelWnd(B0B70)::OnActivateApp(bActive = 1, dwThreadID = 0) { FW = B0B70, AW = B0B70, F = 0 }
Обработчик сообщения
WM_ACTIVATEAPP.
Посылается всем окнам верхнего уровня, которые принадлежат потоку активируемого окна, тоесть1AC4
. Окно в этом сценарии всего одно. Foreground Window (FW) и Active Window (AW) уже установлены.
--> 1AC4: CTopLevelWnd(B0B70)::OnNcActivate(bActive = 1) { FW = B0B70, AW = B0B70, F = 0 } <-- 1AC4: CTopLevelWnd(B0B70)::OnNcActivate(bActive = 1) { FW = B0B70, AW = B0B70, F = 0 }
Обработчик сообщения
WM_NCACTIVATE
. Код возврата в этом случае игнорируется.
--> 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 2, pWndOther = 0, bMinimized = 0) { FW = B0B70, AW = B0B70, F = 0 }
Обработчик сообщения
WM_ACTIVATE
. По умолчанию вызываетSetFocus(self)
, устанавливает фокус на активируемое окно.
<-> 1AC4: CBTProc(HCBT_SETFOCUS, hSetFocusWnd = B0B70, hKillFocusWnd = 0) { FW = B0B70, AW = B0B70, F = 0 }
ОС сообщает приложению, что фокус ввода собирается переместиться из ниоткуда
(hKillFocusWnd = 0)
в окноB0B70
(этоCTopLevelWnd
). Focused Window (F) пока что неопределен.
--> 1AC4: CTopLevelWnd(B0B70)::OnSetFocus(pOldWnd = 0) { FW = B0B70, AW = B0B70, F = B0B70 } <-- 1AC4: CTopLevelWnd(B0B70)::OnSetFocus(pOldWnd = 0) { FW = B0B70, AW = B0B70, F = B0B70 }
Уведомление о попадании фокуса в окно.
<-- 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 2, pWndOther = 0, bMinimized = 0) { FW = B0B70, AW = B0B70, F = B0B70 }
Выход из обработчика
WM_ACTIVATE
.
<-> 1AC4: GetMsgProc(wParam = PM_REMOVE, lParam = { message = WM_LBUTTONDOWN } { FW = B0B70, AW = B0B70, F = B0B70 }
Здесь ОС сделала все, что хотела, поэтому собирается вернуть управление в приложение.
--> 1AC4: CTracingEdit(1709F4)::OnLButtonDown(nFlags = 1, point = {116, 13}) { FW = B0B70, AW = B0B70, F = B0B70 }
Нажатие мыши наконец дошло до элемента управления.
<-> 1AC4: CBTProc(HCBT_SETFOCUS, hSetFocusWnd = 1709F4, hKillFocusWnd = B0B70) { FW = B0B70, AW = B0B70, F = B0B70 }
Эдемент управления реагирует на сообщение изменением фокуса. Поле ввода забирает фокус на себя, о чем говорят следующие строки.
--> 1AC4: CTopLevelWnd(B0B70)::OnKillFocus(pNewWnd = 1709F4) { FW = B0B70, AW = B0B70, F = 1709F4 } <-- 1AC4: CTopLevelWnd(B0B70)::OnKillFocus(pNewWnd = 1709F4) { FW = B0B70, AW = B0B70, F = 1709F4 } --> 1AC4: CTracingEdit(1709F4)::OnSetFocus(pOldWnd = B0B70) { FW = B0B70, AW = B0B70, F = 1709F4 } <-- 1AC4: CTracingEdit(1709F4)::OnSetFocus(pOldWnd = B0B70) { FW = B0B70, AW = B0B70, F = 1709F4 }
<-- 1AC4: CTracingEdit(1709F4)::OnLButtonDown(nFlags = 1, point = {116, 13}) { FW = B0B70, AW = B0B70, F = 1709F4 }
Здесь мы наконец возвращаемся к циклу сообщений.
<-> 1AC4: GetMsgProc(wParam = PM_NOREMOVE, lParam = { message = WM_LBUTTONUP } { FW = B0B70, AW = B0B70, F = 1709F4 } <-> 1AC4: GetMsgProc(wParam = PM_REMOVE, lParam = { message = WM_LBUTTONUP } { FW = B0B70, AW = B0B70, F = 1709F4 }
И получаем из очереди
WM_LBUTTONUP
.
--> 1AC4: CTracingEdit(1709F4)::OnLButtonUp(nFlags = 0, point = {116, 13}) { FW = B0B70, AW = B0B70, F = 1709F4 } <-- 1AC4: CTracingEdit(1709F4)::OnLButtonUp(nFlags = 0, point = {116, 13}) { FW = B0B70, AW = B0B70, F = 1709F4 }
Обработчик которого не делает ничего полезного.
Какие выводы можно сделать из вышенаписанного?
- Элементы управления могут управлять активацией корневых окон.
- Код ОС, активирующий окна и устанавливающий фокус ввода, выполняется после попадания асинхронного сообщения в очередь и до изымания этого сообщения из очереди приложением (если окно активируется мышью). В противном случае - просто ниже по стеку от вызовов
GetMessage
илиPeekMessage
с флагомPM_REMOVE
. - Если окно активируется не мышью, а другим способом (Alt-TAB, например), - происходит все то же, за исключением обработчиков
WM_MOUSEACTIVATE
,WM_LBUTTONDOWN
+WM_LBUTTONUP
(GetMessage
не изымает асинхронные сообщения из очереди, а все процедуры активации происходят без выхода изGetMessage
). - ОС не восстанавливает фокус ввода. Если окно было активно и фокус ввода находился на кнопке - то при деактивации и последующей активации он автоматически не вернется обратно на кнопку. За этим нужно сделить самостоятельно. Или воспользоваться оконными процедурами диалоговых окон, которые выполняют эти действия за нас.
- Лучшее место для восстановления фокуса - обработчик
WM_ACTIVATE
. Если при выходе из него фокус все еще неопределен - ОС устанавливает его на активируемое окно, так же, как делает это обработчикWM_ACTIVATE
по умолчанию. - При щелчке на элементы управления (кнопки, списки, поля воода) ОС автоматически не устанавливает фокус ввода в целевое окно. Этим занимаются сами элементы управления, обрабатывая
WM_?BUTTONDOWN
.
При деактивации окна вызываются обработчики сообщений WM_NCACTIVATE
, WM_ACTIVATE
, WM_ACTIVATEAPP
, WM_KILLFOCUS
. Обратите внимание как изменяется состояние рабочего стола и потока во время вызова обработчиков по умолчанию. Например на момент входа в CTopLevelWnd::OnNcActivate
Foreground Window неопределен (равен нулю), а после вызова обработчика по умолчанию - он уже равен дескриптору окна Notepad. Поскольку пачка всех этих сообщений обрабатывается синхронно - то они посылаются последовательно активирующимся окнам и деактивирующимся, даже если окна принадлежат разным потокам или даже процессам.
В следующей части мы рассмотрим этот сценарий более детально - посмотрим как перемешиваются сообщения активирущегося окна и деактивирующегося. И как можно управлять активацией, вместе с ломанием стереотипа о том, что на рабочем столе может быть лишь одно активированное окно, а также единственное окно с логическим фокусом ввода.