Search code examples
c#winformswindows-messagescommon-dialog

Why can't I detect Windows messages that are being sent to my CommonDialog? What is the right way of intercepting them?


I am trying to detect when the user clicks on a Form and a CommonDialog.

Forms are fairly straightforward. I create a MessageFilter class that intercepts messages:

class MessageFilter : IMessageFilter
{
    private const int WM_LBUTTONDOWN = 0x0201;
    public bool PreFilterMessage(ref Message message)
    {
        if (message.Msg == WM_LBUTTONDOWN)
        {
            Console.WriteLine("activity");
        }
        return false;
    }
}

And I register the message filter:

MessageFilter mf = new MessageFilter();
Application.AddMessageFilter(mf);

Form form = new Form();
form.ShowDialog();

Application.RemoveMessageFilter(mf)

When I run my Console Application and click on the Form, I see "activity" logged to the console.

When I replace Form with a CommonDialog:

SaveFileDialog dialog = new SaveFileDialog();
dialog.ShowDialog();

I can no longer detect mouse clicks even though I can see Windows messages being dispatched to the CommonDialog (FWIW, I can't detect any messages):

enter image description here

How come I can't intercept those messages, then?


Something that crossed my mind was that since Application.AddMessageFilter is thread-specific, maybe if the CommonDialog was being created on a different thread than the one that calls dialog.ShowDialog(), I wouldn't get any of those messages.

However, I did a quick test where I tried sending a WM_CLOSE message to all CommonDialogs on the thread that calls dialog.ShowDialog(), and it worked:

int threadId = 0;
Thread thread = new Thread(() =>
{
    threadId = NativeMethods.GetCurrentThreadIdWrapper();
    SaveFileDialog dialog = new SaveFileDialog();
    dialog.ShowDialog();
});
thread.SetApartmentState(ApartmentState.STA);
thread.Start();

Thread.Sleep(2000);
NativeMethods.CloseAllWindowsDialogs(threadId);
Thread.Sleep(2000);

And NativeMethods looks like:

static class NativeMethods
{
    public static int GetCurrentThreadIdWrapper()
    {
        return GetCurrentThreadId();
    }

    public static void CloseAllWindowsDialogs(int threadId)
    {
        EnumThreadWndProc callback = new EnumThreadWndProc(CloseWindowIfCommonDialog);
        EnumThreadWindows(threadId, callback, IntPtr.Zero);
        GC.KeepAlive(callback);
    }

    private static bool CloseWindowIfCommonDialog(IntPtr hWnd, IntPtr lp)
    {
        if (IsWindowsDialog(hWnd))
        {
            UIntPtr result;
            const int WM_CLOSE = 0x0010;
            const uint SMTO_ABORTIFHUNG = 0x0002;
            SendMessageTimeout(hWnd, WM_CLOSE, UIntPtr.Zero, IntPtr.Zero, SMTO_ABORTIFHUNG, 5000, out result);
        }

        return true;
    }

    private static bool IsWindowsDialog(IntPtr hWnd)
    {
        const int MAX_PATH_LENGTH = 260; // https://learn.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file#maximum-path-length-limitation
        StringBuilder sb = new StringBuilder(MAX_PATH_LENGTH);
        GetClassName(hWnd, sb, sb.Capacity);

        return sb.ToString() == "#32770";
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern int GetCurrentThreadId();

    private delegate bool EnumThreadWndProc(IntPtr hWnd, IntPtr lp);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern bool EnumThreadWindows(int tid, EnumThreadWndProc callback, IntPtr lp);

    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);

    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint msg, UIntPtr wp, IntPtr lp, uint fuFlags, uint timeout, out UIntPtr lpdwResult);
}

Why can't I intercept CommonDialog messages? What I can do about it?


Solution

  • How about setting a local mouse hook?

    Works well on my project.

    public const int WM_LBUTTONDOWN = 0x0201;
    // add other button messages if necessary
    
    public const int WH_MOUSE = 7;
    
    private IntPtr _hookHandle;
    
    private void HookStart() {
        int threadId = GetCurrentThreadId();
    
        HookProc mouseClickHandler = new HookProc(MouseClickHandler);
        _hookHandle = SetWindowsHookEx(WH_MOUSE, mouseClickHandler, IntPtr.Zero, (uint) threadId);
        if (_hookHandle == IntPtr.Zero) throw new Exception("Hooking failed!");
    }
    
    private void HookStop() {        
        if (UnhookWindowsHookEx(_hookHandle) == IntPtr.Zero) throw new Exception("Unhooking failed!");
    }
    
    private IntPtr MouseClickHandler(int nCode, IntPtr wParam, IntPtr lParam) {
        if (nCode >= 0 && wParam == (IntPtr) WM_LBUTTONDOWN) {
            // user clicked
        }
        return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
    }
    
    public delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
    
    [DllImport("User32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    public static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, uint threadId);
    [DllImport("User32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    public static extern int CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
    [DllImport("User32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    public static extern int UnhookWindowsEx(IntPtr idHook);
    [DllImport("kernel32.dll")]
    public static extern int GetCurrentThreadId();