0

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?

pushkin
  • 9,575
  • 15
  • 51
  • 95
  • [About Dialog Boxes - Message Boxes and Modal Dialog Boxes](https://learn.microsoft.com/en-us/windows/desktop/dlgbox/about-dialog-boxes#message-boxes). [HookProc of the FileDialog class](https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/FileDialog.cs,747) and [Common Dialog](https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/CommonDialog.cs,146). Remarks section of [IMessageFilter Interface](https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.imessagefilter) – Jimi Feb 19 '19 at 00:28
  • @Jimi I don't understand why you linked to a section about Message boxes when I'm talking about CommonDialogs. The section below it about Modals wasn't particularly helpful - or I'm not sure what you wanted me to glean from that. Regarding `HookProc`, I thought about that before, but I was hoping for something that can apply to other CommonDialogs, not just FIleDialog. Second, it looks like I'd have to override HookProc in another class of mine that extends FileDialog, which I'd rather not do if possible. – pushkin Feb 19 '19 at 15:00
  • @Jimi Furthermore, what am I supposed to learn from the Remarks section of IMessageFilter? That it dispatches to Controls and Forms, and CommonDialogs are... neither? Not sure – pushkin Feb 19 '19 at 15:00

1 Answers1

2

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();
pushkin
  • 9,575
  • 15
  • 51
  • 95
KimMeo
  • 320
  • 1
  • 10
  • I can't get this to work. When I pass `0` as the threadId param to `SetWindowsHookEx`, it returns a null handle, and I crash. When I use the current thread ID (which is more what I want), it returns a valid handle, and `YOUR_EVENT_FUNC` is triggered basically every tick, but I never enter the `if` block. Is `w ==` supposed to be `l ==`, since `w` according to the docs `l` should contain information about the message? (even when I use `l` instead of `w`, the `if` block is never entered for some reason) (as an aside, some of your extern method types are incorrect) – pushkin Feb 20 '19 at 18:47
  • ㄴ YOUR_EVENT_FUNC should be triggered every tick, because it is called when you move your mouse(WH_MOUSE_LL gets every event if mouse). Also, w contains the hooking message's type and l is pointer to [msllhookstruct](https://learn.microsoft.com/en-us/windows/desktop/api/winuser/ns-winuser-tagmsllhookstruct). So, don't change it. – KimMeo Feb 21 '19 at 00:52
  • Replace WM_LBUTTONDOWN to WM_LBUTTONUP. And, we are makeing low level hook so the threadid on SetwindowsHookEx should be 0, but your program got error with 0... If this snippet does't fix your problem, I'll delete this post. Sry. – KimMeo Feb 21 '19 at 01:42
  • Your snippet got me closer, but I had to use the current thread ID instead of 0 and I opted for WH_MOUSE instead of the LL version. I can edit your snippet to show what I did if you don't mind. – pushkin Feb 21 '19 at 14:49