zoukankan      html  css  js  c++  java
  • Windows 消息循环(1)

    本文从消息循环是如何驱动程序的这个角度,对 Windows 消息循环进行概览性介绍。

    使用 EN5 课件获得更好的阅读体验:
    【希沃白板5】课件分享 : 《Windows培训 - 消息循环》
    https://r302.cc/q2d1jB
    点击链接直接预览课件

    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的窗口程序,即可看到。

  • 相关阅读:
    【转载】浮点数在内存中的存储格式
    流媒体完全手册
    Windows Mobile 5.0 SDK 下载地址
    实时传输协议 RTP
    JDK环境变量配置
    扩展Windows Mobile模拟器存储空间的方法
    WinSock学习笔记
    实时传输协议 RTCP
    NHibernate从入门到精通系列(4)——持久对象的生命周期(上)
    实时流协议 RTSP
  • 原文地址:https://www.cnblogs.com/jasongrass/p/10423196.html
Copyright © 2011-2022 走看看