Search code examples
visual-c++wtl

How to emulate a modal dialog in WTL?


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

Solution

  • 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