在《Duilib源码分析(一)整体框架》、《Duilib源码分析(二)控件构造器—CDialogBuilder》以及《Duilib源码分析(三)XML解析器—CMarkup》中我们已从粗略的角度去分析框架操作流程和消息流程,只能对其有基本的印象,此处我们将通过实际的举例分析,duilib创建的工程,在整个资源解析、控件创建、控件加载与绘制,控件数据处理等管理的整个过程进行整合:
为了便于分析,我们仍然从项目中附带的工程“TestApp1”进行更深入的学习,以下执行流程为具体的大致步骤和操作内容;
从入口点WinMain:
1. CPaintManagerUI整个绘制UI的管理器,调用静态成员函数设置当前应用程序实例句柄以及资源路径(主要为各种xml和图片资源);
2. 初始化COM组件;
3. 创建窗口对象实例,并调用接口Create创建窗口;
4. 调用ShowWindow显示当前窗口界面;
5. 调用CPaintManagerUI的静态成员MessageLoop,执行消息循环;
6. 卸载COM组件。
以上几个简单的步骤基本上是所有duilib应用程序的执行大体框架;而最重要的莫过于Create、MessageLoop;
在调用Create函数中执行了几个重要的操作:
0. 注册窗口类、创建窗口;
1. 触发WM_CREATE消息(事实上还有其他的消息,但暂时先无需关心), 创建控件构造器CDialogBuilder,并以其内部CMarkup解析指定的test1.xml文件,并解析XML文件各节点;
2. 解析完成后,调用构建器的_Parse接口,实现对整个CMarkup各节点控件的申请、创建并以各控件所在xml文件中的布局组织控件树;
3. 对于构建完控件树后,得到树根控件并通过CPaintManagerUI绘制UI管理器的接口AttachDialog添加至绘制管理中;
4. 通过AddNotifier添加通告对象至CPaintManagerUI中,以便于可收到通告消息,触发void Notify(TNotifyUI& msg)调用,用户可以在该触发中做一些响应操作;
所以以上的几个操作基本上完成了需要绘制和控件响应的基本条件;
在调用MessageLoop中,实现消息循环(先忽略我们不太感兴趣的消息):
0. 收到WM_PAINT消息,在里面先完成了通告消息windowinit,使得触发Notify,此时可以做一些初始化操作;
1. 另外创建窗体的兼容DC、兼容位图;接着重要的是调用m_pRoot->DoPaint(m_hDcOffscreen, ps.rcPaint);此调用将完成所有被管理的控件树位置计算、绘制到兼容DC;
2. 然后调用BitBlt实现兼容DC到窗体DC的位块传输、拷贝并要求整个窗体重绘;
OK,以上两个主要操作完成了所有的主要功能包括xml文件解析,控件树的创建,绘制的管理、贴图,控件消息响应等;
因为我们知道duilib中各个控件是没有窗体句柄的,那么接下来,我们继续看各个控件对应的各种消息是如何被得知和传递的;
首先我们以该工程中的OnAlphaChanged接口说明,我们采用倒叙跟踪的方式;
1. 该接口在OnPrepare接口中被添加至一个被称为“alpha_controlor”的控件对象的OnNotify通告消息函数中,而OnPrepare函数在Notify接口中被调用,被调用的时机为
MessageLoop中收到WM_PAINT时候完成的通告消息windowinit;
2. 当鼠标左击按下时,CPaintManagerUI将收到WM_LBUTTONDOWN消息,所以我们要确定点击位于哪个控件上,跟踪该消息,内部通过WM_LBUTTONDOWN消息对应的
lParam参数获取到鼠标点击的位置,并通过FindControl查找到指定的鼠标位置下控件(整个查找过程为遍历当前控件以及其下所有子控件,每个控件根据自己的位置和鼠标的位置确定
是否为自己并返回自己);
3. 对得到的控件,调用该控件的SetFocus以及Event,在SetFocus中调用CPaintManagerUI的SetFocus构造TEventUI(一般将CPaintManagerUI的消息转化为duilib自定义的控件的消息)
并调用SendNotify,这样便触发了对应控件的通告消息并最终调用到OnAlphaChanged;在调用控件的Event时重新构造TEventUI结构参数,调用DoEvent完成触发事件响应;
OK,目前基本上已经知道了控件的消息数据流及其响应,以及控件绘制,不过当单个控件无效需要重绘时不应该调用耗费很大开销的WM_PAINT来导致所有的控件重绘这肯定是不科学的,所以
对于某些控件若不涉及到整个或是其他控件重绘的将直接对各个控件内部调整位置、状态,然后调用CPaintManagerUI的Invalidate进行区域性的重绘即可。
最后一步,再谈CPaintManagerUI::MessageLoop;
0. 在创建窗口的过程中一般会先注册窗口过程函数CWindowWnd::__WndProc,并保存窗口句柄和对象;那么可以认为消息被转换、翻译和分发后都会先向该处理函数发送并调用;
1. 调用本窗口类对象的HandleMessage接口,实现消息可以在类对象中接手并处理;
2. HandleMessage接口中先处理自己感兴趣的消息,若不处理则交给CPaintManagerUI对象的MessageHandler处理;
3. MessageHandler中会预先过来、预处理部分注册的消息;其他部分消息处理并转化为duilib的消息并执行一些通告、事件等操作,若MessageHandler未处理的则交给CWindowWnd的
HandleMessage,事实上该内部通过::CallWindowProc调用默认窗口过程处理函数::DefWindowProc进行大多数的默认处理(windows会自行并合理地处理,可不用关心);
4. 然而以上步骤是否真的只有这些,肯定不止;继续跟踪CPaintManagerUI::MessageLoop实现;
5. 该消息循环中在消息被翻译、转化前先调用了CPaintManagerUI::TranslateMessage,该函数内部先预处理已注册的预处理消息和加速器消息,并根据需要确定是否处理或继续传递;
6. 对于其不处理的消息,才会调用::TranslateMessage和::DispatchMessage翻译、分发该消息,而之后才会执行上述步骤的0~3。以上即为所有的窗体消息执行流程。
项目总结:
duilib整体上以无HWND的情况下自绘完成界面绘制显示,各个控件完成自己的绘制并通过贴图的方式实现整个窗体的绘制;另外采用双缓冲区绘制界面避免了闪烁,此外对某个控件失效重绘
也是采用句柄重绘的方式极大的减少绘制开销;通过XML文件配置界面布局、内容,比较灵活可扩展,用户可根据需要修改源码和增加感兴趣的属性实现,很多时候用户只需要把大量时间用在应用层、
业务逻辑层;内部各控件资源管理统一并内部实现自己的资源管理和一些功能组件,使得可不需依赖STL、MFC、BOOST等,移植方面只能是windows系列或以上版本,但要移植到其他平台则也需要
花费很大的时间和工作量;另外就是资源路径、加载,因内部CPaintManagerUI保存为静态变量,所以同一个程序如MFC中需多处创建duilib窗口可能导致资源混乱的情况,可统一资源路径或修改源码
实现以解决该问题;当然最重要的是小巧、轻便、资源可以打包,可结合打包工具轻松制作安装包,分发给最终用户;但内部部分控件绘制、数据处理等细节方面比较多可能还存在BUG等不足;
duilib相对于MFC不需要太多依赖和庞大的组件库;相对于原始纯WIN32开发更快捷,用户可根据需要、项目规模和功能需求选择该项目作为开发界面库。
uilib应为duilib原身,整体结构一致;相对duilib,uilib会多一些控件如动画、图表、GIF动画等,CDxAnimationUI、CDuiTimer、CAnimationTabLayoutUI、以及其他的工具、
托盘CDuiTrayIcon、CDuiAutoComplete、阴影CShadowUI等等,不过在使用和知名度上duilib更为广泛,可根据需要移植uilib的部分实现到duilib以满足需要,在项目中建议使用duilib。