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

Implementing popups in MFC (part 1)

Создание всплывающих окон - задача не из легких. Особенно если это не банальная контекстная подсказка, а, скажем, окно с элементами управления или необходимостью пользовательского взаимодействия (мышь, клавиатура). Самый распостраненный случай всплывающего окна - Combo Box.

Великий и могучий Combo Box.

Combo Box - это сложный элемент управления, как правило состоящий из списка (List Box), поля ввода (Text Box) и несколько кастрированной кнопки (Button).

Особенности работы Combo Box:

  • При нажатии на кнопку (это не единственный способ) появляется выпадающий список
  • Это окно по своей сути является стандартным List Box-ом, но на стероидах (с именем класса ComboLBox)
  • Фокус ввода остается на самом Combo Box-е, всплывшее окно со списком не активируется
  • Выпадающее окно производит захват мыши вызовом SetCapture, в результате чего все мышиные сообщения для всех окон процесса попадают исключительно в оконную процедуру всплывшего окна
  • При щелчке мышью вне выпадающего окна - оно скрывается сразу же при обработке WM_?BUTTONDOWN, при этом захват мыши отменяется вызовом ReleaseCapture, что приводит к получению WM_?BUTTONUP окном, которое находилось под курсором в момент отжатия кнопки
  • При щелчке внутри выпадающего окна, оно реагирует закрытием во время обработки WM_?BUTTONUP
  • Список реагирует на нажатия стрелочных кнопок (вверх, вниз); очевидно, что сообщения WM_KEYDOWN приходят в сам Combo Box, а он их передает в выпадающий список
  • При потере Combo Box-ом фокуса ввода - список прячется

Важные выводы:

  1. ComboBox работает правильно вне зависимости от наличия окон в различных UI потоках
  2. Во время отображения окна выпадающего списка лишь оно из всех окон процесса получает мышиные сообщения - соответственно не должно происходить никаких реакций на движение курсором мыши вне этого окна (всплывающие подсказки, подсветка кнопок, изменение картинки курсора и т.д.)
  3. Щелчок мышью вне окна съедается

А если не Combo Box?

В ОС существует множество выпадающих окон кроме Combo Box. Среди них - Date and Time Picker, меню (стандартное меню ОС, навороченное меню типа Ribbon или другие реализации, например WPF) и множество других. И я вас уверяю - единой концепции взаимодействия с пользователем у них нет...

Window menu

Стандартное меню, которое предоставляет операционная система, работает безотказно, но весьма ограничено в отображаемом контенте и функциональности. Я остановил процесс по таймеру во время трекинга меню функцией TrackPopupMenu. Эта функция блокирует выполнение программы и реализована ядром. Но при этом цикл сообщений крутится внутри нее, поэтому программа остается отзывчивой:

> Win32App.exe!CTopLevelWnd::OnTimer(unsigned int nIDEvent)  Line 211 C++
  Win32App.exe!CWnd::OnWndMsg(unsigned int message, unsigned int wParam, long lParam, long * pResult)  Line 2411 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!_NtUserTrackPopupMenuEx@24()  + 0xc bytes 
  user32.dll!_TrackPopupMenu@28()  + 0x1b bytes 
  Win32App.exe!CMenu::TrackPopupMenu(unsigned int nFlags, int x, int y, CWnd * pWnd, const tagRECT * lpRect)  Line 1310 + 0x26 bytes C++

Я даже не поленился и просмотрел исходный код в ворованных архивах Windows 2000. Это меню очень тесно интегрировано с оконными процедурами и всем, что обслуживает UI. Не мудрено, что во время останова программы, когда на экране меню, вся система начала вести себя немного странно.

В отличии от Combo Box, контекстное меню ОС не съедает щелчок вне окна меню, но при этом так же не пропускает WM_MOUSEMOVE и WM_NCMOUSEMOVE.

Codejock menu

А это реализация навороченного меню библиотекой Codejock Xtreme Toolkit Pro. WM_MOUSEMOVE и WM_NCMOUSEMOVE не блокируются, даже контекстная подсказка выскочила, что не очень то правильно. Точней они не блокируются для текущих контекстных окон (а их там два), блокируются для оставшихся тулбаров этой библиотеки (Ribbon в даном случае), а для остальных окон приложения (полоса прокрутки, область редактирования текста, бордера и пр.) - снова не блокируются.

Как и в случае с контекстным меню ОС, не съедает щелчек вне окон контекста.

Собственная реализация TrackPopupMenu устанавливает различные хуки для слежения за сообщениями, механизм SetCapture/ReleaseCapture здесь уже не при делах.

Кстати, прототип этой реализации - Microsoft Office 2007/2010, в котором WM_MOUSEMOVE блокируется для всех окон вне контекста, а WM_NCMOUSEMOVE работает слегка через жопу - при наведении на бордер окна курсор меняет свой внешний вид, кнопки минимизации, максимизации и закрытия окон прикидываются мертвыми, но если пошуршать курсором у правого-верхнего края окна, то крестик будет предательски нам подмигивать, выдавая ошибку в реализации этой задумки. Scenic Ribbon в, скажем, MS Paint имеет те же проблемы.

Visual Studio 2010 menu

На этой картинке находится кусочек навороченного меню Visual Studio 2010, очевидно реализованного управляемым кодом. WM_MOUSEMOVE блокируется, а вот в случае WM_NCMOUSEMOVE все несколько сложнее. По моим наблюдениям не блокируется движение мышью по заголовку окна, кнопки минимизации, максимизации и закрытия окон реагируют на наведение подсветкой, но при этом контекстная подсказка не появляется (как это сделано - пока что загадка). При наведении на полосу прокрутки никакой реакции нет.

Как и в случае с контекстным меню ОС, не съедает щелчек вне окон контекста.

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

Предательские реализации всплывающих контекстных окон

Explorer menu

Контекстная подсказка этой кнопочки гордо сообщает нам о возможности Change your view. А при нажатии появляется якобы контекстное меню, которое почему-то приводит к деактивации главного окна Explorer. Взгляните на цвет заголовка, он ясно дает понять - окно более не активно. Предательское окно ворует фокус ввода, он находится на элементе управления типа Slider. Причем рамка фокуса изначально не отображается, лишь когда нажать клавиши стрелок (вверх или вниз).

При этом на блокировку WM_??MOUSEMOVE нет даже намека - все элементы управления реагируют на перемещение курсора, всплывают подсказки, причем под этим меню. А самое вкусное - WM_?BUTTONDOWN проглатывается, а последующий WM_?BUTTONUP проходит. Результат схлопывания меню зависит от реализации элемента управления, по которому был произведен щелчок, - ответная реакция на WM_?BUTTONDOWN или же на WM_?BUTTONUP.

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

Весьма странная реализация, если сравнить с предыдущими.

Альтернативный подход

Открыв контекстное меню Firefox, вы убедитесь, что оно никак не влияет на остальные окна, что, впрочем, выглядит вполне логично. Щелчки обрабатываются как обычно, но перед этим меню пропадает с экрана.

С другой стороны, отсутствие реакции окон на движение мышью подсказывает пользователю, что где-то открыто контекстное меню или модальный диалог, но лишь потому, что ОС Windows работает подобным образом.

Реализация собственных контекстных меню

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

  1. Реакция окон (этого же приложения) вне контекста на перемещение указателя мыши
  2. Щелчки мышью по окнам (этого же приложения) вне контекста
  3. Деактивация активного окна приложения

Отслеживание мыши

Для слежения за мышью есть такие подходы:

  • SetCapture/ReleaseCapture

    С помощью этих функций можно произвести захват/освобождение мыши и направить все сообщения в оконную процедуру одного окна. Но именно с этим связана огромная проблема - мы же хотим отображать сложные окна с элементами управления, которые тоже должны получать мышиные сообщения. В результате обработчики WM_??MOUSE?? выпадающего окна должны будут сами обеспечивать попадание нужных сообщений в оконные процедуры дочерних окон. Нужных - хорошо написано, а на деле - всех возможных. Фактически мы будем эмулировать то, что делает сама ОС Windows. Может показаться, что достаточно лишь найти окно под курсором функцией WindowFromPoint и вызвать оконную процедуру найденного окна, но на самом деле нюансов огромное множество, как и эмулируемых сообщений.

    Сам по себе этот сценарий не позволяет контролировать механизм проглатывания щелчков вне контекста. Допустим, при щелчке мы хотим спрятать выпадающее окно, а после этого получить реакцию на щелчок окном, по которому он был произведен. Из-за SetCapture сообщение WM_LBUTTONDOWN извлекается из очереди и попадает в оконную процедуру выпадающего окна, где код определяет положение курсора и прячет окно, если оно находилось вне его области. Далее нужно обеспечить обработку этого сообщения, для этого вызываем AfxCallWndProc (или просто SendMessage). И делаем мы это из обработчика WM_LBUTTONDOWN нашего выпадающего окна! А что если реакцией на щелчок будет повторное выпадение окна (возможно вовсе не этого)? Мы пополним стек потока на добрую дюжину вызовов. И вот здесь все зависит от цикла сообщений. Есть два варианта:

    1. Показать окно и выйти из метода ShowPopup (неблокирующий вызов).

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

    2. Показать окно и запустить свой цикл сообщений в методе ShowPopup (как это делает TrackPopupMenu, блокирующий вызов).

      В этом случае мы можем вызвать переполнение стека банальными щелчками мыши, которые постоянно приводят к пропаданию текущего выпадающего окна и появлению нового - метод ShowPopup будет вызываться рекурсивно, каждый раз запуская новый цикл сообщений. Это весомый аргумент против. А бороться с ним крайне сложно. Фактически нужно куда-то сохранить полученное сообщение, которое привело к закрытию выпадающего окна, и уведомить внешний цикл сообщений, что его нужно обработать. К сожалению MFC такой тонкой настройки не предоставляет, придется заменить его цикл сообщений своим (банально скопировав код MFC и добавив туда свое). Это уже как-то слишком сложно для поддержки выпадающих окон.

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

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

  • Перехватчики (hooks) WH_GETMESSAGE, WH_MOUSE и WH_MOUSE_LL

    С их помощью можно следить за очередью сообщений, отлавливать интересные события. Перехватчики - единственный способ гарантированно получить доступ к содержимому очереди сообщений. Можно было написать свой цикл сообщений (вариант блокирующего вызова ShowPopup) и в промежутке между GetMessage и DispatchMessage выполнять анализ и предпринимать действия. Но есть одна проблема - циклы сообщений могут запускаться в результате реакций на действия пользователя. К примеру, если открыть Date and Time picker, - он запускал вложенный цикл сообщений в версиях ОС Windows до Vista, а также в версиях Common Controls до 6-й. Все это приводит к неконтролируемой потере управления над циклом сообщений и, как последствие, невозможности отлавливать интересующие события.

    Перехватчики WH_GETMESSAGE и WH_MOUSE получают управление перед возвратом функциями GetMessage и PeekMessage управления в случае наличия сообщения в очереди (удовлетворяющего критериям). Однако WH_MOUSE позволяет выполнять лишь мониторинг сообщений, а WH_GETMESSAGE - модифицировать ко всему прочему. WH_MOUSE_LL в этом плане похож на своего собрата WH_MOUSE, но система дает ему управление перед тем, как поместить сообщение в очередь. Это дает возможность спрятать всплывающее окно прежде, чем цикл сообщений получит щелчок мышью, который к этому привел.

Стратегии реализации

Реализация набора классов, которые позволяют показывать всплывающие окна, зависит от следующих факторов:

  • Механизм работы метода ShowPopup - блокирующий или неблокирующий
  • Необходимость проглатывания сообщений, которые приводят к закрытию всплывающего окна
  • Фильтрация перемещений указателя мыши
  • Отсутствие привязки к библиотеке, с момощью которой окно было создано (MFC, ATL, ...) - возможность работы с окнами, передаваемыми по дескриптору (HWND)
  • Возможность предоставить некую политику функционирования, с помощью которой можно сделать тонкую настройку всего механизма и реализовать сложные сценарии

Я остановился на блокирующем ShowPopup, с внутренним циклом обработки сообщений и возможностью настройки поведения через политику. Интерфейс моего класса CPopupController выглядит следующим образом:

public:
   CPopupController(IPopupEnvironment *pEnvironment);
 
public:
   void ShowPopup(HWND hWnd, HWND hOwner, const POINT &ptAnchor, IPopupPolicy *pPopupPolicy = NULL);
   void DismissPopup();

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

  1. Деактивация приложения (foreground thread больше не является потоком этого приложения)
  2. Щелчки вне контекста
  3. Уничтожение окна
  4. Изменение состояний окна на такие, в которых режим меню смысла больше не имеет (окно invisible, disabled etc.)

Реализовать это поведение достаточно просто - привязаться к всплывающему окну и реагировать на сообщения, присылаемые ОС Windows. Для этого можно использовать класс CWindowSubclass:

BEGIN_MSG_MAP(CPopupController)
   MESSAGE_HANDLER(WM_ACTIVATE, OnActivate)
   MESSAGE_HANDLER(WM_ACTIVATEAPP, OnActivateApp)
   MESSAGE_HANDLER(WM_SHOWWINDOW, OnShowWindow)
   MESSAGE_HANDLER(WM_ENABLE, OnEnable)
   MESSAGE_HANDLER(WM_SYSCOMMAND, OnSysCommand)
   MESSAGE_HANDLER(WM_MOUSEACTIVATE, OnMouseActivate)
   MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
END_MSG_MAP()
LRESULT CPopupController::OnActivate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(lParam);
 
   if (wParam == WA_INACTIVE) {
      if (IPopupPolicy * const pPopupPolicy = GetPopupPolicy()) {
         if (pPopupPolicy->DismissPopupOnLosingActivation()) {
            DismissPopup();
         }
      }
      else
         DismissPopup();
   }
   bHandled = FALSE;
   return 0;
}
 
LRESULT CPopupController::OnActivateApp(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(lParam);
 
   if (wParam == FALSE) {
      DismissPopup();
   }
 
   bHandled = FALSE;
   return 0;
}
 
LRESULT CPopupController::OnShowWindow(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(lParam);
 
   if (wParam == FALSE) {
      DismissPopup();
   }
 
   bHandled = FALSE;
   return 0;
}
 
LRESULT CPopupController::OnEnable(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(lParam);
 
   if (wParam == FALSE) {
      DismissPopup();
   }
 
   bHandled = FALSE;
   return 0;
}
 
LRESULT CPopupController::OnSysCommand(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(lParam);
   
   if (wParam == SC_CLOSE) {
      DismissPopup();
   }
   
   bHandled = FALSE;
   return 0;
}
 
LRESULT CPopupController::OnMouseActivate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(wParam);
   UNREFERENCED_PARAMETER(lParam);
 
   if (m_pPopupPolicy) {
      const IPopupPolicy::eMouseActivateResult maResult = m_pPopupPolicy->MouseActivateAction();
      if (maResult != IPopupPolicy::eDefaultMouseActivateAction) {
         return maResult;
      }
   }
 
   bHandled = FALSE;
   return 0;
}
 
LRESULT CPopupController::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
   UNREFERENCED_PARAMETER(uMsg);
   UNREFERENCED_PARAMETER(wParam);
   UNREFERENCED_PARAMETER(lParam);
   
   ContinueMessageLoop(false);
 
   bHandled = FALSE;
   return 0;
}

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

Если вдруг код приложения по какой-то причине спрячет окно, уничтожит его, или же поменяет его состояние на disabled, - контроллер автоматически выполнит закрытие режима меню.

Следующий шаг - игра в кошки-мышки. Будем гоняться за мышью и лапкой (точней рукой сурового программиста) давать ей по носу. Для этого нам необходим перехватчик сообщений WH_GETMESSAGE. Здесь в игру вступает класс CPopupManager:

bool EnterPopupMode(CPopupController *pController);
bool IsPopupMode() const;
void ExitPopupMode(CPopupController *pController);

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

Полная версия менеджера выглядит следующим образом:

#pragma once
 
class CPopupController;
 
class CPopupManager : public CNoTrackObject
{
 
public:
   CPopupManager();
 
   static CPopupManager &Instance();
 
   bool EnterPopupMode(CPopupController *pController);
   bool IsPopupMode() const;
   void ExitPopupMode(CPopupController *pController);
 
private:
   static LRESULT CALLBACK GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam);
   static LRESULT CALLBACK CBTProc(int nCode, WPARAM wParam, LPARAM lParam);
   
   bool PreFilterMessage(LPMSG pMsg, bool preview);
   bool ActivatingWindow(HWND hWnd, LPCBTACTIVATESTRUCT pCBAS);
   bool SettingFocus(HWND hSetFocusWnd, HWND hKillFocusWnd);
 
private:
   CPopupController *m_pController;
   HHOOK m_hGetMessageHook;
   HHOOK m_hCBTHook;
 
};
#include "stdafx.h"
#include "PopupManager.h"
#include "PopupController.h"
#include "PopupPolicy.h"
 
CProcessLocal<CPopupManager> afxPopupManager;
 
CPopupManager::CPopupManager() : m_pController(NULL), m_hGetMessageHook(NULL), m_hCBTHook(NULL)
{
}
 
CPopupManager &CPopupManager::Instance()
{
   return(*afxPopupManager);
}
 
bool CPopupManager::EnterPopupMode(CPopupController *pController)
{
   ATLASSERT(pController && !IsPopupMode());
   if (!pController || IsPopupMode())
      return false;
 
   m_hGetMessageHook = ::SetWindowsHookEx(WH_GETMESSAGE, &CPopupManager::GetMsgProc, NULL, ::GetCurrentThreadId());
   ATLASSERT(m_hGetMessageHook);
 
   if (pController->GetPopupPolicy()) {
      m_hCBTHook = ::SetWindowsHookEx(WH_CBT, &CPopupManager::CBTProc, NULL, ::GetCurrentThreadId());
      ATLASSERT(m_hCBTHook);
   }
 
   m_pController = pController;
 
   return true;
}
 
bool CPopupManager::IsPopupMode() const
{
   return m_pController != NULL;
}
 
void CPopupManager::ExitPopupMode(CPopupController *pController)
{
   ATLASSERT(IsPopupMode());
   ATLASSERT(m_pController == pController);
 
   m_pController = NULL;
 
   if (m_hCBTHook) {
      ATLVERIFY(::UnhookWindowsHookEx(m_hCBTHook));
      m_hCBTHook = NULL;
   }
 
   if (m_hGetMessageHook) {
      ATLVERIFY(::UnhookWindowsHookEx(m_hGetMessageHook));
      m_hGetMessageHook = NULL;
   }     
}
 
LRESULT CPopupManager::GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam)
{
   if (nCode < 0) {
      return(::CallNextHookEx(NULL, nCode, wParam, lParam));
   }
 
   if (nCode == HC_ACTION) {
      LPMSG const pMsg = reinterpret_cast<LPMSG>(lParam);
      if (WM_MOUSEFIRST <= pMsg->message && pMsg->message <= WM_MOUSELAST
          || WM_NCMOUSEMOVE <= pMsg->message && pMsg->message <= WM_NCXBUTTONDBLCLK
          || WM_KEYFIRST <= pMsg->message && pMsg->message <= WM_KEYLAST) {
         if (Instance().PreFilterMessage(pMsg, wParam == PM_NOREMOVE)) {
            return 0;
         }
      }
   }
 
   return(::CallNextHookEx(NULL, nCode, wParam, lParam));
}
 
LRESULT CPopupManager::CBTProc(int nCode, WPARAM wParam, LPARAM lParam)
{
   if (nCode < 0) {
      return(::CallNextHookEx(NULL, nCode, wParam, lParam));
   }
 
   if (nCode == HCBT_ACTIVATE) {
      // The system is about to activate a window.
      if (Instance().ActivatingWindow(reinterpret_cast<HWND>(wParam), reinterpret_cast<LPCBTACTIVATESTRUCT>(lParam))) {
         return 0;
      }
   }
   else if (nCode == HCBT_SETFOCUS) {
      // A window is about to receive the keyboard focus.
      if (Instance().SettingFocus(reinterpret_cast<HWND>(wParam), reinterpret_cast<HWND>(lParam))) {
         return 0;
      }
   }
 
   return(::CallNextHookEx(NULL, nCode, wParam, lParam));
}
 
bool CPopupManager::PreFilterMessage(LPMSG pMsg, bool preview)
{
   ATLASSERT(m_pController);
   if (m_pController)
      return m_pController->PreFilterMessage(pMsg, preview);
   return false;
}
 
bool CPopupManager::ActivatingWindow(HWND hWnd, LPCBTACTIVATESTRUCT pCBAS)
{
   ATLASSERT(m_pController);
   if (m_pController)
      return m_pController->ActivatingWindow(hWnd, pCBAS);
   return false;
}
 
bool CPopupManager::SettingFocus(HWND hSetFocusWnd, HWND hKillFocusWnd)
{
   ATLASSERT(m_pController);
   if (m_pController)
      return m_pController->SettingFocus(hSetFocusWnd, hKillFocusWnd);
   return false;
}

В случае наличия политики также устанавливается перехватчик WH_CBT, который необходим для слежения за фокусом ввода и активациями окон.

Возвращаемся к кошкам-мышкам:

bool CPopupController::PreFilterMessage(LPMSG pMsg, bool preview) {
   if (IPopupPolicy * const pPopupPolicy = GetPopupPolicy()) {
      const IPopupPolicy::ePreFilterResult preFilterResult = pPopupPolicy->PreFilterMessage(pMsg, preview);
      if (preFilterResult == IPopupPolicy::ePreFilterBlockMessage) {
         pMsg->message = WM_NULL;
         return true;
      }
      else if (preFilterResult == IPopupPolicy::ePreFilterAcceptMessage) {
         return false;
      }
      // eContinueRouting
   }
 
   if (Dismissing())
      return false;
   
   bool dismiss = false;
   
   const HWND hCapture = ::GetCapture();   
   const bool isMouseButtonAction = IsMouseButtonAction(pMsg->message);
 
   if (hCapture) {
      if (isMouseButtonAction) {
         POINT ptAction = { GET_X_LPARAM(pMsg->lParam),  GET_Y_LPARAM(pMsg->lParam) };
         ATLVERIFY(::ClientToScreen(hCapture, &ptAction));
 
         const HWND hActionWnd = ::WindowFromPoint(ptAction);
         dismiss = !hActionWnd || hActionWnd != hCapture && !IsOwnerOrSelf(GetPopupHWnd(), hActionWnd);
      }
   }
   else {
      if (isMouseButtonAction) {
         dismiss = !IsOwnerOrSelf(GetPopupHWnd(), pMsg->hwnd);
      }
      else if (pMsg->message == WM_MOUSEMOVE) {
         pMsg->message = WM_NULL;
         return true;
      }
   }
 
   if (dismiss) {
      DismissPopup();
   }
 
   return dismiss;
}

Это код делает следующее:

  1. В первую очередь дает возможность политике фильтровать сообщения
  2. Ничего не делает если режим меню в состоянии закрытия
  3. Выясняет состояние захвата мыши и совершаемое мышью действие
  4. Если мышь захвачена неким окном - нужно выяснить какому окну предназначено сообщение в случае, если бы захват мыши не был установлен вовсе
  5. Если целевое окно - то, которое выполнило захват, одно из его дочерних окон, а также само выпадающее окно (вместе с его содержимым) - сообщение нужно пропустить
  6. В противном случае - выйти из режима меню
  7. Когда захвата мыши нет - выйти из режима меню в случае, когда произвели щелчок по окну вне контекста, а также проглотить сообщение WM_MOUSEMOVE вне контекста. WM_NCMOUSEMOVE я решил пропускать - ничего страшного в этом нет, пускай полосы прокрутки и остальные неклиентские элементы реагируют на происходящее

Зачем нужны все эти танцы с захватом мыши? Чтобы работали элементы управления типа Combo Box, Button и пр. Combo Box, как было описано выше, сам показывает выпадающее окно, используя SetCapture для фильтрации мыши.

Далее рассмотрим подготовку и вход в режим меню. Этим занимается метод ShowPopup:

void CPopupController::ShowPopup(HWND hPopup, HWND hOwner, const POINT &ptAnchor, IPopupPolicy *pPopupPolicy/* = NULL*/)
{
   CPopupManager &manager = CPopupManager::Instance();
 
   ATLASSERT(!manager.IsPopupMode());
   ATLASSERT(!m_hPopup);
   if (manager.IsPopupMode() || m_hPopup)
      return;
   
   const bool popupOk = hPopup && ::IsWindow(hPopup) && ::IsWindowEnabled(hPopup)
                               && (::GetWindowLongPtr(hPopup, GWL_STYLE) & WS_CHILD) == 0
                               && ::GetWindowThreadProcessId(hPopup, NULL) == ::GetCurrentThreadId();
   ATLASSERT(popupOk);
   if (!popupOk)
      return;
 
   if (hOwner && ::IsWindow(hOwner) && (::GetWindowLongPtr(hOwner, GWL_STYLE) & WS_CHILD) != 0) {
      ATLASSERT(FALSE);
      hOwner = ::GetAncestor(hOwner, GA_ROOT);
   }
   else
      hOwner = ::GetActiveWindow();
 
   const bool ownerOk = hOwner && ::GetWindowThreadProcessId(hOwner, NULL) == ::GetCurrentThreadId();
   ATLASSERT(ownerOk);
   if (!ownerOk)
      return;
   
   m_hPopup = hPopup;
   ATLASSERT(GetPopupHWnd() == hPopup);
 
   m_state.reset();
 
   if (const CWindowSubclass &window = CWindowSubclass(GetPopupHWnd(), this)) {
      if (const PopupScope &popupScope = PopupScope(manager, this)) {
         if (pPopupPolicy)
            m_pPopupPolicy = pPopupPolicy;
 
         SetLastError(0);         
         if (!::SetWindowLongPtr(GetPopupHWnd(), GWL_HWNDPARENT, reinterpret_cast<LONG_PTR>(hOwner))) {
            ATLVERIFY(GetLastError() == 0);
         }
         const DWORD swpFlags = SWP_NOSIZE | SWP_NOSENDCHANGING
                                | (m_pPopupPolicy && m_pPopupPolicy->ShowWithoutActivation() ? SWP_NOACTIVATE : 0);
 
         RECT rcWindow = { ptAnchor.x, ptAnchor.y, ptAnchor.x, ptAnchor.y };
         ATLVERIFY(::GetWindowRect(GetPopupHWnd(), &rcWindow));
         const SIZE szWindow = { rcWindow.right - rcWindow.left, rcWindow.bottom - rcWindow.top };
         ATLVERIFY(::CalculatePopupWindowPosition(&ptAnchor, &szWindow, TPM_LEFTALIGN, NULL, &rcWindow));
 
         ATLVERIFY(::SetWindowPos(GetPopupHWnd(), HWND_TOP, rcWindow.left, rcWindow.top, 0, 0, swpFlags));
 
         if (!::IsWindowVisible(GetPopupHWnd())) {
            if (!::AnimateWindow(GetPopupHWnd(), 200, AW_BLEND)) {
               ATLVERIFY(::SetWindowPos(GetPopupHWnd(), HWND_TOP, 0, 0, 0, 0, swpFlags | SWP_NOMOVE | SWP_SHOWWINDOW));
            }
         }
 
         if (!Dismissing() && ::IsWindowVisible(GetPopupHWnd())) {
            do {
               RunMessageLoop();
            } while (Dismissing(false) && m_pPopupPolicy && m_pPopupPolicy->CancelDismiss());
         }
 
         if (pPopupPolicy)
            m_pPopupPolicy = NULL;         
      }
   }
 
   if (::IsWindowVisible(GetPopupHWnd())) {
      ::ShowWindow(GetPopupHWnd(), SW_HIDE);
   }
 
   m_hPopup = NULL;
}

Что здесь происходит:

  1. Проверки состояний менеджера (нельзя входить в режим меню рекурсивно);
  2. Проверки входящих параметров, состояний окон и т.д.;
  3. Контекстный (scoped) сабклассинг окна;
  4. Контекстный (scoped) вход в режим меню;
  5. Привязка окна-владельца к выпадающему окну;
  6. Определение позиции выпадающего окна и его показ (с анимацией, если это возможно);
  7. Цикл циклов сообщений;
  8. Очистка состояния.

Цикл сообщений выглядит следующим образом:

void CPopupController::RunMessageLoop()
{
   ContinueMessageLoop(true);
   
   MSG dummy;
   while (ContinueMessageLoop()) {
      while (::PeekMessage(&dummy, NULL, 0, 0, PM_NOREMOVE)) {
         if (!ContinueMessageLoop()) {
            break;
         }
 
         if (!PumpMessage()) {
            ::PostQuitMessage(0);
            DismissPopup();
            break;
         }
 
         if (!ContinueMessageLoop()) {
            break;
         }
      }
      if (ContinueMessageLoop()) {
         ::WaitMessage();
      }
   }
}

Я вот только что прикинул - цикл сообщений можно было бы перенести в менеджер. Подумаю на досуге.

Важный момент - использование PeekMessage с параметром PM_NOREMOVE. Это сделано для того, чтобы иметь возможность выйти из режима меню до обработки сообщения. Как следствие - иметь возможность не проглатывать сообщение, которое привело к закрытию меню.

Обратите внимание на реализацию PumpMessage:

bool CPopupController::PumpMessage() {
   return m_pEnvironment->PumpMessage();
}

Теперь пример использования этого чуда:

void CChildView::OnRButtonUp(UINT nFlags, CPoint point) {
   POINT ptPopup = point;
   ClientToScreen(&ptPopup);
   
   struct XPopupPolicy : IPopupPolicy
   {
      //virtual bool ShowWithoutActivation() { return false; }
      //virtual eMouseActivateResult MouseActivateAction() { return eNoActivate; }
   };
   
   CPopupDlg popup;
   popup.Create(IDD_POPUP, ::AfxGetMainWnd());
   XPopupPolicy xPopupPolicy;
   CMFCPopupEnvironment mfcPopupEnv;
   CPopupController popupController(&mfcPopupEnv);
   popupController.ShowPopup(popup.GetSafeHwnd(), ::AfxGetMainWnd()->GetSafeHwnd(), ptPopup, &xPopupPolicy);
 
   __super::OnRButtonUp(nFlags, point);
}

Мой диалог содержит пару кнопок и выпадающий список. Все это отлично работает.

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

Код в свободный доступ не выкладываю, жадный. Спасибо за внимание.

Wide monitor in portrait mode

А вы пробовали установить портретный режим для широкоформатных мониторов?

How to subclass a window multiple times

Иногда бывает необходимо привязаться к окну и добавить логики по обработке оконных сообщений. Механизм имеет всем известное название сабклассинг. MFC, как и многие библиотеки, поддерживает сабклассинг окон. Проблема лишь в том, что сделать это можно лишь один раз. А если надо много раз? А если к нам попадает хэндл окна и мы понятия не имеем кем оно было создано? Проблемка, однако. Но на помощь как всегда приходит могучий ATL.

#pragma once
 
#include <atlwin.h>
 
class CWindowSubclass
{
 
public:
   CWindowSubclass(CMessageMap *pObject, DWORD msgMapID = 0);
   CWindowSubclass(HWND hWnd, CMessageMap *pObject, DWORD msgMapID = 0);
   ~CWindowSubclass();
 
public:   
   void SwitchMessageMap(DWORD msgMapID);
   BOOL SubclassWindow(HWND hWnd);
   HWND UnsubclassWindow(bool force = false);
   HWND GetHWnd() const;
   operator bool() const;
 
private:   
   static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
   LRESULT DefWindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam);   
 
private:
   HWND m_hWnd;
   CWndProcThunk m_thunk;
   WNDPROC m_pfnSuperWindowProc;
   CMessageMap *m_pObject;
   DWORD m_msgMapID;
};
#include "stdafx.h"
#include "WindowSubclass.h"
 
CWindowSubclass::CWindowSubclass(CMessageMap *pObject,  DWORD msgMapID/* = 0*/)
   :  m_hWnd(NULL),         
      m_pObject(pObject),
      m_msgMapID(msgMapID),
      m_pfnSuperWindowProc(NULL)
{
}
 
CWindowSubclass::CWindowSubclass(HWND hWnd,  CMessageMap *pObject, DWORD msgMapID/* = 0*/)
   :  m_hWnd(NULL),
      m_pObject(pObject),
      m_msgMapID(msgMapID),
      m_pfnSuperWindowProc(NULL)
{
   SubclassWindow(hWnd);
}
 
CWindowSubclass::~CWindowSubclass()
{
   if (m_hWnd) {
      UnsubclassWindow(TRUE);
   }
}
 
HWND CWindowSubclass::GetHWnd() const
{
   return(m_hWnd);
}
 
CWindowSubclass::operator bool() const
{
   return m_hWnd != NULL;
}
 
void CWindowSubclass::SwitchMessageMap(DWORD msgMapID)
{
   m_msgMapID = msgMapID;
}
 
LRESULT CWindowSubclass::DefWindowProc(UINT uMsg,  WPARAM wParam,  LPARAM lParam)
{
#ifdef STRICT
   return ::CallWindowProc(m_pfnSuperWindowProc, m_hWnd, uMsg, wParam, lParam);
#else
   return ::CallWindowProc(reinterpret_cast<FARPROC>(m_pfnSuperWindowProc), m_hWnd, uMsg, wParam, lParam);
#endif
}
 
LRESULT CWindowSubclass::WindowProc(HWND hWnd,  UINT uMsg,  WPARAM wParam,  LPARAM lParam)
{
   CWindowSubclass * const pThis = reinterpret_cast<CWindowSubclass *>(hWnd);
 
   ATLASSERT(pThis);
   if (!pThis) {
      return 0;
   }
 
   ATLASSERT(pThis->m_hWnd);
   ATLASSERT(pThis->m_pObject);
   if (!pThis->m_hWnd || !pThis->m_pObject) {
      return 0;
   }
 
   LRESULT lRes = 0;
   BOOL bRet = pThis->m_pObject->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam, lParam, lRes, pThis->m_msgMapID);
   if (!bRet) {
      if (uMsg != WM_NCDESTROY)
         lRes = pThis->DefWindowProc(uMsg, wParam, lParam);
      else
      {
         const LONG_PTR pfnWndProc = ::GetWindowLongPtr(pThis->m_hWnd, GWLP_WNDPROC);
         lRes = pThis->DefWindowProc(uMsg, wParam, lParam);
         if (pThis->m_pfnSuperWindowProc && ::GetWindowLongPtr(pThis->m_hWnd, GWLP_WNDPROC) == pfnWndProc) {
            ::SetWindowLongPtr(pThis->m_hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(pThis->m_pfnSuperWindowProc));
         }
         pThis->m_hWnd = NULL;
      }
   }
   return lRes;
}
 
 
BOOL CWindowSubclass::SubclassWindow(HWND hWnd)
{      
   ATLASSUME(m_hWnd == NULL);
   ATLASSERT(::IsWindow(hWnd));
 
   const BOOL result = m_thunk.Init(WindowProc, this);
   if (result == FALSE)
      return result;
 
   const WNDPROC pProc = m_thunk.GetWNDPROC();
   const WNDPROC pfnWndProc = reinterpret_cast<WNDPROC>(::SetWindowLongPtr(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(pProc)));
   if (pfnWndProc == NULL)
      return FALSE;
   m_pfnSuperWindowProc = pfnWndProc;
   m_hWnd = hWnd;
   return TRUE;
}
 
HWND CWindowSubclass::UnsubclassWindow(bool force/* = false*/)
{
   ATLASSUME(m_hWnd != NULL);
 
   const WNDPROC pOurProc = m_thunk.GetWNDPROC();
   const WNDPROC pActiveProc = reinterpret_cast<WNDPROC>(::GetWindowLongPtr(m_hWnd, GWLP_WNDPROC));
 
   HWND hWnd = NULL;
   if (force || pOurProc == pActiveProc) {
      if (!::SetWindowLongPtr(m_hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(m_pfnSuperWindowProc)))
         return NULL;
 
      m_pfnSuperWindowProc = NULL;
      hWnd = m_hWnd;
      m_hWnd = NULL;
   }
   return hWnd;
}

Использовать этот класс проще простого, достаточно передать наследника ATL::CMessageMap в качестве параметра конструктора вместе с дескриптором окна. Плюс привычная для ATL конструкция:

BEGIN_MSG_MAP(MyCMessageMapDerivedClass)
   MESSAGE_HANDLER(WM_MESSAGE, OnMessage)
END_MSG_MAP()

Класс можно использовать множество раз для одного окна. За это спасибо механизму переходников ATL.

Copyright 2007-2011 Chabster