WIN32介面開發之三:DUI雛形開發(一)
前言:這部分涉及工程比較大,所以我打算分開為兩篇來寫,第一篇完成基本框架的構建,第二篇新增上EVENT和NOTIFY機制。
完成目標:仿照DirectUI,完成一個基本雛形,開發一個佈局控制元件(Dialog),和一個按鈕控制元件(Button),通過XML來佈局窗體,最後按鈕響應點選、滑鼠移動等事件資訊,使用者還可以通過NOTIFY機制來定製,使用者具體行為時,介面所要做的動作。給大家看下最終介面吧,一個背景和四個按鈕。
正常狀態:
點選按鈕狀態(點選按鈕時,並拖動窗體)
正文
一、CDialogBuilder的構建
1、先看下我們佈局的XML
2、兩個檔案:UIMarkup.h和UIMarkup.cpp"<XML>" "<Dialog bk=\"C:\\bg.png\" pos=\"0 0 390 212\">" "<Canvas pos=\"0 0 100 212\">" "<Button pos=\"0 0 60 60\" />" "<Button pos=\"70 70 100 100\" />" "</Canvas>" "<Canvas pos=\"100 0 300 212\">" "<Button pos=\"120 20 160 60\" />" "<Button pos=\"170 170 200 200\" />" "</Canvas>" "</Dialog>" "</XML>"
這兩個檔案裡主要完成的功能就是載入XML,並且對XML對容進行分析,構建出結點樹,具體程式碼就不講了,只要知道所完成的功能就可以了,大家有興趣可以看看,
但有幾個介面,要注意一下:
3、CDialogBuilder講解CMarkup::Load(LPCTSTR pstrXML) //載入並分析XML,構建XML結點樹 CMarkup::GetRoot() //獲取XML樹的根結點 CMarkupNode CMarkupNode::GetParent();//獲取父結點 CMarkupNode CMarkupNode::GetSibling();//獲取下一緊臨的兄弟結點 CMarkupNode CMarkupNode::GetChild(); //獲取孩子結點 CMarkupNode CMarkupNode::GetChild(LPCTSTR pstrName);//根據名字獲取孩子結點 bool CMarkupNode::HasAttributes(); bool CMarkupNode::HasAttribute(LPCTSTR pstrName); //指定結點是否具有屬性 int CMarkupNode::GetAttributeCount(); //具有的屬性個數 LPCTSTR CMarkupNode::GetAttributeName(int iIndex);//根據索引獲取屬性名 LPCTSTR CMarkupNode::GetAttributeValue(int iIndex); LPCTSTR CMarkupNode::GetAttributeValue(LPCTSTR pstrName); bool CMarkupNode::GetAttributeValue(int iIndex, LPTSTR pstrValue, SIZE_T cchMax); bool CMarkupNode::GetAttributeValue(LPCTSTR pstrName, LPTSTR pstrValue, SIZE_T cchMax);//獲取對應屬性名的屬性的值的幾個函式
完成功能:
1、利用上面的CMarkUp類分析XML,構建出XML結點樹
2、然後根據XML中的結點,NEW 出控制元件,並利用XML結點樹的屬性,構建出控制元件的屬性
先看定義:
可以看到,CDialogBuilder比較簡單,只有兩個函式,Create()和_Parse(),另一個變數m_xml是用來分析XML,並構建XML結點樹的。class CDialogBuilder { public: CDialogBuilder(); ~CDialogBuilder(); public: CControlUI* Create(LPCTSTR pstrXML); private: CControlUI* _Parse(CMarkupNode* parent, CControlUI* pParent = NULL); CMarkup m_xml; };
看下Create函式
CControlUI* CDialogBuilder::Create(LPCTSTR pstrXML)
{
if( !m_xml.Load(pstrXML) ) return NULL;//載入XML,並生成XML結點樹
// NOTE: The root element is actually discarded since the _Parse() methods is
// parsing children and attaching to the current node.
CMarkupNode root = m_xml.GetRoot();//獲取XML結點樹的根結點
return _Parse(&root);//分析結點樹
}
下面看看_Parse函式完成的功能:CControlUI* CDialogBuilder::_Parse(CMarkupNode* pRoot, CControlUI* pParent)
{
IContainerUI* pContainer = NULL;
CControlUI* pReturn = NULL;
for( CMarkupNode node = pRoot->GetChild() ; node.IsValid(); node = node.GetSibling() ) {
LPCTSTR pstrClass = node.GetName();
SIZE_T cchLen = _tcslen(pstrClass);
CControlUI* pControl = NULL;
switch( cchLen ) {
case 6:
if( _tcscmp(pstrClass, _T("Canvas")) == 0 ) pControl = new CContainerUI;
else if( _tcscmp(pstrClass, _T("Button")) == 0 ) pControl = new CButtonUI;
else if ( _tcscmp(pstrClass, _T("Dialog")) == 0) pControl=new CDialogUI;
break;
}/////根據XML樹,生成對應的控制元件
ASSERT(pControl);
if( pControl == NULL ) return NULL;
// Add children
if( node.HasChildren() ) {
_Parse(&node, pControl);//利用遞規,遍歷XML樹
}
// Attach to parent
if( pParent != NULL ) {//如果它的父親不為空,說明它的父結點肯定具有CONTAINER屬性,所以把它加到它的父結點的CONTAINER中
if( pContainer == NULL ) pContainer = static_cast<IContainerUI*>(pParent->GetInterface(_T("Container")));
ASSERT(pContainer);
if( pContainer == NULL ) return NULL;
pContainer->Add(pControl);
}
// Process attributes
if( node.HasAttributes() ) {//分析屬性,然後設定控制元件屬性
TCHAR szValue[500] = { 0 };
SIZE_T cchLen = lengthof(szValue) - 1;
// Set ordinary attributes
int nAttributes = node.GetAttributeCount();
for( int i = 0; i < nAttributes; i++ ) {
pControl->SetAttribute(node.GetAttributeName(i), node.GetAttributeValue(i));
}
}
// 返回根結點,對於我們的XML,返回的是CDialogUI的例項指標
if( pReturn == NULL ) pReturn = pControl;
}
return pReturn;
}
二、控制元件與容器的構建
一、總體關係
這裡我們先講下,控制元件類和佈局類的區別,在這裡我們在構建幾個類,先看下繼承圖(這篇文章先不管INotifyUI的事)
1、一個控制元件基類(CControlUI)和一個按鈕控制元件類(CButtonUI)
控制元件基類,包含有控制元件繪製的最基本的幾個函式,當然都是虛擬函式,比如:SetAttribute(設定控制元件屬性),GetInterface(獲取控制元件的this指標),DoPaint(繪圖)
2、一個容器基類,故名思義,容器是用來盛控制元件的,所以它必須具有的幾個函式:
Add(將控制元件新增到此容器的子佇列中),Remove(刪除某個子控制元件)、RemoveAll(清空子控制元件佇列)、GetItem(獲取某個子控制元件)、GetCount(獲取子控制元件的個數)
所以這幾個函式也是容器最基類IContainerUI的函式,
3、CContainerUI,這個才是稍微有意義點的佈局類,它用於在窗體上劃分出不同的區域,然後在區域中佈局控制元件.這裡最值得注意的地方是派生出它的基類,它是兩個基類的派生類(IContainerUI和CControlUI),所以容器也是控制元件,這一點在後面繪圖程式碼中特點重要,在繪圖時,我們將所有的容器和控制元件全部都定義為CControlUI,所以如果當前的如果是CDialogUI的指標的話,它就會根據繼承關係,先到CDialogUI中執行相關函式,這點尤其注意!!!!(這裡暫時搞不明白也沒關係,後面遇到的時候,我會重新講)
4、CDialogUI,這是窗體的構建類,裡面包含所在構建窗體的大小、背景圖案,現在只加了這兩個屬性,其它都沒加
二、控制元件類的實現
1、CContolUI的實現
class CControlUI
{
public:
CControlUI();
virtual ~CControlUI();
public:
virtual void SetAttribute(LPCTSTR pstrName, LPCTSTR pstrValue);//設定屬性
virtual LPVOID GetInterface(LPCTSTR pstrName);//獲取當前control的this指標
virtual void DoPaint(HWND hwnd,HDC hDC)=0;//控制元件的繪製函式,注意這裡把它宣告為純虛擬函式,強制子類中必須對其進行實現
//設定控制元件屬性的幾個函式
void SetPos(RECT rc);///設定控制元件位置
RECT GetPos();
virtual CControlUI* GetParent();//設定當前控制元件的父結點
virtual void SetParent(CControlUI* parent);
virtual void SetHwnd(HWND hwnd);//設定窗體控制代碼
public:
void Invalidate();
protected:
RECT m_RectItem;///控制元件位置
CControlUI *m_pParent;//父結點指標
HWND m_hwnd;//儲存傳過來的窗體的控制代碼
};
這裡大家可能會有個疑問,控制元件要窗體控制代碼幹嘛,這裡呢,因為我們只有一個窗體,也就是CreateWindowEx函式建立後,返回的那個HWND,我們這裡儲存的就是這個HWND,那我們要它有什麼用呢,這是因為我們這裡的控制元件全部都是模擬的控制元件的行為而已,是並沒有控制代碼的,所以也不可能像MFC那樣根據控制元件的控制代碼單獨重新整理了,所以我們要重新整理控制元件的話,就得重新整理整個窗體(當然,我們會利用快取技術區域性繪製來提高效率),我們要重新整理窗體就得利用SendMessage(hwnd,msg,wparam,lparam),注意這裡的第一個變數就是窗體的控制代碼,當然這裡要儲存了,就這樣的道理。
看各個函式的具體實現
void CControlUI::SetPos(RECT rc)
{
m_RectItem = rc;
}
RECT CControlUI::GetPos()
{
return m_RectItem;
}
這倆函式很簡單,就是設定控制元件位置和獲取控制元件位置的。void CControlUI::SetAttribute(LPCTSTR pstrName, LPCTSTR pstrValue)
{
if( _tcscmp(pstrName, _T("pos")) == 0 ) {
RECT rcPos = { 0 };
LPTSTR pstr = NULL;
rcPos.left = _tcstol(pstrValue, &pstr, 10); ASSERT(pstr);
rcPos.top = _tcstol(pstr + 1, &pstr, 10); ASSERT(pstr);
rcPos.right = _tcstol(pstr + 1, &pstr, 10); ASSERT(pstr);
rcPos.bottom = _tcstol(pstr + 1, &pstr, 10); ASSERT(pstr);
SetPos(rcPos);
}
}
這個函式是設定控制元件屬性,哪裡用到這個函數了呢,嘿嘿,還記得不,CDialogBuilder的_Parse函式裡void CControlUI::SetParent(CControlUI* parent)
{
m_pParent=parent;
}
CControlUI* CControlUI::GetParent()
{
return m_pParent;
}
設定父結點,也是在CDialogBuilder的_Parse函式裡用到LPVOID CControlUI::GetInterface(LPCTSTR pstrName)
{
if( _tcscmp(pstrName, _T("Control")) == 0 ) return this;
return NULL;
}
根據要獲取的指標名,返回當前例項的this指標,哪裡用到了呢,也是CDialogBuilder的_Parse函式裡,下面是_Parse裡的,我摘過來 if( pParent != NULL ) {
if( pContainer == NULL ) pContainer = static_cast<IContainerUI*>(pParent->GetInterface(_T("Container")));
ASSERT(pContainer);
if( pContainer == NULL ) return NULL;
pContainer->Add(pControl);
}
看到了吧,獲取的是父結點的Container的指標,然後將控制元件加到父結點中,下面繼續void CControlUI::SetHwnd(HWND hwnd)
{
m_hwnd=hwnd;
}
void CControlUI::Invalidate()
{
SendMessage(m_hwnd,WM_PAINT,NULL,NULL);
}
SetHwnd用來儲存窗體的HWND,然後是Invalidate函式的實現,也就是向窗體傳送WM_PAINT訊息2、CButtionUI的實現
定義:
class CButtonUI:public CControlUI
{
public:
CButtonUI();
~CButtonUI();
public:
virtual void SetAttribute(LPCTSTR pstrName, LPCTSTR pstrValue);
virtual LPVOID GetInterface(LPCTSTR pstrName);
virtual void DoPaint(HWND hwnd,HDC hDC);
};
可以看到,很簡單,設定屬性、獲取THIS指標,然後就是繪圖類,看具體實現void CButtonUI::SetAttribute(LPCTSTR pstrName, LPCTSTR pstrValue)
{//因為實現的是最簡易版,Button裡也就不設定什麼獨有的屬性了,直接返回基類的SetAttribute(),設定Pos屬性
return CControlUI::SetAttribute(pstrName,pstrValue);
}
LPVOID CButtonUI::GetInterface(LPCTSTR pstrName)
{
if( _tcscmp(pstrName, _T("Button")) == 0 ) return this;
return CControlUI::GetInterface(pstrName);
}
void CButtonUI::DoPaint(HWND hwnd,HDC hDC)
{
assert(hDC);
Graphics graph(hDC);
graph.FillRectangle(&SolidBrush(Color::Green),m_RectItem.left,m_RectItem.top,m_RectItem.right-m_RectItem.left,m_RectItem.bottom-m_RectItem.top);
graph.ReleaseHDC(hDC);
}
這裡要說明的一點就是,DoPaint()函式,因為我們這篇文章裡面還沒有新增EVENT事件通知功能,所以我們就先初始化的時候,把按鈕統一畫成綠色。三、容器類的實現
1、容器最基類IContainerUI的實現
class IContainerUI
{
public:
virtual CControlUI* GetItem(int iIndex) const = 0;
virtual int GetCount() const = 0;
virtual bool Add(CControlUI* pControl) = 0;
virtual bool Remove(CControlUI* pControl) = 0;
virtual void RemoveAll() = 0;
};
其實這個類沒有任何的實際意義,就是個介面類,它的這幾個函式全部都宣告為純虛擬函式2、CContainerUI的實現
定義:
class CContainerUI : public CControlUI, public IContainerUI
{
public:
CContainerUI();
virtual ~CContainerUI();
public:
///先實現繼承的純虛擬函式
virtual CControlUI* GetItem(int iIndex) const;
virtual int GetCount() const;
virtual bool Add(CControlUI* pControl);
virtual bool Remove(CControlUI* pControl);
virtual void RemoveAll();
public:
void SetAttribute(LPCTSTR pstrName, LPCTSTR pstrValue);///設定屬性
virtual LPVOID GetInterface(LPCTSTR pstrName);//獲取THIS指標
virtual void DoPaint(HWND hwnd,HDC hDC);//繪圖函式
void LoadBackground(LPCTSTR Parth);//根據路徑載入影象,並儲存在m_pImage中
protected:
CStdPtrArray m_items; //當前容器中的子變數佇列
Gdiplus::Image* m_pImage;//容器的背景
};
大家看到了吧,這裡除了實現IContainerUI幾個純虛擬函式以外,其它幾個函式SetAttribute、GetInterface、DoPaint也都是控制元件所具有的。看下具體實現:CControlUI* CContainerUI::GetItem(int iIndex) const
{
if( iIndex < 0 || iIndex >= m_items.GetSize() ) return NULL;
return static_cast<CControlUI*>(m_items[iIndex]);
}
int CContainerUI::GetCount() const
{
return m_items.GetSize();
}
bool CContainerUI::Add(CControlUI* pControl)
{
return m_items.Add(pControl);
}
bool CContainerUI::Remove(CControlUI* pControl)
{
for( int it = 0;it < m_items.GetSize(); it++ ) {
if( static_cast<CControlUI*>(m_items[it]) == pControl ) {
delete pControl;
return m_items.Remove(it);
}
}
return false;
}
void CContainerUI::RemoveAll()
{
for( int it = 0;it < m_items.GetSize(); it++ ) delete static_cast<CControlUI*>(m_items[it]);
m_items.Empty();
}
這幾個就不講了,就是向佇列裡新增、刪除變數的操作。LPVOID CContainerUI::GetInterface(LPCTSTR pstrName)
{
if( _tcscmp(pstrName, _T("Container")) == 0 ) return static_cast<IContainerUI*>(this);
return CControlUI::GetInterface(pstrName);
}
獲取this指標void CContainerUI::LoadBackground(LPCTSTR Parth)
{
m_pImage = Gdiplus::Image::FromFile(Parth);
}
void CContainerUI::SetAttribute(LPCTSTR pstrName, LPCTSTR pstrValue)
{
if( _tcscmp(pstrName, _T("bk")) == 0 ) LoadBackground(pstrValue);
else CControlUI::SetAttribute(pstrName, pstrValue);
}
設定屬性,這裡只在CControlUI的基礎上增加了一個屬性,bk-----背景void CContainerUI::DoPaint(HWND hwnd,HDC hDC)
{
for( int it = 0; it < m_items.GetSize(); it++ ) {
CControlUI* pControl = static_cast<CControlUI*>(m_items[it]);
pControl->DoPaint(hwnd,hDC);
}
}
最重要的,繪製函式,我這裡沒有對它本身進行繪製,只是遍歷當前容器中所有的控制元件,然後再它裡面的控制元件進行逐個繪製,這裡我要留下一個疑問,如果容器裡巢狀有容器是怎麼完成繪製的呢????!!!!3、CDialogUI實現
定義:
class CDialogUI:public CContainerUI
{
public:
CDialogUI();
~CDialogUI();
public:
void DoPaintBackground(HDC hdc);//畫背景
virtual LPVOID GetInterface(LPCTSTR pstrName);//獲取this指標
};
大家可以看到這個CDialogUI的實現是非常簡單的,只有一個DoPaintBackground(),這個函式只是用來畫背景的
實現:
void CDialogUI::DoPaintBackground(HDC hdc)
{
Gdiplus::Graphics graph(hdc);
graph.SetSmoothingMode(Gdiplus::SmoothingModeNone);
graph.DrawImage(m_pImage, 0, 0, m_RectItem.right-m_RectItem.left, m_RectItem.bottom-m_RectItem.top);
graph.ReleaseHDC(hdc);
}
LPVOID CDialogUI::GetInterface(LPCTSTR pstrName)
{
if( _tcscmp(pstrName, _T("Dialog")) == 0 ) return static_cast<IContainerUI*>(this);
return CContainerUI::GetInterface(pstrName);
}
好了,到這幾個控制元件的實現就全部實現了,下面看窗體的建立。
三、窗體建立
基本思想,我們為了讓使用者不必做太多的工作,我們建一個基類,這個基類完成視窗類註冊、建立視窗、訊息響應等功能,而使用者只需要從我們的基類派生,並且簡單呼叫Create()函式就可以了。一、建立窗體基類(CWindowWnd)
定義
class CWindowWnd{
public:
CWindowWnd();
~CWindowWnd();
HWND GetHWND() const{return m_hWnd;}
bool RegisterWindowClass(WNDPROC fWndProc,HINSTANCE hInstance,LPCTSTR szClassName);//註冊視窗類
HWND Create();//建立窗體
void ShowWindow(bool bShow = true, bool bTakeFocus = true);
void SetInstance(HINSTANCE instance){m_instance=instance;}
protected:
virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
static LRESULT CALLBACK __WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
protected:
HWND m_hWnd;// 儲存所建立窗體的控制代碼
HINSTANCE m_instance;//儲存WinMain入口,引數裡的hInstance,因為註冊視窗類要用到
};
注意一點:這裡必須把視窗訊息處理函式(_WndProc)定義為靜態函式,否則就定義為全域性函式我們看下具體實現,由簡到難講:
bool CWindowWnd::RegisterWindowClass(WNDPROC fWndProc,HINSTANCE hInstance,LPCTSTR szClassName)
{
WNDCLASSEX wce={0};
wce.cbSize=sizeof(wce);
wce.style=CS_HREDRAW|CS_VREDRAW;
wce.lpfnWndProc=fWndProc;
wce.cbClsExtra=0;
wce.cbWndExtra=0;
wce.hInstance=hInstance;
wce.hIcon=NULL;
wce.hCursor=LoadCursor(NULL,IDC_ARROW);
wce.hbrBackground=(HBRUSH)(6);//(HBRUSH)(COLOR_WINDOW+1);
wce.lpszMenuName=NULL;
wce.lpszClassName=szClassName;
wce.hIconSm=NULL;
ATOM nAtom=RegisterClassEx(&wce);
if(nAtom==0) return false;
return true;
}
注冊視窗類,沒什麼好講的了,標準流程。不過這裡要注意一下,fWndProc是訊息處理函式,這個函式必須是靜態的或是全域性的!!!!!void CWindowWnd::ShowWindow(bool bShow /*= true*/, bool bTakeFocus /*= false*/)
{
ASSERT(::IsWindow(m_hWnd));
if( !::IsWindow(m_hWnd) ) return;
::ShowWindow(m_hWnd, bShow ? (bTakeFocus ? SW_SHOWNORMAL : SW_SHOWNOACTIVATE) : SW_HIDE);
}
顯示視窗,呼叫系統API ----ShowWindow()HWND CWindowWnd::Create()
{
if (!RegisterWindowClass(__WndProc,m_instance,L"transparent"))
{//註冊視窗類,看到了吧,視窗處理函式的函式名為:_WndProc,後面講
assert(L"註冊視窗失敗");
}
assert(this);
m_hWnd = ::CreateWindowEx(WS_EX_LAYERED, L"transparent", _T(""),WS_POPUP|WS_MAXIMIZE|WS_MINIMIZE|WS_SYSMENU,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, (HINSTANCE)::GetModuleHandle(NULL),(LPVOID)this);
if(m_hWnd == NULL || !::IsWindow(m_hWnd))
return NULL;
return m_hWnd;
}
建立視窗,流程是,先註冊視窗類,如果註冊成功,就呼叫CreateWindowEx建立視窗。這裡最注意的一個地方,單獨把CreateWindowEx調出來m_hWnd = ::CreateWindowEx(WS_EX_LAYERED, L"transparent", _T(""),WS_POPUP|WS_MAXIMIZE|WS_MINIMIZE|WS_SYSMENU,CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, (HINSTANCE)::GetModuleHandle(NULL),(LPVOID)this);
注意最後一個引數:傳進去的當前CWindowWnd的this指標!!!!這個引數會做為lparam傳進CREATESTRUCT結構體裡的lpCreateParams引數裡,在WM_NCCREATE訊息裡可以獲取到。那傳進去他有什麼用呢?往下看,_WndProc訊息處理函式LRESULT CWindowWnd::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
return ::CallWindowProc(::DefWindowProc, m_hWnd, uMsg, wParam, lParam);
}
LRESULT CALLBACK CWindowWnd::__WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CWindowWnd* pThis = NULL;
if( uMsg == WM_NCCREATE ) {
LPCREATESTRUCT lpcs = reinterpret_cast<LPCREATESTRUCT>(lParam);
pThis = static_cast<CWindowWnd*>(lpcs->lpCreateParams);
pThis->m_hWnd = hWnd;
::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LPARAM>(pThis));
}
else {
pThis = reinterpret_cast<CWindowWnd*>(::GetWindowLongPtr(hWnd, GWLP_USERDATA));
if( uMsg == WM_NCDESTROY && pThis != NULL ) {
::SetWindowLongPtr(pThis->m_hWnd, GWLP_USERDATA, 0L);
pThis->m_hWnd = NULL;
return true;
}
}
if( pThis != NULL ) {
return pThis->HandleMessage(uMsg, wParam, lParam);
}
else {
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
注意在遇到WM_NCCREATE訊息時,首先將lParam強轉成LPCREATESTRUCT 變數,我們上面說過我們傳過來的CWindowWnd的this指標,儲存在CREATESTRUCT結構體的lpCreateParams引數裡
pThis = static_cast<CWindowWnd*>(lpcs->lpCreateParams);
這句,就是取出this指標!,有同學可能會問,為什麼非要這樣得到this指標,_WndProc不是CWindowWnd的成員函式麼,不是直接有this麼?注意,_WndProc是靜態函式,會在CWindowWnd建立之前編譯,所以,由於在編譯_WndProc時,沒有CWindowWnd還沒有被例項化,當然要報錯了。所以我們要通過傳參的方式得到this指標,那接下來的問題就是,那下次我怎麼再在這個函式裡得到this指標呢,這裡我們用SetWindowLongPtr把他儲存在GWLP_USERDATA域,然後呼叫GetWindowLongPtr就可以得到了。
::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LPARAM>(pThis));
這句的意思就是把THIS指標儲存在GWLP_USERDATA域。
在其它訊息到來的是,我們先通過GetWindowLongPtr得到THIS指標,然後呼叫虛擬函式pThis->HandleMessage(uMsg, wParam, lParam);把訊息轉到HandleMessage函式中處理,而使用者的函式是派生自CWindowWnd的,所以只需要在HandleMessage函式中處理訊息就可以了。
二、使用者視窗類
class CStartPage: public CWindowWnd
{
public:
CStartPage();
~CStartPage();
public:
virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam);
LPCTSTR GetDialogResource();//傳進去XML字串
void Paint(HWND m_hWnd);//響應WM_PAINT訊息的函式
private:
WCHAR m_resource[556];//XML字串
CDialogBuilder m_dialogBuilder;
CControlUI *m_root;
HDC hdcBKMemory;//記憶體DC,參見《之二----GDI+中的區域性刷新技術》
HBITMAP hBKBitmap;
HGDIOBJ hBKBitmapOld;
};
看下具體實現:LPCTSTR CStartPage::GetDialogResource()
{
char temp[]=
"<XML>"
"<Dialog bk=\"C:\\bg.png\" pos=\"0 0 390 212\">"
"<Canvas pos=\"0 0 100 212\">"
"<Button pos=\"0 0 60 60\" />"
"<Button pos=\"70 70 100 100\" />"
"</Canvas>"
"<Canvas pos=\"100 0 300 212\">" //一定要給canvas加上POS,因為我們根據滑鼠點來查詢時,首先看是否在CANVAS的區域內,如果不在,就直接返回NULL了
"<Button pos=\"120 20 160 60\" />"
"<Button pos=\"170 170 200 200\" />"
"</Canvas>"
"</Dialog>"
"</XML>";
int iStrlen=sizeof(temp);
MultiByteToWideChar(CP_ACP,0,(LPCSTR)(temp),iStrlen,m_resource,iStrlen);
return m_resource;
}
這個函式很簡單,就是載入XML,並儲存在m_resource字串中LRESULT CStartPage::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg){
case WM_DESTROY:
{
PostQuitMessage(100);
::SelectObject( hdcBKMemory, hBKBitmapOld); //不要把預設的點陣圖選回來,如果選回來的話,我們新建的點陣圖就被替換掉了,當然我們上面畫的東東也就沒有了
::DeleteObject(hBKBitmapOld);//這三個在清除的時候,一塊清除
::DeleteObject(hBKBitmap); //先不要刪除,先儲存起來,後面再跟hmdmDC一起刪除
::DeleteDC(hdcBKMemory);
}
break;
case WM_CREATE:
{
m_root=m_dialogBuilder.Create(GetDialogResource());///傳入XML文件,讓其分析
SendMessage(GetHWND(),WM_PAINT,NULL,NULL);
}
break;
case WM_PAINT:
{
Paint(GetHWND());
}
break;
}
return CWindowWnd::HandleMessage(uMsg, wParam, lParam);
注意兩個地方,在CREATE的時候,傳入XML,然後傳送WM_PAIT訊息。然後在WM_PAIT中,Paint函式繪圖void CStartPage::Paint(HWND m_hWnd)
{
RECT rcWindow;
GetWindowRect(m_hWnd,&rcWindow);
SIZE sizeWindow;
/////因為根結點,必是DLG結點,肯定是個容器,所以將它強制轉換成CContainerUI*,定位到
CDialogUI *pdlg=(CDialogUI*)m_root;
RECT Pos=pdlg->GetPos();
sizeWindow.cx=Pos.right-Pos.left;
sizeWindow.cy=Pos.bottom-Pos.top;
HDC hDC = ::GetDC(m_hWnd);
if (hdcBKMemory==NULL)
{
hdcBKMemory = CreateCompatibleDC(hDC);
//建立背景畫布
BITMAPINFOHEADER stBmpInfoHeader = { 0 };
int nBytesPerLine = ((sizeWindow.cx * 32 + 31) & (~31)) >> 3;
stBmpInfoHeader.biSize = sizeof(BITMAPINFOHEADER);
stBmpInfoHeader.biWidth = sizeWindow.cx;
stBmpInfoHeader.biHeight = sizeWindow.cy;
stBmpInfoHeader.biPlanes = 1;
stBmpInfoHeader.biBitCount = 32;
stBmpInfoHeader.biCompression = BI_RGB;
stBmpInfoHeader.biClrUsed = 0;
stBmpInfoHeader.biSizeImage = nBytesPerLine * sizeWindow.cy;
PVOID pvBits = NULL;
hBKBitmap = ::CreateDIBSection(NULL, (PBITMAPINFO)&stBmpInfoHeader, DIB_RGB_COLORS, &pvBits, NULL, 0);
assert(hBKBitmap != NULL);
hBKBitmapOld = ::SelectObject( hdcBKMemory, hBKBitmap);
pdlg->DoPaintBackground(hdcBKMemory);//繪製窗體背景
}
HDC hdcEnd = CreateCompatibleDC(hDC);//新建相容DC
BITMAPINFOHEADER stBmpInfoHeader2 = { 0 };
int nBytesPerLine2 = ((sizeWindow.cx * 32 + 31) & (~31)) >> 3;
stBmpInfoHeader2.biSize = sizeof(BITMAPINFOHEADER);
stBmpInfoHeader2.biWidth = sizeWindow.cx;
stBmpInfoHeader2.biHeight = sizeWindow.cy;
stBmpInfoHeader2.biPlanes = 1;
stBmpInfoHeader2.biBitCount = 32;
stBmpInfoHeader2.biCompression = BI_RGB;
stBmpInfoHeader2.biClrUsed = 0;
stBmpInfoHeader2.biSizeImage = nBytesPerLine2 * sizeWindow.cy;
PVOID pvBits2 = NULL;
HBITMAP hbmpMem2 = ::CreateDIBSection(NULL, (PBITMAPINFO)&stBmpInfoHeader2, DIB_RGB_COLORS, &pvBits2, NULL, 0);//新建畫布
HGDIOBJ hEndBitmapOld=SelectObject(hdcEnd,hbmpMem2);
POINT ptSrc = { 0, 0};
BLENDFUNCTION blendFunc;
blendFunc.BlendOp = 0;
blendFunc.BlendFlags = 0;
blendFunc.AlphaFormat = 1;
blendFunc.SourceConstantAlpha = 255;//AC_SRC_ALPHA
::AlphaBlend(hdcEnd,0,0,sizeWindow.cx,sizeWindow.cy,hdcBKMemory,0,0,sizeWindow.cx,sizeWindow.cy,blendFunc);//將背景複製到新畫布上
m_root->DoPaint(m_hWnd,hdcEnd);/////////////繪製畫布和控制元件
POINT ptWinPos = { rcWindow.left, rcWindow.top };
//UpdateLayeredWindow
HMODULE hFuncInst = LoadLibrary(_T("User32.DLL"));
typedef BOOL (WINAPI *MYFUNC)(HWND, HDC, POINT*, SIZE*, HDC, POINT*, COLORREF, BLENDFUNCTION*, DWORD);
MYFUNC UpdateLayeredWindow;
UpdateLayeredWindow = (MYFUNC)::GetProcAddress(hFuncInst, "UpdateLayeredWindow");
if(!UpdateLayeredWindow(m_hWnd, hDC, &ptWinPos, &sizeWindow, hdcEnd, &ptSrc, 0, &blendFunc, ULW_ALPHA))
{
assert(L"UpdateLayeredWindow 呼叫失敗");
TCHAR tmp[255] = {_T('\0')};
}//使用UpdateLayeredWindow更新到當前窗體上
//釋放資源
SelectObject(hdcEnd,hEndBitmapOld);
::DeleteObject(hFuncInst);
::DeleteObject(hEndBitmapOld);
::DeleteObject(hbmpMem2);
::DeleteDC(hdcEnd);
::DeleteDC(hDC);
}
Paint()函式中用了雙緩衝,具體講解,參看《WIN32介面開發之二:GDI+中的區域性刷新技術》這裡我只講兩個地方:
1、窗體背景的繪製
CDialogUI *pdlg=(CDialogUI*)m_root;
因為我們在定義m_root時,定義的是CControlUI,但它實際是CDialogUI的例項,所以將它強轉成CDialogUI指標,然後記憶體記憶體DC裡呼叫pdlg->DoPaintBackground(hdcBKMemory);//繪製窗體背景
來繪製窗體背景。2、容器及控制元件的繪製
m_root->DoPaint(m_hWnd,hdcEnd);/////////////繪製畫布和控制元件
主要是靠這個函式來完成的!這個得著重講一下,看它是怎麼完成繪製的。我們先看一下根據XML生成的控制元件樹
從上面這個圖就能看得出來,CDialogUI包含兩個子控制元件(Canvas),這也就是我說的容器裡包含容器,而每個Canvas又包含兩個Button控制元件。下面我們看DoPaint的呼叫順序,首先m_root雖然被定義為CControlUI的變數,但實際是CDialogUI的例項指標,所以會沿著繼承圖去找CDialogUI的DoPaint,而CDialogUI是沒有DoPaint的,所以會去找它的子類CContainerUI的DoPaint,並且執行,我們重新看下這個DoPaint方法
void CContainerUI::DoPaint(HWND hwnd,HDC hDC)
{
for( int it = 0; it < m_items.GetSize(); it++ ) {
CControlUI* pControl = static_cast<CControlUI*>(m_items[it]);
pControl->DoPaint(hwnd,hDC);
}
}
從這裡就可以看到CDialogUI的DoPaint執行順序了:1、找到CDialogUI的第一個孩子結點(一個CContainerUI例項,假設為ContainerA),然後呼叫這個例項的DoPaint
2、執行ContainerA->DoPaint(),依然是這個函式,也就是說,它會挨個執行那兩個Button的DoPaint,完成之後返回。
3、找到CDialogUI的第二個孩子結點(同樣,一個CContainerUI例項,假設為ContainerB),然後呼叫這個例項的DoPaint
4、執行ContainerB>DoPaint(),同樣,它也是同樣執行這個函式,然後逐個執行它的那個Button的DoPaint,之後返回。
三、WinMain函式
void Message(){
MSG msg={0};
while(GetMessage(&msg,NULL,0,0)){
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
ULONG_PTR gdiplusToken = 0;
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nShowCmd){
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
CStartPage *startPage=new CStartPage();
startPage->SetInstance(hInstance);
startPage->Create();
startPage->ShowWindow(true);
Message();
delete startPage;
Gdiplus::GdiplusShutdown(gdiplusToken);
return 0;
}
最終的程式介面:(現在還沒有新增EVENT和NOTIFY,還不具有事件響應功能,下篇實現)宣告:本文只僅交流,轉載請標明出處,感謝金山影音漂亮的介面圖片。