首页 / 资讯中心 / 文章详情

VS2019下DuiLib入门:从空项目到XML窗口示例

VS2019下DuiLib入门:从空项目到XML窗口示例 ★ FEATURED ARTICLE
前阵子一个朋友拉着我看个内部小工具VS2019环境界面代码清一色DuiLib的CWindowWnd和CPaintManagerUI。这库虽然老但存量项目实在太多指不定哪天就得接手。与其每次翻老工程配环境不如自己从VS2019的WIN32空项目开始搭一个DuiLib简单示例把工程配置、窗口骨架、XML布局完整走一遍。这篇文章就是这份实操记录给那些同样被存量DuiLib代码困住的人一个可以直接照抄的起点。1. 先说说DuiLib是什么什么场景还需要它1.1 DirectUI思路用一个窗口撑起整个界面DuiLib全称DirectUI Library核心思路和传统Win32开发完全不一样。传统Win32程序里窗口上放一个按钮系统就真的创建一个按钮子窗口背后对应一个HWND。控件一多窗口句柄、消息传递、自绘样式就会变得非常繁琐。而DuiLib的思路是整个程序只有一个真正的HWND界面上你能看到的Button、Label、Edit全部是绘制出来的逻辑控件并没有自己的窗口句柄。这个设计带来的直接好处是资源占用低、渲染灵活、消息处理集中。所有鼠标键盘事件先到这个唯一窗口再由CPaintManagerUI管理的一棵控件树按命中测试分发下去。打个比方传统控件像一栋楼里每户都有自己的户口本都自己处理自己的事DuiLib像一个集体宿舍所有事情统一报到值班室再按房间号分发。前者规范后者轻量且好统一做样式。1.2 现在还在用DuiLib的场景和它的明显短板这几年C桌面端新项目基本是Qt、WinUI、wxWidgets的天下DuiLib不太可能被选进新项目。但有两类场景你大概率还会碰到存量软件维护很多工控上位机、医疗设备客户端、内部管理系统界面是多年前用DuiLib写的。需求改动不大但要能编译、能跑、能小改。轻量工具开发想给内部写个小工具不指望华丽效果只希望界面比原生Win32控件好看一点、布局方便一点DuiLib反而比Qt轻得多一个几MB的exe就搞定了。它的问题也很明确社区活跃度低文档稀缺高DPI适配不理想登录框这类复杂表单要自己画逻辑。如果你手头有DuiLib的维护任务先接受它是个能干活但不精致的库后面很多心态问题都能避免。2. VS2019空项目工程配置从向导到链接DuiLib库2.1 创建空项目和整理DuiLib源码在VS2019里创建新项目搜索Windows桌面向导然后选择空项目。注意VS2019已经找不到老的Win32项目模板名字了Windows桌面向导就是它的后继者。向导里勾选空项目会得到一个干干净净没有main函数的WIN32空工程。然后需要一份DuiLib的源代码。我的习惯是放在解决方案目录下的third_party/duilib文件夹里和主工程保持清晰边界。拿到代码后第一件事不是急着编译而是先看DuiLib.h顶部的宏定义。DuiLib不同分支的导出方式不太一样有的版本默认定义UILIB_STATIC按静态库方式使用有的需要你手动编DuiLib.dll使用方定义导入宏。你先确认这一点后面链接报错时能少走很多弯路。2.2 我推荐的工程组织方式单独编一个DuiLib静态库直接在主工程里把DuiLib所有.cpp拉进来确实最快但工程文件会非常混乱以后升级换版本也麻烦。更稳妥的方式是单独建一个DuiLib静态库工程编译出DuiLib.lib主工程只负责链接。组织方式做法优点缺点适用场景直接编进主工程把所有cpp加入主工程配置简单调试方便工程混乱编译慢一次性Demo单独静态库建DuiLib库工程主工程链接lib边界清晰主工程干净多一道引用配置长期维护正式项目选静态库方案时在解决方案里新建一个静态库项目把DuiLib的源码文件全部加入然后注意三件事项目平台选x86老代码在x64下偶尔有兼容问题示例阶段先用x86最稳。预编译头设置为不使用因为DuiLib源码不会去包含stdafx.h默认开着会报C1010错误。运行库设置为多线程(/MT)Debug对应多线程调试(/MTd)要和主工程保持一致。编译通过后生成DuiLib.lib这个库的配置就算结束了。2.3 主工程属性里最容易漏掉的两项入口点与运行库回到主工程右键打开属性页按下面几条配置常规 - 配置类型应用程序常规 - 字符集使用Unicode字符集C/C - 常规 - 附加包含目录添加$(SolutionDir)third_party\duilibC/C - 预处理器 - 预处理器定义加UILIB_STATIC如果版本默认已定义则不加链接器 - 常规 - 附加库目录添加DuiLib.lib所在目录链接器 - 输入 - 附加依赖项填写DuiLib.lib链接器 - 系统 - 子系统Windows (/SUBSYSTEM:WINDOWS)C/C - 代码生成 - 运行库Debug用/MTdRelease用/MT很多人在这一步卡住症状是链接时报unresolved external symbol _main referenced in function ___tmainCRTStartup。原因是空项目默认子系统是控制台CRT启动代码去找main入口而你的代码写的是WinMain或wWinMain。把子系统改成Windows后这个错误立刻消失。如果改成Windows子系统后仍报入口相关错误可以在链接器 - 高级 - 入口点里手动填wWinMainCRTStartup。通常不填也行但填上更保险。3. 空窗体是如何立起来的DuiLib窗口骨架代码3.1 WinMain入口初始化、创建窗口、进入消息循环DuiLib的入口函数和普通Win32程序一样只是后续逻辑都围绕它自己的类来组织。一个最简化的WinMain形如下面这段代码#include windows.h #include tchar.h #include DuiLib.h using namespace DuiLib; int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPTSTR /*lpCmdLine*/, int nCmdShow) { CPaintManagerUI::SetInstance(hInstance); CPaintManagerUI::SetResourcePath(_T(skin)); // 这里的CMainWindow到下一节再具体展开 CMainWindow* pWindow new CMainWindow(); pWindow-Create(NULL, _T(DuiLib空项目示例), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE); pWindow-CenterWindow(); pWindow-ShowWindow(nCmdShow); CPaintManagerUI::MessageLoop(); return 0; }SetInstance就是让DuiLib记住当前模块句柄加载资源和窗口类时都要用到。SetResourcePath设置资源目录告诉DuiLib皮肤、XML布局文件去哪个文件夹找这里设置的是exe同目录下的skin子目录。Create来自CWindowWnd类它会自己注册窗口类并创建真正的HWND。最后MessageLoop是DuiLib封装的消息循环和标准GetMessage循环作用类似但会额外处理DuiLib内部定时器、焦点切换等消息。3.2 窗口类CWindowWnd、INotifyUI、CPaintManagerUI 的分工DuiLib写一个窗口绕不开三个类的分工CWindowWnd窗口外壳负责创建HWND、注册窗口类、接管Win32消息最终把消息转交给CPaintManagerUI。CPaintManagerUIUI管理器整个控件树的大脑。它持有所有控件指针负责绘制、布局、事件分发、焦点管理。INotifyUI通知接口控件产生点击、选择、鼠标移入等事件时会回调到继承该接口的类上。主窗口类的典型写法如下class CMainWindow : public CWindowWnd, public INotifyUI { public: CMainWindow() {} virtual ~CMainWindow() {} virtual LPCTSTR GetWindowClassName() const { return _T(DuiLibSimpleDemo); } virtual void Notify(TNotifyUI msg) { // 控件事件统一在这里处理 } virtual void OnFinalMessage(HWND /*hWnd*/) { delete this; } virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { if (uMsg WM_CREATE) { m_pm.Init(m_hWnd); // XML加载逻辑后面补 return 0; } LRESULT lResult 0; if (m_pm.MessageHandler(uMsg, wParam, lParam, lResult)) { return lResult; } return CWindowWnd::HandleMessage(uMsg, wParam, lParam); } protected: CPaintManagerUI m_pm; };GetWindowClassName返回窗口内部类名用来区分不同窗口模板。OnFinalMessage在窗口销毁后回调因为窗口对象是用new创建的所以这里delete this否则每次关闭窗口都泄漏一份内存。HandleMessage相当于是窗口过程的C封装你不需要自己写Win32窗口过程函数。3.3 从HandleMessage到MessageHandler的流转路径很多人第一次看这段代码会问为什么HandleMessage里要把消息先交给m_pm.MessageHandler因为DuiLib的窗口过程收到消息后要先让CPaintManagerUI有机会处理。比如鼠标在某个控件上按下CPaintManagerUI需要命中测试找到被点到的控件然后把按下事件发给那个控件。如果CPaintManagerUI判断某个消息它不需要处理返回false才轮到CWindowWnd::HandleMessage做常规处理比如关闭窗口、系统命令等。这个顺序不能反否则控件就接收不到鼠标和键盘事件了。这里有一个调试要点当界面表现异常时可以在这两个方法里打断点观察消息到底是被CPaintManagerUI消费掉了还是走了默认窗口过程。这条消息流是整个DuiLib运行机制的主干搞懂它很多界面问题都能定位。4. 加上XML和事件联动一个能点击的示例4.1 main.xml布局用VerticalLayout和Button把界面搭出来DuiLib的界面布局通过XML描述这套描述语言理解成本不高。在skin目录下创建main.xml内容如下?xml version1.0 encodingutf-8? Window VerticalLayout bkcolor#FFF0F0F0 inset10,10,10,10 Label namelabelHello text这是一个DuiLib示例 aligncenter height60 / HorizontalLayout height30 Button namebtnHello text点我试试 width100 height28 / Button namebtnExit text退出 width100 height28 / /HorizontalLayout /VerticalLayout /WindowXML里最外层固定是Window里面可以放各种布局容器和控件。VerticalLayout是垂直布局子控件从上到下排列HorizontalLayout是水平布局子控件从左到右排列。这样一组合就能搭出常见的上下结构界面。bkcolor是背景色inset是内边距顺序是左、上、右、下。Label的aligncenter表示文字水平居中text属性设置初始文本。Button的name属性非常重要它是C代码侧查找控件的钥匙后面事件处理全靠它。4.2 C侧的处理Notify、FindControl、SetText有了XML界面后回到CMainWindow类把XML加载进窗口并处理按钮点击事件virtual void Notify(TNotifyUI msg) { if (msg.sType _T(click)) { if (msg.pSender-GetName() _T(btnHello)) { CLabelUI* pLabel static_castCLabelUI*( m_pm.FindControl(_T(labelHello))); if (pLabel) { pLabel-SetText(_T(Hello, DuiLib!)); } } else if (msg.pSender-GetName() _T(btnExit)) { Close(); } } }在HandleMessage的WM_CREATE分支里加载XML的代码补上if (uMsg WM_CREATE) { m_pm.Init(m_hWnd); CDialogBuilder builder; CControlUI* pRoot builder.Create( _T(main.xml), (UINT)0, this, m_pm); if (pRoot ! NULL) { m_pm.AttachDialog(pRoot); } return 0; }这段代码的意图是通过CDialogBuilder解析main.xml生成一棵CControlUI控件树然后通过AttachDialog把控件树挂到当前窗口上。builder.Create的第三个参数传this表示后续控件通知都发给当前窗口的Notify方法。事件处理逻辑里msg.sType是事件类型DuiLib常用事件有click、mouseenter、mouseleave等。msg.pSender是被点击的控件指针通过GetName()和XML里的name属性对应。FindControl按名字在控件树里查找找到后直接强转成目标类型调用SetText更新文本。这是DuiLib里最典型的交互写法。4.3 编译运行后的预期效果与自我验证把代码补齐后编译运行正常情况会出现一个灰色背景的窗口窗口标题是DuiLib空项目示例中间一行文字这是一个DuiLib示例下方有两个按钮。点击点我试试上方文字变成Hello, DuiLib!点击退出窗口关闭。如果点击按钮没反应按顺序检查三件事main.xml里的name和C代码里的字符串是否完全一致大小写也要一致。main.xml是否真的被复制到了exe同目录的skin文件夹下。Notify函数是否被正确绑定可以在函数开头打断点确认事件是否到达。窗口能弹出来但看不到文字优先怀疑XML文件编码。用VS2019打开main.xml另存为时选UTF-8带签名的编码中文乱码问题基本都能解决。5. 配置过程中容易卡住的几个坑5.1 链接报错LNK2038/2019运行库与导入导出宏不一致从空项目开始配置DuiLib最容易遇到两类链接错误。一类是LNK2038: mismatch detected for RuntimeLibrary。字面意思是主工程和DuiLib库工程使用的运行库不一致。一个用/MD一个用/MTCRT实现就产生了两个副本链接器认为这不是同一个运行时。解决方法很无脑让两边Debug都选/MTdRelease都选/MT重新编译DuiLib库再链接。另一类是LNK2019: unresolved external symbol指向某个DuiLib类的方法无法解析。原因大多是你在预处理器里没有定义UILIB_STATIC但链接的却是静态库或者反过来头文件默认要导入DLL你没有提供DuiLib.dll的导入库。解决办法就是回到2.3节看DuiLib.h里是怎么判断的让预处理器定义和你的库组织形式保持一致。5.2 XML文件找不到、中文乱码、资源路径对不上XML加载失败时窗口不是报错而是一个空白窗口或者干脆不显示内容。这是因为CDialogBuilder::Create返回NULL时代码没有分支去提示你原因。SetResourcePath(_T(skin))里的路径是相对exe所在目录的不是相对项目目录。从VS里按F5调试时exe在x64\Debug或Debug目录下所以你必须把skin目录也复制过去。两种做法在工程属性 - 生成事件 - 后期生成事件命令行里写xcopy /Y /E $(ProjectDir)skin $(OutDir)skin或者把main.xml加入工程属性里设置复制到输出目录-如果较新则复制。我推荐用xcopy命令整个目录一次搞定以后再往skin里加图片也不用挨个设属性。设置完成后按F5运行检查exe输出目录下是否真的出现了skin\main.xml这是排查此类问题最直接的方式。5.3 高分屏下DuiLib界面发虚的实际体验这个坑不是配置问题而是DuiLib本身局限。在缩放比例不是100%的显示器上老版DuiLib的GDI绘制会被Windows强行位图拉伸界面文字和边缘明显发虚。Windows 10/11下缩放150%时特别明显。想彻底解决得找一个对高DPI做过适配的分支版本。但如果是老项目里固定使用的某个DuiLib版本就别折腾了。实际维护中的常见做法是给exe加系统DPI感知清单让系统不要对它做位图缩放代价是程序内部不感知DPI变化在某些分辨率下控件比例会不太协调。如果你的工具只在固定机器上跑最简单就是建议用户把显示缩放调到100%或100%以上效果反而最干净。5.4 调试DuiLib事件流的小技巧DuiLib资料少网上能搜到的经验往往过时。我的经验是不要只靠猜直接在源码里打断点看事件流。两个最值得下断点的位置CPaintManagerUI::MessageHandler所有Windows消息进入DuiLib的地方。CControlUI::DoEvent控件处理具体事件的地方鼠标点击、键盘输入都会经过这里。当某个按钮点击没反应时先看MessageHandler是否接到的消息再看消息分发后是否走到了对应控件的DoEvent最后看Notify有没有回调。三步走下来基本能确定是消息没进DuiLib、还是命中了错误控件、还是事件回调没写到。这种断点排查思路比到处查资料有效得多因为DuiLib的源码就是它最真实的文档。这套流程走下来我最大的体会是DuiLib的坑说来说去就那么几个字符集、运行库、资源路径、导出宏。一旦你用空项目亲手配过一次把配置步骤和目录结构记熟后续再做类似工具反而会觉得比那些重量级框架更省事。现在这个模板我已经存了好几份哪个内部小工具需要界面就直接拷过去改XML和Notify逻辑。如果你也在维护老DuiLib项目建议先搭好这样一套干净的最小工程以后排查问题时能少走很多弯路。
阅读完成 · 觉得有帮助?
咨询建站