<menu id="w8yyk"><menu id="w8yyk"></menu></menu>
  • <dd id="w8yyk"><nav id="w8yyk"></nav></dd>
    <menu id="w8yyk"></menu>
    <menu id="w8yyk"><code id="w8yyk"></code></menu>
    <menu id="w8yyk"></menu>
    <xmp id="w8yyk">
    <xmp id="w8yyk"><nav id="w8yyk"></nav>
  • 網站首頁 > 物聯資訊 > 技術分享

    MFC-消息分派

    2016-09-28 00:00:00 廣州睿豐德信息科技有限公司 閱讀
    睿豐德科技 專注RFID識別技術和條碼識別技術與管理軟件的集成項目。質量追溯系統、MES系統、金蝶與條碼系統對接、用友與條碼系統對接

    前言

           由于工作需要,這幾天學了一點MFC,在AFX里看到很多熟悉的東西,如類型信息,序列化,窗口封裝和消息分派。幾乎每個界面庫都必須提供這些基礎服務,但提供的手法卻千差萬別。MFC大量地借用了宏,映射表來實現,而VCL則更多的在語言級別上給與支持。這其實是很容易理解的,因為C++是一個標準,不會因某個應用而隨便擴展語言;相反Delphi完全由一個公司掌握,因此每支持一項新技術,變化最大的往往是語言本身。

    學習MFC的代碼,再對照VCL的實現,這真是一個很有意思的過程,其中可以看到兩個框架在一些設計思想上是殊途同歸的,所不同的是表現手法,以及封裝的程度。我計劃將這段時間閱讀MFC的心得寫成一系列文章,其中可能會穿插與VCL的對比,不管你熟悉VCL還是MFC,通過這些文章或許可從另一個角度來看待自己熟悉的框架。

    這是第一篇:消息分派。

    消息處理函數表

           MFC和VCL在對消息進行封裝的時候,都沒有使用虛函數機制。原因是虛函數帶來了不必要的空間開銷。那么它們用什么來代替虛函數,即可減少空間浪費,也可實現類似虛函數的多態呢?讓我們從一個例子開始。

    假設父類ParentWnd處理了100個消息,并且將這100個處理函數聲明為虛函數;此時有一個子類ChildWnd它只需要處理2個消息,另外98個交由ParentWnd默認處理,但是ChildWnd的虛表仍然占有100個函數指針,其中2個指向自己的實現,另外98個指向父類的實現,情況大概像下面這樣:

     

          指向父類實現的函數指針浪費了空間,當控件類非常多的時候,這種浪費就非常明顯。因此必須走另一條路,將不必要的函數指針去掉,如下圖所示:

     

     

           ChildWnd去掉函數指針之后,當有一個消息需要Fun100處理時,ChildWnd就束手無策了。需要一個方法讓ChildWnd能夠找到父類的表,我們再對這個數據結構進行改進如下:

     

        現在看來好多了,如果ChildWnd有一個消息需要Fun1處理,則查找ChildWnd的MsgHandlers,找到Fun1函數指針調用之;如果需要Fun100處理,發現ChildWnd的MsgHandlers沒有Fun100,則通過ParentTable找到父類的MsgHandlers繼續查找。如此一直查找,到最后再找不到,就調用DefWindowProc作默認處理。

     

           MFC和VCL都是通過類似的方法實現消息分派的。只是VCL有編譯器的支持,直接將這個表放到VMT中,因此實現起來非常簡單,只需在控件類里作如下聲明:

    procedure WMMButtonDown(var Message: TWMMButtonDown); message WM_MBUTTONDOWN;

    TObject.Dispatch會將WM_MBUTTONDOWN正確分派到WMMButtonDown。

           MFC就沒有這么簡單,它需要手工去構建這個表,如果一個類想處理消息,它必須聲明一些結構和函數,代碼類似這樣:

    struct AFX_MSGMAP_ENTRY

    {

        UINT nMessage;   // 消息

        AFX_PMSG pfn;    // 消息處理函數

    };

     

    struct AFX_MSGMAP

    {

        const AFX_MSGMAP* pBaseMap;  //指向基類的消息映射

        const AFX_MSGMAP_ENTRY* lpEntries; //消息映射表

    };

     

    class CMFCTestView : public CView

    {

    protected:

        void OnLButtonDown(UINT nFlags, CPoint point);

    private:

        static const AFX_MSGMAP_ENTRY _messageEntries[];

    protected:

        static const AFX_MSGMAP messageMap;

        virtual const AFX_MSGMAP* GetMessageMap() const;

    };

    仔細看_messageEntries和messageMap的聲明,是不是和上面的圖非常相似,接下來看看實現部分如何初始化這個表:

    const AFX_MSGMAP* CMFCTestView::GetMessageMap() const

        { return & CMFCTestView::messageMap; }

     

    const AFX_MSGMAP CMFCTestView::messageMap =

    { & CView::messageMap, & CMFCTestView::_messageEntries[0] };

     

    const AFX_MSGMAP_ENTRY CMFCTestView::_messageEntries[] =

    {

        { WM_LBUTTONDOWN, (AFX_PMSG)&OnLButtonDown }

        {0, (AFX_PMSG)0 }

    };

     

    void CMFCTestView::OnLButtonDown(UINT nFlags, CPoint point)

    {

        CView::OnLButtonDown(nFlags, point);

    }

           messageMap的第一個成員指向其父類的messageMap,即& CView::messageMap;第二個成員則指向下面的消息映射表;

    GetMessageMap是一個虛函數,顯然是為了父類分派消息的時候能夠找到正確的消息映射結構,后面會看到這一點。

           _messageEntries數組為消息映射表,第一個元素處理WM_LBUTTONDOWN消息,其處理函數是OnLButtonDown,這個函數在CMFCTestView聲明和實現;第二個元素標識了映射表的結尾。

           現在,你想處理什么消息,都可以往_messageEntries里加新的元素并指定你的處理函數,只是如果每一個類都需要手工寫這些代碼,那將是很繁瑣的事情;幸好C++有宏,可以用宏來作一些簡化的代碼,先將消息映射的聲明用DECLARE_MESSAGE_MAP()表示,則類聲明變成下面這樣:

    class CMFCTestView : public CView

    {

    DECLARE_MESSAGE_MAP()

    };

           而實現部分,變成了下面這樣:

    BEGIN_MESSAGE_MAP(CMFCTestView, CView)

        ON_WM_LBUTTONDOWN()

    END_MESSAGE_MAP()

           這就是MFC的消息映射宏,實際的代碼和這里有一些出入,不過大體是差不多的,其核心作用就是構造消息處理函數表。現在打開VC去看看那幾個宏,是不是覺得其實很簡單。

           建好消息映射表后,接下來要看看消息如何流到指定的處理函數里。

    消息流向

           我們可以認為消息的最初進入點是CWnd::WindowProc,在以后的文章會說明消息如何流到這個函數。CWnd是所有窗口類的基類,CMFCTestView當然也是CWnd的子孫類。WindowProc的代碼很簡單,調用OnWndMsg進行消息分派,如果沒有處理函數,則調用DefWindowProc作默認處理:

    LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)

    {

        // OnWndMsg does most of the work, except for DefWindowProc call

        LRESULT lResult = 0;

        if (!OnWndMsg(message, wParam, lParam, &lResult))

            lResult = DefWindowProc(message, wParam, lParam);

        return lResult;

    }

           最重要的是OnWndMsg成員函數,我將里面的代碼作了簡化,去掉了命令通知消息和一些優化代碼,最終的代碼如下:

    BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)

    {

        LRESULT lResult = 0;

        const AFX_MSGMAP* pMessageMap;

        //取得消息映射結構,GetMessageMap為虛函數,所以實際取的是CMFCTestView的消息映射

        pMessageMap = GetMessageMap();

        // 查找對應的消息處理函數

        for (pMessageMap != NULL; pMessageMap = pMessageMap->pBaseMap)

            if (message < 0xC000)

                if ((lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, message, 0, 0)) != NULL)

                    goto LDispatch;

        ... ...

    LDispatch:

        //通過聯合來匹配正確的函數指針類型

        union MessageMapFunctions mmf;

        mmf.pfn = lpEntry->pfn;

        int nSig;

        nSig = lpEntry->nSig;

        //nSig代表函數指針的類型,通過一個大大的Case來匹配。

        switch (nSig)

        {

        case AfxSig_bD:

            lResult = (this->*mmf.pfn_bD)(CDC::FromHandle((HDC)wParam));

            break;

        ... ...

        case AfxSig_vwp:

            {

                CPoint point((DWORD)lParam);

                (this->*mmf.pfn_vwp)(wParam, point);

                break;

            }

        }

       

        return TRUE;

    }

           在代碼中看到GetMessageMap的調用,根據前面的分析已經知道這是一個虛函數,所以實際調用到的是CMFCTestView::GetMessageMap(),也就是這里取到了CMFCTestView的消息映射結構。

           接下來根據當前的消息表尋找對應的消息處理函數,調用AfxFindMessageEntry查找消息映射表,如果找不到就繼續到基類去找,這就是For循環做的事情。AfxFindMessageEntry使用內嵌匯編來提高查找的效率, VCL也是這樣做的。

           如果找到處理函數,則調用goto LDispatch;跳到下面的Case語句,根據nSig判斷函數指針的實際類型,最后轉換并調用之,此時我們的OnLButtonDown函數就被調用到了。

    我們看到了Goto語句的使用,也看到下面大大的Case,這都是OO設計的禁忌,特別是下面的Case,為什么要用Case呢?這是由消息映射宏的設計決定的。看看MFC實際的消息映射表結構是怎么樣的:

    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)

    };

    最關鍵的是nSig和pfn,pfn雖然聲明為AFX_PMSG類型的函數指針,但實際的函數類型卻各各不同,列其中的幾個來看看:

    #define ON_WM_MOVE() /

    { WM_MOVE, 0, 0, 0, AfxSig_vvii, /

    (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(int, int))&OnMove },

     

    #define ON_WM_SIZE() /

    { WM_SIZE, 0, 0, 0, AfxSig_vwii, /

    (AFX_PMSG)(AFX_PMSGW)(void (AFX_MSG_CALL CWnd::*)(UINT, int, int))&OnSize },

    WM_MOVE消息和WM_SIZE消息的函數類型就不一樣,那么CWnd::OnWndMsg在分派的時候如何知道這個函數指針的確切類型呢,答案就是nSig,它指明了處理這個消息的函數類型,如上面的WM_MOVE是AfxSig_vvii。CWnd::OnWndMsg在分派消息的時候,要根據nSig將pfn強制轉換成相應的函數類型。

    在我看來,這實在是非常拙劣的設計,假如新的Windows出現了一批新的消息,而這些消息需要指定新的函數類型,這個時候要怎么做:

    1.         更新AFXMSG_.H頭文件,提供新的消息宏和nSig類型。

    2.         修改AFXIMPL.H頭文件,為MessageMapFunctions聯合加上新的函數類型。

    3.         修改CWnd::OnWndMsg,在Case后面加上新的轉換。

    如果要通過修改上層框架的代碼來支持消息處理的擴展,那就太不明智了。

           大概MS也考慮到這種弊端,因此給出了一個通用的消息宏:

    #define ON_MESSAGE(message, memberFxn) /

    { message, 0, 0, 0, AfxSig_lwl, /

    (AFX_PMSG)(AFX_PMSGW)(LRESULT (AFX_MSG_CALL CWnd::*)(WPARAM, LPARAM))&memberFxn },

        這個宏傳一個消息名和一個函數名,其函數類型固定是LRESULT (CWnd::*)(WPARAM, LPARAM)。假設我們要處理WM_RBUTTONDOWN消息,可以這樣做:

           首先在消息映射宏里這樣寫:

    BEGIN_MESSAGE_MAP(CMFCTestView, CView)

        ON_MESSAGE(WM_RBUTTONDOWN, OnRButtonDown)

    END_MESSAGE_MAP()

           然后聲明和實現OnRButtonDown成員函數:

    LRESULT CMFCTestView::OnRButtonDown(WPARAM wParam, LPARAM lParam)

    {

        MessageBox("OnRBUttonDown");

        return Default();

    }

    對于其他消息也類似這樣處理。這種方式要比前面的好得多,至少消息處理函數的形式統一起來了。而MFC如果都以這種形式作為處理函數,也沒有必要在OnWndMsg里面加一個Case來強制轉換各種函數類型。

    但這種方法的缺點也很明顯,就是參數意義很模糊,幾乎很難從wParam看出它的作用。有沒有一種方法,使得消息宏的聲明如ON_MESSAGE一樣統一,又能讓處理函數的參數變得有意義呢?實際上這樣的方法已經在VCL中存在了,那就是用一個布局相容的結構來做參數,如:

    // 通用窗口消息結構

    typedef struct tagWNDMSG {

        UINT message;

        WPARAM wParam;

        LPARAM lParam;

        HRESULT hr;

    } WNDMSG, *PWNDMSG;

    // 按鈕點下消息結構

    typedef struct tagLBUTTONDOWNMSG {

        UINT message;

        UINT Flag;

        WORD X;

        WORD Y;

        HRESULT hr;

    } LBUTTONDOWNMSG, *PLBUTTONDOWNMSG;

    WNDMSG和LBUTTONDOWNMSG在內存中的布局是一樣的,可以聲明一個一致的函數指針類型,以及一個統一的消息宏,如下面這樣:

    // 消息處理器類型

    typedef void (CWnd:: *MSGHANDER) (WNDMSG &msg);

    // 通用消息宏,MsgHandler的參數形式必須與MSGHANDER相同

    #define ADDMESSAGEHANDLER(message, MsgHandler) /

        { message, 0, 0, 0, 0, (AFX_PMSG)&MsgHandler },

    以后就統一用ADDMESSAGEHANDLER來加消息宏。CWnd:OnWndMsg的代碼也會變得干凈很多,根據message找到消息處理器后就不需要Case了,只需要將消息參數裝進一個WNDMSG結構并傳給消息處理器然后調用。代碼類似下面這樣:

    BOOL CWnd::OnWndMsg( UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult )

    {

        ... ...

        //找處理函數

        const AFX_MSGMAP_ENTRY* lpEntry = FindMessageEntry(GetMessageMap(), message);  

        if (lpEntry != NULL)

        {

            MessageMapPorc mmp;

            mmp.pfn = lpEntry->pfn;

            //生成WNDMSG結構

            WNDMSG msg;

            msg.hr = 0;

            msg.wParam = wParam;

            msg.lParam = lParam;

            //調用處理函數

            (this->*mmp.MsgProc)(msg);

            *pResult = msg.hr;

            return TRUE;

        }

        else

            return FALSE;

    }

    對于每一個消息處理器,如果沒有對應的消息結構,就用通用的WNDMSG做參數,如果有的話可以用該消息結構做參數,只要其內存布局與WNDMSG一樣。如下面的代碼:

    void CWnd::OnBtnDown( LBUTTONDOWNMSG &msg )

    {

        ... ...

    }

    在OnBtnDown函數里,直接訪問LBUTTONDOWNMSG里面的X和Y,而不再需要去取lParam的高位和低位。剩下的問題就是針對每個消息提供相應的消息結構。

     

    如果你用MFC做開發,覺得上面的修改如何呢?也許對MFC就輕駕熟的你能提出原來的消息分派的諸多好處,也能指出這個修改的諸多壞處,不管如何,都歡迎你在這里提出來,畢竟有交流才有進步嗎。

     

    from:http://blog.csdn.net/linzhengqun/article/details/1905671

    RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成
    最近免费观看高清韩国日本大全