微软MFC技术中的消息循环-LMLPHP

 我是荔园微风,作为一名在IT界整整25年的老兵,今天来聊聊微软MFC技术中的消息循环。

在很多windows程序中,都有如下代码:

//Win32应用程序入口函数WinMain()
int WINAPI WinMain(
  HINSTANCE hInstance,      //指向当前实例的句柄
  HINSTANCE hPrevInstance,      //指向先前实例的句柄
  LPSTR lpCmdLine,      //命令行
  int nCmdShow         //显示状态
)
{
  WNDCLASS wndclass;      //创建一个窗口类 
  wndclass. cbClsExtra=0;     //窗口类无扩展
  wndclass. cbWndExtra=0;    //窗口实例无扩展
  wndclass. hbrBackground=(HBRUSH)GetStockObject (BLACK_BRUSH);  //窗口背景为黑
  wndclass.hCursor=LoadCursor(NULL,IDC_ARROW);    //窗口采用箭头光标
   wndclass. hIcon=LoadIcon(NULL, IDI_APPLICATION);  //窗口的最小化图标为缺省
  wndclass. hInstance=hInstance:  //当前实例句柄
  wndclass. lpfnWndProc=WinHouProc    //定义窗口处理函数
  wndclass.lpszClassName="VC++";      //窗口类为“VC++”
  wndclass.lpszMenuName=NULL;        //窗口无菜单
  wndclass.style=CS_HREDRAW|CS_PARENTDC;          //设置窗口类型
  RegisterClass(&wndclass);        //注册窗口类
 
  HWND hwnd;
  hwnd=CreateWindow("VC++","消息机制",WS_OVERLAPPEDWINDOW,0,0, 600, 400, NULL, NULL, hInstance, NULL);       //创建窗口
 
  ShowWindow(hwnd, SW_SHOWNORMAL);         //显示窗口
  UpdateWindow(hwnd);                 //更新窗口
 
  MSG msg;
  while(GetMessage 6msg, NULL,0,0))       //获取消息
  {
      TranslateMessage (&msg);
      DispatchMessage (&msg);
   }
  return 0;
}

在上面代码的第30行中,提到了循环。只有消息不断地被循环。才会实时得到窗口的当前状态。否则按照程序的顺序执行结构,此窗口只会闪下就被关闭,根本不会等待用户输入的任何命令。因此消息循环非常必要,即使窗口不会闪一下就关,它的状态也一直是初始状态,因为没有消息的循环,窗口的当前状态没法被线程捕获。首先从设置断点、函数传递角度来看下MFC程序启动时是如何进入消息循环。

下面就分两种情况介绍消息循环机制。

非对话框程序的消息循环机制

打开我之前帖子中所建的单文档 MFC程序,名字为Message。在源文件 Message.cpp中找到下面这一行代码,并在此处设置断点。

CMessageApp theApp;

按F11键单步执行后,会发现非对话框程序的运行点进入函数顺序为如下。

第一步  CMessageApp theApp;

第二步  _tWinMain()

第三步  Afx WinMain

第四步  CMessageApp::Initlnstance()

第五步  CWinThread::Run()
  
而非对话框程序的消息循环是从 CWinThread::Run()开始。来看下 MFC自带的thrdcore.cpp源文件中包含 CWinThread::Run()函数的部分代码如下。

MFC自带程序 CWinThread::Run()函数示例:thrdcore.cpp

int CWinThread::Run ()
{
  ASSERT_VALID(this)

   BOOL bIdle=TRUE;    //空闲状态标志量
  LONG lIdleCount=0;

  for (;;)  //无限循环,一直获取和发送消息,直到接收到WM_QUIT消息
   {
      //阶段1:检查看是否可以做空闲工作
      while (bIdle && !::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
       {
         //如果空闲则呼叫OnIdle()函数
        if (!OnIdle(lIdleCount++))
            bIdle=FALSE;
       }
       //阶段2:启动消息处理
      do
       {
         //当接收WM_QUIT,则退出
         if (!PumpMessage())
             return ExitInstance();

         //当获取"正常"消息时,重新设置空闲状态量
        if (IsIdleMessage(&m_msgCur))
         {
             bIdle=TRUE;
              lIdleCount=0;
           }
       }while ( ::PeekMessage (&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
  }
  ASSERT (FALSE);
}

大家可以从自己VC++平台安装路径下找到thrdcore.cpp源文件。如“C:\ProgramFiles\Microsoft Visual Studio\VC98\MFC\SRC"

在上面代码中,第8行表示这是一个无限循环,程序会一直处于获取和发送消息的处理状态中,除非接收到WM_QUIT消息。如第22行所示。for循环中有两个阶段,下面就这两个阶段进行分析。这2个阶段是顺序执行的,总是先执行阶段1后阶段2。

(1)阶段1的while循环是判断当前线程是否空闲并且消息队列中是否有消息需要处理,如果线程当前空闲且无消息可处理,则处于空闲状态,并一直判断;否则直接进入阶段2,进行消息处理。

(2)阶段2是判断是否收到消息,如果获取消息,继续执行;否则,退出循环。而此do-while 循环中首先判断 PumpMessage()的返回值,如果获取到 WM_QUIT消息,退出程序,否则向下执行,当设置断点调试一个非对话框程序时,单步执行第22行时,会进入PumpMessage()函数体中。如下所示:

BOOL CWinThread::PumpMessage()
{
  return AfxInternalPumpMessage ();
  //此函数内容相当于老版本的PumpMessage()内容
}

单步执行到此函数体时,再按F5键继续运行程序,运行点会一直停留在此处。因此,非对话框的消息机制就是在一个无限循环中不断从消息队列中获取消息、窗口消息被发送到相应的窗口处理过程。

对话框程序的消息循环机制

同上面所讲,打开名为MFC_Message的基于对话框程序。在源文件MFC_Message.cpp中找到下面这一行代码,并在此处设置断点。

CMFC_MessageApp theApp;

单步调试该程序后,会发现对话框程序的运行点进入函数顺序与非对话框程序的不同。非对话框程序的消息循环是从CWinThread::Run()开始,而对话框程序根本不会进入CWinThread::Run()函数。因此对话框程序的运行点进入函数顺序如下。

第一步  CMFC_MessageApp    theApp;

第二步  CMFC_MessageApp:: InitInstance()

第三步  CMFC_MessageDlg dlg;

第四步  dlg.DoModal()

因为它们的应用程序对象的InitInstance()不一样。可以通过下面两段代码进行比较。

非对话框MFC程序InitInstance()函数示例:Message.cpp

BOOL CMessageApp::InitInstance()
{
  ......
  CSingleDocTemplate* pDocTemplate;     //定义一个单文档模板
  pDocTemplate=new CSingleDocTemplate(  //定义模板的各种属性
    IDR_MAINFRAME,     //菜单
     RUNTIME_CLASS (CMessageDoc),  //文档类
    RUNTIME_CLASS(CMainFrame),  //框架类
     RUNTIME_CLASS(CMessageView));  //视图类

  if( !pDocTemplate)
      return FALSE;  //如果创建不成功返回FALSE
  AddDocTemplate (pDocTemplate);      //将此模板对象加入模板中
 
  CCommandLineInfo cmdInfo;
  ParseCommandLine(cmdInfo);
 
  if ( !ProcessShellCommand (cmdInfo))
  return FALSE;      //程序启动时创建新文档失败返回FALSE
 
   m_pMainWnd->Showwindow (SW_SHOW);  //显示窗口
  m_pMainWnd->Updatewindow ();     //更新窗口
  return TRUE:
}

对话框MFC程序 Initinstance()函数例: MFC_Message.cpp

BOOL CMFC_MessageApp::InitInstance()
{
   ......
  CMEC_MessageDlg dlg;  //声明一个对话框类对象
  m_pMainWnd =&dlg;  //将其指针赋给窗口类对象
  INT_PTR nResponse =dlg.DoModal     //延模对话框,消息循环在这里开始
   ......
 
  return FALSE;
}


非对话框MFC程序InitInstance()函数示例代码中,执行完CMessageApp::InitInstance()函数,其第22行为返回正确;而对话框MFC程序InitInstance()函数示例代码执行完CMFC_MessageApp::InitInstance()函数,其第10行为返回错误。同时,设置断点进行单步调试,会发现对话框程序的消息循环发生在DoModal()函数里,如对话框MFC程序InitInstance()函数示例代码的第07行所示。而此函数中有一个RunModalLoop()函数用于消息循环。

总结

前面分别讲解了非对话框和对话框程序的消息循环,接下来总结一下消息循环是什么。下面分2个方面来对对话框和非对话MF程序进行对比分析:

(一)发生消息循环的函数位置。

非对话框MFC程序的消息循环发生在CWinThread::Run()中,而对话框MFC程序的消息循环则发生在DoModal()中。

(二)接收消息的对象。

非对话框MFC程序的消息是与窗口交换的,而对话框MFC程序的消息则是与对话框进行交互的。

提示:因为对话框不是严格意义上的窗口,所以对话框MFC程序运行时不会进入_tWinMain()函数。

作者简介:荔园微风,1981年生,高级工程师,浙大工学硕士,软件工程项目主管,做过程序员、软件设计师、系统架构师,早期的Windows程序员,Visual Studio忠实用户,C/C++使用者,是一位在计算机界学习、拼搏、奋斗了25年的老将,经历了UNIX时代、桌面WIN32时代、Web应用时代、云计算时代、手机安卓时代、大数据时代、ICT时代、AI深度学习时代、智能机器时代,我不知道未来还会有什么时代,只记得这一路走来,充满着艰辛与收获,愿同大家一起走下去,充满希望的走下去。

07-12 07:03