在SOUI中已經提供了大部分常用的控件,但是內置控件不可能滿足用戶的所有要求,因此一個真實的應用少不得還要做一些自定義控件。
學習一個新東西,最簡單的辦法就是依葫蘆畫瓢。事實上在SOUI系統中內置控件和自定義控件的開發流程是完全一樣的,因此只需要打開SOUI的源代碼,隨便找一個控件看一下就大體差不多了。
下面我以controls.extend目錄下的的SRadioBox2控件為例對控件開發過程需要注意的地方做一點說明。
要開發一個控件,首先要確定的是應該從哪個控件來繼承。選擇一個合適的基類是正確開發自定義控件的前提。
之所以要開發一個SRadioBox2控件,我需要解決的問題很簡單:SRadioBox控件總是在左邊顯示一個圓圈,這個圓圈有時候不是我想要的。
因此我需要做的就是繼承SRadioBox控件的行為,重寫WM_PAINT的處理。
因此就有了下面的代碼:
class SRadioBox2 : public SRadioBox { public: SRadioBox2(void); ~SRadioBox2(void); }
需要注意的是,所有SOUI控件都是在namespace SOUI中,因此自定義控件也最好是放在SOUI這個namespace里。
有了上面的骨架,下面來逐步添加內容。
首先我們需要給自定義控件定義一個在XML中可以識別的標簽。
只需要在類的最開始增加一行:
class SRadioBox2 : public SRadioBox { SOUI_CLASS_NAME(SRadioBox2,L"radio2") public: SRadioBox2(void); ~SRadioBox2(void); }
SOUI_CLASS_NAME告訴XML解析器,碰到radio2時自動創建SRadioBox2對象。
實際上這一行更重要的作用是用來做對象類型運行時識別(RTTI),有了這個機制,在編譯器關閉C++的RTTI時仍然可以安全的進行類型轉換。
然后我們需要處理控件的WM_PAINT消息。
為了處理這個消息,我們需要加入消息映射表及消息處理函數:
class SRadioBox2 : public SRadioBox { SOUI_CLASS_NAME(SRadioBox2,L"radio2") public: SRadioBox2(void); ~SRadioBox2(void); protected: void OnPaint(IRenderTarget *pRT); SOUI_MSG_MAP_BEGIN() MSG_WM_PAINT_EX(OnPaint) SOUI_MSG_MAP_END() };
SOUI控件的消息處理機制是和WTL中抄過來的,和MFC也很相似。只是對於部分消息,由於對於消息的參數的解釋不一樣,消息映射的宏會有一點變化。
如這里的WM_PAINT消息,在SOUI里wparam傳遞的是一個IRenderTarget指針,而傳統的Win32傳遞的是一個HDC。因此我們需要使用MSG_WM_PAINT_EX代替WTL中使用的MSG_WM_PAINT。
大家可能會問有哪些消息映射宏SOUI和WLT不一樣?
實際上對於一個有經驗的程序員,他應該可以找到MSG_WM_PAINT_EX的宏定義,並且在定義附近就可以找到所有的SOUI和WLT不同的映射宏。
下面是到目前為止SOUI中所有和WTL不同的宏:
// BOOL OnEraseBkgnd(IRenderTarget * pRT) #define MSG_WM_ERASEBKGND_EX(func) \ if (uMsg == WM_ERASEBKGND) \ { \ SetMsgHandled(TRUE); \ lResult = (LRESULT)func((IRenderTarget *)wParam); \ if(IsMsgHandled()) \ return TRUE; \ } // void OnPaint(IRenderTarget * pRT) #define MSG_WM_PAINT_EX(func) \ if (uMsg == WM_PAINT) \ { \ SetMsgHandled(TRUE); \ func((IRenderTarget *)wParam); \ lResult = 0; \ if(IsMsgHandled()) \ return TRUE; \ } // void OnNcPaint(IRenderTarget * pRT) #define MSG_WM_NCPAINT_EX(func) \ if (uMsg == WM_NCPAINT) \ { \ SetMsgHandled(TRUE); \ func((IRenderTarget *)wParam); \ lResult = 0; \ if(IsMsgHandled()) \ return TRUE; \ } // void OnSetFont(IFont *pFont, BOOL bRedraw) #define MSG_WM_SETFONT_EX(func) \ if (uMsg == WM_SETFONT) \ { \ SetMsgHandled(TRUE); \ func((IFont*)wParam, (BOOL)LOWORD(lParam)); \ lResult = 0; \ if(IsMsgHandled()) \ return TRUE; \ } // void OnSetFocus() #define MSG_WM_SETFOCUS_EX(func) \ if (uMsg == WM_SETFOCUS) \ { \ SetMsgHandled(TRUE); \ func(); \ lResult = 0; \ if(IsMsgHandled()) \ return TRUE; \ } // void OnKillFocus() #define MSG_WM_KILLFOCUS_EX(func) \ if (uMsg == WM_KILLFOCUS) \ { \ SetMsgHandled(TRUE); \ func(); \ lResult = 0; \ if(IsMsgHandled()) \ return TRUE; \ } // void OnNcMouseHover(int nFlag,CPoint pt) #define MSG_WM_NCMOUSEHOVER(func) \ if(uMsg==WM_NCMOUSEHOVER)\ {\ SetMsgHandled(TRUE); \ func(wParam,CPoint(GET_X_LPARAM(lParam),GET_Y_LPARAM(lParam))); \ lResult = 0; \ if(IsMsgHandled()) \ return TRUE; \ } // void OnNcMouseLeave() #define MSG_WM_NCMOUSELEAVE(func) \ if(uMsg==WM_NCMOUSELEAVE)\ {\ SetMsgHandled(TRUE); \ func(); \ lResult = 0; \ if(IsMsgHandled()) \ return TRUE; \ } // void OnTimer(char cTimerID) #define MSG_WM_TIMER_EX(func) \ if (uMsg == WM_TIMER) \ { \ SetMsgHandled(TRUE); \ func((char)wParam); \ lResult = 0; \ if(IsMsgHandled()) \ return TRUE; \ } #define WM_TIMER2 (WM_USER+5432) //定義一個與HWND定時器兼容的SOUI定時器 #define MSG_WM_TIMER2(func) \ if (uMsg == WM_TIMER2) \ { \ SetMsgHandled(TRUE); \ func(wParam); \ lResult = 0; \ if(IsMsgHandled()) \ return TRUE; \ }
通常情況下,自定義控件還需要處理一些自定義的屬性,這時我需要還需要增加一個屬性映射表(如SGifPlayer):
SOUI_ATTRS_BEGIN() ATTR_CUSTOM(L"skin", OnAttrSkin) //為控件提供一個skin屬性,用來接收SSkinObj對象的name SOUI_ATTRS_END()
完成上面幾步,一個自定義控件基本上就完成了。
可能還需要實現幾個修改基類行為的虛函數。