使用SOUI開發客戶端UI程序,通常也推薦使用XML代碼來創建窗口,這樣創建的窗口使用方便,當窗口大小改變時,內部的子窗口也更容易協同變化。
但是最近不斷有網友咨詢如何使用代碼來創建SOUI子窗口,特此在這里統一解答。
要回答這個問題,首先要了解SOUI窗口創建及布局的流程。
先從swnd.cpp里抄一段創建子窗口的代碼:
1 BOOL SWindow::CreateChildren(pugi::xml_node xmlNode) 2 { 3 TestMainThread(); 4 for (pugi::xml_node xmlChild=xmlNode.first_child(); xmlChild; xmlChild=xmlChild.next_sibling()) 5 { 6 if(xmlChild.type() != pugi::node_element) continue; 7 8 if(_wcsicmp(xmlChild.name(),KLabelInclude)==0) 9 {//在窗口布局中支持include標簽 10 SStringW strSrc = S_CW2T(xmlChild.attribute(L"src").value()); 11 pugi::xml_document xmlDoc; 12 SStringTList strLst; 13 14 if(2 == ParseResID(strSrc,strLst)) 15 { 16 LOADXML(xmlDoc,strLst[1],strLst[0]); 17 }else 18 { 19 LOADXML(xmlDoc,strLst[0],RT_LAYOUT); 20 } 21 if(xmlDoc) 22 { 23 CreateChildren(xmlDoc.child(KLabelInclude)); 24 }else 25 { 26 SASSERT(FALSE); 27 } 28 }else if(!xmlChild.get_userdata())//通過userdata來標記一個節點是否可以忽略 29 { 30 SWindow *pChild = SApplication::getSingleton().CreateWindowByName(xmlChild.name()); 31 if(pChild) 32 { 33 InsertChild(pChild); 34 pChild->InitFromXml(xmlChild); 35 } 36 } 37 } 38 return TRUE; 39 }
這個函數的功能是為this從XML中創建它的子窗口,主要注意代碼中紅色部分。
其中第30行是從SOUI的窗口類廠根據XML結點名new出一個窗口對象。
第33行把創建出來的窗口插入到this的子窗口鏈表里去。
而第34行的功能是從子窗口對應的XML結點初始化子窗口屬性。
很多網友以為只要完成上面步驟就應該可以顯示窗口了,但結果確大失所望。
SOUI一個重要特點就是能夠自動布局,這個過程的秘密就在於SWindow::OnRelayout方法。
1 void SWindow::OnRelayout(const CRect &rcOld, const CRect & rcNew) 2 { 3 SWindow *pParent= GetParent(); 4 if(pParent) 5 { 6 pParent->InvalidateRect(rcOld); 7 pParent->InvalidateRect(rcNew); 8 }else 9 { 10 InvalidateRect(m_rcWindow); 11 } 12 13 SSendMessage(WM_NCCALCSIZE);//計算非客戶區大小 14 15 UpdateChildrenPosition(); //更新子窗口位置 16 17 CRect rcClient; 18 GetClientRect(&rcClient); 19 SSendMessage(WM_SIZE,0,MAKELPARAM(rcClient.Width(),rcClient.Height())); 20 }
當this窗口位置改變后都會進入OnRelayout方法。
注意函數第15行:UpdateChildrenPosition();這個調用的功能就是將this的所有子控件按照控件中定義的布局屬性來自動布局。
要實現窗口的布局,除了調用父窗口的UpdateChildrenPosition()方法外,當然也可以使用SWindow::Move方法直接修改窗口位置。
看到這里大家應該已經明白是什么原因了。
當然上述方法是SOUI中使用的窗口創建及布局方法,具體到應用程序中使用代碼創建控件還有一個地方需要注意。
關鍵的問題是在SOUI系統中編譯默認使用MT(靜態鏈接)方式來鏈接CRT。
MT方式編譯時使用CRT分配內存是內存是屬性調用的模塊(DLL)的,內存的釋放也因此必須在該模塊內執行。
如果用戶直接使用new來分配一個窗口對象,並把它插入到SOUI窗口鏈表中,在窗口釋放的時機是在SWindow::OnFinalRelease()中(實際是基類TObjRefImpl2<IObjRef,SWindow>的方法)。
SWindow的代碼段是編譯在soui.dll中,因此默認執行內存釋放的代碼是在soui.dll中,從而導致程序崩潰。
要解決這個問題有兩種方法:
對於系統控件,用戶應該使用SApplication::getSingleton().CreateWindowByName(xmlChild.name());來創建窗口對象。
而對於用戶自己派生實現的擴展窗口類並沒有向SOUI的窗口類廠注冊時,只能使用new方法來創建窗口對象。注意SWindow的基類:TObjRefImpl2<IObjRef,SWindow>
1 template<class T> 2 class TObjRefImpl : public T 3 { 4 public: 5 TObjRefImpl():m_cRef(1) 6 { 7 } 8 9 virtual ~TObjRefImpl(){ 10 } 11 12 //!添加引用 13 /*! 14 */ 15 virtual long AddRef() 16 { 17 return InterlockedIncrement(&m_cRef); 18 } 19 20 //!釋放引用 21 /*! 22 */ 23 virtual long Release() 24 { 25 long lRet = InterlockedDecrement(&m_cRef); 26 if(lRet==0) 27 { 28 OnFinalRelease(); 29 } 30 return lRet; 31 } 32 33 //!釋放對象 34 /*! 35 */ 36 virtual void OnFinalRelease() 37 { 38 delete this; 39 } 40 protected: 41 volatile LONG m_cRef; 42 }; 43 44 template<class T,class T2> 45 class TObjRefImpl2 : public TObjRefImpl<T> 46 { 47 public: 48 virtual void OnFinalRelease() 49 { 50 delete static_cast<T2*>(this); 51 } 52 };
注意代碼中的OnFinalRelease,它是一個虛方法。因此對於使用new創建的窗口對象,只需要在窗口類中抄一段代碼如下即可:
1 class myctrl : public SWindow 2 { 3 SOUI_CLASS_NAME(myctrl,L"myctrl") 4 public: 5 //... 6 virtual void OnFinalRelease() {delete this;} 7 //... 8 };
感謝大家的支持!