在Windows应用程序中,窗体是由一种称为“UI线程(User Interface Thread)”的特殊类型的线程创建的。

  首先,UI线程是一种“线程”,所以它具有一个线程应该具有的所有特征,比如有一个线程函数和一个线程ID。

  其次,“UI线程”又是“特殊”的,这是因为UI线程的线程函数中会创建一种特殊的对象——窗体,同时,还一并负责创建窗体上的各种控件。

  窗体和控件大家都很熟悉了,这些对象具有接收用户操作的功能,它们是用户使用整个应用程序的媒介,没有这样一个媒介,用户就无法控制整个应用程序的运行和停止,往往也无法直接看到程序的运行过程和最终结果。

  那么,窗体和控件又是如何作到对用户操作进行响应的呢?这一响应是不是由窗体和控件自己“主动”完成的?

  换句话说:

  窗体和控件具不具备独立地响应用户操作(比如键盘和鼠标操作)的功能?

  答案是否定的。

  那就奇怪了,比如我们用鼠标点击了一个按钮,并且看到它“陷”下去了,然后又还原,之后,我们确实看到了程序执行了此按钮所对应的任务。难道不是按钮来响应用户操作的吗?

  这实际上是一个错觉。这个错觉产生的根源在于不了解Windows内部的运作机理。

  简单地说,窗体和控件之所以能响应用户操作,关键在于负责创建它们的UI线程拥有一个“消息循环(Message Loop)”。这个消息循环由线程函数负责启动,通常具有以下的“模样”(以C++代码表示):

    MSG msg; //代表一条消息
    BOOL bRet;
    //从UI线程消息队列中取出一条消息
    while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
    {
        if (bRet == -1)
        {
            //错误处理代码,通常是直接退出程序
        }
        else
        {
            TranslateMessage(&msg); //转换消息格式
            DispatchMessage(&msg); //分发消息给相应的窗体
        }
    }

  可以看到,所谓消息循环,其实就是一个While循环语句罢了。

  其中,GetMessage()函数每次从消息队列中取出一条消息,此消息的内容被填充到变量msg中。

  TranslateMessage()函数主要用于将WM_KEYDOWN和WM_KEYUP消息转换WM_CHAR消息。

  提示:

  使用C++开发Windows程序时,各种消息都有一个对应的符号常量,比如,这里的WM_KEYDOWN和WM_KEYUP代表用户按下一个键后所产生的消息。

  消息处理的关键是DispatchMessage()函数。这个函数根据取出的消息中所包含的窗体句柄,将这一消息转发给引此句柄所对应的窗体对象。

  而窗体负责响应消息的函数称为“窗体过程(Window Procedure)”,窗体过程是一个函数,每个窗体一个,它大致拥有以下的“模样”(C++代码):

    LRESULT CALLBACK MainWndProc(……)
    {
        //……
        switch (uMsg) //依据消息标识符进行分类处理
        {
            case WM_CREATE:
                // 初始化窗体.
                return 0;
            case WM_PAINT:
                // 绘制窗体
                return 0;
            //
            //处理其他消息
            //
            default:
                //如果窗体没有定义处理此种消息的代码,则转去调用系统默认的消息处理函数
                return DefWindowProc(hwnd, uMsg, wParam, lParam);
        }
        //……
    }

  可以看到,“窗体过程”不过就是一个多分支语句罢了,在这个语句中,窗体对不同类型的消息进行处理。

  在Windows中,UI控件也被视为一个“Window”,它也拥有自己的“窗体过程”,因此,它也可以同窗体一样,具备处理消息的能力。

  由此我们可以知道UI线程所完成的大致工作就是:

  UI线程启动一个消息循环,每次从本线程所对应的消息队列中取出一条消息,然后根据消息所包容的信息,将其转发给特定的窗体对象,此窗体对象所对应的“窗体过程”函数被调用以处理这些消息。

  上述描述只介绍了事情的后半段,还需要了解事情的前半段,那就是:

  用户操作消息是怎样“跑”到UI线程的消息队列中的?

  我们知道,Windows同时可以运行多个进程,每个进程又拥有多个线程,其中有一些线程是UI线程,这些UI线程可能会创建不止一个窗体,那么问题发生了:

  用户在屏幕上某个位置按了一下鼠标,相关信息是怎样传给特定的UI线程,并最终由特定窗体的“窗体过程”负责处理?

  答案是操作系统负责完成消息的投寄工作。

  操作系统会监控计算机上的键盘和鼠标等输入设备,为每一个输入事件(由用户操作所引发,比如用户按了某个键)生成一个消息。根据事件发生时的情况(比如当前激活的窗体负责接收用户按键,而依据用户点击鼠标的坐标可以知道用户在哪个窗体区域内点击了鼠标),操作系统会确定出此消息应该发给哪个窗体对象。

  这些生成的消息会统一地先临时放置在一个“系统消息队列(system message queue)”中,然后,操作系统有一个专门的线程负责从这一队列中取出消息,根据消息的目标对象(就是窗体的句柄),将其移动到创建它的UI线程所对应的消息队列中。操作系统在创建进程和线程时,都同时记录了大量的控制信息(比如通过进程控制块和句柄表可以查找到进程所创建的所有线程和引用的核心对象),因此,根据窗体句柄来确定此消息应属于哪个UI线程对于操作系统来说是很简单的一件事。

  注意,每个UI线程都有一个消息队列,而不是每个窗体一个消息队列!

  那么,操作系统是不是会为每一个线程都创建一个消息队列呢?

  答案是:只有当一个线程调用Win32 API中的GDI(Graphics Device Interface)和User函数时,操作系统才会将其看成是一个UI线程,并为它创建一个消息队列。

  需要注意的是,消息循环是由UI线程的线程函数启动的,操作系统不管这件事,它只管为UI线程创建消息队列。因此,如果某个UI线程的线程函数中没有定义消息循环,那么,它所拥有的窗体是无法正确绘制的。

  请看以下代码:

    class Program
    {
        static void Main(string[] args)
        {
            Form1 frm = new Form1();
            frm.Show();
            Console.ReadKey();
        }
    }

  上述代码属于一个控制台应用程序,在Main()函数中,创建了一个Form1窗体对象,调用它的Show()方法显示,然后调用Console.ReadKey()方法等待用户按键结束进程。

  程序运行的截图如下:

  如上图所示,会发现窗体显示一个空白方框,不接收任何的鼠标和键盘操作。

  原因何在?

  产生这一现象的原因可以解释如下:

  由于控制台程序需要运行于一个“控制台窗口”中,因此,操作系统认为它是一个UI线程,会为其创建一个消息队列。

  Main()函数由于是程序入口点,所以执行它的线程是进程的第一个线程(即主线程),在主线程中,创建了一个Form1窗体对象,对其Show()方法的调用只是设置其Visible属性=true,这将导致Windows调用相应的Win32 API函数显示窗体,但这一调用并非阻塞调用,也没有启动一个消息循环,所以Show()方法很快返回,继续执行下一句“Console.ReadKey();”,此句的执行导致主线程调用相应的Win32 API函数等待用户按钮,阻塞执行。

  注意,如果这时用户用鼠标点击窗体,尝试与窗体交互,相应的消息的确发到了控制台应用程序主线程的消息队列中,但主线程并未启动一个消息循环(你看到Main()函数中有任何的循环语句吗?)以取出消息队列中的消息并“分发”给窗体,因此,窗体函数没被调用,自然无法正确绘制了。

  如果窗体本身是调用ShowDialog()方法显示的,这是一个阻塞调用,它会在内部启动一个消息循环,此消息循环可以从主线程的消息队列是提取消息,从而让此窗体成为一个“正常”的窗体。

  当用户关闭窗体后,Main()方法后继的代码继续执行,直到运行结束。

  如果在创建窗体对象并调用Show()方法显示后,主线程没有调用“Console.ReadKey();”之类方法“暂停”,而是直接退出,这将导致操作系统中止整个进程,回收所有核心对象,因此,创建的窗体也会被销毁,不可能再看见它。

  现在再考虑复杂一些:如果我们在另一个线程中创建并显示窗体,又将如何?

class Program
    {
        static void Main(string[] args)
        {
            Thread th = new Thread(ShowWindow);
            th.Start();//在另一个线程中创建并显示窗体
            Console.WriteLine("窗体已创建,敲任意键退出...");
            Console.ReadKey();
            Console.WriteLine("主线程退出...");
         }
   
        static void ShowWindow()
        {
            Form1 frm = new Form1();
            frm.ShowDialog();
        }
    }

  程序运行结果如下:

  可以看到,由于窗体使用ShowDialog()显示,因此,控制台窗口和应用程序窗体都能正常地接收用户的键盘和鼠标消息。即使主线程退出了,只要窗体没有关闭,操作系统会认为“进程”仍在执行,因此,控制台窗口会保持显示,直到窗体关闭,整个进程才结束。

  在这种情况下,本示例程序中有两个UI线程,一个是控制台窗口,另一个创建应用程序窗体的那个线程。

  如果在线程函数中创建窗体后,改为Show()方法显示,由于Show()方法没有启动消息循环,所以窗体不能正确绘制,并且会随着创建它的UI线程的终止而被操作系统回收资源。

  有趣的是,我们可以使用Visual Studio设置“控制台应用程序”不创建“控制台窗口”,只需将项目类型改为“Windows Application”即可。

  这时,示例程序运行时,Visual Studio会报告错误:

  引发这一错误的原因是应用程序主线程不再创建控制台窗口,操作系统不再认为它是UI线程,不为其创建消息队列,主线程将无法接收到任何按键消息, 因此Console.ReadKey()底层调用的Win32API函数无法正常运行,引发程序异常。

/******************************************windows消息循环标准实例*****************************


  1. #include <windows.h>

  2. LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);

  3. int WINAPI WinMain(HINSTANCE hINstance,

  4. HINSTANCE hPrevInstance,

  5. LPSTR lpszCmdParam,

  6. int nCmdShow)

  7. {

  8. static char szAppName[]="xianshi";

  9. HWND hwnd;

  10. MSG msg;

  11. WNDCLASS wndclass;

  12. if(! hPrevInstance)

  13. {

  14. wndclass.style = CS_HREDRAW | CS_VREDRAW;

  15. wndclass.lpfnWndProc = WndProc;

  16. wndclass.cbClsExtra=0;

  17. wndclass.cbWndExtra=0;

  18. wndclass.hInstance=hINstance;

  19. wndclass.hIcon = LoadIcon(NULL,IDI_APPLICATION);

  20. wndclass.hCursor = LoadCursor(NULL,IDC_ARROW);

  21. wndclass.hbrBackground=(HBRUSH)GetStockObject(LTGRAY_BRUSH);

  22. wndclass.lpszMenuName = NULL;

  23. wndclass.lpszClassName = szAppName;

  24. RegisterClass(&wndclass);

  25. }

  26. hwnd=CreateWindow(

  27. szAppName,

  28. "The XianShi",

  29. WS_OVERLAPPEDWINDOW,

  30. CW_USEDEFAULT,CW_USEDEFAULT,

  31. CW_USEDEFAULT,CW_USEDEFAULT,

  32. NULL,

  33. NULL,

  34. hINstance,

  35. NULL);

  36. ShowWindow(hwnd,nCmdShow);

  37. UpdateWindow(hwnd);

  38. while(GetMessage(&msg,NULL,0,0))

  39. {

  40. TranslateMessage(&msg);

  41. DispatchMessage(&msg);

  42. }

  43. return msg.wParam;

  44. }

  45. LRESULT CALLBACK WndProc(HWND hwnd,

  46. UINT message,

  47. WPARAM wParam,

  48. LPARAM lParam)

  49. {

  50. HDC hdc;

  51. PAINTSTRUCT ps;

  52. RECT rect;

  53. static LOGFONT lf;

  54. HFONT hnewFont;//**********

  55. HFONT holdFont;//**********

  56. switch(message)

  57. {

  58. case WM_CREATE:

  59. return 0;

  60. case WM_PAINT:

  61. lf.lfHeight=-64;

  62. lf.lfWeight=500;

  63. lf.lfPitchAndFamily=DEFAULT_PITCH & FF_DONTCARE;

  64. lf.lfCharSet=GB2312_CHARSET;

  65. strcpy(lf.lfFaceName,"黑体");

  66. hnewFont=CreateFontIndirect(&lf);

  67. hdc=BeginPaint(hwnd,&ps);

  68. GetClientRect(hwnd,&rect);

  69. GetClientRect(hwnd,&rect);

  70. holdFont=(HFONT)SelectObject(hdc,hnewFont);

  71. SetTextColor(hdc,RGB(255,0,0));

  72. SetBkColor(hdc,RGB(255,255,0));

  73. DrawText(hdc,

  74. "VC中显示字体与背景",

  75. -1,

  76. &rect,

  77. DT_SINGLELINE | DT_CENTER | DT_VCENTER);

  78. SelectObject(hdc,holdFont);

  79. DeleteObject(hnewFont);

  80. EndPaint(hwnd,&ps);

  81. return 0;

  82. case WM_DESTROY:

  83. PostQuitMessage(0);

  84. return 0;

  85. }

  86. return DefWindowProc(hwnd,message,wParam,lParam);

  87. }

一 Windows中有一个系统消息队列,对于每一个正在执行的Windows应用程序,系统为其建立一个“消息队列”,即应用程序队列,用来存放该程序可能创建的各种窗口的消息。应用程序中含有一段称作“消息循环”的代码,用来从消息队列中检索这些消息并把它们分发到相应的窗口函数中。

二 Windows为当前执行的每个Windows程序维护一个「消息队列」。在发生输入事件之后,Windows将事件转换为一个「消息」并将消息放入程序的消息队列中。程序通过执行一块称之为「消息循环」的程序代码从消息队列中取出消息:
while(GetMessage (&msg, NULL, 0, 0))       
{        
    TranslateMessage (&msg) ;        
    DispatchMessage (&msg) ;       
}

msg变量是型态为MSG的结构,型态MSG在WINUSER.H中定义如下:
typedef struct tagMSG       
{       
    HWND   hwnd ;        
    UINT   message ;        
    WPARAM wParam ;        
    LPARAM lParam ;        
    DWORD  time ;        
    POINT  pt ;       
}       
MSG, * PMSG ;
      
POINT数据型态也是一个结构,它在WINDEF.H中定义如下:
typedef struct tagPOINT       
{       
    LONG  x ;       
    LONG  y ;       
}       
POINT, * PPOINT;
TranslateMessage(&msg); 将msg结构传给Windows,进行一些键盘转换。(关于这一点,我们将在第六章中深入讨论。)
DispatchMessage(&msg);又将msg结构回传给Windows。然后,Windows将该消息发送给适当的窗口消息处理程序,让它进行处理。这也就是说,Windows将呼叫窗口消息处理程序。在HELLOWIN中,这个窗口消息处理程序就是WndProc函数。处理完消息之后,WndProc传回到Windows。此时,Windows还停留在DispatchMessage呼叫中。在结束DispatchMessage呼叫的处理之后,Windows回到HELLOWIN程序中,并且接着从下一个GetMessage呼叫开始消息循环。
        
三 队列化消息与非队列化消息
    
消息能够被分为「队列化的」和「非队列化的」。队列化的消息是由Windows放入程序消息队列中的。在程序的消息循环中,重新传回并分配给窗口消息处理程序。非队列化的消息在Windows呼叫窗口时直接送给窗口消息处理程序。也就是说,队列化的消息被「发送」给消息队列,而非队列化的消息则「发送」给窗口消息处理程序。任何情况下,窗口消息处理程序都将获得窗口所有的消息--包括队列化的和非队列化的。窗口消息处理程序是窗口的「消息中心」。

队列化消息基本上是使用者输入的结果,以击键(如WM_KEYDOWN和WM_KEYUP消息)、击键产生的字符(WM_CHAR)、鼠标移动(WM_MOUSEMOVE)和鼠标按钮(WM_LBUTTONDOWN)的形式给出。队列化消息还包含时钟消息(WM_TIMER)、更新消息(WM_PAINT)和退出消息(WM_QUIT)。

非队列化消息则是其它消息。在许多情况下,非队列化消息来自呼叫特定的Windows函数。例如,当WinMain呼叫CreateWindow时,Windows将建立窗口并在处理中给窗口消息处理程序发送一个WM_CREATE消息。当WinMain呼叫ShowWindow时,Windows将给窗口消息处理程序发送WM_SIZE和WM_SHOWWINDOW消息。当WinMain呼叫UpdateWindow时,Windows将给窗口消息处理程序发送WM_PAINT消息。键盘或鼠标输入时发出的队列化消息信号,也能在非队列化消息中出现。例如,用键盘或鼠标选择了一个菜单项时,键盘或鼠标消息就是队列化的,而说明菜单项已选中的WM_COMMAND消息则可能就是非队列化的。

四 SendMessage()与PostMessage()之间的区别是什么?
它们两者是用于向应用程序发送消息的。PostMessagex()将消息直接加入到应用程序的消息队列中,不等程序返回就退出;而SendMessage()则刚好相反,应用程序处理完此消息后,它才返回。我想下图能够比较好的体现这两个函数的关系:

五 函数peekmessage和getmessage的区别?

两个函数主要有以下两个区别:
1.GetMessage将等到有合适的消息时才返回,而PeekMessage只是撇一下消息队列。
2.GetMessage会将消息从队列中删除,而PeekMessage可以设置最后一个参数wRemoveMsg来决定是否将消息保留在队列中。

UI线程和Windows消息队列相关推荐

  1. windows消息队列。DispatchMessage。 PostMessage

    如果HWND为NULL, 表明向当前线程发送Quit消息.. 大概是为了多任务的需要,Windows接管了所有的资源,以便于进行统一处理. 其中最××的就是接管了输入,并推出了消息队列的概念. Win ...

  2. Windows 消息队列

    Windows 消息队列 消息队列的功能 使用消息队列的场合 1. 客户端常常从网络上断开连接 2. 客户端和服务端都在线 消息 1. 消息的类型 2. 消息优先级 3. 消息的传递模式 4. 事务消 ...

  3. Windows消息队列

    Windows消息队列(优先队列) 消息队列是Windows系统的基础.对于每个进程,系统维护一个消息队列.如果在进程中有特定事件发生,如点击鼠标.文字改变等,系统将把这个消息加到队列当中.同时,如果 ...

  4. 7-26 Windows消息队列

    7-26 Windows消息队列(25 分) 消息队列是Windows系统的基础.对于每个进程,系统维护一个消息队列.如果在进程中有特定事件发生,如点击鼠标.文字改变等,系统将把这个消息加到队列当中. ...

  5. 7-26 Windows消息队列(25 分)

    7-26 Windows消息队列(25 分) 消息队列是 Windows 系统的基础.对于每个进程,系统维护一个消息队列.如果在进程中有特定事件发生,如点击鼠标.文字改变等,系统将把这个消息加到队列当 ...

  6. 5-2 Windows消息队列 (25分)

    5-2 Windows消息队列   (25分) 消息队列是Windows系统的基础.对于每个进程,系统维护一个消息队列.如果在进程中有特定事件发生,如点击鼠标.文字改变等,系统将把这个消息加到队列当中 ...

  7. 7-8 Windows消息队列 (25 分)

    消息队列是Windows系统的基础.对于每个进程,系统维护一个消息队列.如果在进程中有特定事件发生,如点击鼠标.文字改变等,系统将把这个消息加到队列当中.同时,如果队列不是空的,这一进程循环地从队列中 ...

  8. 7-26 Windows消息队列 (25 分)(详解+思路+超时解决)

    一:题目 消息队列是Windows系统的基础.对于每个进程,系统维护一个消息队列.如果在进程中有特定事件发生,如点击鼠标.文字改变等,系统将把这个消息加到队列当中.同时,如果队列不是空的,这一进程循环 ...

  9. RTX线程通信之——消息队列

    文章目录 Message Queue 消息队列 RTX 消息队列 API 使用案例 小结 参考资料 Message Queue 我们知道,Thread and Event Flags.Semaphor ...

最新文章

  1. Leangoo看板协作工具“免费版”和“企业版”差异
  2. Linux 基础学习:文件权限与种类
  3. 【机器学习】NeuralProphet,这个时序工具包也太强了吧...
  4. RIPv2与EIGRP的自动汇总区别
  5. 计算机软考培训网校,计算机软考培训中心
  6. mysql典型安装和自定义_Mysql8.0.19下载安装—windows版本自定义安装
  7. 推荐一个业界最小的可自定义算法的加密芯片
  8. vue.js详细教程--优优优
  9. linux内核中led驱动的分布,Linux内核模块驱动之---led驱动
  10. Linux中查看进程命令ps aux,ps -ef,ps -A,ps -a
  11. 使用StatSVN编译代码变更(未成功)
  12. 实现图片抠图,拖拽验证功能
  13. 玩单片机需要学数电、模电吗?
  14. 如何使用prism进行统计分析(Analysis)?
  15. JAVA代码实现人物照片的人像分割 | 百度AI
  16. Python execjs(执行js脚本)
  17. [HTML]入门小知识,列表?框架?表格?来吧。纯手工制作,满满都是智慧
  18. Postman参数化
  19. js运行后自动刷新页面的问题
  20. Win10 - 电池信息报告ERROR:(0xfa5)

热门文章

  1. MySQL更新会影响查询吗_mysql更新查询不会执行
  2. matlab调用opencv库,matlab调用opencv库
  3. 列宽一字符等于多少厘米_字符串匹配算法总结——BF、KMP、BM
  4. java自带的xml解析,使用Java自带SAX工具解析XML
  5. android studio新建layout,【Android Studio安装部署系列】十六、Android studio在layout目录下新建子目录...
  6. spring 调用python_Spring http服务远程调用实现过程解析
  7. 大佬终于把鸿蒙OS讲明白了,收藏了!
  8. 看技术笔记,提高嵌入式基础
  9. LL-verilog语法-generate语句
  10. clickhouse原理解析与开发实战 pdf_Spring全家桶集合:SpringBoot+SpringCloud实战,Spring源码原理...