不同的SOUI控件可以產生不同的事件。SOUI系統中提供了兩種事件處理方式:事件訂閱 + 事件處理映射表(參見第八篇:SOUI中控件事件的響應)
事件訂閱由於直接將事件及事件處理函數連接,不存在事件分發的問題,這里主要介紹使用事件映射表時的事件分發。
在回答這個問題前,首先了解一下什么是事件分發。
在大型項目中,程序邏輯可能非常復雜,如果將所有UI中控件的事件處理集中在一個消息/事件映射表里,代碼的可維護性會變得非常差。解決這個問題常見的方法就是將事件進行分類(如根據來源分類),不同類別的事件采用一個獨立的事件處理對象來處理,這就是事件分發的核心。
目前流行的UI通常采用Tab控件來組織UI,不同的功能放到不同的Tab頁中,不同的Tab頁可能互不相干的功能模塊,對於類似這樣的情形很自然的會想到采用事件分發機制來實現模塊之間邏輯的解耦(如下圖中SoTool采用的UI)。
在上面的UI中,雖然整個UI被TAB分成了6個頁面,但是6個頁面都存在於同一個宿主窗口中。
一般情況下,如果UI相對比較簡單,我們推薦直接在宿主窗口的事件處理映射表中統一處理控件事件。
但是當出現如上圖這樣復雜的界面時,最好是將不同功能頁的事件處理在不同的對象中分別處理。
在MFC中,一個類要處理消息,這個類通常派生自CCmdTarget(可能記錯了,太久不用MFC了),主窗口收到的消息會自動路由到這個消息處理對象中。
在WTL中,WTL提供了一組消息映射宏:CHAIN_MSG_MAP,CHAIN_MSG_MAP_MEMBER等以便將消息分發到同樣實現了消息映射表的任意C++對象。
SOUI的事件分發采用了WTL消息分發類似的機制,同樣采用事件映射宏的方式來構造事件映射表,下面是SOUI中幾個主要的和事件分發相關的宏:
#define EVENT_MAP_BEGIN() \ protected: \ virtual BOOL _HandleEvent(SOUI::EventArgs *pEvt)\ { \ UINT uCode = pEvt->GetID(); \ #define EVENT_MAP_DECLEAR() \ protected: \ virtual BOOL _HandleEvent(SOUI::EventArgs *pEvt);\ #define EVENT_MAP_BEGIN2(classname) \ BOOL classname::_HandleEvent(SOUI::EventArgs *pEvt)\ { \ UINT uCode = pEvt->GetID(); \ #define EVENT_MAP_END() \ return __super::_HandleEvent(pEvt); \ } \ #define EVENT_MAP_BREAK() \ return FALSE; \ } \ #define CHAIN_EVENT_MAP(ChainClass) \ if(ChainClass::_HandleEvent(pEvt)) \ return TRUE; \ #define CHAIN_EVENT_MAP_MEMBER(theChainMember) \ { \ if(theChainMember._HandleEvent(pEvt)) \ return TRUE; \ } #define EVENT_CHECK_SENDER_ROOT(pRoot) \ { \ SWindow *pWnd = sobj_cast<SWindow>(pEvt->sender);\ if(!pWnd->IsDescendant(pRoot)) \ return FALSE; \ } // void OnEvent(EventArgs *pEvt) #define EVENT_HANDLER(cd, func) \ if(cd == uCode) \ { \ func(pEvt); return TRUE; \ }
下面是SoTool中的MainDlg中的事件處理:
//soui消息 EVENT_MAP_BEGIN() EVENT_NAME_COMMAND(L"btn_close", OnClose) EVENT_NAME_COMMAND(L"btn_min", OnMinimize) EVENT_NAME_COMMAND(L"btn_max", OnMaximize) EVENT_NAME_COMMAND(L"btn_restore", OnRestore) CHAIN_EVENT_MAP_MEMBER(m_imgMergerHandler) CHAIN_EVENT_MAP_MEMBER(m_codeLineCounter) CHAIN_EVENT_MAP_MEMBER(m_2UnicodeHandler) CHAIN_EVENT_MAP_MEMBER(m_folderScanHandler) CHAIN_EVENT_MAP_MEMBER(m_calcMd5Handler) EVENT_MAP_END()
上面代碼中,EVENT_MAP_BEGIN()和EVENT_MAP_END()這兩個宏構造出一個空的事件處理函數,該函數自動將未處理的事件交給基類的事件處理函數處理。
如果基類中沒有事件處理函數,顯然這個事件映射表編譯不能通過,此時SOUI提供了另一個EVENT_MAP_BREAK()來代替。
上面的事件分發表中,我使用CHAIN_EVENT_MAP_MEMBER宏將來自不同頁面的控件事件傳遞到不同的事件處理對象中。
下面代碼是m_imgMergerHandler對象頭文件。
class CImageMergerHandler : public IFileDropHandler { friend class CMainDlg; public: CImageMergerHandler(void); ~CImageMergerHandler(void); void OnInit(SWindow *pRoot); void AddFile(LPCWSTR pszFileName); protected: virtual void OnFileDropdown(HDROP hDrop); void OnSave(); void OnClear(); void OnModeHorz(); void OnModeVert(); EVENT_MAP_BEGIN() EVENT_CHECK_SENDER_ROOT(m_pPageRoot) EVENT_NAME_COMMAND(L"btn_save", OnSave) EVENT_NAME_COMMAND(L"btn_clear", OnClear) EVENT_NAME_COMMAND(L"radio_horz", OnModeHorz) EVENT_NAME_COMMAND(L"radio_vert", OnModeVert) EVENT_MAP_BREAK() SWindow *m_pPageRoot; SImgCanvas *m_pImgCanvas; };
可以看到這里的事件映射表使用了EVENT_MAP_BREAK來結束。
在SOUI中推薦使用控件的name屬性來標識一個控件(name屬性是一個wchar*的字符串,使用name雖然在事件分發時采用字符串比較,較基於整數id屬性的比較效率低一點,好處在於代碼的可讀性好),不同的頁面中的控件如果出現相同的name該如何識別呢?
在SOUI中使用了一點小技巧:在事件處理對象中實現一個oninit函數,該函數在maindlg中處理WM_INITDIALOG時被調用,在oninit中保存了一個頁面根節點的指針:
SWindow *m_pPageRoot;
在事件映射表的開始,我們采用EVENT_CHECK_SENDER_ROOT(m_pPageRoot)這個宏來識別那些來自本頁面的事件。如果事件是來自其它頁面則不處理。