目录
3.2、基本控件CLabelUI、CButtonUI和CEditUI等
3.4、布局类:CVerticalLayOutUI、CHorizontalLayoutUI和CTileLayoutUI
C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.htmlWindows C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_2276111.html 我们的软件引入开源的UI界面库Duilib已经有一段时间了,通过项目实践已经对Duilib开源库有了进一步的理解和认知,积累了相关的开发经验。本文从Duilib的基础入手,并对消息流程,控件关系以及Duilib的整体框架等方面进行详细讲解。
1、Duilib简介
Duilib是一个基于DirectUI设计的开源UI界面库,专为Windows平台设计,遵循BSD协议,可免费用于商业项目。Duilib的设计初衷是简化UI界面开发流程,提高开发效率,通过提供一系列预定义的控件和强大的界面绘制能力,使得开发人员能够轻松构建高效、绚丽且易于扩展的界面应用程序。
Duilib界面库的出现解决了使用传统MFC界面库开发软件不美观、界面细节不好处理、开发效率低下、生成程序体积大等一系列问题。
Duilib其简约易扩展的设计以及稳定高效的实现被广大公司普遍接受和认可,广泛应用于多种PC客户端软件的开发中,包括IM(即时通讯)、视频客户端、股票行情软件、导航软件、手机辅助软件、安全软件等多个行业,比如ZOOM视频会议客户端、PC版微信、网易易信、爱奇艺PPS客户端、百度网盘客户端、酷我音乐、酷狗音乐、华为PC版手机助手等、2345好压等产品。
Duilib的主要特点包括:
- 开源免费:Duilib遵循BSD协议,允许用户自由使用、修改和分发源代码,无需支付任何费用。
- 界面与逻辑分离:Duilib通过XML和脚本构造界面,使得界面布局和风格描述与业务逻辑代码相分离,提高了代码的可维护性和可扩展性。
- 强大的界面绘制能力:Duilib基于GDI在窗口上自绘,无需依赖其他图形库或框架,能够很好地解决传统MFC界面库在美观性、界面细节处理等方面的问题。
- 丰富的控件和组件:Duilib提供了包括按钮、编辑框、文本框、组合框、滚动条、进度条、列表在内的多种预定义控件,同时也支持自定义控件的开发,可以很好地满足各种复杂界面的需求。
- 易于扩展和定制:Duilib的架构和设计理念使得它易于扩展和定制,用户可以根据自己的需求添加新的控件或修改现有控件的行为。
总之,Duilib是一款功能强大、易于使用且开源免费的DirectUI界面库,它通过提供丰富的控件和组件、强大的界面绘制能力以及界面与逻辑相分离的设计理念,极大地提高了用户界面的开发效率和质量。对于需要在Windows平台上开发高质量用户界面的开发人员来说,Duilib无疑是一个值得考虑的选择。
下面将详细介绍一下Duilib的总体框架及内部实现细节,以供大家借鉴或参考。
2、总体框架
DUILib由控件层和核心层两部分组成,具体功能划分如下:
控件层部分主要是UI绘制的各个控件,分为控件和容器两部分,容器是对控件的管理。全部控件均由核心层进行管理和绘制。
核心层包括UI的绘制、XML解析以及消息的管理。UI的绘制主要基于GDI接口,对于xml进行解析后调用UIDialogBuilder对窗体进行创建,通过CreateEx创建窗口句柄,进而对消息进行管理,完成界面响应。
3、UI控件
3.1、控件基类:CControlUI
CControlUI在整个控件体系中非常重要,它是所有控件的基类,也是组成控件树的基本元素,控件树中所有的节点都是一个CControlUI。该控件基类基本包括了所有控件公共的属性,如:位置,大小,颜色,是否有焦点,是否被启用等等。作为所有控件的基类,除了包含一些共性的东西之外,还提供了很多虚函数,子类控件可以去重新这些虚函数,以实现子类控件独有的效果和特性。
另外每个控件中还有几个事件管理的对象——CEventSource,这些对象会在特定的时机被触发,如OnInit,调用其中保存的各个回调函数。
对于控件树中的每一个节点,其类型都是CControlUI,但是其实这些节点可能是文字,可能是图像,也有可能是列表,于是DUI提供了CControlUI::GetInterface接口使不同控件指针之间可以获取。具体方法是,传入一个字符串,传出指向控件的指针:
LPVOIDCControlUI::GetInterface(LPCTSTRpstrName)
{
if(_tcscmp(pstrName,_T("Control"))==0) return this;
return NULL;
}
3.2、基本控件CLabelUI、CButtonUI和CEditUI等
Duilib实现了非常多的基本控件,主要有:
- CLabelUI(标签控件)/CTextUI(文本控件)/CEditUI(编辑框控件)/CRichEditUI(富文本框控件)
- CButtonUI(按钮控件)/CCheckBoxUI(勾选框控件)/CComboBoxUI(组合框控件)/COptionUI(选择按钮控件)
- CScrollBarUI(滚动条控件)/CProgressUI(进度条控件)/CSliderUI(滑动条控件)
- CListUI(列表控件)
- CDateTimeUI(日期控件)/CActiveXUI(ActiveX控件)/CWebBrowserUI(浏览器控件)
我们在构建UI界面时,主要使用上述控件,这和MFC库的中控件是类似的,但控件的种类没有MFC的多,如果需要特别效果的控件,则需要自己去实现。此外,duilib中的控件和MFC控件不同,MFC中的每个控件都是一个窗口,而duilib中的控件并不是一个窗口,都是绘制上去的效果。
MFC控件都是一个窗口,每个窗口都会占用若干个用于窗口绘制的GDI对象,而duilib控件不是窗口,不会像窗口一样占用GDI对象。当窗口中的控件很多时,MFC控件会占用大量的GDI对象,而duilib控件没有这个问题,这也是duilib界面库相对于MFC界面库的一个优势。以前我们在用MFC开发支持IM聊天的软件时,每个聊天窗口都会占用200多个GDI对象,当测试人员进行压力测试时打开接近50个聊天窗口时,整个程序的GDI对象总数就会达到1万个,就会达到Windows程序的GDI总数1万的上限,导致UI界面绘制异常,紧接着程序就会发生闪退。我们将界面库由MFC换成duilib后,每个聊天窗口占用的GDI对象就大幅下降了,就几十个了!
3.3、容器基类:CContainerUI
CContainerUI是对基本控件基类对象的管理容器,其内部用一个数组来保存所有的CControlUI的对象,后续的所有工作,就都是基于这个对象来进行的了。
这样在CContainerUI里面,主要实现以下几个功能:
- 子控件的查找:CContainerUI::FindControl。
- 子控件的生命周期管理:是否销毁(在Remove的时候自动销毁)/是否延迟销毁(交给CPaintMangerUI去一起销毁)。
- 滚动条:所有的容器都支持滚动条,在其内部会对键盘和鼠标滚轮事件进行处理(CContainerUI::DoEvent),对其内部所有的元素调整位置,最后在绘制的时候实现滚动的效果。
- 绘制:由于容器中有很多元素,所以为了加快容器的绘制,绘制的时候会获取其真正需要绘制的区域,如果子控件不在此区域中,那么就不予绘制了。
3.4、布局类:CVerticalLayOutUI、CHorizontalLayoutUI和CTileLayoutUI
除了基本控件之外,duilib为了方便大家对界面控件元素进行排列布局,提供了几个通用的布局,即垂直布局CVerticalLayOutUI、水平布局CHorizontalLayoutUI和平铺布局CTileLayoutUI等。
这些布局均继承于容器类CContainerUI,CContainerUI中抽象出了一些共性的东西。我们在编写xml去构建UI界面时,主要使用这几个布局去排布窗口中的控件去实现界面的排布,其中垂直布局CVerticalLayOutUI和水平布局CVerticalLayOutUI最为常用。不同的布局,其内部的控件排布方法是不同的(具体的控件排布控制可以查看布局类的SetPos接口),要根据界面的构成,划分出不同的区域,选择不同的布局。
在垂直布局CVerticalLayOutUI中,控件默认是在垂直方向上排布的,如下所示:
在水平布局CVerticalLayOutUI中,控件默认是在水平方向上排布的,如下所示:
在平铺布局CTileLayoutUI中,控件默认是一个方格一个方格排布的,比如企业微信的表情窗口中一个个表情格:
这些布局的排列特性,会专门的讲解,此处就不再赘述了。
在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)
专栏1:(该精品技术专栏的订阅量已达到480多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!欢迎订阅!)
C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的项目问题实战分析实例(很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!
专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!
专栏2:
C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795
常用的C++软件辅助分析工具有PE工具、Dependency Walker、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!
专栏3:
C/C++基础与进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html
以多年的开发实战为基础,总结并讲解一些的C/C++基础与进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域的多个方面的内容,同时给出C/C++及网络方面的常见笔试面试题,并详细讲述Visual Studio常用调试手段与技巧!
专栏4:
VC++常用功能开发汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/124272585
将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。
专栏5:
Windows C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.html
根据多年C++软件开发实践,详细地总结了Windows C++ 应用软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。
3.5、控件绘制
对于控件的绘制部分,主要通过类CRenderEngine完成,调用系统的GDI接口进行绘制。
CRenderEngine中几个重要的接口的实现过程如下:
1) LoadImage实现步骤:
- 读取文件,资源和zip包中图像文件数据到内存[通过HIWORD(bitmap.m_lpstr)区别从资源中或文件中加载MAKEINTRESOURCE。
- 创建DIB。
- 通过stbi_load_from_memory加载数据文件并转化后复制到DIB中。
- 返回TImageInfo结构。
2)DrawImage实现步骤:
- CreateCompatibleDC (创建内存设备句柄)
- SelectObject() (将位图选入内存设备句柄)
- AlphaBlend (alpha混合)
- BitBlt() or StretchBlt() (绘制到内存设备句柄)
- SelectObject() (将老的位图选入内存设备句柄)
- DeleteDC (释放内存设备句柄)
3)DrawImageString实现步骤:
- 分析标识字符串获取属性
- 获取图像数据
- 绘制图像
4、UI构建
我们在实现DUI界面时,主要是通过编写xml文件去构建的,xml中添加DUI控件,可以设置各个控件、容器出现的位置、大小、图片的加载效果、文字的颜色等一系列属性,同时去控制控件的排布位置号。
DUI框架通过对xml文件的解析,将xml中的控件都创建出来,并将xml中给各个控件配置的属性读出来,设置到对应控件类对象中,并将所有的控件类对象保存到内存列表中。当我们点击某个控件时,调用接口FindControl直接找到操作的是哪个控件,然后DUI框架对这个控件进行相应的处理。
4.1、XML解析
DUI对于xml的读入与解析,主要由CMarkup类来实现,对于每个xml节点的管理应用CMrakupNode类完成。
CMrakupNode是一个xml节点,记录了本节点的属性值以及与其他各节点的关系。DUI框架会根据CMrakupNode中记录的xml控件节点信息,去创建对应的DUI控件,并将xm了中设置的属性值保存到创建的DUI控件对象中。
4.2、XML配置说明
对于xml的配置下面给出一段源配置文件进行说明:
<?xml version="1.0" encoding="UTF-8"?><!—编译器类型说明 编码设置 -->
<Window size="800,600" mininfo="600,400" caption="0,0,0,32" sizebox="4,4,4,4">
<!-- 窗口的初始尺寸(宽800,高600)、窗口的最小尺寸(宽600,高400)、标题栏拖拽区域(高32)、可拖拽边框大小(这里添加sizebox后就可以拖拽边框调整大小了) -->
<VerticalLayout bkcolor="#FFF0F0F0" bkcolor2="#FFAAAAA0"> <!-- 整个窗口的背景色 -->
<HorizontalLayout height="32" bkcolor="#FFE6E6DC" bkcolor2="#FFAAAAA0"> <!-- 标题栏背景色 bkcolor、bkcolor2、bkcolor3分别是渐变色的三个值-->
<VerticalLayout /> <!-- 占空位,占据左边所有的空位-->
<VerticalLayout width="77"> <!-- 右边三个控件所占的宽度-->
<Button name="minbtn" tooltip="最小化" float="true" pos="0,5,22,24" width="23" normalimage=" file='SysBtn\MinNormal.bmp' " hotimage=" file='SysBtn\MinFocus.bmp' " pushedimage=" file='SysBtn\MinFocus.bmp' "/>
<!-- name="closebtn" 唯一标识按钮,其他按钮的name不能与其重复
tooltip="关闭" 就是那个提示条的文字
float="true" 代表按钮的位置是绝对定位,其位置由pos属性指定
pos="44,5,74,24" 代表按钮的位置矩阵,分别为矩阵左、上、右、下四个点 相对说在的如VerticalLayout 里面的位置 不是最大框位置
width="28" 代表按钮图片显示的宽度(这个可以不填,但是由于按钮图片没有做好,如果不填的话,图片会被拉伸有点失真)
normalimage 代表正常状态下按钮显示的图片路径
hotimage 代表鼠标移上去时,按钮显示的图片路径
pushedimage 代表鼠标点击按钮时,按钮显示的图片路径
如果pos属性放在后面,就会以pos为准,height属性放在后面就会以height为准
-->
</VerticalLayout> <!—每个对象要对应好,一个声明一个结束-->
</HorizontalLayout>
4.3、控件生成
对于界面是在xml中进行配置的,CMrakup负责进行xml的读入和解析,CMrakup对象是定义在类CDialogBuilder中的。在解析想xml时,遇到窗口控件,去创建对应的DUI控件,并将xml中设置的属性值保存到创建的DUI控件对象中。
CDialogBuilder内部实际上就是一个xml的解析,依次创建各式控件。对于基本控件和容器直接进行创建,对于属性直接进行读入和保存。除了创建控件,这个类还将一些可以复用的资源提取出来放入CPaintManagerUI中统一管理,如字体和图片等等。
DUI对象生成顺序:
- 全局资源, 如Image,Font等;
- Window的属性;
- 各个顶级控件, 典型的是一个Layout;
- 对各个控件递归地构造其子控件对象。
DUI控件属性的设置:
- 首先对控件施用Default属性;
- 如果控件对于某个属性定义了自己的值,那么就更新一下此属性.比如Font属性。
5、UI管理
CPaintManagerUI类是整个DUI的调度系统,通过此类完成了消息管理,控件管理等一系列操作。
在Duilib中,一个Windows的原生窗口和一个CPaintManagerUI一一对应。其主要负责如下几个内容:1、控件管理;2、资源管理;3、转化并分发Windows原生的窗口消息。
为了实现上面这些功能,其中有几个用于管理控件和资源的关键的数据结构:
- m_pRoot:保存根控件的节点
- m_mNameHash:保存控件名称Hash和控件对象指针的关系
- m_mOptionGroup:保存控件相关的Group,这个Group并不是TabOrder,他用于实现Option控件
- m_aCustomFonts:用来管理字体资源
- m_mImageHash:用来管理图片资源
这些结构基本都可以看作是一堆列表和Map,这样可以用其来实现控件和资源的管理了。
CStdPtrArray/CStdValArray/CStdString/CStdStringPtrMap类可以看做是简单的容器,此类包括Add等属性,可将新的成员记录的本地的map中集中管理。
6、消息传递
IMessageFileterUI和INotifyUI是两个用于消息传递的接口类,需要进行消息处理的类对象,都是从这个类继承。IMessageFileterUI主要处理窗口自定义消息,INotifyUI则用来处理控件通知消息(比如按钮按下通知等),最终在处理消息的类中,以类似于MFC的消息映射一样,将控制通知消息、窗口自定义消息,与对应的消息处理函数绑定起来:
在duilib中,用来表示窗口的最基础的类是CWindowWnd,在这个类中实现了如下基本的内容:
1)原生窗口的创建(CWindowWnd::Create)
2)最基本的消息处理函数(CWindowWnd::__WndProc)和消息分发(CWindowWnd::HandleMessage)
3)模态窗口(CWindowWnd::ShowModal)
duilib通过这个类,将原生窗口的消息分发给其派生类,最后传给整个控件体系。
6.1、duilib消息处理机制
我们先来看看CWindowWnd窗口基类的窗口处理过程函数:
- 1)创建:通过CreateEx最后一个参数传递类指针this, 在消息处理函数中对WM_NCCREATE消息进行处理, 通过SetWindowLongPtr(GWLP_USERDATA)进行保存,把窗口对象的指针同窗口句柄绑定起来
- 2)消息处理:在消息处理函数通过GetWindowLongPtr(GWLP_USERDATA)得到其类指针, 调用虚函数HandleMessage()相应其他消息。
- 3)销毁:消息处理函数中对WM_NCDESTROY消息进行处理, 调用原消息处理函数, 重置(GWLP_USERDATA), 调用虚函数OnFinalMessage()做最后处理。
一旦我们使用CWindowWnd类创建了窗口之后,消息就会通过CWindowWnd::HandleMessage进行分发,我们可以在此对原始的窗口消息进行处理。
一般情况下为了对UI的整体管理,CWindowWnd会将收到的消息直接转发给CPaintManagerUI,由CPaintManagerUI来对其进行默认处理。在CPaintManagerUI::MessageHandler()中,定义了CPaintManagerUI对于所有消息的处理方法,其内部会对很多窗口消息进行处理,并将其分发到对应的控件上去,完成消息响应。
其中有如下几点需要注意:
- 消息流程的主循环是: CPaintManagerUI::MessageLoop。
- Event: Duilib会把消息封装成Event, 通知给各个控件。
- Notify: 如果想在窗口类中处理一些控件消息, 就可以调用这个函数, 让控件通知窗口. 窗口类会在OnCreate时把自己注册到接收Notify的队列里面。
以鼠标点击事件的处理为例,其消息处理过程如下:
- CWindowWnd收到WM_LBUTTONDOWN消息后发给CPaintManagerUI,由此类进行具体处理。
- CPaintManagerUI调用FindControl(pt);查找当前鼠标位置的叶节点控件,递归操作(在控件树上面进行搜索)。
- 找到控件后调用pControl->Event(event);控件对消息进行相应的函数, 这里Button会把自己设置成pushed状态。
- 设置m_pEventClick = pControl;//PaintManager会把该控件记录为当前被点击的。
- CWindowWnd收到WM_LBUTTONUP消息后发给CPaintManagerUI,调用m_pEventClick的Event函数, 响应鼠标左键弹起的消息,同时调用函数SendNotify, 把Button自己被点击过的消息发送给主窗口, 主窗口进行响应。
对于窗口标准消息和窗口自定义消息,可以在窗口类中重写HandleMessage接口进行拦截处理;对于控件通知消息,可以在处理控件通知消息的类中重写Notify接口去拦截消息,对消息进行处理。
6.2、WM_PAINT窗口绘制消息
窗口收到WM_PAINT消息以及处理该消息的流程:
- CPaintManager::MessageLoop。
- 处理WM_PAINT消息。
- 调用Root(CContainerUI类型)控件的DoPaint函数。
- CContainerUI类型的DoPaint会首先绘制绘制自己, 然后调用各个子控件的绘制(这样的效果就是子控件展示在父控件上面)。
- CControlUI类型没有控件, DoPaint函数里面会调用一些控件自己的私有函数, 根据控件目前的状态, 完成绘制功能。
- 绘制顺序:PaintBkColor->PaintBkImage-> PaintStatusImage->PaintText ->PaintBorder。
- 调用关系: MessageLoop调用Root的DoPaint, 调用子控件的DoPaint, DoPaint调用RenderEngine的函数, 最底层就是windows的GDI函数了。