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-ом фокуса ввода - список прячется
Важные выводы:
- ComboBox работает правильно вне зависимости от наличия окон в различных UI потоках
- Во время отображения окна выпадающего списка лишь оно из всех окон процесса получает мышиные сообщения - соответственно не должно происходить никаких реакций на движение курсором мыши вне этого окна (всплывающие подсказки, подсветка кнопок, изменение картинки курсора и т.д.)
- Щелчок мышью вне окна
съедается
А если не Combo Box?
В ОС существует множество выпадающих окон кроме Combo Box. Среди них - Date and Time Picker, меню (стандартное меню ОС, навороченное
меню типа Ribbon или другие реализации, например WPF) и множество других. И я вас уверяю - единой концепции взаимодействия с пользователем у них нет...
Стандартное меню, которое предоставляет операционная система, работает безотказно, но весьма ограничено в отображаемом контенте и функциональности. Я остановил процесс по таймеру во время трекинга меню функцией > 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, контекстное меню ОС не съедает щелчок вне окна меню, но при этом так же не пропускает |
А это реализация Как и в случае с контекстным меню ОС, не съедает щелчек вне окон контекста. Собственная реализация Кстати, прототип этой реализации - Microsoft Office 2007/2010, в котором |
На этой картинке находится кусочек Как и в случае с контекстным меню ОС, не съедает щелчек вне окон контекста. |
Я хочу отметить, что все эти менюшки
- обыкновенные окна, ничего сверхестественного. И они никогда не активируются и не крадут фокус ввода. Истинные джентльмены.
Предательские реализации всплывающих контекстных окон
Контекстная подсказка этой кнопочки гордо сообщает нам о возможности При этом на блокировку Кстати, открыл для себя что градация слайдера имеет дискретную структуру с 4 шагами снизу (где разделители нарисованы) и непрерывную (с очень мелким шагом) в области, где разделителей нет. Иконки плавно меняют свой размер. Весьма странная реализация, если сравнить с предыдущими. |
Альтернативный подход
Открыв контекстное меню Firefox, вы убедитесь, что оно никак не влияет на остальные окна, что, впрочем, выглядит вполне логично. Щелчки обрабатываются как обычно, но перед этим меню пропадает с экрана.
С другой стороны, отсутствие реакции окон на движение мышью подсказывает пользователю, что где-то открыто контекстное меню или модальный диалог, но лишь потому, что ОС Windows работает подобным образом.
Реализация собственных контекстных меню
Прежде чем заниматься кодированием, следует определить технические требования к реализации всплывающего окна. Как видите - нюансов достаточное количество. Технических нюансов, которые в результате влияют на удобство интерфейса:
- Реакция окон (этого же приложения) вне контекста на перемещение указателя мыши
- Щелчки мышью по окнам (этого же приложения) вне контекста
- Деактивация активного окна приложения
Отслеживание мыши
Для слежения за мышью есть такие подходы:
SetCapture
/ReleaseCapture
С помощью этих функций можно произвести захват/освобождение мыши и направить все сообщения в оконную процедуру одного окна. Но именно с этим связана огромная проблема - мы же хотим отображать сложные окна с элементами управления, которые тоже должны получать мышиные сообщения. В результате обработчики
WM_??MOUSE??
выпадающего окна должны будут сами обеспечивать попадание нужных сообщений в оконные процедуры дочерних окон. Нужных - хорошо написано, а на деле - всех возможных. Фактически мы будем эмулировать то, что делает сама ОС Windows. Может показаться, что достаточно лишь найти окно под курсором функциейWindowFromPoint
и вызвать оконную процедуру найденного окна, но на самом деле нюансов огромное множество, как и эмулируемых сообщений.Сам по себе этот сценарий не позволяет контролировать механизм проглатывания щелчков вне контекста. Допустим, при щелчке мы хотим спрятать выпадающее окно, а после этого получить реакцию на щелчок окном, по которому он был произведен. Из-за
SetCapture
сообщениеWM_LBUTTONDOWN
извлекается из очереди и попадает в оконную процедуру выпадающего окна, где код определяет положение курсора и прячет окно, если оно находилось вне его области. Далее нужно обеспечить обработку этого сообщения, для этого вызываемAfxCallWndProc
(или простоSendMessage
). И делаем мы это из обработчикаWM_LBUTTONDOWN
нашего выпадающего окна! А что если реакцией на щелчок будет повторное выпадение окна (возможно вовсе не этого)? Мы пополним стек потока на добрую дюжину вызовов. И вот здесь все зависит от цикла сообщений. Есть два варианта:- Показать окно и выйти из метода
ShowPopup
(неблокирующий вызов).Цикл сообщений при этом остается прежним (как он реализован в приложении), проблем со стеком здесь быть не должно, но это вносит свои трудности в удобство использования такого программного интерфейса - нельзя держать объекты на стеке (окна, контроллеры, политики и т.д.), при выходе из обработчика окно продолжает
висеть
на экране и следить за обстановкой, ожидая момента, когда можно спрятаться и уничтожить все следы своего сущевствования, уведомив владельца о произошедшем. - Показать окно и запустить свой цикл сообщений в методе
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();
Для начала разберемся в каких случаях всплывающее окно должно быть закрыто без привязки к его внутренностям (поведение меню):
- Деактивация приложения (foreground thread больше не является потоком этого приложения)
- Щелчки вне контекста
- Уничтожение окна
- Изменение состояний окна на такие, в которых режим меню смысла больше не имеет (окно 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; }
Это код делает следующее:
- В первую очередь дает возможность политике фильтровать сообщения
- Ничего не делает если режим меню в состоянии закрытия
- Выясняет состояние захвата мыши и совершаемое мышью действие
- Если мышь захвачена неким окном - нужно выяснить какому окну предназначено сообщение в случае, если бы захват мыши не был установлен вовсе
- Если целевое окно - то, которое выполнило захват, одно из его дочерних окон, а также само выпадающее окно (вместе с его содержимым) - сообщение нужно пропустить
- В противном случае - выйти из режима меню
- Когда захвата мыши нет - выйти из режима меню в случае, когда произвели щелчок по окну вне контекста, а также проглотить сообщение
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; }
Что здесь происходит:
- Проверки состояний менеджера (нельзя входить в режим меню рекурсивно);
- Проверки входящих параметров, состояний окон и т.д.;
- Контекстный (scoped) сабклассинг окна;
- Контекстный (scoped) вход в режим меню;
- Привязка окна-владельца к выпадающему окну;
- Определение позиции выпадающего окна и его показ (с анимацией, если это возможно);
- Цикл циклов сообщений;
- Очистка состояния.
Цикл сообщений выглядит следующим образом:
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); }
Мой диалог содержит пару кнопок и выпадающий список. Все это отлично работает.
На этом я заканчиваю первую часть. В следующий раз я расскажу что такое политика и опишу сложный сценарий выпадающего окна с переходами между полем ввода на форме в него (меню) и обратно без закрытия (а это весьма сложно, поверьте).
Код в свободный доступ не выкладываю, жадный. Спасибо за внимание.