MFC 消息机制入门



  • MFC消息机制

    综述

    在SDK中消息其实非常容易理解,当窗口建立后便会有一个函数(窗口处理函数)开始执行一个消息循环,我们还可以清楚的看到消息处理的脉络。一个switch case语句就可以搞定,消息循环直到遇到WM_QUIT消息才会结束,其余的消息均被拦截后调用相应的处理函数。

    但在封装了API的MFC中,消息似乎变的有些复杂了,我们看不到熟悉的switch case语句了,取而代之的是一个叫消息映射的东西。

    为什么MFC要引入消息映射机制,你可以想象一下,在现在的程序开发活动中,你的一个程序是否拥有多个窗体,主窗口就算只有一个,那菜单、工具条、控件这些都是子窗口,那我们需要写多少个switch case,并且还要为每个消息分配一个消息处理函数,这样做是多么的复杂呀。

    因此MFC采用了一种新的机制。利用一个数组,将窗口消息和相对应的消息处理函数进行映射,你可以理解成这是一个表。这种机制就是消息映射。这张表在窗口基类CWnd定义,派生类的消息映射表如果你没有动作它是空的,也就是说如果你不手工的增加消息处理函数,则当派生窗口接受一个消息时会执行父类的消息处理函数。这样做显然是高效的。

    Windows中,消息使用统一的结构体(MSG)来存放信息.

    • ​ hwnd 接收消息的32位窗口句柄。窗口可以是任何类型的屏幕对象, 因为Win32能够维护大多数可视对象的句柄(窗口、对话框、按钮、编辑框等)。

      ​ message 用于区别其他消息的常量值,这些常量可以是Windows单元中预定义的常量,也可以是自定义的常量。

      ​ wParam 通常是一个与消息有关的常量值,也可能是窗口或控件的句柄。通常用于存储小段信息,如标志
      ​ lParam 通常是一个指向内存中数据的指针,通常用于存储消息所需的对象。

      ​ 由于wParam,lParam和指针都是32位的,需要时可以强制类型转换。具体表示什么,与message相关,

      而wParam,lParam是其最灵活的两个变量,为不同的消息类型时,存放数据的含义也不一样。

      ​ time表示产生消息的时间,pt表示产生消息时鼠标的位置。

      img

       typedef uint wparam;//就是无符号整形
       typedef long lparam;//就是长整形
      

    消息种类

    消息分类

    img

    • 1、标准WINDOWS消息:这类消息是以WM_为前缀,不过WM_COMMAND例外。

      窗口消息:即与窗口的内部运作有关的消息,如创建窗口,绘制窗口,销毁窗口等。可以是一般的窗口,也可以是MainFrame,Dialog,控件等。 如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL等

    • 2、命令消息:命令消息以WM_COMMAND为消息名。在消息中含有命令的标志符ID,以区分具体的命令。由菜单,工具栏等命令接口对象产生。当用户从菜单选中一个命令项目、按下一个快捷键或者点击工具栏上的一个按钮,都将发送WM_COMMAND命令消息。

      LOWORD(wParam)表示菜单项,工具栏按钮或控件的ID;如果是控件, HIWORD(wParam)表示控件消息类型。

      对于下列宏的解释

      #define LOWORD(l) ((WORD)(l))

      #define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))

    • 3、控件通知消息

      控件通知消息是指这样一种消息,一个窗口内的子控件发生了一些事情,需要通知父窗口。

      通知消息只适用于标准的窗口控件如按钮、列表框、组合框、编辑框,以及Windows公共控件如树状视图、列表视图等。例如,单击或双击一个控件、在控件中选择部分文本、操作控件的滚动条都会产生通知消息。她类似于命令消息,当用户与控件窗口交互时,那么控件通知消息就会从控件窗口发送到它的主窗口。但是这种消息的存在并不是为了处理用户命令,而是为了让主窗口能够改变控件,例如加载、显示数据。例如按下一个按钮,他向父窗口发送的消息也可以看作是一个控件通知消息;单击鼠标所产生的消息可以由主窗口直接处理,然后交给控件窗口处理。

      • NMHDR

        随着控件的种类越来越多,越来越复杂(如列表控件、树控件等),仅仅将wParam,lParam将视为一个32位无符号整数,已经装不下太多信息了。

        为了给父窗口发送更多的信息,微软定义了一个新的WM_NOTIFY消息来扩展WM_COMMAND消息。

        WM_NOTIFY消息仍然使用MSG消息结构,只是此时wParam为控件ID,lParam为一个NMHDR指针,

        不同的控件可以按照规则对NMHDR进行扩充,因此WM_NOTIFY消息传送的信息量可以相当的大。截屏2020-05-11 下午7.12.19

        NMHDR
        {
        HWnd hWndFrom ; //相当于原WM_COMMAND传递方式的lParam
        UINT idFrom ; //相当于原WM_COMMAND传递方式的wParam(low-order)
        UINT code ; //相当于原WM_COMMAND传递方式的Notify Code(wParam"s high-order)
        };
        
    • 4、windwos也允许程序员定义自己的消息,使用SendMessage或PostMessage来发送消息。

    或者, windows 消息还可以按以下分类

    • (1) 队列消息(Queued Messages)
      消息会先保存在消息队列中,消息循环会从此队列中取出消息并分发到各窗口处理
      如:WM_PAINT,WM_TIMER,WM_CREATE,WM_QUIT,以及鼠标,键盘消息等。
      其中,WM_PAINT,WM_TIMER只有在队列中没有其他消息的时候才会被处理,
      WM_PAINT消息还会被合并以提高效率。其他所有消息以先进先出(FIFO)的方式被处理。

    • (2) 非队列消息(NonQueued Messages)
      消息会绕过系统消息队列和线程消息队列,直接发送到窗口过程进行处理
      如:WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR,WM_WINDOWPOSCHANGED

    消息 id

    • 系统定义消息ID范围:

      [0x0000, 0x03ff] 例:WM_NULL(0x0000)表示空消息 WM_CLOSE(0x0010)表示窗口关闭消息

    • 用户自定义的消息ID范围:

      WM_USER: 0x0400-0x7FFF (例:WM_USER+10)
      WM_APP(winver> 4.0):0x8000-0xBFFF (例:WM_APP+4)
      RegisterWindowMessage:0xC000-0xFFFF【用来和其他应用程序通信,为了ID的唯一性,使用::RegisterWindowMessage来得到该范围的消息ID 】

    消息处理基本流程

    win32 消息处理流程

    每一个线程都对应有一个消息队列,利用API函数GetMessage从消息队列中获取消息,然后利用TranslateMessage翻译消息(主要是一些键盘消息),再利用DispatchMessage将消息分发给对应的窗口过程函数处理。

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

    mfc 消息处理流程

    BOOL AFXAPI AfxInternalPumpMessage()
        {
            MSG msg;
            ::GetMessage(&msg, NULL, NULL, NULL);
            if (!AfxPreTranslateMessage(&msg))
            {
                ::TranslateMessage(&msg);
    	    ::DispatchMessage(&msg);
            }
            return TRUE;
    }
    

    增加了类似过滤的函数AfxPreTranslateMessage。该函数会调用CWnd类的PreTranslateMessage函数,函数返回True则消息将不会被处理。我们经常会通过重载CWnd类的PreTranslateMessage来改变MFC的消息控制流程。

    AfxWndProc->WindowProc->OnWndMsg

    实现方法

    基本方法

    除了一些没有基类的类或CObject的直接派生类外,其他的类都可以自动生成消息映射表。下面的讲解都以CMainFrame为例。消息映射表如下:

    C++代码:

    BEGIN_MESSAGE_MAP(CMainFrame, CFrameWndEx)
    
    ON_WM_CREATE()
    
    ON_COMMAND(ID_VIEW_CUSTOMIZE, &CMainFrame::OnViewCustomize)
    
    ON_REGISTERED_MESSAGE(AFX_WM_CREATETOOLBAR, &CMainFrame:OnToolbarCreateNew)
    
    ON_COMMAND_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnApplicationLook)
    
    ON_UPDATE_COMMAND_UI_RANGE(ID_VIEW_APPLOOK_WIN_2000, ID_VIEW_APPLOOK_WINDOWS_7, &CMainFrame::OnUpdateApplicationLook)
    
    ON_WM_SETTINGCHANGE()
    
    END_MESSAGE_MAP()
    

    **在BEGIN_MESSAGE_MAG和END_MESSAGE_MAP之间的内容成为消息映射入口项。**消息映射除了在CMainFrame的实线文件中添加消息映射表外,在类的定义文件MainFrame.h中还会添加一个宏调用:

    DECLARE_MESSAGE_MAP()

    一般这个宏调用写在类定义的结尾处。

    不管是自动添加还是手动添加都有三个步骤:

    **★1、**在类定义中加入消息处理函数的函数声明,注意要以afx_msg打头。

    例如MainFrame.h中WM_CREATE的消息处理函数

    **声明:**afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);

    **★2、**在类的消息映射表中添加该消息的消息映射入口项。

    例如WM_CREATE的消息映射入口项:ON_WM_CREATE()。

    **★3、**在类的实现中添加消息处理函数的函数实现。

    例如,MainFrm.cpp中WM_CREATE的消息处理函数的实现:

    int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
    {
    //TODO
    }
    

    通过以上三个步骤以后,WM_CREATE等消息就可以在窗口类中被消息处理函数处理了。

    进一步解释

    • 在MFC的框架结构下,可以进行消息处理的类的头文件里面都会含有DECLARE_MESSAGE_MAP()宏,这里主要进行消息映射和消息处理函数的声明。可以进行消息处理的类的实现文件里一般都含有如下的结构。

      BEGIN_MESSAGE_MAP(CInheritClass, CBaseClass)
      //{{AFX_MSG_MAP(CInheritClass)
      //}}AFX_MSG_MAP
      END_MESSAGE_MAP()
      

      DECLARE_MESSAGE_MAP()宏的定义如下:

      #define DECLARE_MESSAGE_MAP() \
        private: \
        static const AFX_MSGMAP_ENTRY _messageEntries[]; \
        protected: \
        static AFX_DATA const AFX_MSGMAP messageMap; \
        virtual const AFX_MSGMAP* GetMessageMap() const; \
      

      其中AFX_MSGMAP_ENTRY和AFX_MSGMAP的定义如下:

      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 nSig; // signature type (action) or pointer to message #
         AFX_PMSG pfn; // routine to call (or special value)
        };
        //因此静态数组变量_messageEntries[]实际上定义了一张表,表中的每一项指定了相应的对象所要处理的消息和处理此消息的函数的对应关系,因而这张表也称为消息映射表
      
      struct AFX_MSGMAP
        {
         const AFX_MSGMAP* pBaseMap;
         const AFX_MSGMAP_ENTRY* lpEntries;
        };
         //通过这个链表,使得在某个类中调用基类的的消息处理函数很容易,因此,“父类的消息处理函数是子类的缺省消息处理函数”就顺理成章了。
      
    • BEGIN_MESSAGE_MAP()和END_MESSAGE_MAP()它们的定义如下:

      #define BEGIN_MESSAGE_MAP(theClass, baseClass) \
      	PTM_WARNING_DISABLE \
      	const AFX_MSGMAP* theClass::GetMessageMap() const \
      		{ return GetThisMessageMap(); } \
      	const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() \
      	{ \
      		typedef theClass ThisClass;						   \
      		typedef baseClass TheBaseClass;					   \
      		static const AFX_MSGMAP_ENTRY _messageEntries[] =  \
      		{
      
      #define END_MESSAGE_MAP() \
      		{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
      	}; \
      		static const AFX_MSGMAP messageMap = \
      		{ &TheBaseClass::GetThisMessageMap, &_messageEntries[0] }; \
      		return &messageMap; \
      	}								  \
      	PTM_WARNING_RESTORE
      

    如何定义自己的消息

    若要重写基类中定义的处理程序,只需在派生类中定义一个具有相同原型的函数,并创建此处理程序的消息映射项。我们通过ClassWizard可以建立大多数窗口消息或自定义的消息,通过ClassWizard可以自动建立消息映射,和消息处理函数的框架,我们只需要把我们要做的事情填空,添加你要做的事情到处理函数。这个非常简单,就不细说了。但是也许我们需要添加一些ClassWizard不支持的窗口消息或自定义消息,那么就需要我们亲自动手建立消息映射和消息处理的框架,通常步骤如下:

    • 第一步:定义消息。Microsoft推荐用户自定义消息至少是WM_USER+100,因为很多新控件也要使用WM_USER消息。

      #define WM_MYMESSAGE (WM_USER + 100)
      
    • 第二步:实现消息处理函数。该函数使用WPRAM和LPARAM参数并返回LPESULT。

      LPESULT CMainFrame::OnMyMessage(WPARAM wParam, LPARAM lParam)
      {
      // TODO: 处理用户自定义消息,填空就是要填到这里。
      return 0;
      }
      
    • 第三步:在类头文件的AFX_MSG块中说明消息处理函数:

      // {{AFX_MSG(CMainFrame)
      afx_msg LRESULT OnMyMessage(WPARAM wParam, LPARAM lParam);
      //}}AFX_MSG
      DECLARE_MESSAGE_MAP()
      
    • 第四步:在用户类的消息块中,使用ON_MESSAGE宏指令将消息映射到消息处理函数中。

      ON_MESSAGE( WM_MYMESSAGE, OnMyMessage )
      

    参考

    https://www.cnblogs.com/kekec/p/3210696.html

    https://www.cnblogs.com/greatverve/archive/2012/11/04/mfc-message.html

    https://blog.csdn.net/zz709196484/article/details/76033033

    https://blog.csdn.net/ljd_1986413/article/details/6258604

    https://blog.csdn.net/lijie45655/article/details/6358779

    https://blog.csdn.net/hisinwang/article/details/8045017?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-2

    https://blog.csdn.net/bcbobo21cn/article/details/69666539

    https://blog.csdn.net/gongxifacai_believe/article/details/50988139


登录后回复
 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

与 Dian 的连接断开,我们正在尝试重连,请耐心等待