MFC框架机制详解

MFC框架机制详解

1.1 Windows消息机制要点

1.1.1 窗口过程

​ 每个窗口会有一个称为窗口过程的回调函数(WndProc),它带有四个参数,分别为:窗口句柄(Window Handle), 消息ID(Message ID), 和两个消息参数(wParam, lParam), 当窗口收到消息时系统就会调用此窗口过程来处理消息。(所以叫回调函数)

1.1.2 消息类型

  • 1. 系统定义消息(System-Defined Messages)

在SDK中事先定义好的消息,非用户定义的,其范围在[0x0000, 0x03ff]之间 (1024),

可以分为以下三类:

  1. 窗口消息(Windows Message)

与窗口的内部运作有关,如创建窗口,绘制窗口,销毁窗口等。可以是一般的窗口,也可以是Dialog,控件等。所有派生自CWnd 的类才有资格接收标准消息。

第一个消息是:WM_NULL = $0000;

最后一个消息是:WM_DDE_LAST = WM_DDE_FIRST (03E0)+8=03E0)+8=03E8=1000;

中间有几个地方并不连续;

如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL…

  • 命令消息(Command Message)

  • 与处理用户请求有关, 如单击菜单项或工具栏或控件时, 就会产生命令消息。WM_COMMAND=$0111, LOWORD(wParam)表示菜单项,工具栏按钮或控件的ID。如果是控件, HIWORD(wParam)表示控件消息类型。

    所有派生自CCmdTarget 的类都有能力接收WM_COMMAND消息。

  • 控件通知(Notify Message)

  • 控件通知消息, 这是最灵活的消息格式, 其Message, wParam, lParam分别为:WM_NOTIFY= $004E, 控件ID,指向NMHDR的指针。NMHDR包含控件通知的内容, 可以任意扩展。

  • 2. 用户程序定义消息(Application-Defined Messages)

  • 用户自定义的消息, 对于其范围有如下规定:

    WM_USER: 0x0400-0x7FFF (ex. WM_USER+10)
    WM_APP(winver> 4.0): 0x8000-0xBFFF (ex.WM_APP+4)
    RegisterWindowMessage: 0xC000-0xFFFF

  • 3. 消息队列(Message Queues)

  • Windows中有两种类型的消息队列

    1. 系统消息队列(System Message Queue)

    这是一个系统唯一的Queue,设备驱动(mouse, keyboard)会把操作输入转化成消息存在系统队列中,然后系统会把此消息放到目标窗口所在的线程的消息队列(thread-specific message queue)中等待处理

  • 线程消息队列(Thread-specific Message Queue)

  • 每一个GUI线程都会维护这样一个线程消息队列。(这个队列只有在线程调用GDI函数时才会创建,默认不创建)。然后线程消息队列中的消息会被送到相应的窗口过程(WndProc)处理。

    注意: 线程消息队列中WM_PAINT,WM_TIMER只有在Queue中没有其他消息的时候才会被处理,WM_PAINT消息还会被合并以提高效率。其他所有消息以先进先出(FIFO)的方式被处理。

  • 4. 队列消息(Queued Messages)和非队列消息(Non-Queued Messages)

    1. 队列消息(Queued Messages)

    消息会先保存在消息队列中,消息循环会从此队列中取消息并分发到各窗口处理
    如鼠标,键盘消息。

  • 非队列消息(NonQueued Messages)

  • 消息会绕过系统消息队列和线程消息队列直接发送到窗口过程被处理
    如:WM_ACTIVATE, WM_SETFOCUS,
    WM_SETCURSOR, WM_WINDOWPOSCHANGED

    注意: postMessage发送的消息是队列消息,它会把消息Post到消息队列中; SendMessage发送的消息是非队列消息, 被直接送到窗口过程处理

  • 5. PostMessage(PostThreadMessage), SendMessage

  • PostMessage:把消息放到指定窗口所在的线程消息队列中后立即返回。 PostThreadMessage:把消息放到指定线程的消息队列中后立即返回。
    SendMessage:直接把消息送到窗口过程处理, 处理完了才返回。

  • 6. GetMessage, PeekMessage

  • PeekMessage会立即返回 可以保留消息

    GetMessage在有消息时返回 会删除消息

  • 7. TranslateMessage, TranslateAccelerator

  • TranslateMessage: 把一个virtual-key消息转化成字符消息(character message),并放到当前线程的消息队列中,消息循环下一次取出处理。

    TranslateAccelerator: 将快捷键对应到相应的菜单命令。它会把WM_KEYDOWN 或 WM_SYSKEYDOWN转化成快捷键表中相应的WM_COMMAND 或WM_SYSCOMMAND消息, 然后把转化后的 WM_COMMAND或WM_SYSCOMMAND直接发送到窗口过程处理, 处理完后才会返回。

  • 8. 消息死锁( Message Deadlocks)

  • 假设有线程A和B, 现在有以下下步骤

    1) 线程A SendMessage给线程B, A等待消息在线程B中处理后返回
    2) 线程B收到了线程A发来的消息,并进行处理,在处理过程中,B也向线程A SendMessgae,然后等待从A返回。
    因为此时, 线程A正等待从线程B返回, 无法处理B发来的消息, 从而导致了线程A,B相互等待, 形成死锁。多个线程也可以形成环形死锁。
    可以使用 SendNotifyMessage或SendMessageTimeout来避免出现死锁。

  • 9. BroadcastSystemMessage

  • 我们一般所接触到的消息都是发送给窗口的, 其实, 消息的接收者可以是多种多样的,它可以是应用程序(applications), 可安装驱动(installable drivers), 网络设备(network drivers), 系统级设备驱动(system-level device drivers)等,
    BroadcastSystemMessage这个API可以对以上系统组件发送消息。

    1.2 MessageMAP的形成

    ​ MFC也定义了丰富的宏来简化消息响应的代码,要想真正了解MFC的消息机制,必需弄清楚这些宏。

    1. 第一个宏:DECLARE_MESSAGE_MAP()

    作用:为一个消息响应类声明必需的成员变量和成员函数。

    我们在窗口类、应用程序类、文档类、视图类、以及这些类的子类的定义中,都能看到DECLARE_MESSAGE_MAP()宏,通常被自动化工具声明在类的最后部分,如:

    // 生成的消息映射函数
    protected:
    DECLARE_MESSAGE_MAP()
    };
      
      
    • 1
    • 2
    • 3
    • 4

    DECLARE_MESSAGE_MAP()宏定义如下(在DLL类型和WINDOWS程序类型下,定义会有不同,本文只分析非DLL类型,下同):

    
    #define DECLARE_MESSAGE_MAP()
    
    private:
    static const AFX_MSGMAP_ENTRY _messageEntries[];
    protected:
    static const AFX_MSGMAP messageMap;
    virtual const AFX_MSGMAP* GetMessageMap() const;
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可以看到,宏DECLARE_MESSAGE_MAP()定义了两个静态成员变量,并重载了一个虚函数。下面分析一下这三个成员:_messageEntries被定义为一个AFX_MSGMAP_ENTRY类型的数组。

    结构体AFX_MSGMAP_ENTRY的定义如下:

    struct AFX_MSGMAP_ENTRY
    {
    UINT nMessage; // windows message
    UINT nCode; // control code or WM_NOTIFY code
    UINT nID; // control ID (or 0 for windows messages)
    UINT nLastID; // used for entries specifying a range of control id's
    UINT_PTR nSig; // signature type (action) or pointer to message #
    AFX_PMSG pfn; // routine to call (or special value)
    }; 
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    通过查看源代码中的注释,可以看出AFX_MSGMAP_ENTRY定义了一个消息入口,或者说定义了一个消息到函数的映射关系。 nMessage和nCode确定一条消息的内容,nID和nLastID确定了一条消息的来源,而nSig和pfn确定了消息的响应函数和调用方式。

    通过对消息响应过程的源码分析可知,nSig事实上是一系列编码,每一种编码代表一种响应函数的类型,包括返回值、参数信息等。在响应消息的时候,将把pfn指向的函数指针强制类型转换为nSig代表的函数类型,然后再调用。

    pfn的类型AFX_PMSG定义如下,意为CCmdTarget的成员函数:

    typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);
      
      
    • 1

    由此我们可以得出:静态成员_messageEntries是一个消息到函数的映射表,或叫消息入口表。通过查找此表,可以找到消息的响应函数。

    DECLARE_MESSAGE_MAP()宏声明的另一个静态成员变量messageMap被定义为AFX_MSGMAP类型。AFX_MSGMAP定义如下:

    struct AFX_MSGMAP
    {
    
    #ifdef _AFXDLL
    
    const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
    
    #else
    
    const AFX_MSGMAP* pBaseMap;
    
    #endif
    
    const AFX_MSGMAP_ENTRY* lpEntries;
    }; 
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    过滤掉_AFXDLL(当MFC工程是以动态链接库为目标代码编译时,使用_AFXDLL宏)的影响,可以简化为如下:

    struct AFX_MSGMAP
    {
    const AFX_MSGMAP* pBaseMap;
    const AFX_MSGMAP_ENTRY* lpEntries;
    }; 
      
      
    • 1
    • 2
    • 3
    • 4
    • 5

    可见结构体AFX_MSGMAP中定义了两个指针,pBaseMap指向另一个AFX_MSGMAP,lpEntries指向一个消息入口表。可以推想,在响应消息时,一定是在lpEntries指向的消息入口表中寻找响应函数,也可能会在pBaseMap指向的结构体中做同样的响应函数寻找操作。其图示如下:

    至于DECLARE_MESSAGE_MAP()宏重载的虚函数GetMessageMap,可以猜测只是用来返回成员messageMap 的地址而已。因为GetMessageMap是虚函数,所以系统只要通过调用消息响应类的基类CCmdTarget类的GetMessageMap函数,便可以找到最后一级子类的消息映射信息。我们将在接下来对其它几个宏的分析中得到相同的结论。

  • 第二个重要的宏:BEGIN_MESSAGE_MAP

  • 作用:定义DECLARE_MESSAGE_MAP宏声明的静态变量。

    BEGIN_MESSAGE_MAP定义的源代码如下:

    //以下这一段是重点,理解了这一段,就全部理解了MFC框架中的消息映射机制。

    
    #define BEGIN_MESSAGE_MAP(theClass, baseClass)
    
    const AFX_MSGMAP* theClass::GetMessageMap() const
    { return &theClass::messageMap; }
    ///////////////////////////////////////////////////////
    AFX_COMDAT const AFX_MSGMAP theClass::messageMap =//定义Message
    { &baseClass::messageMap, &theClass::_messageEntries[0] };//传入消息入口地址
    ///////////////////////////////////////////////////////
    AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] =//消息条目内容
    {
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    BEGIN_MESSAGE_MAP宏有两个参数,theClass表示为当前类,bassClass为当前类的父类。

    BEGIN_MESSAGE_MAP宏首先定义了函数GetMessageMap的函数体,如前文所述,直接返回当前类的成员变量messageMap的地址。

    const AFX_MSGMAP* theClass::GetMessageMap() const
    { return &theClass::messageMap; } 
      
      
    • 1
    • 2

    然后初始化了当前类的成员变量messageMap。messageMap的pBaseMap指针指向其父类(基类)的messageMap成员,lpEntries指针指向当前类的_messageEntries数组的首地址。

    AFX_COMDAT const AFX_MSGMAP theClass::messageMap =
    { &baseClass::messageMap, &theClass::_messageEntries[0] }; 
      
      
    • 1
    • 2

    最后,定义了_messageEntries数组初始化代码的开始部分。

    AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] =
    { 
      
      
    • 1
    • 2
  • 第二个重要的宏END_MESSAGE_MAP()

  • 作用:定义_messageEntries数组初始化代码的结束部分。

    
    #define END_MESSAGE_MAP()
    
    {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }
    }; 
      
      
    • 1
    • 2
    • 3
    • 4
    • 5

    在DECLARE_MESSAGE_MAP和END_MESSAGE_MAP之间还有一些宏,如ON_COMMAND、ON_WM_CREATE 等,这些宏最终都会被生成一条AFX_MSGMAP_ENTRY结构体数据,并成为_messageEntries消息映射表数据的一个元素。我们以常见的ON_COMMAND宏为例。ON_COMMAND宏的源代码为:

    
    #define ON_COMMAND(id, memberFxn)
    
    { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSigCmd_v,
    static_cast (memberFxn) },
      
      
    • 1
    • 2
    • 3
    • 4
    • 5

    通过以上分析,我们可以得到一个链表式的数据结构,子类的messageMap成员为链表的头节点。链表的每个节点都包含一个消息入口表。MFC的消息处理函数CCmdTarget::OnCmdMsg正是通过这样一个链表查找到消息的响应函数,并调用该函数来响应消息。

    messageMap

    msgmap

    1.3 整体过程分析

    1. 鼠标点击,产生单击事件,鼠标设备驱动程序根据用户事件,转换成消息,并放置于WINDOWS的系统队列中

    2. WINDOWS将系统队列中的消息取出,并投掷于消息对应的应用程序所属的线程队列。

    3. 每个应用程序在创建时,系统都会为其创建一个消息队列,发送给应用程序的消息都存放在该消息队列中,等待被处理。 而应用程序的消息引擎

    MSG msg;
    while (GetMessage(msg, NULL, NULL, NULL))
    {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
    }
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    会不停的从自己的专属消息队列中获取消息,并进行消息的翻译和转发(TranslateMessage和DiapatchMessage)

    GetMessage:从线程队列中取消息,取出后对应的消息会从队列中删除;若无消息,则阻塞

    TranslateMessage:把键盘消息转换成对应的ASCII字符内存,并重新放置于队列中,等待取出 DispatchMessage:在注册窗口对象时,有如下代码:及设置对口的消息处理回调函数

    WNDCLASS wc;
    ......
    wc.lpfnWndProc = (WNDPROC)WndProc;
    ......
      
      
    • 1
    • 2
    • 3
    • 4

    在窗口类的定义中,有如下代码:

    DECLARE_MESSAGE_MAP()
    ......
    BEGIN_MESSAGE_MAP(CMsgTestDlg, CDialog)
    ......
    ON_MESSAGE(WM_USER_SEND_MSG, &CMsgTestDlg::HandleSendMsg)
    ON_MESSAGE(WM_USER_POST_MSG, &CMsgTestDlg::HandlePostMsg)
    ......
    END_MESSAGE_MAP()
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    该段代码,是的所有该类型的对话框对象共享一个消息MAP表,DispatchMessage会根据消息所属的窗口,调用回调函数WndProc,而WndProc则getMessageMap,根据消息类型,在map中进行匹配查找,找到对应的处理函数,并调用,则消息处理完毕。

    1. 4 系统消息队列

    当操作系统启动并初始化时,线程Raw Input Thread(RIT)就会启动,并创系统硬件输入队列(System Hardware Input Queue)(SHIQ). 对于外部的硬件事件(鼠标或者键盘),硬件驱动会将事件转换成消息,并存放到SHIQ中,而RIT线程就专门负责处理SHIQ中的消息,把消息分发到对应线程的消息队列里面。

    1.5 线程消息队列

    对于每个用MFC开发的GUI程序,他们都有一个CMy***App,该类继承自CWinApp,而CWinApp继承自CWinThread, CWinApp是一个GUI线程,系统会为其维护一个THREADINFO结构,

    消息队列包含在一个叫THREADINFO的结构中,有四个队列:

    1. Sent Message Queue 发送消息队列
    2. Posted Message Queue 登记消息队列
    3. Visualized Input Queue 输入消息队列
    4. Reply Message Queue 响应消息队列ml>

    Sent Message Queue: 该队列保存其他程序通过SendMessage给该线程发送的消息
    Posted Message Queue: 该队列保存其他队列通过PostMessage给该线程发送的消息
    Visualized Input Queue: 保存系统队列分发过来的消息,比如鼠标或者键盘的消息

    Reply Message Queue: 保存向窗体发送消息后的结果,比如sendMessage操作结束后,接收消息方会发送一个Reply消息给发送方的Reply队列中,以唤醒发送队列。

    这些队列如何产生的?线程是内核对象,我们看看线程信息是如何定义的,包含哪些内容,分析如下THREADINFO结构体定义,可以得出一些信息:
    threadinfo

    2.0 MFC技术内幕:执行期类型识别与动态创建

    2.1 WIN32运行机制

    1. Win32 SDK程序的入口点 WinMain()

    • (1) 定义应用程序要用的变量;

    HWND hwnd;
    MSG msg;
    ...
      
      
    • 1
    • 2
    • 3
  • (2) 定义窗口类(WNDCLAS)变量wndclass;

  • WNDCLAS wndclass;
      
      
    • 1
  • (3) 根据窗口类结构填写各条款,形成初始化的窗口类;

  • wndclass.style = CS_HREDRAW | CS_VREDRAW;
    ...
      
      
    • 1
    • 2
  • (4) 利用RegisterClass()函数注册初始化好的窗口类wndclass,注册失败则输出信息并返回操作系统,成功则跳过if继续执行;

  • if(!RegisterClass(&wndclass))
    { //注册失败处理 }
      
      
    • 1
    • 2
  • (5) 根据窗口类建立窗口:

  • hwnd = CreateWindow(...);
      
      
    • 1
  • (6) 在屏幕上显示窗口;

  • ShowWindow(...);
    UpdateWindow(...); // 发出WM_PAINT消息
      
      
    • 1
    • 2
  • (7) 进入消息循环;

  • while(GetMessage(&msg, NULL, 0, 0)) // 从调用线程的消息队列里取得一个消息并将其放于指定的结构msg { 
    TranslateMessage(&msg); // 将虚拟键消息转换为字符消息
    DispatchMessage(&msg); // 调度一个消息给窗口程序
    }
      
      
    • 1
    • 2
    • 3
    • 4
  • (8) return (msg.wParam);

  • 其中窗口函数:
    LRESULT CALLBACK WndProc(HWND hWnd, UINT message,
    WPARAM wParam, LPARAM lParam)
    {
    ....
    switch (message) {
    case WM_CREATE:
    // 响应WM_CREATE消息的处理过程
    case WM_PAINT:
    //...
    default:
    return (DefWindowProc(hWnd, message, wParam, lParam)); // 不愿处理的消息交给默认处理函数解决
    }
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
  • MFC编写的Windows应用程序的运行机制:

    1. 创建应用程序类(CWinApp的派生类)全局对象 theAppe

    2. 调用应用程序类的构造函数初始化对象 theApp

    • 配置内存,并设定成员初值
  • 调用WinMain()主函数

    1. 获取theApp的指针;

    pApp = AfxGetApp();
    pThread = AfxGetThread();
      
      
    • 1
    • 2
  • 全局初始化();

  • AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow)
      
      
    • 1
    • 设置错误处理模式:
    SetErrorMode();
      
      
    • 1
    • 设置模块资源句柄和填写应用程序初始化状态数据;

    • 初始化主线程特定数据,并把消息队列尽量加大;

    AfxInitThread();
      
      
    • 1
  • 应用程序初始化;

  • pApp->InitApplication(); // 这个函数在MFC中已作废,最好不要重载它
    pThread->InitInstance(); // 注册窗口类,产生窗口,显示窗口,须改写(注:下面是MDI程序中InitInstance()中LoadFrame()函数引发的窗口注册,创建等一系列过程,只写出了主要的一些函数调用过程,具体函数作用查查MFC类手册或MSDN,我就不罗嗦了: CFrameWnd::LoadFrame(), CFrameWnd::Create(), CWnd::CreateEx(), CFrameWnd::PreCreateWindow(), AfxEndDeferRegisterClass(), _AfxRegisterWithIcon(), AfxRegisterClass(), CreateWindowEx();)
    m_pMainWnd->ShowWindow(SW_SHOW);
    m_pMainWnd->UpdateWindow();
      
      
    • 1
    • 2
    • 3
    • 4
  • 运行程序, 进入消息处理循环;

  • pThread->Run();
      
      
    • 1
    • 如果消息队列中没有消息: OnIdle();
    • 当消息队列中有消息且不是WM_QUIT,分发给消息处理函数:PumpMessage();
    • 当收到WM_QUIT消息:ExitInstance();
  • 调用析构函数,退出应用程序,控制权交还操作系统;

  • 其中Run()中的消息映射过程如下(大致的函数调用过程):

    wndcls.lpfnWndProc = DefWindowProc; // 窗口类注册的窗口函数
    CWnd::DefWindowProc();
    ::CallWindowProc(pfnWndProc, m_hWnd, nMsg, wParam, lParam);
    AfxWndProc();
    AfxCallWndProc();
    CWnd::WindowProc();
    CWnd::OnWndMsg();
    如果是WM_COMMAND: OnCommand()
    OnCmdMsg();
    如果是WM_NOTIFY: OnNotify()
    OnCmdMsg();
      
      
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    其中重点在CWnd::WindowProc()及其后的消息分派过程:
    当一个消息抵达时,框架调用了CMainFrame从CWnd继承下来的虚拟WindowProc()函数;
    WindowProc函数调用OnWndMsg函数;
    而OnWndMsg又调用GetMessageMap来获取一个指向CMainFrame::messageMap的指针,并搜索CMainFrame::_messageEntries来获取一个其消息ID与当前正等待处理的消息ID相匹配的条目,如果找到了该条目,对应的CMainFrame函数(其地址与该消息ID一同存储在_messageEntries数组中)就被调用。否则,OnWndMsg参考CMainFrame::messageMap获得一个指向CFrameWnd::messageMap的指针并为基类重复该过程。如果基类没有该消息的处理程序,则框架将上升一个级别,参考基类的基类,相当系统地沿着继承链向上走,直到它找到一个消息处理程序或者该消息传递Windows进行默认处理为止;”

    声明:摘抄于网上的一些资料

    MFC框架机制详解

    猜你喜欢

    转载自blog.csdn.net/huangguangzhi88/article/details/85368700