【问题标题】:Dispatching managed Win32 WndProc on a sepparate thread在单独的线程上调度托管 Win32 WndProc
【发布时间】:2021-06-09 20:46:27
【问题描述】:

我正在通过非托管 CreateWindowEx 创建一个窗口,使用 PInvoke 作为服务器,以便调度来自不同进程的 SendMessage 调用。这应该包含在 同步 函数中(类注册 + 窗口创建),如下所示:

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

但是,MSDN 声明 GetMessage retrieves a message from the calling thread's message queue,因此这是不可能的,因为它被包装在不同的线程/任务中。我不能简单地将CreateWindowEx 函数调用移动到taskFactory.StartNew() 范围内。

关于如何实现这一目标的任何想法?也许从 GetMessage 更改为 PeekMessage 可能(不过,第二个可能会占用大量 CPU)?

要求:

  1. Start 应该是同步的
  2. 每个Start调用都应该注册一个新类
  3. 消息循环应该沿着GetMessage在不同的线程中调度

【问题讨论】:

    标签: c# winapi sendmessage wndproc


    【解决方案1】:

    我不能简单地将 CreateWindowEx 函数调用移动到 taskFactory.StartNew() 范围内。

    抱歉,您将不得不这样做。尽管您可以向驻留在另一个线程中的窗口发送/发布消息,但检索和分派消息不能跨线程边界工作。创建一个窗口、销毁该窗口以及为该窗口运行一个消息循环,都必须在同一个线程上下文中完成。

    在您的情况下,这意味着所有这些逻辑都必须在您传递给 taskFactory.StartNew() 的回调中。

    关于如何实现这一目标的任何想法?也许从 GetMessage 更改为 PeekMessage 可能(第二个可能会占用大量 CPU)?

    这不会解决您的问题。 GetMessage()PeekMessage() 都只从调用线程的消息队列中拉取消息,并且该拉取只能返回调用线程拥有的窗口的窗口消息。这在他们的文档中明确说明:

    GetMessage

    hWnd

    类型:HWND

    要检索其消息的窗口句柄。 窗口必须属于当前线程。

    如果 hWnd 为 NULL,GetMessage 检索消息属于当前线程的任何窗口,以及当前线程的消息队列中hwnd 值为 NULL 的任何消息(参见 @ 987654329@ 结构)。因此如果hWnd 为NULL,窗口消息和线程消息都会被处理。

    如果hWnd为-1,GetMessage只检索当前线程的消息队列中hwnd值为NULL的消息,即PostMessage(当hWnd参数为NULL时)或@发布的线程消息987654334@.

    PeekMessage

    hWnd

    类型:HWND

    要检索其消息的窗口句柄。 窗口必须属于当前线程。

    如果hWnd 为NULL,PeekMessage 将检索属于当前线程的任何窗口 的消息,以及当前线程的消息队列中hwnd 值为NULL 的任何消息(参见@ 987654338@ 结构)。因此如果hWnd为NULL,窗口消息和线程消息都会被处理。

    如果hWnd 为-1,PeekMessage 只检索当前线程的消息队列中hwnd 值为NULL 的消息,即PostMessage 发布的线程消息(当hWnd 参数为NULL 时)或PostThreadMessage

    GetMessage()PeekMessage() 之间的唯一区别在于:

    • GetMessage() 在队列为空时等待消息,而PeekMessage() 则不会。

    • PeekMessage() 可以返回消息而不将其从队列中删除,而GetMessage() 不能。

    现在,话虽如此,请尝试以下操作。它将保持与您的原始代码相同的语义,即它在退出之前等待创建新窗口。窗口创建只是在任务线程而不是调用线程中执行:

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

    【讨论】:

    • 我知道这会让它“工作”,但是Start 将不再同步,因为它会在窗口句柄创建之前返回。在Start 调用之后,我需要窗口句柄。目前,实现我想要做的唯一方法是在类构造函数中创建窗口并有一个标志来管理启动/停止以忽略 WndProc 上的消息,但这也不是我愿意做的事情。
    • CreateWindowEx()移动到任务线程后,您可以让Start()具有与以前类似的行为,只需让Start()等待任务线程创建窗口,然后再进入线程它的消息循环。我已经更新了我的答案以展示一个例子。
    • 太棒了,雷米! ManualResetEvent[Slim] 是我真正想要的!抱歉回复延迟。
    • Remy,想知道如何在WndProc 中异步发送消息?每条消息(请求)都将包装在一个单独的线程中。我正在寻找的显然是ReplyMessage,但我不知道它是否可能。
    • 消息已发送 > WndProc 接收消息 > WndProc 为该消息创建一个线程 > 通过最近创建的线程返回一个值。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多