4

Modal dialogs are nice and easy to use. Problem is that they don't allow me to handle the message loop myself. So I thought I could perhaps use a modeless dialog to emulate a modal one and still be in charge of the message loop myself in order to handle accelerators.

Goal

What I want to achieve in general is the ability to press Ctrl+C (and Ctrl+Ins) while the dialog has the focus and then I want to be able to react to that by copying some information into the clipboard. So if anyone knows a way to do that with modal dialogs in WTL, that also would answer my question.

What I am doing right now

Now what I currently do is deriving my dialog class from CDialogImpl<T> and CMessageFilter in order to put me in charge of PreTranslateMessage. In there I simply use CAccelerator::TranslateAccelerator and CWindow::IsDialogMessage to process accelerators and dialog box messages.

In OnInitDialog I populate the accelerator table and add the message filter to the ("global") message loop. The accelerator table has the same resource ID as the dialog itself:

m_accel.Attach(AtlLoadAccelerators(IDD));
CMessageLoop* pLoop = _Module.GetMessageLoop();
pLoop->AddMessageFilter(this);

Then I created a surrogate for DoModal by the name PretendModal which uses the "global" message loop.

Now the effect (other than the dialog appearing on the task bar) that I am seeing is that the application, once the modal dialog gets closed, cannot be closed anymore. To be precise, the main message loop receives WM_QUIT (the ATLTRACE2 in WTL::CMessageLoop::Run() gives that away, but it still hangs after this stunt (main frame window gets closed, WM_QUIT gets posted, but the process does not exit). The whole thing behaves the same if I use a separate CMessageLoop inside PretendModal (instead of the "global" one).

Even moving another separate new instance of CMessageLoop into its own thread (after all message loops are thread-local) does not seem to resolve this issue. This leaves me puzzled as to what exactly I am doing wrong here.

NB: The handler for IDCANCEL and IDOK removes the dialog class (i.e. the message filter) from the message loop.

Question

What am I doing wrong in my attempt to emulate a modal dialog using a modeless one? Alternatively, how can I catch Ctrl+C (and Ctrl+Ins) when using a modal dialog derived just from CDialogImpl<T>.


The class

class CAboutDlg :
    public CDialogImpl<CAboutDlg>,
    public CMessageFilter
{
    CAccelerator m_accel;
public:
    enum { IDD = IDD_ABOUT };

    BEGIN_MSG_MAP(CAboutDlg)
        MESSAGE_HANDLER(WM_INITDIALOG, OnInitDialog)
        COMMAND_ID_HANDLER(IDOK, OnCloseCmd)
        COMMAND_ID_HANDLER(IDCANCEL, OnCloseCmd)
    END_MSG_MAP()

    virtual BOOL PreTranslateMessage(MSG* pMsg)
    {
        if (!m_accel.IsNull() && m_accel.TranslateAccelerator(m_hWnd, pMsg))
            return TRUE;
        return CWindow::IsDialogMessage(pMsg);
    }

    LRESULT OnInitDialog(UINT, WPARAM, LPARAM, BOOL&)
    {
        m_accel.Attach(AtlLoadAccelerators(IDD));
        if (m_bModal)
        {
            CMessageLoop* pLoop = _Module.GetMessageLoop();
            pLoop->AddMessageFilter(this);
        }
        return TRUE;
    }

    void PretendModal(HWND hwndParent = ::GetActiveWindow())
    {
        CMessageLoop* pLoop = _Module.GetMessageLoop();
        if (pLoop && ::IsWindow(hwndParent))
        {
            HWND dlg = Create(*this);
            if (::IsWindow(dlg))
            {
                ShowWindow(SW_SHOW);
                pLoop->Run();
            }
        }
    }

    LRESULT OnCloseCmd(WORD, WORD, HWND, BOOL&)
    {
        if (m_bModal)
            EndDialog(0);
        else
        {
            CMessageLoop* pLoop = _Module.GetMessageLoop();
            pLoop->RemoveMessageFilter(this);
            ::DestroyWindow(*this);
        }
        return 0;
    }
};
0xC0000022L
  • 20,597
  • 9
  • 86
  • 152
  • It's hard to believe the whole message loop has to be replaced just to capture a keypress! Needed something similar because I was looking for way to catch the Escape key. Apparently the ATL/WTL modal boxes don't close by default when ESC is pressed. Before this method I tried to use `SetWindowsHook`, which worked but is considered even worse. I still wonder if there isn't a more elegant way to connect a message filter to the "standard" `DoModal` message loop... – E. van Putten Jul 27 '18 at 15:06
  • @E.vanPutten: If there is, it has eluded me. But I don't think the solution is all too bad. It seems to be playing by the rules without diverging too much from the expected behavior for modal dialogs. – 0xC0000022L Jul 27 '18 at 21:54
  • You are right, it is not too bad. But it is still a lot of work for just catching a few keystrokes. Playing by the rules (more) is also why I tried to stay away from that windows (message) hook. – E. van Putten Jul 28 '18 at 08:28

1 Answers1

1

So meanwhile I managed to achieve what I wanted. This seems to be working nicely and I haven't found any negative side effects as of yet.

In order to do what I want, I introduced an EmulateModal() function which kind of mimics the DoModal() function of DialogImpl.

That function looks as follows:

void EmulateModal(_In_ HWND hWndParent = ::GetActiveWindow(), _In_ LPARAM dwInitParam = NULL)
{
    ATLASSERT(!m_bModal);
    ::EnableWindow(hWndParent, FALSE);
    Create(hWndParent, dwInitParam);
    ShowWindow(SW_SHOW);
    m_loop.AddMessageFilter(this);
    m_loop.Run();
    ::EnableWindow(hWndParent, TRUE);
    ::SetForegroundWindow(hWndParent);
    DestroyWindow();
}

The m_loop member is a CMessageLoop owned by the CDialogImpl-derived class (which also inherits from CMessageFilter as shown in the question).

The only other special handling that is needed, was to add the following code to the command ID handler which watches for IDOK and IDCANCEL (which in my case are both meant to close the dialog), i.e. inside OnCloseCmd.

if(m_bModal)
{
    EndDialog(wID);
}
else
{
    m_loop.RemoveMessageFilter(this);
    PostMessage(WM_QUIT);
}

It is important to remove the message filter (i.e. PreTranslateMessage) from the message loop prior to calling DestroyWindow(). It is also very important to exit the "inner" message loop which is owned by the CDialogImpl-derived class and whose Run() is being called from EmulateModal() above.

So the gist is this:

  1. get rid of the PretendModal() method from my question
  2. make use of an "inner" message loop instead of using the top-level one
0xC0000022L
  • 20,597
  • 9
  • 86
  • 152
  • 1
    There's an ever so subtle bug lingering in this code. I believe you can make it surface by temporarily switching to another application, after launching the modal dialog. If you now dismiss the dialog, the system moves the wrong window to the foreground. The core issue is, that the code enables the owner window too late (after destroying the dialog). For a more comprehensive explanation see [The correct order for disabling and enabling windows](https://blogs.msdn.microsoft.com/oldnewthing/20040227-00/?p=40463/). – IInspectable Feb 17 '18 at 09:11
  • @IInspectable: thanks for pointing it out. I'll have a look at it and adjust my code accordingly. I think I understand the nature of the problem and what needs to be done. – 0xC0000022L Feb 20 '18 at 15:45
  • @IInspectable: turns out in my own code I already was calling `::SetForegroundWindow` after enabling the parent window in `EmulateModal`. That should do the trick, right? Or would that be prone to said flicker and I should therefore move the enabling and setting to foreground to _before_ the `DestroyWindow`? – 0xC0000022L Feb 20 '18 at 16:51
  • Calling [SetForegroundWindow](https://msdn.microsoft.com/en-us/library/windows/desktop/ms633539.aspx) may fail when called after the system has activated another window, causing the taskbar button to flash. The restrictions placed on that API call aren't entirely clear to me, and the misleading documentation doesn't help much in sorting it all out. If I were to implement this, I'd probably play it safe, and activate the owner before destroying the modal UI. – IInspectable Feb 23 '18 at 11:37
  • @IInspectable: "activate" as in `EnableWindow()` or "activate" as in `ShowWindow()`. – 0xC0000022L Feb 23 '18 at 19:40
  • 1
    *"Activate"* in the final sentence was the wrong term. I meant to write *"**enable** the owner before destroying the modal UI"*. That's enough for the window manager to activate the owner once the modal UI is destroyed. – IInspectable Feb 24 '18 at 08:12