Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

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;
    
    1. flags

      Флажки состояний потока - мигающая каретка, меню, окно в процессе изменения размера или положения и т.п.

    2. hwndActive

      Дескриптор активного окна потока.

    3. hwndFocus

      Дескриптор окна потока, которое имеет логический фокус.

    4. hwndCapture

      Дескриптор окна потока, которое захватило мышь вызовом SetCapture.

    5. hwndMenuOwner

      Дескриптор окна потока, которое владеет текущим меню.

    6. hwndMoveSize

      Дескриптор окна потока, которое в процессе изменения размера или положения.

    7. hwndCaret

      Дескриптор окна потока, которое отображает каретку.

    8. 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. Обработчики интересных нам сообщений содержат трассировочный код, который отправляет полезную информацию в лог. Текст окон содержит идентификатор оконного потока, а за ним - дескриптор окна.

Трассировочная информация содержит следующие элементы:

  1. --> отмечает точку входа в обработчик, <-- - выхода; <-> идентифицирует операцию;
  2. далее следует идентификатор текущего потока;
  3. после имя класса (если мы попали в обработчик оконного сообщения);
  4. в скобках указывается дескриптор окна (если это окно либо элемент управления);
  5. потом имя функции или метода с параметрами;
  6. завершает это все статус рабочего стола/потока - Foreground Window (FW), Active Window (AW) и Focused Window (F).

На все потоки установлены хуки WH_GETMESSAGE и WH_CBT, трассирующие интересные события.

Цикл сообщений - свой. Каждому вызову GetMessage предшествует PeekMessage с флагом PM_NOREMOVE. Это сделано для того, чтобы мы видели сообщения, которые вытягивает сам поток, а также сообщения, которые вынимает оттуда ОС без нашего ведома.

Сценарий 1 - активация\деактивация окна приложения.

Scenario 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 }


  1. <-> 1AC4: GetMsgProc(wParam = PM_NOREMOVE, lParam = { message = WM_LBUTTONDOWN } { FW = 0, AW = 0, F = 0 }

    В очереди появилось сообщение WM_LBUTTONDOWN. Но ОС пока еще ничего не делает. Действия ниже ОС выполняет лишь когда сообщение изымается из очереди вызовом GetMessage или PeekMessage с флагом PM_REMOVE (а именно эти вызовы будут выше по стеку нежели выполняющийся далее код).


  2. --> 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++
    

  3. <-> 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) - не определены (равны нулю).


  4. --> 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) уже установлены.


  5. --> 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. Код возврата в этом случае игнорируется.


  6. --> 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 2, pWndOther = 0, bMinimized = 0) { FW = B0B70, AW = B0B70, F = 0 }

    Обработчик сообщения WM_ACTIVATE. По умолчанию вызывает SetFocus(self), устанавливает фокус на активируемое окно.


  7. <-> 1AC4: CBTProc(HCBT_SETFOCUS, hSetFocusWnd = B0B70, hKillFocusWnd = 0) { FW = B0B70, AW = B0B70, F = 0 }

    ОС сообщает приложению, что фокус ввода собирается переместиться из ниоткуда (hKillFocusWnd = 0) в окно B0B70 (это CTopLevelWnd). Focused Window (F) пока что неопределен.


  8. --> 1AC4: CTopLevelWnd(B0B70)::OnSetFocus(pOldWnd = 0) { FW = B0B70, AW = B0B70, F = B0B70 }
    <-- 1AC4: CTopLevelWnd(B0B70)::OnSetFocus(pOldWnd = 0) { FW = B0B70, AW = B0B70, F = B0B70 }

    Уведомление о попадании фокуса в окно.


  9. <-- 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 2, pWndOther = 0, bMinimized = 0) { FW = B0B70, AW = B0B70, F = B0B70 }

    Выход из обработчика WM_ACTIVATE.


  10. <-> 1AC4: GetMsgProc(wParam = PM_REMOVE, lParam = { message = WM_LBUTTONDOWN } { FW = B0B70, AW = B0B70, F = B0B70 }

    Здесь ОС сделала все, что хотела, поэтому собирается вернуть управление в приложение.


  11. --> 1AC4: CTracingEdit(1709F4)::OnLButtonDown(nFlags = 1, point = {116, 13}) { FW = B0B70, AW = B0B70, F = B0B70 }

    Нажатие мыши наконец дошло до элемента управления.


  12. <-> 1AC4: CBTProc(HCBT_SETFOCUS, hSetFocusWnd = 1709F4, hKillFocusWnd = B0B70) { FW = B0B70, AW = B0B70, F = B0B70 }

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


  13. --> 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 }

  14. <-- 1AC4: CTracingEdit(1709F4)::OnLButtonDown(nFlags = 1, point = {116, 13}) { FW = B0B70, AW = B0B70, F = 1709F4 }

    Здесь мы наконец возвращаемся к циклу сообщений.


  15. <-> 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.


  16. --> 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 }

    Обработчик которого не делает ничего полезного.

Какие выводы можно сделать из вышенаписанного?

  1. Элементы управления могут управлять активацией корневых окон.
  2. Код ОС, активирующий окна и устанавливающий фокус ввода, выполняется после попадания асинхронного сообщения в очередь и до изымания этого сообщения из очереди приложением (если окно активируется мышью). В противном случае - просто ниже по стеку от вызовов GetMessage или PeekMessage с флагом PM_REMOVE.
  3. Если окно активируется не мышью, а другим способом (Alt-TAB, например), - происходит все то же, за исключением обработчиков WM_MOUSEACTIVATE, WM_LBUTTONDOWN + WM_LBUTTONUP (GetMessage не изымает асинхронные сообщения из очереди, а все процедуры активации происходят без выхода из GetMessage).
  4. ОС не восстанавливает фокус ввода. Если окно было активно и фокус ввода находился на кнопке - то при деактивации и последующей активации он автоматически не вернется обратно на кнопку. За этим нужно сделить самостоятельно. Или воспользоваться оконными процедурами диалоговых окон, которые выполняют эти действия за нас.
  5. Лучшее место для восстановления фокуса - обработчик WM_ACTIVATE. Если при выходе из него фокус все еще неопределен - ОС устанавливает его на активируемое окно, так же, как делает это обработчик WM_ACTIVATE по умолчанию.
  6. При щелчке на элементы управления (кнопки, списки, поля воода) ОС автоматически не устанавливает фокус ввода в целевое окно. Этим занимаются сами элементы управления, обрабатывая WM_?BUTTONDOWN.

При деактивации окна вызываются обработчики сообщений WM_NCACTIVATE, WM_ACTIVATE, WM_ACTIVATEAPP, WM_KILLFOCUS. Обратите внимание как изменяется состояние рабочего стола и потока во время вызова обработчиков по умолчанию. Например на момент входа в CTopLevelWnd::OnNcActivate Foreground Window неопределен (равен нулю), а после вызова обработчика по умолчанию - он уже равен дескриптору окна Notepad. Поскольку пачка всех этих сообщений обрабатывается синхронно - то они посылаются последовательно активирующимся окнам и деактивирующимся, даже если окна принадлежат разным потокам или даже процессам.

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

2 коммент.:

Анонимный комментирует...

Hi there, I do believe your site may be having internet browser compatibility issues.
Whenever I look at your site in Safari, it looks fine however when opening in IE, it's got some overlapping issues. I simply wanted to provide you with a quick heads up! Aside from that, fantastic website!

my website :: iphone 5 keyboard

Анонимный комментирует...

webwinkel beginnen en belasting webwinkel beginnen in wat

Feel free to surf to my page; http://www.nuwebwinkelbeginnen.nl/

Отправить комментарий

Copyright 2007-2011 Chabster