C#消息泵探索

632 阅读2分钟

 ​消息泵

消息泵,又叫消息循环。

消息循环使用一个图形化用户界面下Microsoft Windows。具有GUI的Windows 程序是由事件驱动的。Windows为每个创建窗口的线程维护一个单独的消息队列。通常只有第一个线程创建窗口。Windows 放置消息每当鼠标活动发生在该线程的窗口上时,每当该窗口具有焦点时键盘活动发生时,以及其他时候,都将进入该队列。进程还可以将消息添加到自己的队列中。为了接受用户输入以及出于其他原因,具有窗口的每个线程必须不断地从其队列中检索消息,并对其采取行动。通过编写一个循环调用 GetMessage(阻塞消息并检索它),然后调用 DispatchMessage(调度消息),并无限重复,从而使进程做到这一点。这就是消息循环。主程序中通常有一个消息循环,它运行在主线程上,并且在每个创建的模态对话框中都有附加的消息循环。留言给进程的每个窗口都通过其消息队列,并由其消息循环处理。

因此,综上所述,消息循环是一种事件循环。

​消息泵的实现

private bool LocalModalMessageLoop(Form form)
            {
                try
                {
                    NativeMethods.MSG msg = default(NativeMethods.MSG);
                    bool flag = false;
                    bool flag2 = true;
                    while (flag2)
                    {
                        if (UnsafeNativeMethods.PeekMessage(ref msg, NativeMethods.NullHandleRef, 0, 0, 0))
                        {
                            if (msg.hwnd != IntPtr.Zero && SafeNativeMethods.IsWindowUnicode(new HandleRef(null, msg.hwnd)))
                            {
                                flag = true;
                                if (!UnsafeNativeMethods.GetMessageW(ref msg, NativeMethods.NullHandleRef, 0, 0))
                                {
                                    continue;
                                }
                            }
                            else
                            {
                                flag = false;
                                if (!UnsafeNativeMethods.GetMessageA(ref msg, NativeMethods.NullHandleRef, 0, 0))
                                {
                                    continue;
                                }
                            }

                            if (!PreTranslateMessage(ref msg))
                            {
                                UnsafeNativeMethods.TranslateMessage(ref msg);
                                if (flag)
                                {
                                    UnsafeNativeMethods.DispatchMessageW(ref msg);
                                }
                                else
                                {
                                    UnsafeNativeMethods.DispatchMessageA(ref msg);
                                }
                            }

                            if (form != null)
                            {
                                flag2 = !form.CheckCloseDialog(closingOnly: false);
                            }
                        }
                        else
                        {
                            if (form == null)
                            {
                                break;
                            }

                            if (!UnsafeNativeMethods.PeekMessage(ref msg, NativeMethods.NullHandleRef, 0, 0, 0))
                            {
                                UnsafeNativeMethods.WaitMessage();
                            }
                        }
                    }

                    return flag2;
                }
                catch
                {
                    return false;
                }
            }

以上便是WinForm中关于消息泵实现的核心源码了,我们看到在进入方法后,会先执行UnsafeNativeMethods.PeekMessage方法进行消息的读取。

PeekMessage函数为一个消息检查线程消息队列,并将该消息(如果存在)放于指定的结构。

接下来我们看一下微软对PeekMessage的定义吧。

我们可以看到该方法会获取到当前线程(其实也就是主线程)From触发的任何类型消息,当没有获取到消息或者窗体的字符集为Unicode时,将通过GetMessageW函数获取当前线程消息队列的消息,反之则调用GetMessageA。

我们可以通过winuser.h中的定义可以看到GetMessageA、GetMessageW均由GetMessage派生,二者的区别在于当前窗体的字符集是否为UNICODE。

GetMessage函数则是用于从当前线程的消息队列里取得一个消息并将其放于指定的结构。可通过GetMessage取得与指定窗口联系的消息和由PostThreadMesssge寄送的线程消息。GetMessage接收一定范围的消息值。GetMessage不接收属于其他线程或应用程序的消息。

而PostThreadMesssge的作用则是将一个消息放入到当前线程的消息队列里,不等待线程处理消息就返回。

这篇文章主要带大家宏观观察一下消息泵的几个主要组成部分,下篇文章将进行详细剖析。

然后进行的就是消息的预处理过程,日后有时间详细分析一下,先把预处理的核心代码贴在下面。

[SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
        internal static PreProcessControlState PreProcessControlMessageInternal(Control target, ref Message msg) {
            if (target == null) {
                target = Control.FromChildHandleInternal(msg.HWnd);
            }

            if (target == null) {
                return PreProcessControlState.MessageNotNeeded;
            }

            // reset state that is used to make sure IsInputChar, IsInputKey and 
            // ProcessUICues are not called multiple times.
            // ISSUE: Which control should these state bits be set on? probably the target.
            target.SetState2(STATE2_INPUTKEY,  false);
            target.SetState2(STATE2_INPUTCHAR, false);
            target.SetState2(STATE2_UICUES,    true);

            Debug.WriteLineIf(ControlKeyboardRouting.TraceVerbose, "Control.PreProcessControlMessageInternal " + msg.ToString());
            
            try {
                Keys keyData = (Keys)(unchecked((int)(long)msg.WParam) | (int)ModifierKeys);

                // Allow control to preview key down message.
                if (msg.Msg == NativeMethods.WM_KEYDOWN || msg.Msg == NativeMethods.WM_SYSKEYDOWN) {
                    target.ProcessUICues(ref msg);                    

                    PreviewKeyDownEventArgs args = new PreviewKeyDownEventArgs(keyData);
                    target.OnPreviewKeyDown(args);
                    
                    if (args.IsInputKey) {
                        Debug.WriteLineIf(ControlKeyboardRouting.TraceVerbose, "PreviewKeyDown indicated this is an input key.");
                        // Control wants this message - indicate it should be dispatched.
                        return PreProcessControlState.MessageNeeded;
                    }                    
                }

                PreProcessControlState state = PreProcessControlState.MessageNotNeeded;
                
                if (!target.PreProcessMessage(ref msg)) {
                    if (msg.Msg == NativeMethods.WM_KEYDOWN || msg.Msg == NativeMethods.WM_SYSKEYDOWN) {

                        // check if IsInputKey has already procssed this message
                        // or if it is safe to call - we only want it to be called once.
                        if (target.GetState2(STATE2_INPUTKEY) || target.IsInputKey(keyData)) {
                            Debug.WriteLineIf(ControlKeyboardRouting.TraceVerbose, "Control didn't preprocess this message but it needs to be dispatched");
                            state = PreProcessControlState.MessageNeeded;
                        }
                    }
                    else if (msg.Msg == NativeMethods.WM_CHAR || msg.Msg == NativeMethods.WM_SYSCHAR) {

                        // check if IsInputChar has already procssed this message
                        // or if it is safe to call - we only want it to be called once.
                        if (target.GetState2(STATE2_INPUTCHAR) || target.IsInputChar((char)msg.WParam)) {
                            Debug.WriteLineIf(ControlKeyboardRouting.TraceVerbose, "Control didn't preprocess this message but it needs to be dispatched");
                            state = PreProcessControlState.MessageNeeded;
                        }
                    }                  
                }
                else {
                    state = PreProcessControlState.MessageProcessed;
                }

                return state;
            }
            finally {
                target.SetState2(STATE2_UICUES, false);
            }
        }

结论

消息泵的本质就是一个主线程的消息队列,通过监听键盘的动作产生的消息,去除掉诸如F10、Menu、Tab等会影响焦点指示器和键盘提示的键,将剩余有用消息放进消息队列,用于读取。