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

Показаны сообщения с ярлыком MFC. Показать все сообщения
Показаны сообщения с ярлыком MFC. Показать все сообщения

MFC fails again

MFC, I love you. Really. Your source code is a fantastic example of how not to do it. Don't do it ever.

The problem

OK, so here's the problem - given an MFC dialog (or dialog control), sometimes we have no focus cues when tabbing through UI elements. You press Tab, use cursor keys and yet focus rectangle is not there. Forever. What might the problem be? Let's find out how this works internally.

Focus rect internals

First of all open any system dialog (for example taskbar Properties) using mouse only and ensure there is no focus rectangle on currently focused UI element. Press Tab and watch for it to immediately appear. Whatever you do from now on (excluding cases where the whole window hierarchy is hidden or recreated), focus rect is always on the screen. Now try to open this dialog using keyboard operation (you can open taskbar context menu and select Properties menu item using keyboard keys). Focus rect is shown immediately. What's happening here?

The answer begins it's path in dlgbegin.c, function InternalCreateDialog:

HWND InternalCreateDialog(
    HANDLE hmod,
    LPDLGTEMPLATE lpdt,
    DWORD cb,
    HWND hwndOwner,
    DLGPROC lpfnDialog,
    LPARAM lParam,
    UINT fSCDLGFlags)
{
...
}

The most interesting part is the following:

 /*
  * UISTATE: if keyboard indicators are on and this is a topmost dialog
  * set the internal bit.
  */
 if (TEST_KbdCuesPUSIF) {
     /*
      * If property page, UISTATE bits were copied from parent when I was created
      * Top level dialogs act as containers and initialize their state based on
      * the type of the last input event, after sending UIS_INITIALIZE
      */
     if (!TestwndChild(pwnd)) {
         SendMessageWorker(pwnd, WM_CHANGEUISTATE, MAKEWPARAM(UIS_INITIALIZE, 0), 0, FALSE);
     }
 }

TEST_KbdCuesPUSIF tests current execution environment for keyboard cues enablement (field check in some global structure), TestwndChild ensures top level dialog. Then the window gets WM_CHANGEUISTATE message with two arguments - UIS_INITIALIZE and zero. Documentation says Top level dialogs act as containers and initialize their state based on the type of the last input event. What does this mean? The answer is in the default window procedure (dwp.c):

/***************************************************************************\
* xxxDefWindowProc (API)
*
* History:
* 10-23-90 MikeHar Ported from WaWaWaWindows.
* 12-07-90 IanJa   CTLCOLOR handling round right way
\***************************************************************************/

LRESULT xxxDefWindowProc(
    PWND pwnd,
    UINT message,
    WPARAM wParam,
    LPARAM lParam)
{
...
}

Take a look at WM_CHANGEUISTATE handler:

case WM_CHANGEUISTATE:
    {
        WORD wAction = LOWORD(wParam);
        WORD wFlags = HIWORD(wParam);
        BOOL bRealChange = FALSE;

        if (wFlags & ~UISF_VALID || wAction > UIS_LASTVALID ||
            lParam || !TEST_KbdCuesPUSIF) {
                return 0;
        }

        if (wAction == UIS_INITIALIZE) {
            if (gpsi->bLastRITWasKeyboard) {
                wAction = UIS_CLEAR;
            } else {
                wAction = UIS_SET;
            }
            wFlags = UISF_HIDEFOCUS | UISF_HIDEACCEL;
            wParam = MAKEWPARAM(wAction, wFlags);
        }

        UserAssert(wAction == UIS_SET || wAction == UIS_CLEAR);
        /*
         * If the state is not going to change, there's nothing to do here
         */
        if (wFlags & UISF_HIDEFOCUS) {
            bRealChange = (!!TestWF(pwnd, WEFPUIFOCUSHIDDEN)) ^ (wAction == UIS_SET);
        }
        if (wFlags & UISF_HIDEACCEL) {
            bRealChange |= (!!TestWF(pwnd, WEFPUIACCELHIDDEN)) ^ (wAction == UIS_SET);
        }

        if (!bRealChange) {
            break;
        }

        /*
         * Children pass this message up
         * Top level windows update their children's state and
         * send down to their imediate children WM_UPDATEUISTATE.
         */
        if (TestwndChild(pwnd)) {
            ThreadLockAlways(pwnd->spwndParent, &tlpwndParent);
            lt = xxxSendMessage(pwnd->spwndParent, WM_CHANGEUISTATE, wParam, lParam);
            ThreadUnlock(&tlpwndParent);
            return lt;
        } else {
            return xxxSendMessage(pwnd, WM_UPDATEUISTATE, wParam, lParam);
        }

        }
    break;

case WM_QUERYUISTATE:
    return (TestWF(pwnd, WEFPUIFOCUSHIDDEN) ? UISF_HIDEFOCUS : 0) |
           (TestWF(pwnd, WEFPUIACCELHIDDEN) ? UISF_HIDEACCEL : 0);
    break;

case WM_UPDATEUISTATE:
    {
        WORD wAction = LOWORD(wParam);
        WORD wFlags = HIWORD(wParam);

        if (wFlags & ~UISF_VALID || wAction > UIS_LASTVALID ||
            lParam || !TEST_KbdCuesPUSIF) {
                return 0;
        }

        switch (wAction) {
            case UIS_INITIALIZE:
                /*
                 * UISTATE: UIS_INITIALIZE sets the UIState bits
                 * based on the last input type
                 */
                if (!gpsi->bLastRITWasKeyboard) {
                    SetWF(pwnd, WEFPUIFOCUSHIDDEN);
                    SetWF(pwnd, WEFPUIACCELHIDDEN);
                    wParam = MAKEWPARAM(UIS_SET, UISF_HIDEACCEL | UISF_HIDEFOCUS);
                } else {
                    ClrWF(pwnd, WEFPUIFOCUSHIDDEN);
                    ClrWF(pwnd, WEFPUIACCELHIDDEN);
                    wParam = MAKEWPARAM(UIS_CLEAR, UISF_HIDEACCEL | UISF_HIDEFOCUS);
                }
                break;

            case UIS_SET:
                if (wFlags & UISF_HIDEACCEL) {
                    SetWF(pwnd, WEFPUIACCELHIDDEN);
                }
                if (wFlags & UISF_HIDEFOCUS) {
                    SetWF(pwnd, WEFPUIFOCUSHIDDEN);
                }
                break;

            case UIS_CLEAR:
                if (wFlags & UISF_HIDEACCEL) {
                    ClrWF(pwnd, WEFPUIACCELHIDDEN);
                }
                if (wFlags & UISF_HIDEFOCUS) {
                    ClrWF(pwnd, WEFPUIFOCUSHIDDEN);
                }
                break;

            default:
                break;
         }

        /*
         * Send it down to its immediate children if any
         */
         if (pwnd->spwndChild) {

            PBWL pbwl;
            HWND *phwnd;
            TL tlpwnd;

            pbwl = BuildHwndList(pwnd->spwndChild, BWL_ENUMLIST, NULL);
            if (pbwl == NULL)
                return 0;

            for (phwnd = pbwl->rghwnd; *phwnd != (HWND)1; phwnd++) {
                /*
                 * Make sure this hwnd is still around.
                 */
                if ((pwnd = RevalidateHwnd(*phwnd)) == NULL)
                    continue;

                ThreadLockAlways(pwnd, &tlpwnd);
                xxxSendMessage(pwnd, message, wParam, lParam);
                ThreadUnlock(&tlpwnd);
            }
            FreeHwndList(pbwl);
        }
    }
    break;

We are interested in the following code:

if (wAction == UIS_INITIALIZE) {
    if (gpsi->bLastRITWasKeyboard) {
        wAction = UIS_CLEAR;
    } else {
        wAction = UIS_SET;
    }
    wFlags = UISF_HIDEFOCUS | UISF_HIDEACCEL;
    wParam = MAKEWPARAM(wAction, wFlags);
}

It checks whether last input event was the one from keyboard (bLastRITWasKeyboard, RIT - raw input thread) and prepares appropriate action - set UISF_HIDEFOCUS and UISF_HIDEACCEL flags or clear them. The system tries to understand user experience type (mouse or keyboard) and initializes window state for better interaction. That's why you don't see focus or accelerator cues having opened a dialog using mouse whereas they are both on if you used your keyboard.

There is a little trick to have the system on: move the mouse right after you triggered a dialog using keyboard, or press any key after you triggered a dialog using mouse. The effect speaks for itself.

The rest of the handler simply decides where to send WM_UPDATEUISTATE message. WM_UPDATEUISTATE's handler just saves the state to internal OS structure for particular window. As you have already guessed, WM_QUERYUISTATE reads the state back.

What does this all mean? There is a window state for focus and accelerator cues and a set of messages for it's manipulation. This state is kept in sync by all windows of the same hierarchy. OS initializes the state for best user experience.

Who uses the state

At least common controls, of course. The pattern is simple (listview.c for example):

case WM_UPDATEUISTATE:
{
    DWORD dwUIStateMask = MAKEWPARAM(0xFFFF, UISF_HIDEFOCUS);

    // we care only about focus not accel, and redraw only if changed
    if (CCOnUIState(&(plv->ci), WM_UPDATEUISTATE, wParam & dwUIStateMask, lParam))
    {
        if(plv->iFocus >= 0)
        {
            // an item has the focus, invalidate it
            ListView_InvalidateItem(plv, plv->iFocus, FALSE, RDW_INVALIDATE | RDW_ERASE);
        }
    }

    goto DoDefault;
}

It just listens to UI state updates and does appropriate actions (saves the state to local structure, invalidates the window etc.).

Who updates the state

As I've already mentioned, focus rect is shown immediately as you pressed focus navigation key (Tab for example). This is done by ::IsDialogMessage OS function:

case WM_SYSKEYDOWN:
    /*
     * If Alt is down, deal with keyboard cues
     */
    if ((HIWORD(lpMsg->lParam) & SYS_ALTERNATE) && TEST_KbdCuesPUSIF) {
        if (TestWF(pwnd, WEFPUIFOCUSHIDDEN) || (TestWF(pwnd, WEFPUIACCELHIDDEN))) {
                SendMessageWorker(pwndDlg, WM_CHANGEUISTATE,
                                  MAKEWPARAM(UIS_CLEAR, UISF_HIDEACCEL | UISF_HIDEFOCUS), 0, FALSE);
            }
    }
    break;
case WM_KEYDOWN:
    code = (UINT)SendMessage(lpMsg->hwnd, WM_GETDLGCODE, lpMsg->wParam,
            (LPARAM)lpMsg);
    if (code & (DLGC_WANTALLKEYS | DLGC_WANTMESSAGE))
        break;

    switch (lpMsg->wParam) {
    case VK_TAB:
        if (code & DLGC_WANTTAB)
            break;
        pwnd2 = _GetNextDlgTabItem(pwndDlg, pwnd,
                (GetKeyState(VK_SHIFT) & 0x8000));

        if (TEST_KbdCuesPUSIF) {
            if (TestWF(pwnd, WEFPUIFOCUSHIDDEN)) {
                SendMessageWorker(pwndDlg, WM_CHANGEUISTATE,
                                      MAKEWPARAM(UIS_CLEAR, UISF_HIDEFOCUS), 0, FALSE);
            }
        }

WM_SYSKEYDOWN and WM_KEYDOWN analyze keyboard input and trigger UI state updates by sending WM_CHANGEUISTATE. All child windows then receive state change update, focused window shows up a shiny focus rectangle, labels begin to draw accelerator cues. Simple, huh?

So what the fuck is wrong might be in MFC

As you already know (I hope) MFC focus navigation is implemented via CWnd::PreTranslateMessage virtual function which eventually calls CWnd::IsDialogMessage method:

BOOL CWnd::IsDialogMessage(LPMSG lpMsg)
{
 ASSERT(::IsWindow(m_hWnd));

 if (m_nFlags & WF_OLECTLCONTAINER)
  return afxOccManager->IsDialogMessage(this, lpMsg);
 else
  return ::IsDialogMessage(m_hWnd, lpMsg);
}

In case of ActiveX controls in your dialog template (or any other shit involving OCC state initialization), WF_OLECTLCONTAINER flag is set and the control passes to COccManager::IsDialogMessage instead of API ::IsDialogMessage. Believe me or not, COccManager::IsDialogMessage analyzes keyboard input and moves the focus. The biggest problem with this method - it doesn't consider UI layout the way ::IsDialogMessage does. ZOrder enumeration is done using CWnd::GetNextDlgTabItem (see correct overload with COleControlSiteOrWnd * parameter), which moves through controls registered in COleControlContainer - m_pCtrlCont->m_listSitesOrWnds. As you might have already guessed, this container is populated by MFC during dialog creation (from template of course) and is not updated afterwards. So if you add your controls dynamically (WTL/MFC/whatever) - be ready for focus navigation problems. The solution is to update container with necessary items for each control you create/recreate etc. Plus you have to keep items order to reflect proper ZOrder. What a crap!

Finally, the answer

COccManager::IsDialogMessage doesn't fucking send WM_CHANGEUISTATE as ::IsDialogMessage does. It doesn't fucking do it. Fucking MFC, fuck you. FUCK YOU.

CEdit with an ellipsis

Sometimes developers are devoted to absolutely rediculous tasks. Like having an established conception broken for some reason. You spend a lot of time cracking things up to acomplish your mission. Recently I was working on an edit control with an ellipsis if the text does not fit.

I came up with painting the edit in case it's not focused:

class CEllipsisEdit : public CEdit
{
   DECLARE_DYNAMIC(CEllipsisEdit)
 
protected:
   COLORREF GetBkColor() const;
 
protected:
   CBrush m_bkBrush;
 
protected:
   DECLARE_MESSAGE_MAP()
   afx_msg void OnPaint();
 
};
#include "EllipsisEdit.h"
 
IMPLEMENT_DYNAMIC(CEllipsisEdit, CEdit)
 
// CEllipsisEdit message map
 
BEGIN_MESSAGE_MAP(CEllipsisEdit, CEdit)
   ON_WM_PAINT()
END_MESSAGE_MAP()
 
// CEllipsisEdit message handlers
 
COLORREF CEllipsisEdit::GetBkColor() const
{
   if (m_hWnd) {
      const HWND hParentWnd = GetParent()->GetSafeHwnd();
      if (::GetWindowThreadProcessId(hParentWnd, NULL) == ::GetWindowThreadProcessId(m_hWnd, NULL)) {
         CDC dc;
         dc.CreateCompatibleDC(NULL);
         const bool enabled = (GetStyle() & WS_DISABLED) == 0;
         const UINT colorMsg = enabled ? WM_CTLCOLOREDIT : WM_CTLCOLORSTATIC;
 
         ::SendMessage(hParentWnd, colorMsg, reinterpret_cast<WPARAM>(dc.m_hDC), reinterpret_cast<LPARAM>(m_hWnd));
 
         return dc.GetBkColor();
      }
   }
 
   return ::GetSysColor(COLOR_WINDOW);
}
 
void CEllipsisEdit::OnPaint()
{
   if (::GetFocus() == m_hWnd) {
      __super::OnPaint();
      return;
   }
 
   CString text;
   GetWindowText(text);
 
   if (!m_bkBrush.m_hObject)
      m_bkBrush.CreateSolidBrush(GetBkColor());
   
   CPaintDC dc(this);
   
   RECT rc = { 0, 0, 0, 0 };   
   GetClientRect(&rc);
   dc.FillRect(&rc, &m_bkBrush);
   
   GetRect(&rc);
   dc.SetBkMode(TRANSPARENT);
   dc.SelectObject(GetFont());
   dc.DrawText(text, text.GetLength(), &rc, DT_SINGLELINE | DT_NOPREFIX | DT_WORD_ELLIPSIS);
}

There were several problems:

  1. Edit control would paint itself synchronously. No WM_PAINT message is sent, just GetDC(hwnd) and the paint code, no way to sabstitute painting.
  2. Edit control doesn't have non-client area, borders are drawn in client area.
  3. Have to somehow determine text background color.

Synchronous paint seems to be an issue only with focused control. Borders are drawn as usual for unknown reason. Background color shall be determined by sending WM_CTLCOLOREDIT or WM_CTLCOLORSTATIC to parent or just a COLOR_WINDOW system color as a fallback.

Please note that WM_CTLCOLOR-related messages are not sent to parent widows in different threads! Also don't forget to add DT_NOPREFIX flag when invoking DrawText method.

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);
}

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

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

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

Copyright 2007-2011 Chabster