以金山界面库(openkui)为例思考和分析界面库的设计和实现——代码结构(完)
? ? ? ? 三年前,準備將金山界面庫做一個全面的剖析。后來由于種種原因,這個系列被中斷而一直沒有更新。時過境遷,現在在windows上從事開發的人員越來越少,關注這塊的技術的朋友也很少了。本以為這系列也隨著技術的沒落而不再被人所關注,所以一直沒有更新其的意愿。前些天突然有個朋友對之前《以金山界面庫(openkui)為例思考和分析界面庫的設計和實現——資源讀取模塊分析》做了評論,這讓我重新燃起一種欲望——將尚未完結的系列寫完。于是我打開塵封三年的“草稿箱”里這篇文章,沿著三年前的思路,試著完成這系列博文。(轉載請指明出于breaksoftware的csdn博客)
? ? ? ? 在《問題》一文中,我從一個“無知者”的角度拋出了一系列界面庫設計的問題。在《資源讀取模塊分析》中已經解釋了資源的存在形式。本文我們主要分析下整個界面構建的脈絡。
? ? ? ? 以網頁為例,我們可以通過html+css+javascript去搭建一個界面。這么設計的好處我在《問題》一文中已經有所闡述。openkui庫也是照著這樣的思路去設計的,但是它將組成分的更細,以至于讓我覺得細的似乎太松散了。
? ? ? ? 以Sample1的皮膚資源為例,它是由若干xml文件組成了界面描述:
- xmls.xml 它描述了幾個關鍵XML文件的位置。其中IDR_DLG_MAIN對應的XML文件是描述了界面的總體(后文稱界面描述文件)。IDR_KSC_STYPE對應的XML文件(后文稱樣式描述文件)描述了局部界面的一些屬性,IDR_KSC_SKINE對應的XML文件(后文稱皮膚描述文件)則描述了局部界面對應的類。
- strings.xml 它描述了文字和ID的映射關系。這個非常類似于MFC中的string table。
- images.xml 它描述了圖片文件和ID的映射關系。界面中所要用的圖片資源都應該在這個文件中被描述。
? ? ? ? 其中xmls.xml中記錄的幾個xml和界面的關系最為緊密。而MAIN、STYPE和SKIN的關系也是需要理清的,我們先看界面描述文件內容
<layer title="sample1" width="600" height="470" appwin="1"><header class="mainhead" width="full" height="23"><icon src="ICON_MAIN" pos="5,4"/><text class="dlgtitle" pos="25,6">樣例程序1</text><imgbtn id="60003" class="linkimage" skin="minbtn" pos="-105,1"/><imgbtn id="60002" class="linkimage" skin="maxbtn" pos="-73,1"/><imgbtn id="60001" class="linkimage" skin="closeonlybtn" pos="-43,1"/></header><body class="mainbody" width="full" height="full"><dlg pos="0,0,-0,-0" crbg=F7FBBF><text class="hellowordstyle" pos="50,200">hello world!</text></dlg></body><footer class="mainfoot" width="full" height="23" crbg=FFB9B9></footer>
</layer>? ? ? ? 它是由header、body和footer三部分組成。每個部分又是由一些子模塊組成,如text、imgbtn。以imgbtn為例,我們可以看到它的內部描述了id、class、skin和pos等四個屬性。class則是描述該子模塊是什么一種樣式,我們從linkimage可以猜測出它應該是一個可以點擊的圖片,于是鼠標移動到上面應該變成手型,這個我們在樣式描述文件中可以得到印證
<class name=linkimage cursor=hand/>? ? ? ? skin屬性則是描述了該子模塊的皮膚配圖信息,我們在皮膚描述文件中可以找到
<png name="minbtn" src="IDP_BTN_SYS_MINIMIZE" subwidth="32"/>? ? ? ? minbtn的src字段指向的是一個圖片文件ID,這個我們可以在images.xml中找到
<image id="IDP_BTN_SYS_MINIMIZE" path="images/btn_sys_minimize.png" />? ? ? ? 如此,我們便將這些XML文件的關系理清楚了。可以想象,相同skin和class的兩個模塊,它們可能在位置和大小上存在區別,所以“位置”和“大小”兩個屬性應該是在界面描述文件中,或者說應該以其覆蓋其他文件的屬性。而諸如手型、字體大小、背景色等則是應該在樣式描述文件中描述。至于每個子模塊對應的背景圖片資源,應該在皮膚描述文件中描述。
? ? ? 上述XML中描述的屬性,在界面構建過程中會被讀取。可以想象,這個讀取操作是每個皮膚模塊的基礎功能。打個比方,png這個模塊它需要讀取name、src和subwidth三個屬性。它可能存在對應的get_name,get_src和get_subwidth三個方法用于獲取上述屬性。但是如果一旦增加屬性,則需要新增讀取函數。而且,屬性的值的類型可能也是不同的,比如:
<class name=settingpage crbg=FBFCFD y-margin=10/>? ? ? ? 它的屬性是十進制數或者16進制數。那么接口的設計類型也無法做到統一。這樣的設計存在明顯的問題。所以我們應該統一一套獲取方法,于是kui設計了如下基礎類
class KUILIB_API CKuiObject
{
public:......virtual LPCSTR GetObjectClass() = 0;virtual BOOL Load(TiXmlElement* pXmlElem){for (TiXmlAttribute *pAttrib = pXmlElem->FirstAttribute(); NULL != pAttrib; pAttrib = pAttrib->Next()){SetAttribute(pAttrib->Name(), pAttrib->Value(), TRUE);}return TRUE;}virtual HRESULT SetAttribute(CStringA strAttribName, CStringA strValue, BOOL bLoading){return E_FAIL;}......
}? ? ? ? 所有皮膚相關的類都繼承CKuiObject,它通過load方法遍歷XML文件,并通過SetAttribute方法設置不同的屬性。我們發現,可以統一這么做的一個非常重要的前提是XML庫返回的name和value值都是const char*的。這樣就規避了我們之前對數據類型無法統一的擔憂。但是有些屬性,我們在之后參與計算或者邏輯的時候就是希望它是整形的,那么我們需要怎么處理?從設計的角度說,CKuiObject不應該去關心屬性的類型,因為它無法得知屬性的類型,且即使得知了屬性類型,也無法做到統一的處理(除非使用any類型)。所以,如果真的需要做類型確定,也是應該在不同的子類中做處理,而kui庫就是這么做的。我們再來看基類CKuiObject的SetAttribute方法,它沒有做任何有意義的事情,那么其有意義的功能是在其子類中實現的。這塊的設計和我之前的預想不太一樣,我本以為在CKuiObject類中保存一份屬性的map結構,并通過SetAttribute方法去填充這個結構。不同的繼承類在繪制界面時,則是去讀取這個map結構獲取需要的信息。這樣的設計可以使得屬性的保存和獲取邏輯變得統一,相比于Kui設計中遍布于各個類的各種屬性,明顯統一的map結構更加方便和合理。但是有人會說,這樣就限制了各個類的屬性的類型,使得它們必須是map的value類型(比如string)。其實這個擔憂大可不必,我們可以讓屬性的map是std::map<string,any>的結構,當然這樣就得在KuiObject層確定屬性值的類型了。
? ? ? ? 我們繼續看下各個子類對SetAttribute方法的設計。Kui庫使用一組宏定義的方法去設計SetAttribute方法,這樣就像MFC中的消息映射表,開發者只要維護好這張表就可以了。這種設計可以方便開發者對代碼的修改和擴展。
#define KUIWIN_DECLARE_ATTRIBUTES_BEGIN() \
public: \virtual HRESULT SetAttribute( \CStringA strAttribName, \CStringA strValue, \BOOL bLoading) \{ \HRESULT hRet = __super::SetAttribute( \strAttribName, \strValue, \bLoading \); \if (SUCCEEDED(hRet)) \return hRet; \#define KUIWIN_DECLARE_ATTRIBUTES_END() \return E_FAIL; \\return hRet; \} \? ? ? ? 通過KUIWIN_DECLARE_ATTRIBUTES_BEGIN和KUIWIN_DECLARE_ATTRIBUTES_END的組合我們便可以得到一個完整的SetAttribute函數。可以見得,每次設置屬性時,我們都需要嘗試設置其父類的屬性,如果其父類屬性設置成功了,則不再在此類中設置屬性。對于各個屬性,則是使用如下一些宏進行設置
#define KUIWIN_CHAIN_ATTRIBUTE(varname, allredraw) \if (SUCCEEDED(hRet = varname.SetAttribute(strAttribName, strValue, bLoading))) \{ \return hRet; \} \else \#define KUIWIN_CUSTOM_ATTRIBUTE(attribname, func) \if (attribname == strAttribName) \{ \hRet = func(strValue, bLoading); \} \else \// Int = %d StringA
#define KUIWIN_INT_ATTRIBUTE(attribname, varname, allredraw) \if (attribname == strAttribName) \{ \varname = ::StrToIntA(strValue); \hRet = allredraw ? S_OK : S_FALSE; \} \else \// UInt = %u StringA
#define KUIWIN_UINT_ATTRIBUTE(attribname, varname, allredraw) \if (attribname == strAttribName) \{ \varname = (UINT)::StrToIntA(strValue); \hRet = allredraw ? S_OK : S_FALSE; \} \else ? ? ? ??KUIWIN_CHAIN_ATTRIBUTE宏是為了屬性傳導的。KUIWIN_CUSTOM_ATTRIBUTE宏是為了設置屬性時調用某處理函數的。而KUIWIN_INT_ATTRIBUTE和KUIWIN_UINT_ATTRIBUTE則是將string類型的屬性轉換成int等其他類型的數據的,我們總覽Kui庫,可以發現有若干這種類型轉換的屬性處理宏。這就是我之前所說的,屬性的類型是在不同子類中確定的。我們看一個這組宏使用的例子
KUIWIN_DECLARE_ATTRIBUTES_BEGIN()KUIWIN_CHAIN_ATTRIBUTE(m_imgSkin, TRUE)KUIWIN_COLOR_ATTRIBUTE("crbg", m_crBg, TRUE)KUIWIN_INT_ATTRIBUTE("left", m_lSkinParamLeft, TRUE)KUIWIN_INT_ATTRIBUTE("top", m_lSkinParamTop, TRUE)KUIWIN_ENUM_ATTRIBUTE("part", UINT, TRUE)KUIWIN_ENUM_VALUE("all", Frame_Part_All)KUIWIN_ENUM_VALUE("top", (Frame_Part_All & ~Frame_Part_Bottom))KUIWIN_ENUM_VALUE("middle", (Frame_Part_All & ~(Frame_Part_Bottom | Frame_Part_Top)))KUIWIN_ENUM_VALUE("bottom", (Frame_Part_All & ~Frame_Part_Top))KUIWIN_ENUM_VALUE("left", (Frame_Part_All & ~Frame_Part_Right))KUIWIN_ENUM_VALUE("center", (Frame_Part_All & ~(Frame_Part_Right | Frame_Part_Left)))KUIWIN_ENUM_VALUE("right", (Frame_Part_All & ~Frame_Part_Left))KUIWIN_ENUM_END(m_uDrawPart)KUIWIN_DECLARE_ATTRIBUTES_END()
? ? ? ? 不同皮膚類通過上述宏的組合,實現了各自的屬性設置方法。其主要實現的功能,就是把屬性設置到各自類的成員變量中:要么是直接的成員變量,要么是成員變量的屬性中。如上例,KUIWIN_CHAIN_ATTRIBUTE宏就是將屬性傳遞到m_imgSkin的屬性中。那什么是m_imgSkin呢?可以想象,每個由圖片繪制的皮膚模塊都有圖片的相關屬性,比如圖片的地址等,而這些模塊則可以作為一個對象存在于皮膚模塊類中,以作統一處理。這個就是KUI模塊皮膚類的設計思路。但是個人覺得這不是一種好的設計,我覺得圖片皮膚類(m_imgSkin對應的類)應該是各個模塊圖片皮膚類的父類,即應該是繼承關系,而不應該是包含關系。打個比方,使用圖片方式繪制的按鈕和使用圖片方式繪制的Frame,應該都是一種圖片皮膚類,所以他們應該通過繼承的方式體現“是”這層關系。
? ? ? ? 現在我們來看下m_imgSkin對應的圖片皮膚類的設計
class KUILIB_API CKuiImageSkin: public CKuiImage, public CKuiSkinBase
{KUIOBJ_DECLARE_CLASS_NAME(CKuiImageSkin, "imglst")
? ? ? ? 該類從繼承關系上看,它即是一種圖片類(CKuiImage),也是一種皮膚類(CKuiSkinBase)。而CKuiImageSkin類這是一個圖片組(imglst即image_list)描述的皮膚類。舉個例子,我們的按鈕一般有三態:普通、按下和懸浮。如果我們將這三態對應的背景圖片保存在三張圖中,這樣會增加文件的讀取次數,同時也不利于后期維護。那我們我們就將這三張圖片合并為一張圖片組,這樣一個按鈕對應一個圖片組,圖片數量減少三分之二。當然這兒也不一定是三張圖片,也可能是一張,或者是可以表示更多狀態的八張。
? ? ? ? CKuiImage是一個非常重要的類,它主要完成了圖片的讀取和繪制工作。其內容非常細節,本文不做分析,有興趣的同學可以參看KUILib\Include\kuiwin\kuiimage.h。CKuiSkinBase則是皮膚類的繪制的一層封裝,它負責表達皮膚被繪制在什么位置。因為不是所有的皮膚都是圖片類型的(比如固定底色的),所以使用這層封裝也用于涵蓋所有皮膚的繪制。我們可以看到,Kui中的皮膚類都繼承于CKuiSkinBase
class KUILIB_API CKuiPngSkin: public CKuiSkinBase
{KUIOBJ_DECLARE_CLASS_NAME(CKuiPngSkin, "png")class CKuiSkinButton : public CKuiSkinBase
{KUIOBJ_DECLARE_CLASS_NAME(CKuiSkinButton, "button")class CKuiSkinImgHorzExtend : public CKuiSkinBase
{KUIOBJ_DECLARE_CLASS_NAME(CKuiSkinImgHorzExtend, "imghorzex")
? ? ? ? 這么零散的皮膚基礎類,總得在一個地方進行統籌,現在我們就要講解皮膚基礎類的工廠類——KuiSkin,我們先看看其部分申明
class KuiSkin
{
public:KuiSkin();~KuiSkin();static BOOL LoadSkins(const std::string& strXml){return LoadSkins(strXml.c_str());}static BOOL LoadSkins(LPCSTR lpszXml);static CKuiSkinBase* GetSkin(LPCSTR lpszSkinName){__KuiSkinPool::CPair *pairRet = _Instance()->m_mapPool.Lookup(lpszSkinName);if (pairRet)return pairRet->m_value;elsereturn NULL;}static size_t GetCount();protected:typedef CAtlMap<CStringA, CKuiSkinBase *> __KuiSkinPool;__KuiSkinPool m_mapPool;static KuiSkin* ms_pInstance;static KuiSkin* _Instance(){if (!ms_pInstance)ms_pInstance = new KuiSkin;return ms_pInstance;}void _LoadSkins(TiXmlElement *pXmlSkinRootElem);static CKuiSkinBase* _CreateKuiSkinByName(LPCSTR lpszName){CKuiSkinBase *pNewSkin = NULL;pNewSkin = CKuiImageSkin::CheckAndNew(lpszName);if (pNewSkin)return pNewSkin;pNewSkin = CKuiSkinImgFrame::CheckAndNew(lpszName);if (pNewSkin)return pNewSkin;pNewSkin = CKuiSkinButton::CheckAndNew(lpszName);if (pNewSkin)return pNewSkin;pNewSkin = CKuiSkinImgHorzExtend::CheckAndNew(lpszName);if (pNewSkin)return pNewSkin;pNewSkin = CKuiSkinGradation::CheckAndNew(lpszName);if (pNewSkin)return pNewSkin;pNewSkin = CKuiPngSkin::CheckAndNew(lpszName);if (pNewSkin)return pNewSkin;return NULL;}
};
? ? ? ? 該類讀取皮膚描述文件,并對每個name新建對象。之后界面構建過程中,將通過GetSkin的方法獲取每個皮膚基礎組件。皮膚是界面中一個比較基礎的組件,它是一個區域性質的模塊。而往往界面中的很多控件是由很多基礎的組件組成的,比如一個樹形列表。接下來我們再來看下高于皮膚組件層次的界面模塊。
? ? ? ? 界面中,除了單純的皮膚基礎組件,還有一些更簡單的組件,比如文字。也有些多個基礎組件組合的復雜皮膚模塊,比如進度條。以Sample1為例
<icon src="ICON_MAIN" pos="5,4"/>
<text class="dlgtitle" pos="25,6">樣例程序1</text>? ? ? ??界面描述文件中的icon和text就是區別于我們上面介紹的圖片皮膚類的界面模塊。在KUILib\Include\kuiwin\kuiwndcmnctrl.h類中,我們就可以看到一系列這樣的類
class KUILIB_API CKuiIconWnd : public CKuiWindow
{KUIOBJ_DECLARE_CLASS_NAME(CKuiIconWnd, "icon")class CKuiCheckBox : public CKuiWindow
{KUIOBJ_DECLARE_CLASS_NAME(CKuiCheckBox, "check")class CKuiProgress : public CKuiWindow
{KUIOBJ_DECLARE_CLASS_NAME(CKuiProgress, "progress")class CKuiImageWnd : public CKuiWindow
{KUIOBJ_DECLARE_CLASS_NAME(CKuiImageWnd, "img")? ? ? ? 這些類中最重要的功能是:加載XML文件、繪制和位置計算。于是我們可以發現這些類主要實現了Load、OnPaint和OnNcCalcSize方法。稍微復雜一點的類是進度條類,因為進度條可以分為:進度條外框和進度條填充物兩種圖片,所以它也將是兩個圖片基礎皮膚類組合而成,我們看下其申明
class CKuiProgress : public CKuiWindow
{KUIOBJ_DECLARE_CLASS_NAME(CKuiProgress, "progress")
protected: CKuiSkinBase *m_pSkinBg;CKuiSkinBase *m_pSkinPos;......KUIWIN_BEGIN_MSG_MAP()MSG_WM_PAINT(OnPaint)MSG_WM_NCCALCSIZE(OnNcCalcSize)KUIWIN_END_MSG_MAP()KUIWIN_DECLARE_ATTRIBUTES_BEGIN()KUIWIN_SKIN_ATTRIBUTE("bgskin", m_pSkinBg, TRUE)KUIWIN_SKIN_ATTRIBUTE("posskin", m_pSkinPos, TRUE)KUIWIN_DWORD_ATTRIBUTE("min", m_dwMinValue, FALSE)KUIWIN_DWORD_ATTRIBUTE("max", m_dwMaxValue, FALSE)KUIWIN_DWORD_ATTRIBUTE("value", m_dwValue, FALSE)KUIWIN_UINT_ATTRIBUTE("showpercent", m_bShowPercent, FALSE)KUIWIN_DECLARE_ATTRIBUTES_END()
}? ? ? ? 有興趣的同學可以參看該類中的OnPaint和OnNcCalcSize方法是如何使用圖片基礎皮膚類進行繪制的。
? ? ? ? 我們發現這些皮膚組件類都繼承于CKuiWindow,目測其是一個窗口控件,但是實際上它并不是
class KUILIB_API CKuiWindow : public CKuiObject
{
......
protected:
......KuiStyle m_style;
......
public:BOOL NeedRedrawParent() {return (m_style.m_strSkinName.IsEmpty() && (m_style.m_crBg == CLR_INVALID));}virtual BOOL Load(TiXmlElement* pTiXmlElem){......};// Set container, container is a REAL windowvirtual void SetContainer(HWND hWndContainer) {m_hWndContainer = hWndContainer;}virtual BOOL IsContainer() {return FALSE;}virtual BOOL NeedRedrawWhenStateChange() {if (!m_style.m_strSkinName.IsEmpty()) {CKuiSkinBase* pSkin = KuiSkin::GetSkin(m_style.m_strSkinName);if (pSkin && !pSkin->IgnoreState())return TRUE;}return (CLR_INVALID != m_style.m_crHoverText) || (NULL != m_style.m_ftHover) || (CLR_INVALID != m_style.m_crBgHover);}
......
protected:KUIWIN_BEGIN_MSG_MAP()MSG_WM_CREATE(OnCreate)MSG_WM_PAINT(OnPaint)MSG_WM_DESTROY(OnDestroy)MSG_WM_WINDOWPOSCHANGED(OnWindowPosChanged)MSG_WM_NCCALCSIZE(OnNcCalcSize)MSG_WM_SHOWWINDOW(OnShowWindow)KUIWIN_END_MSG_MAP_BASE()KUIWIN_DECLARE_ATTRIBUTES_BEGIN()KUIWIN_STYLE_ATTRIBUTE("class", m_style, TRUE)......KUIWIN_DECLARE_ATTRIBUTES_END()
};
? ? ? ? 我們從SetContainer的注釋可以看出,Container類型的類才是真正的窗口類。在CKuiWindow類中,我們看到一個成員變量m_style,它就是我們之前介紹的樣式描述文件中的一項。我們還發現m_style中皮膚名的成員變量——m_strSkinName,可以見得皮膚名不僅可以在界面描述文件中確定,也可以在樣式描述文件中確定。CKuiWindow內部實現了很多細節功能,本文不作分析,只要知道它主要做了繪制和計算大小和位置的功能即可,而且要記住它是(偽)窗口類的父類。
? ? ? ? 看過這么多基礎類,我們終于要看這些基礎類的容器——容器類,以Sample1為例,其header、body和footer三者都是容器類。但是需要注意的是,這些容器類的名字并不是header、body或者footer。我們以headerd為例看下對應的代碼
template <class T, class TKuiWin = CKuiDialog, class TBase = ATL::CWindow, class TWinTraits = CKuiDialogViewTraits>
class ATL_NO_VTABLE CKuiDialogViewImpl: public ATL::CWindowImpl<T, TBase, TWinTraits>, public CKuiViewImpl<T>
{friend CKuiViewImpl<T>;public:DECLARE_WND_CLASS_EX(NULL, CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, COLOR_WINDOW)
protected:TKuiWin m_kuiHeader;......BOOL SetXml(LPCSTR lpszXml){......pTiElement = pXmlRootElem->FirstChildElement("header");m_bHasHeader = m_kuiHeader.Load(pTiElement);......}
}? ? ? ? 可以見得header對應的類是CKuiDialog,我們查看CKuiDialog的實現
class CKuiDialog: public CKuiPanel
{KUIOBJ_DECLARE_CLASS_NAME(CKuiDialog, "dlg")class CKuiPanel : public CKuiContainerWnd
{KUIOBJ_DECLARE_CLASS_NAME(CKuiPanel, "div")......
protected:CAtlList<CKuiWindow *> m_lstWndChild;public:BOOL LoadChilds(TiXmlElement* pTiXmlChildElem){KuiSendMessage(WM_DESTROY);BOOL bVisible = IsVisible(TRUE);for (TiXmlElement* pXmlChild = pTiXmlChildElem; NULL != pXmlChild; pXmlChild = pXmlChild->NextSiblingElement()){CKuiWindow *pNewChildWindow = _CreateKuiWindowByName(pXmlChild->Value());if (!pNewChildWindow)continue;pNewChildWindow->SetParent(m_hKuiWnd);pNewChildWindow->SetContainer(m_hWndContainer);pNewChildWindow->Load(pXmlChild);m_lstWndChild.AddTail(pNewChildWindow);}return TRUE;}void SetContainer(HWND hWndContainer){__super::SetContainer(hWndContainer);POSITION pos = m_lstWndChild.GetHeadPosition();while (pos != NULL){CKuiWindow *pKuiWndChild = m_lstWndChild.GetNext(pos);if (pKuiWndChild){pKuiWndChild->SetContainer(hWndContainer);}}}int OnCreate(LPCREATESTRUCT /*lpCreateStruct*/){POSITION pos = m_lstWndChild.GetHeadPosition();while (pos != NULL){CKuiWindow *pKuiWndChild = m_lstWndChild.GetNext(pos);pKuiWndChild->OnCreate(NULL);}return TRUE;}......
}
class CKuiContainerWnd : public CKuiWindow
{
public:virtual CKuiWindow* FindChildByCmdID(UINT uCmdID);virtual void RepositionChilds();virtual void RepositionChild(CKuiWindow *pKuiWndChild);BOOL IsContainer() {return TRUE;}
};
? ? ? ? 我們可以看到,主要的類是CKuiPanel。它在其內部維護了一組偽窗口信息,然后所有操作都是遍歷這些偽窗口類的處理函數實現消息傳遞,比如OnCreate方法的實現。而其父類CKuiContainerWnd則主要是定義一些虛方法,并重寫了CKuiWindow的IsContainer方法,表明繼承于自己的類都是一個容器。
? ? ? ? 我們還要關注下容器類如何和各個組件進行通信。在MFC的多窗口模式下,消息通過消息泵進行傳遞。而Kui除了容器類是窗口類,其他組件類則不是窗口,那么它們之間的消息是怎么傳遞的?我們知道只有窗口才能收到消息,那么可以想到第一步處理消息的地方應該是容器類。以窗口尺寸改變為例,當窗口尺寸改變時,其內部組件也要被調整。首先容器類收到消息
template <class T, class TKuiWin = CKuiDialog, class TBase = ATL::CWindow, class TWinTraits = CKuiDialogViewTraits>
class ATL_NO_VTABLE CKuiDialogViewImpl: public ATL::CWindowImpl<T, TBase, TWinTraits>, public CKuiViewImpl<T>
{
protected:BEGIN_MSG_MAP_EX(CKuiDialogViewImpl)MESSAGE_RANGE_HANDLER_EX(WM_MOUSEFIRST, WM_MOUSELAST, OnToolTipEvent)MSG_WM_SIZE(OnSize)? ? ? ? 在容器類的OnSize中,會調用重置組件位置的邏輯
void OnSize(UINT nType, CSize size)
{......_RepositionItems();
}void _RepositionItems(BOOL bRedraw = TRUE)
{....WINDOWPOS WndPos = { 0, 0, rcClient.left, rcClient.top, rcClient.Width(), rcClient.Height(), SWP_SHOWWINDOW };if (m_bHasHeader){m_kuiHeader.KuiSendMessage(WM_WINDOWPOSCHANGED, 0, (LPARAM)&WndPos);m_kuiHeader.GetRect(rcHeader);}if (m_bHasFooter){m_kuiFooter.KuiSendMessage(WM_WINDOWPOSCHANGED, 0, (LPARAM)&WndPos);m_kuiFooter.GetRect(rcFooter);WndPos.y = rcClient.bottom - rcFooter.Height();WndPos.cy = rcFooter.Height();m_kuiFooter.KuiSendMessage(WM_WINDOWPOSCHANGED, 0, (LPARAM)&WndPos);}if (m_bHasBody){WndPos.y = rcHeader.bottom;WndPos.cy = rcClient.bottom - rcFooter.Height() - rcHeader.bottom;m_kuiBody.KuiSendMessage(WM_WINDOWPOSCHANGED, 0, (LPARAM)&WndPos);}_Redraw();
}? ? ? ? 主界面的header、body和footer都將調用KuiSendMessage方法傳遞消息,從函數命名上看KuiSendMessage承襲了MFC中消息傳遞的模式。但是實際上這個只是一種“寫法”,和MFC那套不是一套機制。它只是一個函數調用
LRESULT KuiSendMessage(UINT Msg, WPARAM wParam = 0, LPARAM lParam = 0){LRESULT lResult = 0;SetMsgHandled(FALSE);ProcessWindowMessage(NULL, Msg, wParam, lParam, lResult);return lResult;}? ? ? ? 而ProcessWindowMessage方法也不是向窗口傳遞消息,而只是調用各個繼承于CKuiWindow類的ProcessWindowMessage方法。
class KUILIB_API CKuiWindow : public CKuiObject
{
......
KUIWIN_BEGIN_MSG_MAP()MSG_WM_CREATE(OnCreate)MSG_WM_PAINT(OnPaint)MSG_WM_DESTROY(OnDestroy)MSG_WM_WINDOWPOSCHANGED(OnWindowPosChanged)MSG_WM_NCCALCSIZE(OnNcCalcSize)MSG_WM_SHOWWINDOW(OnShowWindow)
KUIWIN_END_MSG_MAP_BASE()
......
#define KUIWIN_BEGIN_MSG_MAP() \
protected: \virtual BOOL ProcessWindowMessage( \HWND hWnd, UINT uMsg, WPARAM wParam, \LPARAM lParam, LRESULT& lResult) \{ \#define KUIWIN_END_MSG_MAP() \if (!IsMsgHandled()) \return __super::ProcessWindowMessage( \hWnd, uMsg, wParam, lParam, lResult); \return TRUE; \} \#define KUIWIN_END_MSG_MAP_BASE() \return TRUE; \} \
? ? ? ? 這個時候消息還是在容器窗口中處理,最終它會遍歷容器類所有子模塊,并調用子模塊的KuiSendMessage方法。
class CKuiDialog: public CKuiPanel
{KUIOBJ_DECLARE_CLASS_NAME(CKuiDialog, "dlg")
public:void OnWindowPosChanged(LPWINDOWPOS lpWndPos) {CKuiWindow::OnWindowPosChanged(lpWndPos);_RepositionChilds();}virtual void RepositionChild(CKuiWindow *pKuiWndChild) {......pKuiWndChild->KuiSendMessage(WM_WINDOWPOSCHANGED, NULL, (LPARAM)&WndPos);}
protected:void _RepositionChilds() {POSITION pos = m_lstWndChild.GetHeadPosition();while (pos != NULL) {CKuiWindow *pKuiWndChild = m_lstWndChild.GetNext(pos);RepositionChild(pKuiWndChild);}}
protected:KUIWIN_BEGIN_MSG_MAP()MSG_WM_WINDOWPOSCHANGED(OnWindowPosChanged)KUIWIN_END_MSG_MAP()
};? ? ? ? 各個繼承于CKuiWindow的類的ProcessWindowMessage方法將被調用,同時“消息”將被傳遞到處理WM_WINDOWPOSCHANGED的函數中。默認情況下,會調用CKuiWindow的
void OnWindowPosChanged(LPWINDOWPOS lpWndPos){m_rcWindow.MoveToXY(lpWndPos->x, lpWndPos->y);SIZE sizeRet = {lpWndPos->cx, lpWndPos->cy};KuiSendMessage(WM_NCCALCSIZE, TRUE, (LPARAM)&sizeRet);? ? ? ? 這個時候消息改成了WM_NCCALCSIZE,為什么要改成這個消息?因為這個消息在CKuiWindow的ProcessWindowMessage方法中不會被處理,從而將會被子類的方法處理,這樣就達到了“消息傳遞”的目的。以第一個需要被重繪的ICON為例
class KUILIB_API CKuiIconWnd : public CKuiWindow
{KUIOBJ_DECLARE_CLASS_NAME(CKuiIconWnd, "icon")......
LRESULT OnNcCalcSize(BOOL bCalcValidRects, LPARAM lParam){LPSIZE pSize = (LPSIZE)lParam;pSize->cx = m_nSize;pSize->cy = m_nSize;return TRUE;}? ? ? ? 相對于處理WM_SIZE消息,處理WM_PAINT消息則簡單的多:容器類直接調用模塊的重繪方法。
? ? ? ? 最后回到總體框架。Kui并沒有將這些容器類直接暴露在最外面,而實際通過一系列模板類實現功能
class CKuiDialogView: public CKuiDialogViewImpl<CKuiDialogView>
{
};template <class T, class TKuiView = CKuiDialogView, class TBase = CWindow, class TWinTraits = CControlWinTraits>
class ATL_NO_VTABLE CKuiDialogImpl : public CWindowImpl<T, TBase, TWinTraits>
{
protected:TKuiView m_richView;
}template <class T, class TKuiWin = CKuiDialog, class TBase = ATL::CWindow, class TWinTraits = CKuiDialogViewTraits>
class ATL_NO_VTABLE CKuiDialogViewImpl: public ATL::CWindowImpl<T, TBase, TWinTraits>, public CKuiViewImpl<T>
{
protected:TKuiWin m_kuiHeader;TKuiWin m_kuiBody;TKuiWin m_kuiFooter;
}? ? ? ? 至此,Kui界面庫主要的脈絡給理清了。對于一個完整的界面庫,我只是從一些我關心的角度去分析了其實現的大體步驟。其中很多細節處理雖然有待商榷,但是其中的精髓還是不少的。有興趣的同學可以在源碼中挖掘出自己感興趣的內容。最后附上類圖關系。
總結
以上是生活随笔為你收集整理的以金山界面库(openkui)为例思考和分析界面库的设计和实现——代码结构(完)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 服务器架设笔记——httpd插件支持my
- 下一篇: WMI技术介绍和应用——接收事件