Windows 消息循环

11/16/2018 Windows

Windows 消息循环 && 消息循环在 WPF 中的应用

使用 EN5 课件获得更好的阅读体验:

【希沃白板5】课件分享 : 《Windows培训 - 消息循环》 https://r302.cc/q2d1jB (opens new window) 点击链接直接预览课件

# 1 程序是怎么跑起来的?

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello Cvte.");
        Console.ReadLine();
    }
}

这是一段 C# Main 函数,如果不写 Console.ReadLine(); ,则程序会“一闪而过”,写了 Console.ReadLine(); 程序会阻塞,可以查看结果。 下面看一段复杂一点点的:

Console.WriteLine("Starting, Input Something:");
while (true)
{
    string input = Console.ReadLine();
    if (input == "exit")
    {
        break;
    }
    else
    {
        Console.WriteLine(
            !string.IsNullOrWhiteSpace(input)
            ? $"Your Input to lower is:{input.ToLower()}"
            : "You Inputted Nothing");
    }
}

这里有一个 while 循环,这样程序就可以一直运行了,我们可以说:这个程序由这个 while 循环驱动。

那,Windows 程序是由什么驱动的,答案呼之欲出:“消息循环”。

# 2 消息循环的数据结构

typedef struct {  
    HWND hwnd;      // 消息的目标窗口句柄
    UINT message;   // 消息标识
    WPARAM wParam;  // 16位的参数
    LPARAM lParam;  // 32位的参数
    DWORD time;     // 消息发生的时间
    POINT pt;       // 消息发生时,鼠标的屏幕坐标
} MSG, *PMSG; 

# 2.1 消息的分类

消息的取值范围是 0x0000 - 0xFFFF。

从 0x0000 到 0x03FF,为系统定义的消息,常见的 WM_PAINT、WM_CREATE 等均在其中; 从 0x0400 到 0x7FFF,专用于用户自定义的消息,可以使用 WM_USER + x 的形式自行定义,其中WM_USER 的值就是 0x0400,x 取一个整数; 从 0x8000 到 0xBFFF,从 Windows 95 开始,也用作用户自定义的消息范围,可以使用 WM_APP + x 的形式自行定义。 根据微软的建议,WM_APP类消息用于程序之间的消息通信,而 WM_USER 类消息则最好用于某个特定的窗口类。 微软自己遵循这一惯例,所以,公用控件的消息,如 TVM_DELETEITEM,基本都是 WM_USER 类属。 从 0xC000 到 0xFFFF,这个区段的消息值保留给 RegisterWindowMessage 这个 API,此 API 可以接受一个字符串,把它变换成一个唯一的消息值。

# 3 消息的处理流程

消息产生 => 消息队列 => 消息循环 => 消息处理

# 3.1 消息产生

消息产生的源头

  • 系统 一部分由输入设备(键盘鼠标等)产生,如 WM_MOUSEMOVE 。
    一部分由系统User库自己产生,User部分(或者是系统内的其他部分通过User部分)为了实现自身的正常行为或者管理功能而主动生成的。如 WM_WINDOWPOSCHANGED。

  • 应用程序自定义的消息

消息产生的方式

这里说主要的两个消息产生函数

  • SendMessage 等待消息处理完成后,SendMessage才返回。 深入一点的表达式:等待窗口处理函数返回后,SendMessage才返回。

  • PostMessage 不等待消息处理完成,立刻返回。 PostMessage只管发送消息,消息有没有被送到则并不关心,只要发送了消息,便立刻返回。

两个问题: 问:消息产生之后到了哪里?
答:消息队列。

问:SendMessage 产生的消息,会进入消息队列吗? 答:在同一个线程内,SendMessage 会直接调用目标窗口的窗口过程函数处理消息,并等待其返回。 跨线程的情况,SendMessage 会将消息发送到目标线程的消息队列(高优先级,排序在前)。然后等待目标线程的返回值。

# 3.2 消息队列

  • 系统消息队列 接收输入设备的消息,分配给线程消息队列。
    输入设备(键盘、鼠标或者其他)的驱动程序会把用户的操作输入转化成消息放置于系统队列中,然后系统会把此消息转到目标窗口所在线程的消息队列中等待处理。

  • 线程(UI)消息队列 当前UI线程中的消息。
    每一个GUI线程都会维护这样一个线程消息队列。(这个队列只有在线程调用 User 或者 GDI 函数时才会创建,默认并不创建)。然后线程消息队列中的消息会被本线程的消息循环(有时也被称为消息泵)派送到相应的窗口过程(也叫窗口回调函数)处理。

两个问题: 问:消息队列属于谁?
答:属于UI线程(不属于窗口)。

问:非UI线程有消息队列吗? 没有。

# 3.3 消息循环

while(GetMessage(&msg, NULL,0, 0))  
{  
	TranslateMessage(&msg);  
	DispatchMessage (&msg);  
}   

如上,消息循环就是一个 while 循环,与文章最开始提到 while 向呼应。
其中 GetMessage 取出消息,TranslateMessage 翻译消息,DispatchMessage 调度消息。

问:消息循环属于谁?
答:每一个UI线程有一个消息循环(不是每一个窗口。)

消息循环的另一个样子:

while (!done)
{
    if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
    {
        if (msg.message == WM_QUIT)
        {
            done = TRUE;
        }
        else
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    else
    {
        // 还可以驱动点别的事情,如 openGL 绘图。
    }
}

分别来看:

  • 取出消息

GetMessage
GetMessage会阻塞等待,直到取到一个消息。

PeekMessage PeekMessage则不阻塞,立即返回。
PeekMessage有一个标志参数,这个标志参数指定了如果队列中如果有消息的话,PeekMessage 的行为。 如果该标志中含有 PM_REMOVE,则 PeekMessage 会把新消息返回到 MSG 结构中,正如 GetMessage 的行为那样。 如果标志中指定了 PM_NOREMOVE,则不会从消息队列中移除任何消息。

  • 翻译消息 望文生义地看,翻译消息是对消息数据结构进行某种转换吗?
    不是的,TranslateMessage不修改原有消息,只在特定情况下产生新的消息。

TranslateMessage函数不修改由参数lpMsg指向的消息结构。
仅为那些由键盘驱动器映射为ASCII字符的键产生WM_CHAR消息。
如:
消息WM_KEYDOWN和WM_KEYUP组合产生一个WM_CHAR或WM_DEADCHAR消息。
消息WM_SYSKEYDOWN和WM_SYSKEYUP组合产生一个WM_SYSCHAR或 WM_SYSDEADCHAR 消息。

所以,如果程序中没有字符处理的需要,这句是可以不要的。

  • 分派消息 将消息分配给 hwnd 指定的窗口函数,让其处理。
    如果没有找到对应的窗口,则丢弃。

# 3.4 消息处理

消息在消息循环中被分配到指定的窗口过程函数,由其处理。

// 有删减的窗口过程函数
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_COMMAND:
	case WM_PAINT:
	case WM_CREATE:
	default:
	return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

回顾两个问题: 问:WndProc 函数由谁调用? 答:DispatchMessage or SendMessage。 从上文中可以看到,窗口过程函数不是有程序员自己调用的,而是系统在恰当的时机调用,这个时机就是 DispatchMessage or SendMessage。

问:未处理的消息交给谁? 答:DefWindowProc。 DefWindowProc只处理关闭等感兴趣的消息,其它的消息则忽略。

# 回顾

消息队列和消息循环属于UI线程,窗口没有,其它普通线程没有。
窗口有自己的窗口过程函数,消息在这里被处理。
消息循环驱动整个程序跑起来。

想一睹消息循环究竟是如何跑起来的? 原始 win32 窗口是如何被创建的?

在 VS 中,新建一个win32的窗口程序,即可看到。


win32/MFC/WinForm/WPF 都依靠消息循环驱动,让程序跑起来。

# 4 消息循环在 WPF 中的应用

# 4.1 引入

只听说过 Dispatcher ,哪里来的消息循环?

先瞧一眼 WPF 启动运行堆栈:

可以发现 PushFrameImpl 这个方法。
去看其源码,就发现了熟悉的消息循环 :

可以理解为:Dispatcher 对消息循环的操作进行了“封装” 。 那,Dispatcher 是谁?

# 4.2 Dispatchcer

Provides services for managing the queue of work items for a thread.
提供用于管理线程工作项队列的服务。

大部分WPF对象,都是 DispatcherObject。这意味着,可以在 DispatcherObject 中(如 Window 中), 使用 this.Dispatchcer 获取到 Dispatchcer 。

一般我们会通过三种方式获取 Dispatchcer :

// App.Current.Dispatcher;(Application.Current.Dispatcher)
var dispatcher1 = App.Current.Dispatcher;

// CurrentDispatcher;
var dispatcher2 = System.Windows.Threading.Dispatcher.CurrentDispatcher;

// System.Windows.Threading.DispatcherObject.Dispatcher;
var dispatcher3 = this.Dispatcher;

可分为两类:

  • 当前线程的 Dispatcher: System.Windows.Threading.Dispatcher.CurrentDispatcher;

  • 创建对应对象的 Dispatcher: App.Current.Dispatcher; DispatcherObject.Dispatcher;

可参见: Why not Dispather.CurrentDispatcher - haungtengxiao (opens new window)

Dispatcher 和线程是什么关系?

  • Dispatcher 属于线程(与线程一一对应)。
  • WPF的对象在获取this.Dispatcher属性时,不同对象取的都是同一个Dispatcher实例。(因为都是同一个UI线程创建的。)
  • 在默认的 WPF UI线程中: App.Current.Dispatcher = DispatcherObject.Dispatcher

所有的线程(UI线程,普通线程)都有 Dispatcher 吗? 是的。

在所有线程中,调用 System.Windows.Threading.Dispatcher.CurrentDispatcher 都会得到一个属于这个线程的 Dispatcher 对象。(不用的时候不会创建) 所以:如果你想在一个后台线程中,使用 Dispatcher.CurrentDispatcher.Invoke 将操作封送到 UI 线程,是做不到的。因为这时候获取到的 Dispatcher 不是UI线程的 Dispatcher , 而是当前线程自己的 Dispatcher。

# 4.3 Dispatcher 如何实现跨线程的调用。

最常使用 Dispatcher 的创建就是,在后台线程更新 UI ,那 Dispatcher 是如何做到的呢。

当你调用

Application.Current.Dispatcher.Invoke(() =>
{
    SendMessageBtn.Content = "更新按钮";
});

时,Dispatcher 究竟做了什么,把操作转移到 UI 线程上去了。

关于 Invoke,InvokeSync,BeginInvoke 的区别,参见: 深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分) - walterlv (opens new window)

  1. 将调用的Delegate和优先级包装成一个DispatcherOperation放入Dispatcher维护的优先级队列当中,这个Queue是按DispatcherPriority排序的,总是高优先级的DispatcherOperation先被处理。

  2. 往当前线程的消息队列当中Post一个名为MsgProcessQueue的Message。(这个消息是WPF自己定义的。)这个消息被Post到消息队列之前,还要设置MSG.Handle,这个Handle就是Window 1#的Handle。指定Handle是为了在消息循环Dispatch消息的时候,指定哪个窗口的 WndProc 处理这个消息。

  3. 消息循环读取消息。

  4. 系统根据获取消息的Handle,发现跟Window1#的Handle相同,那么这个消息派发到Window1#的窗口过程,让其处理。

  5. 在窗口过程中,优先级队列当中取一个DispatcherOperation。

  6. 执行DispatcherOperation.Invoke方法,Invoke方法的核心就是调用DispatcherOperation构造时传入的Delegate,也就是Dispatcher.BeginInvoke传入的Delegate。最终这个Foo()方法就被执行了。

# 4.4 回顾

  • WPF 底层仍然靠信息循环来驱动。
  • Dispatcher 使用消息循环来实现跨进程的委托调用。
  • Dispatcher 属于线程,需要理解当前拿到的 Dispatcher 到底是哪个 Dispatcher 。

参考资料:

Windows 消息机制浅析 - bitbit - 博客园 (opens new window)

SendMessage、PostMessage原理-大白菜-51CTO博客 (opens new window)

WPF的消息机制(一)- 让应用程序动起来 - 葡萄城技术团队 - 博客园 (opens new window)

WPF的消息机制(二)- WPF内部的5个窗口之隐藏消息窗口 - 葡萄城技术团队 - 博客园 (opens new window)

WPF的消息机制(三)- WPF内部的5个窗口之处理激活和关闭的消息窗口以及系统资源通知窗口 - 葡萄城技术团队 - 博客园 (opens new window)

Why not Dispather.CurrentDispatcher - haungtengxiao (opens new window)

深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分) - walterlv (opens new window)

Tutorial: Getting Started (opens new window)

更新时间: Friday, March 12, 2021 22:54