Search code examples
c#winapisendmessagewndproc

Dispatching managed Win32 WndProc on a sepparate thread


I'm creating a window through unmanaged CreateWindowEx using PInvoke to work as a server in order to dispatch SendMessage calls from a different process. This should be wrapped in a synchronous function (class registration + window creation), something like this:

public bool Start()
{
    if (!Running)
    {
        var processHandle = Process.GetCurrentProcess().Handle;

        var windowClass = new WndClassEx
        {
            lpszMenuName = null,
            hInstance = processHandle,
            cbSize = WndClassEx.Size,
            lpfnWndProc = WndProc,
            lpszClassName = Guid.NewGuid().ToString()
        };

        // Register the dummy window class
        var classAtom = RegisterClassEx(ref windowClass);

        // Check whether the class was registered successfully
        if (classAtom != 0u)
        {
            // Create the dummy window
            Handle = CreateWindowEx(0x08000000, classAtom, "", 0, -1, -1, -1, -1, IntPtr.Zero, IntPtr.Zero, processHandle, IntPtr.Zero);
            Running = Handle != IntPtr.Zero;

            // If window has been created
            if (Running)
            {
                // Launch the message loop thread
                taskFactory.StartNew(() =>
                {
                    Message message;

                    while (GetMessage(out message, IntPtr.Zero, 0, 0) != 0)
                    {
                        TranslateMessage(ref message);
                        DispatchMessage(ref message);
                    }
                });
            }
        }
    }

    return Running;
}

However, the MSDN states that GetMessage retrieves a message from the calling thread's message queue, therefore that wouldn't be possible as it's wrapped within a different thread/task. I can't simply move the CreateWindowEx function call to be within taskFactory.StartNew() scope.

Any ideas on how to achieve this? Perhaps change from GetMessage to PeekMessage maybe (the second might use a lot of the CPU, though)?

REQUIREMENTS:

  1. Start should be synchronous
  2. A new class should be registered every Start call
  3. Message loop should be dispatched within a different thread along GetMessage

Solution

  • I can't simply move the CreateWindowEx function call to be within taskFactory.StartNew() scope.

    Sorry, but you are going to have to do exactly that. Although you can SEND/POST a message to a window that resides in another thread, retrieving and dispatching messages DOES NOT work across thread boundaries. Creating a window, destroying that window, and running a message loop for that window, MUST all be done in the same thread context.

    In your case, that means all of that logic has to be inside of the callback that you are passing to taskFactory.StartNew().

    Any ideas on how to achieve this? Perhaps change from GetMessage to PeekMessage maybe (the second might use a lot of the CPU, though)?

    That won't solve your issue. Both GetMessage() and PeekMessage() pull messages only from the message queue of the calling thread, and that pull can only return window messages for windows that are owned by the calling thread. This is explicitly stated in their documentations:

    GetMessage

    hWnd

    Type: HWND

    A handle to the window whose messages are to be retrieved. The window must belong to the current thread.

    If hWnd is NULL, GetMessage retrieves messages for any window that belongs to the current thread, and any messages on the current thread's message queue whose hwnd value is NULL (see the MSG structure). Therefore if hWnd is NULL, both window messages and thread messages are processed.

    If hWnd is -1, GetMessage retrieves only messages on the current thread's message queue whose hwnd value is NULL, that is, thread messages as posted by PostMessage (when the hWnd parameter is NULL) or PostThreadMessage.

    PeekMessage

    hWnd

    Type: HWND

    A handle to the window whose messages are to be retrieved. The window must belong to the current thread.

    If hWnd is NULL, PeekMessage retrieves messages for any window that belongs to the current thread, and any messages on the current thread's message queue whose hwnd value is NULL (see the MSG structure). Therefore if hWnd is NULL, both window messages and thread messages are processed.

    If hWnd is -1, PeekMessage retrieves only messages on the current thread's message queue whose hwnd value is NULL, that is, thread messages as posted by PostMessage (when the hWnd parameter is NULL) or PostThreadMessage.

    The only differences between GetMessage() and PeekMessage() are that:

    • GetMessage() waits for a message if the queue is empty, whereas PeekMessage() does not.

    • PeekMessage() can return a message without removing it from the queue, whereas GetMessage() cannot.

    Now, with that said, try something like the following. It will maintain the same semantics as your original code, ie it waits for the new window to be created before exiting. The window creation is just performed in the task thread instead of the calling thread:

    public bool Start()
    {
        if (!Running)
        {
            Handle = IntPtr.Zero;
    
            var readyEvent = new ManualResetEventSlim();
    
            // Launch the message loop thread
            taskFactory.StartNew(() =>
            {
                var processHandle = Process.GetCurrentProcess().Handle;
    
                var windowClass = new WndClassEx
                {
                    lpszMenuName = null,
                    hInstance = processHandle,
                    cbSize = WndClassEx.Size,
                    lpfnWndProc = WndProc,
                    lpszClassName = Guid.NewGuid().ToString()
                };
    
                // Register the dummy window class
                var classAtom = RegisterClassEx(ref windowClass);
    
                // Check whether the class was registered successfully
                if (classAtom != 0u)
                {
                    // Create the dummy window
                    Handle = CreateWindowEx(0x08000000, classAtom, "", 0, -1, -1, -1, -1, IntPtr.Zero, IntPtr.Zero, processHandle, IntPtr.Zero);
                    Running = Handle != IntPtr.Zero; 
                }
    
                readyEvent.Set();
    
                if (Handle != IntPtr.Zero)
                {
                    Message message;
    
                    while (GetMessage(out message, IntPtr.Zero, 0, 0) != 0)
                    {
                        TranslateMessage(ref message);
                        DispatchMessage(ref message);
                    }
    
                    // if the message queue received WM_QUIT other than
                    // from the window being destroyed, for instance by
                    // a corresponding Stop() method posting WM_QUIT
                    // to the window, then destroy the window now...
                    if (IsWindow(Handle))
                    {
                        DestroyWindow(Handle);
                    }
    
                    Handle = IntPtr.Zero;
                }
            });
    
            readyEvent.Wait();
        }
    
        return Running;
    }