前言:这部分涉及工程比较大,所以我打算分开为两篇来写,第一篇完成基本框架的构建,第二篇添加上EVENT和NOTIFY机制。
完成目标:仿照DirectUI,完成一个基本雏形,开发一个布局控件(Dialog),和一个按钮控件(Button),通过XML来布局窗体,最后按钮响应点击、鼠标移动等事件信息,用户还可以通过NOTIFY机制来定制,用户具体行为时,界面所要做的动作。给大家看下最终界面吧,一个背景和四个按钮。
正常状态:
点击按钮状态(点击按钮时,并拖动窗体)
1、先看下我们布局的XML
"<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>"2、两个文件:UIMarkup.h和UIMarkup.cpp
但有几个接口,要注意一下:
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);//获取对应属性名的属性的值的几个函数3、CDialogBuilder讲解
完成功能:
1、利用上面的CMarkUp类分析XML,构建出XML结点树
2、然后根据XML中的结点,NEW 出控件,并利用XML结点树的属性,构建出控件的属性
先看定义:
class CDialogBuilder { public: CDialogBuilder(); ~CDialogBuilder(); public: CControlUI* Create(LPCTSTR pstrXML); private: CControlUI* _Parse(CMarkupNode* parent, CControlUI* pParent = NULL); CMarkup m_xml; };可以看到,CDialogBuilder比较简单,只有两个函数,Create()和_Parse(),另一个变量m_xml是用来分析XML,并构建XML结点树的。
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消息
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事件通知功能,所以我们就先初始化的时候,把按钮统一画成绿色。
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; };其实这个类没有任何的实际意义,就是个接口类,它的这几个函数全部都声明为纯虚函数
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); } }最重要的,绘制函数,我这里没有对它本身进行绘制,只是遍历当前容器中所有的控件,然后再它里面的控件进行逐个绘制, 这里我要留下一个疑问,如果容器里嵌套有容器是怎么完成绘制的呢????!!!!
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); }好了,到这几个控件的实现就全部实现了,下面看窗体的创建。
定义
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消息时,
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+中的局部刷新技术 》
CDialogUI *pdlg=(CDialogUI*)m_root;因为我们在定义m_root时,定义的是CControlUI,但它实际是CDialogUI的实例,所以将它强转成CDialogUI指针,然后内存内存DC里调用
pdlg->DoPaintBackground(hdcBKMemory);//绘制窗体背景来绘制窗体背景。
m_root->DoPaint(m_hWnd,hdcEnd);/////////////绘制画布和控件主要是靠这个函数来完成的!
从上面这个图就能看得出来,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执行顺序了:
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,还不具有事件响应功能,下篇实现)
源码地址:http://download.csdn.net/detail/harvic880925/5820507
声明:本文只仅交流,转载请标明出处,感谢金山影音漂亮的界面图片。