Duilib源码 "类" 逻辑分析.
2017-10-21 20:24
555 查看
1.CControlUI 和 CPaintManagerUI 和 INotifyUI三类联立<能把窗口管理器那个图的notify看懂的话,你的消息机制就没问题了>
bool CPaintManagerUI::MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lRes)[内有消息过滤]->case WM_LBUTTONDOWN:pControl->Event(event);->DoEvent[控件会重载]->m_pManager->if(
event.Type == UIEVENT_CONTEXTMENU ) SendNotify(this, DUI_MSGTYPE_MENU, event.wParam, event.lParam);这就能发送到主窗口进行多继承notify进行处理响应.
Skilla使用duilib已经有一年了,经过一年的摸索,也逐渐地解开了里面的大大小小的秘密。从熟悉Demo到布局特性的了解也是经历了数月的时间,核心机制也是最后才弄明白的,源码的探索也是由表及里的。但是这个速度是非常缓慢的,所以今天Skilla要写这篇文章,让大家可以有主到次地来认识Duilib。
其实,要想以最快的速度把源码弄通,是需要有先后顺序的,说的再简单点就是要抓住核心,核心理解以后细节部分的难题都会迎刃而解。下面以伪代码的形式来分析一下Duilib的源码。
首先,第一个要说的就是CControlUI,这个是我们使用最频繁的,xml里面的每一个标签都会生成一个CControlUI或它的子类对象。
[cpp] view
plain copy
class CControlUI
{
void Event(TEventUI& event) //事件回调
{
DoEvent(event);
}
virtual void DoPaint() //控件的绘制代码
{
//按需求去绘制
CRenderEngine::DrawImage(bkimage,selfRect);
...
}
virtual void DoEvent(TEventUI& event)
{
//控件的事件处理(根据需求修改控件属性)
if (event.Type==UIEVENT_BUTTONDOWN)
{
}else if (event.Type==UIEVENT_BUTTONUP)
{
}else if (event.Type==UIEVENT_KEYDOWN)
{
}
}
void SetWidth(nWidth)
{
width = nWidth; //修改控件属性
Invalidate(); //刷新
}
protected:
//位置属性
int width;
int height;
...
//图形属性
String bkimage;
String bkcolor;
...
};
CControlUI类拥有的属性很多,比如位置、图形、文本等等,并且都有Get和Set,Get方法一般是直接返回属性值,Set方法一般是先给属性赋值然后Invalidate刷新。还有两个核心的方法是DoEvent和DoPaint,前者是事件回调(类似于窗口过程函数,由owner窗口过程函数消息处理时调用),后者为渲染回调(由owner窗口过程函数里的WM_PAINT消息处理时调用)。控件的各种属性根据需求抽象出来的,这点很容易理解,但是DoEvent和DoPaint这两个东西是干嘛的?为什么要有这两个东西?这两个函数是设计者抽象出来的,为了和Owner窗口交互的。控件是设计者设计出来的,是不会响应任何windows事件的,为了让CControlUI能响应各种操作(包括鼠标,键盘等等),需要和窗口有一个沟通的桥梁,但是DoEvent和DoPaint就是干这个用的,一个相当于“输入设备”用来响应操作;另一个相当于“输出设备”用来展现视图。
一个窗口的控件也可谓“芸芸众生”,大的套小的,千奇百怪。那么当窗口消息到来时,是如何决定哪个控件去响应事件,哪个控件去刷新视图呢?下面我们来认识一下窗口绘制管理器CPaintManagerUI,来看伪代码。
CPaintManagerUI,窗口消息及图形绘制管理器类,与窗口绑定,主要作用:
1) 绘制控件。
2) 消息管理。
3) 事件通知。
[cpp] view
plain copy
class CPaintManagerUI
{
public:
bool MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lRes)
{
switch(uMsg)
{
case WM_SIZE: //窗口相关消息
//去操作被管理的窗口
break;
case WM_LBUTTONDOWN: //鼠标类消息(通过鼠标坐标点来确认哪个控件响应)
//根据lParam获取到鼠标消息的坐标点
pt = lParam;
CControlUI* pControl = FindControl(pt); //根据坐标点找到控件
TEventUI event; //拟定点击事件
event.pSender = pControl;
event.ptMouse = pt;
event.Type = UIEVENT_BUTTONDOWN;
pControl->Event(event); //控件去响应事件
m_pFocus = pControl; //当前控件因为被点击了,接受焦点
TEventUI event; //拟定设置焦点事件
event.pSender = pControl;
event.ptMouse = pt;
event.Type = UIEVENT_SETFOCUS;
pControl->Event(event);
break;
case WM_KEYDOWN: //键盘类消息(通过焦点来确定哪个控件响应)
TEventUI event; //拟定键盘事件
event.pSender = m_pFocus;
...
m_pFocus->DoEvent(event); //当前拥有焦点的控件响应键盘事件
break;
...
case WM_PAINT:
//需要刷新的控件去刷新
needUpdateControl->DoPaint();
break;
}
}
protected:
//窗口绘图属性
HWND m_hWndPaint;
int m_nOpacity;
HDC m_hDcPaint;
HDC m_hDcOffscreen;
HDC m_hDcBackground;
//控件树
CControlUI* m_pRoot;
//响应事件后的控件
CControlUI* m_pFocus;
CControlUI* m_pEventHover;
CControlUI* m_pEventClick;
CControlUI* m_pEventKey;
};
CPaintManagerUI的属性和方法也非常多,还是抽出最核心的部分来看,那就是MessageHandler方法。之所以把MessageHandler抽取出来分析,那就是因为它的重要性了,它是挂接窗口消息与控件事件的桥梁。CPaintManagerUI::MessageHandler是窗口消息的集中处理器同样也是所有控件事件的事件源,相当于人体的“神经中枢”。MessageHandler里面处理的消息一共分一下几类:1.鼠标类消息,这类消息一般都有鼠标产生,比如WM_LBUTTONDOWN,WM_MOUSEMOVE等等,鼠标消息转化为控件事件要根据鼠标消息到来时的坐标点,根据这个坐标点在哪个控件的区域内,来决定这个消息属于哪个控件,就像天上掉钱一样,掉到谁家是谁的。找到这个控件后创建一个事件对象,调用该控件的DoEvent方法。2.键盘类消息,这类消息一般由键盘产生,像WM_KEYDOWN这一类的。键盘消息到来时如何确认属于哪个控件呢?就像Windows原生的窗口一样,虽然是虚拟的控件,但同样需要焦点。键盘消息到来时要根据焦点来确定哪个控件拥有该消息,当前焦点在哪个控件上,哪个控件才响应键盘消息。3.渲染消息,指的就是WM_PAINT消息,当有控件需要刷新时,需要刷新的控件调用自身的DoPaint消息。当然这是简单的说法,事实上里面的处理也是很复杂的,时间关系这里就不介绍了。
下面看一下在使用时是怎样的,还是看伪代码
[cpp] view
plain copy
class MyWindow :public CWindowWnd
{
public:
HandleMessage()
{
m_PaintManager.MessageHandler();
}
protected:
CPaintManagerUI m_PaintManager;
};
使用时就相对简单了,把窗口消息交给m_PaintManager,由MessageHandler转换为控件事件,再由控件去处理相应的操作。
----另一个分类解释-----分界线----.
上面所说的就是Duilib里面最关键的代码,理解这个就可以根据需求去派生自己的控件了。像MessageHandler里面没有处理到的消息,比如WM_IME_STARTCOMPOSITION(这个是输入法相关的,属于键盘类消息)可以自己加上,当然同样也要添加相应的控件事件UIEVENT_IME_STARTCOMPOSITION,起什么名字不重要,关键是要区分开。
原理固然如此,但是细节决定成败。一些细节性问题,比如控件是如何创建的、xml是如何解析的、Findcontrol是如何实现的、绘图是如何实现的同样要花时间去研究。
选择Duilib就是因为它的灵活,做出的产品非常细腻。所以读懂源码还是必须的,如果仅仅是停留在使用的层面,远不如去选择一款非开源的界面库,但是拿有限的方法去实现无限的需求基本上是不可能的。
今天就到这里了,如有问题或建议请联系作者:Skilla(QQ:848861075)
前言:前面我们完成了对DUI雏形的构建,但大家有没有注意到一个问题,我们对消息的处理都是在用户构建的类(CStartPage)中,还有,为了在控件中可以实时刷新,所以每个控件都必须带有一个变量m_hwnd,来保存当前窗体的句柄,而且在每次发送EVENT消息时都要赋值,相当麻烦,所以,我们将这些控件都具有的一些操作和变量,全部都集合起来,封装成一个类,这个类就是CPaintManagerUI;
一、基本变量定义
我带大家想想,我们应该把什么功能集成到CPaintManagerUI中呢,也就是哪些功能是不需要用户做的呢,又有哪些功能是控件所共有的呢:
1、CStartPage中的HandleMessage中,对具体消息的处理部分,比如WM_PAITN,WM_LBUTTONDOWN……
2、控件的Notify机制,上节中,每个控件为了可以NOTIFY到当前窗体,每个控件都要保存一个当前窗体的this指针,相当烦琐
CPaintManagerUI类就是对于公共的成员变量及成员方法的集成,以减少代码量,提高代码重用性,所写的一个类;既然是将原有代码进行独立,所以,独立的同时,也必须将原代码中的变量根据CPaintManagerUI的需要传进来,有几个变量是必须需要的;
3、m_hwnd:当前窗口的句柄,SendMessage()时用到;
4、m_hInstance:当前应用程序的HINSTANCE实例,在创建m_ToolTip(提示条)时用到
5、this指针:SendNotify()时,要将侦听NOTIFY消息的窗体发送NOTIFY消息,所以要将当前窗体的THIS指针,传到CPaintManangerUI中,通过CPaintManangerUI::SetNotifier(this)设置;CPaintManangerUI在发送NOTIFY时,会将消息发送给所有的侦听窗体;
我贴出CPaintManagerUI的核心定义:
注意:在源码中定义的变量要比这里的多些,多余的那些变量主要是为了双缓冲绘图所定义的:
[cpp] view
plain copy
class CPaintManagerUI
{
public:
CPaintManagerUI();
~CPaintManagerUI();
public:
void Init(HWND hWnd);//set hwnd of the window
HWND GetHwnd(){return m_hWnd;}
//NOTIFY部分 the part of Notify
void SendNotify(CControlUI* pControl, LPCTSTR pstrMessage, WPARAM wParam= 0, LPARAM lParam= 0);//send notify messages to all listerners;
bool AddNotifier(INotifyUI* pControl);
bool RemoveNotifier(INotifyUI* pControl);
//控件树部分 control-tree set part
bool AttachDialog(CControlUI* pControl);//set the root of the control-tree
bool InitControls(CControlUI* pControl, CControlUI* pParent = NULL);//set the current manager virable to controls
//消息处理部分 message-handler
bool MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lRes);
private:
CStdPtrArray m_aNotifiers;//NOTIFY消息接收的指针队列 listerners array
HWND m_hWnd;//主窗体的句柄 the hwnd of main window
CControlUI* m_pRoot;//XML控件树的根结点 the root of the DLG control tree,in our app this variable always point to CDlialogUI
};
讲解:
跟上面的分析的一样,主要存储的几个部分及其对应函数,分别是:
1、窗体句柄m_hWnd,分别对应:Init(设置)、GetHwnd(获取)
看代码:
[cpp] view
plain copy
void CPaintManagerUI::Init(HWND hWnd)
{
ASSERT(::IsWindow(hWnd));
// Remember the window context we came from
m_hWnd = hWnd;
}
HWND CPaintManagerUI::GetHwnd()
{
return m_hWnd;
}
没什么难度,就是赋值与返回值;
2、消息通知部分,虽然我们当前只有一个监听类(CStartPage),但我们不想写的太死,所以使用了链表,用到了链表,当然就有增加和删除结点的部分了,具体函数解析如下:
(1)、m_aNotifiers,要接收NOTIFY消息的窗体指针队列
(2)、AddNotifier(),增加到队列中;RemoveNotifier()从队列中删除
(3)、SendNotify(),发送消息到接收窗体的Notify()函数中。
实现代码讲解:
[cpp] view
plain copy
bool CPaintManagerUI::AddNotifier(INotifyUI* pControl)
{
ASSERT(m_aNotifiers.Find(pControl)<0);
return m_aNotifiers.Add(pControl);
}
bool CPaintManagerUI::RemoveNotifier(INotifyUI* pControl)
{
for( int i = 0; i < m_aNotifiers.GetSize(); i++ ) {
if( static_cast<INotifyUI*>(m_aNotifiers[i]) == pControl ) {
return m_aNotifiers.Remove(i);
}
}
return false;
}
讲解:这两个函数比较简单,AddNotifyer()就是把当前要接收NOTIFY消息的用户窗体类的this指针添加到数组里;RemoveNotifier()相反,就是从数组中删除指定的窗体类的this指针;
[cpp] view
plain copy
void CPaintManagerUI::SendNotify(CControlUI* pControl, LPCTSTR pstrMessage, WPARAM wParam /*= 0*/, LPARAM lParam /*= 0*/)
{
TNotifyUI Msg;
Msg.pSender = pControl;
Msg.sType = pstrMessage;
Msg.wParam = 0;
Msg.lParam = 0;
Msg.ptMouse =m_ptLastMousePos;
Msg.dwTimestamp = ::GetTickCount();
// Send to all listeners
for( int i = 0; i < m_aNotifiers.GetSize(); i++ ) {
static_cast<INotifyUI*>(m_aNotifiers[i])->Notify(Msg);
}
}
讲解:发送消息函数(SendNotify),非常重要,可以看到,我们先对MSG结构体进行封装,然后发送给监听者。最后一句m_aNotifiers[i])->Notify(Msg),实际相当于CStartPage->Notify(MSG);
3、由于我们的这个CPaintManagerUI包含控件共有属性的集成部分,所以我们必须将CPaintManagerUI与控件关联起来,而这两个函数AttachDialog()、InitControls()就是实现这些功能。
[cpp] view
plain copy
bool CPaintManagerUI::AttachDialog(CControlUI* pControl)
{
ASSERT(::IsWindow(m_hWnd));
// Remove the existing control-tree. We might have gotten inside this function as
// a result of an event fired or similar, so we cannot just delete the objects and
// pull the internal memory of the calling code. We'll delay the cleanup.
if( m_pRoot != NULL ) {
m_aDelayedCleanup.Add(m_pRoot);
::PostMessage(m_hWnd, WM_APP + 1, 0, 0L);
}
// Set the dialog root element
m_pRoot = pControl;
return InitControls(pControl);
}
bool CPaintManagerUI::InitControls(CControlUI* pControl, CControlUI* pParent /*= NULL*/)
{
ASSERT(pControl);
if( pControl == NULL ) return false;
pControl->SetManager(this, pParent != NULL ? pParent : pControl->GetParent());
// We're usually initializing the control after adding some more of them to the tree,
// and thus this would be a good time to request the name-map rebuilt.
return true;
}
讲解:
AttachDialog(),首先判断m_pRoot是否为空,如果不为空,就发送自定义消息WM_APP+1,将其清空;然后将当前的控件树ROOT结点指针赋值为m_pRoot;
InitControls(),就是给所有控件的PaintManagerUI赋值,大家注意执行顺序,这里执行的其实是m_pRoot->SetManager(this,parent);在代码中,我们在两个地方实现了SetManager,一个在CControlUI,另一个是在CContainerUI,而我们的m_pRoot肯定是CDialogUI类型,CDialogUI派生自CContainerUI,CContainerUI派生自CControlUI,所以根据继承关系,会执行CContainerUI的SetManger;具体实现流程:
1、先声明一个变量,CPaintManagerUI* m_pManager;,放在CControlUI中,供CControlUI和CContainerUI关联;
2、CContainerUI的SetManger的具体实现:
[cpp] view
plain copy
void CContainerUI::SetManager(CPaintManagerUI* pManager, CControlUI* pParent)
{
for( int it = 0; it < m_items.GetSize(); it++ ) {
static_cast<CControlUI*>(m_items[it])->SetManager(pManager, this);
}//先设置它所有的子结点 set all his children nodes' manager first
//然后再设置自己 and then set his own's manager
CControlUI::SetManager(pManager, pParent);
}
讲解:大家可以看到,先设置所有的子结点的PaintManager,然后再设置自己;当然如果子结点仍然是CContainerUI的派生类,同样会执行这个函数;所以这样就实现了遍历设置所有控件结点;这里还用到了CControlUI的SetManager,看下具体实现:
[cpp] view
plain copy
void CControlUI::SetManager(CPaintManagerUI* pManager, CControlUI* pParent)
{
m_pManager = pManager;
m_pParent = pParent;
}
4、消息处理部分,这部分没什么特殊的,就是把原来CStartPage中的消息响应移到了这里来。这部分代码就不讲了,大家可以去看看,不难懂;
[cpp] view
plain copy
void CButtonUI::Event(TEventUI& event)
{
if( event.Type == UIEVENT_BUTTONDOWN || event.Type == UIEVENT_DBLCLICK )
{
if( ::PtInRect(&m_RectItem, event.ptMouse)) {
m_uButtonState |= UISTATE_PUSHED | UISTATE_CAPTURED;
m_pManager->SendNotify(this,L"click");
Invalidate();
}
}
if( event.Type == UIEVENT_MOUSEMOVE )
{
if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) {
m_pManager->SendNotify(this,L"drag");
Invalidate();
}
}
}
这里直接调用m_pManager的SendNotify函数,由于在SendNotify中实现了MSG的封装,所以这里的代码就显得格外清爽。我这里只列举了发送NOTIFY消息的两个事件,其它事件没有列举出来,看源码就好。
[cpp] view
plain copy
LRESULT CStartPage::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if( uMsg == WM_CREATE ) {
m_pm.Init(m_hWnd);
CDialogBuilder builder;
CControlUI* pRoot = builder.Create(GetDialogResource());
ASSERT(pRoot && "Failed to parse XML");
m_pm.AttachDialog(pRoot);
m_pm.AddNotifier(this);
SendMessage(GetHWND(),WM_PAINT,NULL,NULL);
//return 0;
}
LRESULT lRes = 0;
if( m_pm.MessageHandler(uMsg, wParam, lParam, lRes) ) return lRes;
return CWindowWnd::HandleMessage(uMsg, wParam, lParam);
}
讲解:不难理解,在这里用户可以对消息进行拦截,然后再进入到MessageHandler中处理,对于我们都不做处理的消息传递给系统,让系统默认处理。
比如在WM_CREATE中,首先是用CPaintManagerUI 变量m_pm初始化,初始化内容包括:窗体句柄、控件树、增加THIS指针到notifier;
由于本文只是集成类的实现,并没有涉及功能的改变,所以也就不再贴图了。
本文由HARVIC完成,如若转载,请标明出处,请大家尊重初创者的版权,谢谢!!
源文地址:http://blog.csdn.net/harvic880925/article/details/9632613
源码地址:http://download.csdn.net/detail/harvic880925/5843901
声明:感谢金山影音漂亮的界面图片,该图片来自网络。
void CPaintManagerUI::Init(HWND hWnd)
{
ASSERT(::IsWindow(hWnd));
// Remember the window context we came from
m_hWndPaint = hWnd; //得到句柄
m_hDcPaint = ::GetDC(hWnd); 得到DC
// We'll want to filter messages globally too
m_aPreMessages.Add(this);
}
paintManager维持着当前的句柄和DC 窗口中保存着一份实例, 所有的绘画操作都交给它去处理了
![](https://oscdn.geek-share.com/Uploads/Images/Content/202010/13/b2916f6733e4fa4a887edf042a127672)
bool CPaintManagerUI::MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lRes)[内有消息过滤]->case WM_LBUTTONDOWN:pControl->Event(event);->DoEvent[控件会重载]->m_pManager->if(
event.Type == UIEVENT_CONTEXTMENU ) SendNotify(this, DUI_MSGTYPE_MENU, event.wParam, event.lParam);这就能发送到主窗口进行多继承notify进行处理响应.
Skilla使用duilib已经有一年了,经过一年的摸索,也逐渐地解开了里面的大大小小的秘密。从熟悉Demo到布局特性的了解也是经历了数月的时间,核心机制也是最后才弄明白的,源码的探索也是由表及里的。但是这个速度是非常缓慢的,所以今天Skilla要写这篇文章,让大家可以有主到次地来认识Duilib。
其实,要想以最快的速度把源码弄通,是需要有先后顺序的,说的再简单点就是要抓住核心,核心理解以后细节部分的难题都会迎刃而解。下面以伪代码的形式来分析一下Duilib的源码。
首先,第一个要说的就是CControlUI,这个是我们使用最频繁的,xml里面的每一个标签都会生成一个CControlUI或它的子类对象。
[cpp] view
plain copy
class CControlUI
{
void Event(TEventUI& event) //事件回调
{
DoEvent(event);
}
virtual void DoPaint() //控件的绘制代码
{
//按需求去绘制
CRenderEngine::DrawImage(bkimage,selfRect);
...
}
virtual void DoEvent(TEventUI& event)
{
//控件的事件处理(根据需求修改控件属性)
if (event.Type==UIEVENT_BUTTONDOWN)
{
}else if (event.Type==UIEVENT_BUTTONUP)
{
}else if (event.Type==UIEVENT_KEYDOWN)
{
}
}
void SetWidth(nWidth)
{
width = nWidth; //修改控件属性
Invalidate(); //刷新
}
protected:
//位置属性
int width;
int height;
...
//图形属性
String bkimage;
String bkcolor;
...
};
CControlUI类拥有的属性很多,比如位置、图形、文本等等,并且都有Get和Set,Get方法一般是直接返回属性值,Set方法一般是先给属性赋值然后Invalidate刷新。还有两个核心的方法是DoEvent和DoPaint,前者是事件回调(类似于窗口过程函数,由owner窗口过程函数消息处理时调用),后者为渲染回调(由owner窗口过程函数里的WM_PAINT消息处理时调用)。控件的各种属性根据需求抽象出来的,这点很容易理解,但是DoEvent和DoPaint这两个东西是干嘛的?为什么要有这两个东西?这两个函数是设计者抽象出来的,为了和Owner窗口交互的。控件是设计者设计出来的,是不会响应任何windows事件的,为了让CControlUI能响应各种操作(包括鼠标,键盘等等),需要和窗口有一个沟通的桥梁,但是DoEvent和DoPaint就是干这个用的,一个相当于“输入设备”用来响应操作;另一个相当于“输出设备”用来展现视图。
一个窗口的控件也可谓“芸芸众生”,大的套小的,千奇百怪。那么当窗口消息到来时,是如何决定哪个控件去响应事件,哪个控件去刷新视图呢?下面我们来认识一下窗口绘制管理器CPaintManagerUI,来看伪代码。
CPaintManagerUI,窗口消息及图形绘制管理器类,与窗口绑定,主要作用:
1) 绘制控件。
2) 消息管理。
3) 事件通知。
[cpp] view
plain copy
class CPaintManagerUI
{
public:
bool MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lRes)
{
switch(uMsg)
{
case WM_SIZE: //窗口相关消息
//去操作被管理的窗口
break;
case WM_LBUTTONDOWN: //鼠标类消息(通过鼠标坐标点来确认哪个控件响应)
//根据lParam获取到鼠标消息的坐标点
pt = lParam;
CControlUI* pControl = FindControl(pt); //根据坐标点找到控件
TEventUI event; //拟定点击事件
event.pSender = pControl;
event.ptMouse = pt;
event.Type = UIEVENT_BUTTONDOWN;
pControl->Event(event); //控件去响应事件
m_pFocus = pControl; //当前控件因为被点击了,接受焦点
TEventUI event; //拟定设置焦点事件
event.pSender = pControl;
event.ptMouse = pt;
event.Type = UIEVENT_SETFOCUS;
pControl->Event(event);
break;
case WM_KEYDOWN: //键盘类消息(通过焦点来确定哪个控件响应)
TEventUI event; //拟定键盘事件
event.pSender = m_pFocus;
...
m_pFocus->DoEvent(event); //当前拥有焦点的控件响应键盘事件
break;
...
case WM_PAINT:
//需要刷新的控件去刷新
needUpdateControl->DoPaint();
break;
}
}
protected:
//窗口绘图属性
HWND m_hWndPaint;
int m_nOpacity;
HDC m_hDcPaint;
HDC m_hDcOffscreen;
HDC m_hDcBackground;
//控件树
CControlUI* m_pRoot;
//响应事件后的控件
CControlUI* m_pFocus;
CControlUI* m_pEventHover;
CControlUI* m_pEventClick;
CControlUI* m_pEventKey;
};
CPaintManagerUI的属性和方法也非常多,还是抽出最核心的部分来看,那就是MessageHandler方法。之所以把MessageHandler抽取出来分析,那就是因为它的重要性了,它是挂接窗口消息与控件事件的桥梁。CPaintManagerUI::MessageHandler是窗口消息的集中处理器同样也是所有控件事件的事件源,相当于人体的“神经中枢”。MessageHandler里面处理的消息一共分一下几类:1.鼠标类消息,这类消息一般都有鼠标产生,比如WM_LBUTTONDOWN,WM_MOUSEMOVE等等,鼠标消息转化为控件事件要根据鼠标消息到来时的坐标点,根据这个坐标点在哪个控件的区域内,来决定这个消息属于哪个控件,就像天上掉钱一样,掉到谁家是谁的。找到这个控件后创建一个事件对象,调用该控件的DoEvent方法。2.键盘类消息,这类消息一般由键盘产生,像WM_KEYDOWN这一类的。键盘消息到来时如何确认属于哪个控件呢?就像Windows原生的窗口一样,虽然是虚拟的控件,但同样需要焦点。键盘消息到来时要根据焦点来确定哪个控件拥有该消息,当前焦点在哪个控件上,哪个控件才响应键盘消息。3.渲染消息,指的就是WM_PAINT消息,当有控件需要刷新时,需要刷新的控件调用自身的DoPaint消息。当然这是简单的说法,事实上里面的处理也是很复杂的,时间关系这里就不介绍了。
下面看一下在使用时是怎样的,还是看伪代码
[cpp] view
plain copy
class MyWindow :public CWindowWnd
{
public:
HandleMessage()
{
m_PaintManager.MessageHandler();
}
protected:
CPaintManagerUI m_PaintManager;
};
使用时就相对简单了,把窗口消息交给m_PaintManager,由MessageHandler转换为控件事件,再由控件去处理相应的操作。
----另一个分类解释-----分界线----.
上面所说的就是Duilib里面最关键的代码,理解这个就可以根据需求去派生自己的控件了。像MessageHandler里面没有处理到的消息,比如WM_IME_STARTCOMPOSITION(这个是输入法相关的,属于键盘类消息)可以自己加上,当然同样也要添加相应的控件事件UIEVENT_IME_STARTCOMPOSITION,起什么名字不重要,关键是要区分开。
原理固然如此,但是细节决定成败。一些细节性问题,比如控件是如何创建的、xml是如何解析的、Findcontrol是如何实现的、绘图是如何实现的同样要花时间去研究。
选择Duilib就是因为它的灵活,做出的产品非常细腻。所以读懂源码还是必须的,如果仅仅是停留在使用的层面,远不如去选择一款非开源的界面库,但是拿有限的方法去实现无限的需求基本上是不可能的。
今天就到这里了,如有问题或建议请联系作者:Skilla(QQ:848861075)
前言:前面我们完成了对DUI雏形的构建,但大家有没有注意到一个问题,我们对消息的处理都是在用户构建的类(CStartPage)中,还有,为了在控件中可以实时刷新,所以每个控件都必须带有一个变量m_hwnd,来保存当前窗体的句柄,而且在每次发送EVENT消息时都要赋值,相当麻烦,所以,我们将这些控件都具有的一些操作和变量,全部都集合起来,封装成一个类,这个类就是CPaintManagerUI;
一、基本变量定义
我带大家想想,我们应该把什么功能集成到CPaintManagerUI中呢,也就是哪些功能是不需要用户做的呢,又有哪些功能是控件所共有的呢:1、CStartPage中的HandleMessage中,对具体消息的处理部分,比如WM_PAITN,WM_LBUTTONDOWN……
2、控件的Notify机制,上节中,每个控件为了可以NOTIFY到当前窗体,每个控件都要保存一个当前窗体的this指针,相当烦琐
CPaintManagerUI类就是对于公共的成员变量及成员方法的集成,以减少代码量,提高代码重用性,所写的一个类;既然是将原有代码进行独立,所以,独立的同时,也必须将原代码中的变量根据CPaintManagerUI的需要传进来,有几个变量是必须需要的;
3、m_hwnd:当前窗口的句柄,SendMessage()时用到;
4、m_hInstance:当前应用程序的HINSTANCE实例,在创建m_ToolTip(提示条)时用到
5、this指针:SendNotify()时,要将侦听NOTIFY消息的窗体发送NOTIFY消息,所以要将当前窗体的THIS指针,传到CPaintManangerUI中,通过CPaintManangerUI::SetNotifier(this)设置;CPaintManangerUI在发送NOTIFY时,会将消息发送给所有的侦听窗体;
我贴出CPaintManagerUI的核心定义:
注意:在源码中定义的变量要比这里的多些,多余的那些变量主要是为了双缓冲绘图所定义的:
[cpp] view
plain copy
class CPaintManagerUI
{
public:
CPaintManagerUI();
~CPaintManagerUI();
public:
void Init(HWND hWnd);//set hwnd of the window
HWND GetHwnd(){return m_hWnd;}
//NOTIFY部分 the part of Notify
void SendNotify(CControlUI* pControl, LPCTSTR pstrMessage, WPARAM wParam= 0, LPARAM lParam= 0);//send notify messages to all listerners;
bool AddNotifier(INotifyUI* pControl);
bool RemoveNotifier(INotifyUI* pControl);
//控件树部分 control-tree set part
bool AttachDialog(CControlUI* pControl);//set the root of the control-tree
bool InitControls(CControlUI* pControl, CControlUI* pParent = NULL);//set the current manager virable to controls
//消息处理部分 message-handler
bool MessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT& lRes);
private:
CStdPtrArray m_aNotifiers;//NOTIFY消息接收的指针队列 listerners array
HWND m_hWnd;//主窗体的句柄 the hwnd of main window
CControlUI* m_pRoot;//XML控件树的根结点 the root of the DLG control tree,in our app this variable always point to CDlialogUI
};
讲解:
跟上面的分析的一样,主要存储的几个部分及其对应函数,分别是:
1、窗体句柄m_hWnd,分别对应:Init(设置)、GetHwnd(获取)
看代码:
[cpp] view
plain copy
void CPaintManagerUI::Init(HWND hWnd)
{
ASSERT(::IsWindow(hWnd));
// Remember the window context we came from
m_hWnd = hWnd;
}
HWND CPaintManagerUI::GetHwnd()
{
return m_hWnd;
}
没什么难度,就是赋值与返回值;
2、消息通知部分,虽然我们当前只有一个监听类(CStartPage),但我们不想写的太死,所以使用了链表,用到了链表,当然就有增加和删除结点的部分了,具体函数解析如下:
(1)、m_aNotifiers,要接收NOTIFY消息的窗体指针队列
(2)、AddNotifier(),增加到队列中;RemoveNotifier()从队列中删除
(3)、SendNotify(),发送消息到接收窗体的Notify()函数中。
实现代码讲解:
[cpp] view
plain copy
bool CPaintManagerUI::AddNotifier(INotifyUI* pControl)
{
ASSERT(m_aNotifiers.Find(pControl)<0);
return m_aNotifiers.Add(pControl);
}
bool CPaintManagerUI::RemoveNotifier(INotifyUI* pControl)
{
for( int i = 0; i < m_aNotifiers.GetSize(); i++ ) {
if( static_cast<INotifyUI*>(m_aNotifiers[i]) == pControl ) {
return m_aNotifiers.Remove(i);
}
}
return false;
}
讲解:这两个函数比较简单,AddNotifyer()就是把当前要接收NOTIFY消息的用户窗体类的this指针添加到数组里;RemoveNotifier()相反,就是从数组中删除指定的窗体类的this指针;
[cpp] view
plain copy
void CPaintManagerUI::SendNotify(CControlUI* pControl, LPCTSTR pstrMessage, WPARAM wParam /*= 0*/, LPARAM lParam /*= 0*/)
{
TNotifyUI Msg;
Msg.pSender = pControl;
Msg.sType = pstrMessage;
Msg.wParam = 0;
Msg.lParam = 0;
Msg.ptMouse =m_ptLastMousePos;
Msg.dwTimestamp = ::GetTickCount();
// Send to all listeners
for( int i = 0; i < m_aNotifiers.GetSize(); i++ ) {
static_cast<INotifyUI*>(m_aNotifiers[i])->Notify(Msg);
}
}
讲解:发送消息函数(SendNotify),非常重要,可以看到,我们先对MSG结构体进行封装,然后发送给监听者。最后一句m_aNotifiers[i])->Notify(Msg),实际相当于CStartPage->Notify(MSG);
3、由于我们的这个CPaintManagerUI包含控件共有属性的集成部分,所以我们必须将CPaintManagerUI与控件关联起来,而这两个函数AttachDialog()、InitControls()就是实现这些功能。
[cpp] view
plain copy
bool CPaintManagerUI::AttachDialog(CControlUI* pControl)
{
ASSERT(::IsWindow(m_hWnd));
// Remove the existing control-tree. We might have gotten inside this function as
// a result of an event fired or similar, so we cannot just delete the objects and
// pull the internal memory of the calling code. We'll delay the cleanup.
if( m_pRoot != NULL ) {
m_aDelayedCleanup.Add(m_pRoot);
::PostMessage(m_hWnd, WM_APP + 1, 0, 0L);
}
// Set the dialog root element
m_pRoot = pControl;
return InitControls(pControl);
}
bool CPaintManagerUI::InitControls(CControlUI* pControl, CControlUI* pParent /*= NULL*/)
{
ASSERT(pControl);
if( pControl == NULL ) return false;
pControl->SetManager(this, pParent != NULL ? pParent : pControl->GetParent());
// We're usually initializing the control after adding some more of them to the tree,
// and thus this would be a good time to request the name-map rebuilt.
return true;
}
讲解:
AttachDialog(),首先判断m_pRoot是否为空,如果不为空,就发送自定义消息WM_APP+1,将其清空;然后将当前的控件树ROOT结点指针赋值为m_pRoot;
InitControls(),就是给所有控件的PaintManagerUI赋值,大家注意执行顺序,这里执行的其实是m_pRoot->SetManager(this,parent);在代码中,我们在两个地方实现了SetManager,一个在CControlUI,另一个是在CContainerUI,而我们的m_pRoot肯定是CDialogUI类型,CDialogUI派生自CContainerUI,CContainerUI派生自CControlUI,所以根据继承关系,会执行CContainerUI的SetManger;具体实现流程:
1、先声明一个变量,CPaintManagerUI* m_pManager;,放在CControlUI中,供CControlUI和CContainerUI关联;
2、CContainerUI的SetManger的具体实现:
[cpp] view
plain copy
void CContainerUI::SetManager(CPaintManagerUI* pManager, CControlUI* pParent)
{
for( int it = 0; it < m_items.GetSize(); it++ ) {
static_cast<CControlUI*>(m_items[it])->SetManager(pManager, this);
}//先设置它所有的子结点 set all his children nodes' manager first
//然后再设置自己 and then set his own's manager
CControlUI::SetManager(pManager, pParent);
}
讲解:大家可以看到,先设置所有的子结点的PaintManager,然后再设置自己;当然如果子结点仍然是CContainerUI的派生类,同样会执行这个函数;所以这样就实现了遍历设置所有控件结点;这里还用到了CControlUI的SetManager,看下具体实现:
[cpp] view
plain copy
void CControlUI::SetManager(CPaintManagerUI* pManager, CControlUI* pParent)
{
m_pManager = pManager;
m_pParent = pParent;
}
4、消息处理部分,这部分没什么特殊的,就是把原来CStartPage中的消息响应移到了这里来。这部分代码就不讲了,大家可以去看看,不难懂;
二、EVNET消息处理
在上面讲完了CPaintManagerUI的具体实现以后,基本上就没有什么好讲的了。最后看下如何在CButtonUI中发送Notify消息。代码如下:[cpp] view
plain copy
void CButtonUI::Event(TEventUI& event)
{
if( event.Type == UIEVENT_BUTTONDOWN || event.Type == UIEVENT_DBLCLICK )
{
if( ::PtInRect(&m_RectItem, event.ptMouse)) {
m_uButtonState |= UISTATE_PUSHED | UISTATE_CAPTURED;
m_pManager->SendNotify(this,L"click");
Invalidate();
}
}
if( event.Type == UIEVENT_MOUSEMOVE )
{
if( (m_uButtonState & UISTATE_CAPTURED) != 0 ) {
m_pManager->SendNotify(this,L"drag");
Invalidate();
}
}
}
这里直接调用m_pManager的SendNotify函数,由于在SendNotify中实现了MSG的封装,所以这里的代码就显得格外清爽。我这里只列举了发送NOTIFY消息的两个事件,其它事件没有列举出来,看源码就好。
三、重写CStartPage::HandleMessage
由于我们将消息处理函数封装到了CPaintManagerUI中,所以我们要对CStartPage的HandleMessage重新实现,具体代码如下:[cpp] view
plain copy
LRESULT CStartPage::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if( uMsg == WM_CREATE ) {
m_pm.Init(m_hWnd);
CDialogBuilder builder;
CControlUI* pRoot = builder.Create(GetDialogResource());
ASSERT(pRoot && "Failed to parse XML");
m_pm.AttachDialog(pRoot);
m_pm.AddNotifier(this);
SendMessage(GetHWND(),WM_PAINT,NULL,NULL);
//return 0;
}
LRESULT lRes = 0;
if( m_pm.MessageHandler(uMsg, wParam, lParam, lRes) ) return lRes;
return CWindowWnd::HandleMessage(uMsg, wParam, lParam);
}
讲解:不难理解,在这里用户可以对消息进行拦截,然后再进入到MessageHandler中处理,对于我们都不做处理的消息传递给系统,让系统默认处理。
比如在WM_CREATE中,首先是用CPaintManagerUI 变量m_pm初始化,初始化内容包括:窗体句柄、控件树、增加THIS指针到notifier;
四、最后
最后就是消除原来放在控件里的m_hwnd和Notify相关的函数了;这里就不一一列举了,代码里我都加了将原来的代码加上双斜线作为删除了,一看就明白,就不说了。看源码吧。由于本文只是集成类的实现,并没有涉及功能的改变,所以也就不再贴图了。
本文由HARVIC完成,如若转载,请标明出处,请大家尊重初创者的版权,谢谢!!
源文地址:http://blog.csdn.net/harvic880925/article/details/9632613
源码地址:http://download.csdn.net/detail/harvic880925/5843901
声明:感谢金山影音漂亮的界面图片,该图片来自网络。
void CPaintManagerUI::Init(HWND hWnd)
{
ASSERT(::IsWindow(hWnd));
// Remember the window context we came from
m_hWndPaint = hWnd; //得到句柄
m_hDcPaint = ::GetDC(hWnd); 得到DC
// We'll want to filter messages globally too
m_aPreMessages.Add(this);
}
paintManager维持着当前的句柄和DC 窗口中保存着一份实例, 所有的绘画操作都交给它去处理了
相关文章推荐
- Duilib 源码分析之 COptionUI 篇
- DuiLib的源码分析
- Duilib之源码分析
- DuiLib 源码分析——UIManager
- 传奇源码分析-客户端(游戏逻辑处理源分析五 服务器端响应)
- openstack 核心部件 openswitch 源码分析和逻辑整理
- Duilib 源码分析之 CResourceManager 篇
- Spring 源码分析(四) ——MVC(四)核心逻辑
- Pig源码分析: 逻辑执行计划优化
- x265源码分析:SAO 函数总结及逻辑关系图
- jQuery选择器源码解读(六):Sizzle选择器匹配逻辑分析
- spark mllib源码分析之逻辑回归弹性网络ElasticNet(二)
- Duilib 源码分析之 文字样式(m_uTextStyle) 篇
- leveldb源码分析--SSTable之逻辑结构
- MyBatis-3.4.2-源码分析14:XML解析之sqlElement(context.evalNodes("/mapper/sql"))
- spark mllib源码分析之逻辑回归弹性网络ElasticNet(一)
- Linux-0.11内核源码分析系列:关于线性地址,逻辑地址,物理地址的关系与区别
- MyBatis-3.4.2-源码分析2:解析XML之settingsAsProperties(root.evalNode("settings"))
- Duilib 源码分析之 CLabelUI 篇
- Duilib之源码分析