Skilla使用duilib已经有一年了,经过一年的摸索,也逐渐地解开了里面的大大小小的秘密。从熟悉Demo到布局特性的了解也是经历了数月的时间,核心机制也是最后才弄明白的,源码的探索也是由表及里的。但是这个速度是非常缓慢的,所以今天Skilla要写这篇文章,让大家可以有主到次地来认识Duilib。
其实,要想以最快的速度把源码弄通,是需要有先后顺序的,说的再简单点就是要抓住核心,核心理解以后细节部分的难题都会迎刃而解。下面以伪代码的形式来分析一下Duilib的源码。
首先,第一个要说的就是CControlUI,这个是我们使用最频繁的,xml里面的每一个标签都会生成一个CControlUI或它的子类对象。
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,来看伪代码。
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消息。当然这是简单的说法,事实上里面的处理也是很复杂的,时间关系这里就不介绍了。
下面看一下在使用时是怎样的,还是看伪代码
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)