尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

DirectUI for VC6.0:老工程焕新界面的自绘界面库实战解析

DirectUI for VC6.0:老工程焕新界面的自绘界面库实战解析 简介面向Visual C 6.0开发者的开源DirectUI界面库资源旨在解决传统Win32界面难以美化和交互体验不足的问题帮助程序员快速构建现代感GUI且兼容VC2015、VC2019等升级环境。资源包共453个文件、约8.35MB核心以h头文件和cpp源文件为主辅以bmp/png皮肤图片、xml样式配置、编译好的lib库和dll动态库便于链接运行与二次定制。目前已有343人学习下载。包内含完整src源码、lib库、document文档、dui主体实现和include接口声明既能直接集成使用也能深入剖析组件化设计、事件驱动机制、GDI绘图渲染、布局管理和皮肤系统等关键知识点。作为开源包开发者可自由修改分发尤其适合需要自绘控件或优化复杂界面性能的中高级VC开发者使用。1. 项目背景与技术定位1.1 这个“老古董”到底是什么DirectUI for VC6.0(开源).rar光看文件名就带着一股浓烈的年代感。它不是一个新潮的跨端框架也不是什么AI驱动的界面引擎而是一个面向 VC6.0 环境的 DirectUI 界面库以开源形式发布。那个时期的 DirectUI通俗点讲就是“不依赖 Windows 标准控件全部界面元素由程序自己绘制”的一套界面解决方案。为什么要自绘如果你在 XP 或者更早的年代用 VC6 写过 MFC 程序一定体会过那种痛苦Button是灰扑扑的方块Edit是白底黑字的系统文本框ListCtrl更是丑得让人不想直视。想换肤只能自绘。自绘一个按钮还好说自绘整个界面、还要处理各种状态切换和交互逻辑那就不是简单的事了。DirectUI 的意义就在于它把这套“全自绘”的底层逻辑封装好了让你在 VC6 下也能写出类似 QQ 皮肤、360 界面那样的现代 UI。适用人群很明确需要在老工程里升级界面、又不想引入庞大依赖的 VC6 用户对 Windows UI 自绘原理感兴趣、想研究历史的同学以及那些维护十多年前遗留代码、被业务方要求“把界面改好看点”的倒霉程序员。1.2 为什么专门点名 VC6.0现在的年轻人可能不太理解为什么一个库要专门为 VC6.0 做适配。VC6.0 是 1998 年发布的编译器对 C 标准的支持停留在很早期的水平模板、STL 的支持都有不少坑。能在这个编译器下编译通过的界面库意味着代码必须遵循那个时代的 C 规范内存管理要靠手写字符串要用CString或裸指针容器得自己实现或者依赖 MFC 的CArray、CMap。所以这个库的价值不只是“DirectUI”这四个字更是“在 VC6.0 这个受限环境下依然能用 DirectUI 思想搭建出完整界面”的工程能力。它等于是在告诉你不用为了界面好看去升级整个工程也不用抛弃 VC6 生态你手上已有的代码资产可以继续用UI 层单独切到 DirectUI 上就行。2. DirectUI 核心原理拆解2.1 单窗口 自绘的基本模型DirectUI 和传统 MFC 界面最大的区别在于窗口模型。传统方式下一个按钮就是一个HWND一个编辑框又是一个HWND整个界面是一棵窗口句柄的树。DirectUI 反过来整个界面只有一个真正的HWND通常是一个全屏覆盖的窗口所有控件比如按钮、编辑框、列表项都是内存中的“逻辑对象”它们没有独立的窗口句柄只是在这个唯一的窗口上画出来的。这个模型带来的优势很明显。第一个是省资源。每个HWND都要占用内核对象界面复杂一点几百个控件就要几百个句柄而 DirectUI 不管界面多复杂始终只有一个句柄。第二个是无缝换肤。因为所有的绘制都是自己控制的什么颜色、字体、图片全部由代码决定不像标准控件那样要发WM_CTLCOLOR消息去“求”系统让你改颜色。第三个是绘制效率高。通过双缓冲技术整个界面的绘制可以做到一次BitBlt完成基本不会闪烁。2.2 消息分发机制DirectUI 需要把 Windows 的消息机制“翻译”成自己的事件体系。窗口只接收原始的WM_LBUTTONDOWN、WM_MOUSEMOVE、WM_PAINT等消息然后根据鼠标坐标去命中“逻辑控件树”中的具体控件把消息转成对应的MouseDown、MouseMove、Paint事件。这个过程很像浏览器里的事件冒泡机制消息先到根节点再根据命中测试派发给子节点。这里最核心的是命中测试HitTest。因为所有控件都不是独立的窗口你需要自己知道鼠标点的位置落在哪个控件的矩形范围内。控件树一般是树形结构根节点是CContainerUI这样的容器控件子节点是具体的按钮、标签、图片控件。命中测试时从根开始逆序遍历子节点因为后绘制的控件在上面所以也要先判断是否命中更上层的控件。这个逻辑虽然不难但边界情况特别多如果两个控件重叠怎么办如果控件设置了圆角命中范围要不要也变成圆角这些都是实现时要考虑的地方。2.3 布局系统DirectUI 的布局和传统 UI 也不一样。它支持相对布局、绝对布局、线性布局等模型。控件不再像 MFC 那样用SetWindowPos一个个摆位置而是在容器的DoLayout回调里按比例或者按规则排布。这个库在 VC6.0 下实现布局的方式我猜是基于 XML 描述界面结构也就是将控件树以 XML 形式定义运行时候加载并创建对应控件对象。这样做的好处是界面和逻辑可以分离设计师改布局时不需要动 C 代码。在那个年代这算非常先进的做法了。虽然和现在的 QML、XAML 比不了但基本思路是一脉相承的。3. 在 VC6.0 中上手这个库的实操要点3.1 工程配置与引入方式拿到压缩包解压后我的习惯是先看目录结构。一般这种开源库都会包含src、include、demo这几个目录。把这个库加入 VC6.0 编译通常有两种做法一是把源码直接加入工程一起编译二是编译成静态库或 DLL然后在自己的工程里链接。我建议你选择“源码直接加入工程”的方式。原因有三一是 VC6.0 对 DLL 的导出支持没有现在这么顺手一不留神就出导出符号混乱的问题二是直接编译源码调试时可以单步进入库内部对理解实现细节帮助很大三是这个库本身不大全部源码编译时间也就几秒钟完全不值得为了省这点时间去做成本更高的封装。注意事项确认工程设置为使用“多字节字符集”而不是 Unicode。VC6.0 时代很多库都还不支持 UnicodeDirectUI 这类库内部的字符串处理多半是基于char*的。把库的include目录加进“工具 - 选项 - 目录 - Include files”或者直接在你的工程设置里的预处理器设置中指定完整路径。如果编译报错提示某个头文件找不到多半是路径问题检查#include的相对路径或者全局搜索一下实际文件位置。3.2 写一个最小 Demo 的生命周期在 VC6.0 里跑通这个库核心就三步初始化库、创建主窗口、启动消息循环。下面是我习惯写的最小骨架#include UIlib.h using namespace DuiLib; class CMyWnd : public CWindowWnd { public: CMyWnd() {} LPCTSTR GetWindowClassName() const { return _T(DuiDemoWindow); } void Init() { // 从 XML 布局文件创建界面 m_PaintManager.Init(m_hWnd); m_PaintManager.LoadFromXML(_T(demo.xml)); } LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { if (uMsg WM_CREATE) { Init(); return 0; } return CWindowWnd::HandleMessage(uMsg, wParam, lParam); } private: CPaintManagerUI m_PaintManager; }; int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPSTR /*lpCmdLine*/, int nCmdShow) { CPaintManagerUI::SetInstance(hInstance); CMyWnd wnd; wnd.Create(NULL, _T(DirectUI Demo), WS_VISIBLE | WS_POPUP | WS_CAPTION, 0, 0, 800, 600); wnd.ShowWindow(true); CPaintManagerUI::MessageLoop(); return 0; }这段代码的关键点有几个。CPaintManagerUI::SetInstance(hInstance)必须在创建任何窗口之前调用它是整个 DirectUI 的“总管”负责消息分发、绘制刷新、控件树管理。忘掉这一步后面基本是黑屏一片。CWindowWnd是 DirectUI 对主窗口的封装。它内部会处理WM_PAINT、WM_ERASEBKGND、WM_SIZE这些基础消息你不需要自己再去调用BeginPaint、EndPaint也不用担心闪烁问题——CPaintManagerUI内部已经做了双缓冲。LoadFromXML(_T(demo.xml))是典型的基于 XML 的描述性布局方式。你需要在程序工作目录或者资源中准备好这个 XML 文件里面声明了窗口上放什么控件、放在哪、有多大。这个 XML 非常像 HTML 的结构标签名对应控件类名属性对应控件的属性。比如一个按钮Button namebtnClose text关闭 width80 height30 pos700, 20, 780, 50 /3.3 XML 布局文件与控件树XML 是 DirectUI 的灵魂。它让界面和代码分离也让你可以不用重新编译就能调整布局。不过要提醒一句这个库是那个年代的产物它的 XML 解析器实现得比较简陋对格式要求很严格比如标签属性必须用双引号不能有未转义的符号注释格式也要规规矩矩。我当年试过在 XML 里写一个中文注释结果解析直接报错崩溃花了好长时间才排查出来。一个典型的 XML 布局例子如下Window size800,600 caption0,0,0,35 roundcorner8,8 Container HorizontalLayout pos5,40,795,595 VerticalLayout width150 Button namebtnHome text首页 height30 / Button namebtnSettings text设置 height30 / /VerticalLayout VerticalLayout pos155,0,795,595 RichEdit nameeditContent bordercolor#FF0000 / /VerticalLayout /HorizontalLayout /Container /Window这里的Window标签是根节点size属性指定窗口大小caption指定标题栏区域roundcorner指定圆角。Container是容器HorizontalLayout和VerticalLayout是布局控件用来做水平排列和垂直排列。pos属性是控件的坐标范围形式为左,上,右,下。控件之间的父子关系就是这个 XML 的层级关系。CPaintManagerUI在解析 XML 时遇到一个子节点就在内存中创建一个对应的控件对象然后挂到父节点的子列表里。4. 核心源码阅读路径与自定义控件4.1 整个源码最值得看的三块这个库不是拿来跑通就算完的阅读它的源码能学到很多底层技巧。我强烈推荐你先从这三块下手第一是CPaintManagerUI的消息处理循环。打开源码找到CPaintManagerUI::MessageHandler你会看到一个超长的switch-case里面处理了WM_PAINT、WM_SIZE、WM_LBUTTONDOWN、WM_MOUSEMOVE、WM_KEYDOWN等等消息。这是理解 DirectUI 消息分发机制的关键。注意看它怎么用FindControl和GetControlAtPoint做命中测试以及怎么用m_bUpdate标志来控制是否立即重绘。第二是控件基类CControlUI。它里面有Paint、DoPaint、Event、SetPos这组虚函数是每个自定义控件都要重写的核心方法。Paint是绘制入口DoPaint是真正绘制的地方Event是事件处理SetPos是布局。熟悉了这组接口整个库的扩展思路就通了。第三是CControlUI::DoPaint内部的双缓冲逻辑。你会发现它并不是直接把内容画到窗口 DC 上而是先创建一个内存 DCCMemoryDC所有控件先往内存 DC 上画最后一次性BitBlt到窗口 DC。这就是为什么 DirectUI 界面怎么拖拽都不闪烁的根本原因。4.2 自定义一个带图标按钮的完整步骤在 DirectUI 里写自定义控件比在 MFC 里自绘要舒服得多。MFC 自绘一个按钮你要处理WM_PAINT、WM_MOUSEHOVER、WM_MOUSELEAVE、WM_LBUTTONDOWN、WM_LBUTTONUP等各种消息还要手动判断各种状态。DirectUI 把这些状态机封装好了你只需要关注绘制逻辑。这里以一个简单的带图标按钮为例说说完整步骤。第一步继承CButtonUI因为要复用按钮的状态逻辑class CIconButtonUI : public CButtonUI { public: CIconButtonUI(); virtual ~CIconButtonUI(); void SetIcon(LPCTSTR lpszFile) { m_sIconFile lpszFile; } LPCTSTR GetIcon() const { return m_sIconFile.GetData(); } virtual void DoPaint(HDC hDC, const RECT rcPaint); private: CDuiString m_sIconFile; };第二步重写DoPaint。这是所有绘制动作发生的地方。典型实现是先调用基类CButtonUI::DoPaint把按钮本身的背景和文字画出来然后再在指定位置绘制图标void CIconButtonUI::DoPaint(HDC hDC, const RECT rcPaint) { // 先绘制按钮自身的背景、边框、文字 CButtonUI::DoPaint(hDC, rcPaint); if (m_sIconFile.IsEmpty()) return; // 计算图标位置画在按钮右侧垂直居中 RECT rcIcon m_rcItem; int iconWidth 16; int iconHeight 16; int offsetX rcIcon.right - iconWidth - 8; int offsetY (rcIcon.bottom - rcIcon.top - iconHeight) / 2; // 加载图片并绘制 IImage* pImage m_pManager-GetImage(m_sIconFile); if (pImage) { pImage-Draw(hDC, offsetX, offsetY, iconWidth, iconHeight); } }这里有个非常关键的坑DoPaint里画图用的hDC是内存 DC不是窗口 DC。如果你把hDC直接保存起来做持久化操作或者对它调用GetWindowDC之类的函数一定会出错。正确的做法是只在这个函数的调用期间使用hDC所有绘制完成就放掉。第三步把自定义控件注册进CPaintManagerUI。因为 XML 解析器在遇到未知标签时需要通过名称找到对应的控件类。注册代码一般放在WinMain里初始化时CPaintManagerUI::RegisterCustomControl(_T(IconButton), CIconButtonUI::CreateControl);CreateControl是一个静态工厂函数返回控件实例CControlUI* CIconButtonUI::CreateControl(LPCTSTR pstrClass) { return new CIconButtonUI; }注册完之后XML 里就可以直接写IconButton ... /了。4.3 绘制中的字体与高DPI适配VC6.0 时代没什么高 DPI 的概念系统字体一般是 96 DPI所有控件坐标写死。放在现在的系统上如果你的程序不做 DPI 感知声明系统会自动给你做“模糊放大”看起来就和马赛克一样。如果你做 DPI 感知又必须处理所有控件坐标要和 DPI 成正比的问题。我的建议是老工程要用这个库就在程序的main或者WinMain里显式声明不感知 DPISetProcessDPIAware();或者在你的.manifest文件里声明asmv3:application asmv3:windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/dpiAware /asmv3:windowsSettings /asmv3:application这样系统就不会对窗口做拉伸模糊不过控件的字号可能会显得很小。最稳妥的办法还是在 XML 里所有控件尺寸都预留足够余量不要写死成正常字体都放不下的最小尺寸。这类问题在这类老库上非常常见如果你要发布到新系统上务必提前测试。5. 常见问题与排查技巧实录5.1 控件点击没反应命中测试失效这是 DirectUI 新手最容易踩的坑。表现出来是窗口画出来了控件也能看到但鼠标点上去按钮不触发事件也没有高亮效果。我排查这类问题的第一反应是检查pos属性是否设了。DirectUI 控件的命中测试依据的是控件在内存中的矩形区域m_rcItem。这个矩形是在布局阶段通过SetPos赋值的。如果你在 XML 里写了控件但没有给pos或者布局容器没有正确计算子控件的位置控件的矩形就是空的(0, 0, 0, 0)命中测试自然永远打不中。还可以打开调试器在命中测试函数里下断点查看pt坐标和控件矩形的关系。如果坐标在矩形内但还是打不中那就要检查事件冒泡是不是被某个上层容器的SetMouseEnabled(false)拦截了。5.2 中文乱码VC6.0 的源码文件默认是 ANSI 编码也就是 GBK。如果你用现在的编辑器比如 VS Code 或 Notepad打开源码并保存成了 UTF-8再回到 VC6 编译所有中文字符串都会乱码界面上的按钮文字全是乱字。解决办法很简单源码文件统一用 ANSI 编码保存。在 VS Code 里可以配置files.encoding为gbkNotepad 里直接“转为 ANSI 编码”。XML 布局文件也一样保持 ANSI 编码即可。如果不小心存成了 UTF-8至少在 XML 文件头部加?xml version1.0 encodingGBK ?但解析器是否支持还得看具体实现不如直接统一编码来得稳妥。5.3 窗口闪烁或者背景残影虽然 DirectUI 是双缓冲但如果你在某个环节绕过了CPaintManagerUI的绘制流程比如直接在消息处理里调用InvalidateRect又或者调用了SetLayeredWindowAttributes做半透明就可能出现诡异的闪烁和残影。还有一个常见来源是WM_ERASEBKGND。DirectUI 已经处理了这个消息并返回 1告诉系统不需要擦除背景。但如果你在HandleMessage里不小心把它传给默认处理函数系统就会用窗口类背景画刷把窗口刷一遍导致视觉上的闪白。排查时可以全局搜索一下WM_ERASEBKGND确认没有双重处理。5.4 控件数量多了以后变卡CPU 单核时代的老库对性能的最主要要求是“别频繁重绘”。如果你发现鼠标移动时整个界面都在重绘那是典型的重绘范围过大的问题。DirectUI 的CPaintManagerUI内部有一个“脏矩形”机制只重绘变化的区域。但如果某个控件在每次消息循环里都调用Invalidate整个界面就会跟着重绘。针对这种性能问题可以调整绘制策略在Update时主动判断控件是否真的需要重绘if (!m_bUpdate) { // 仅在需要更新时重绘 m_bUpdate true; Invalidate(); }还有就是避免在DoPaint里做耗时操作比如加载图片、创建字体。这些应该提前在Init阶段加载然后缓存起来。每次绘制都重新加载图片简直是性能杀手。6. 老库落地新系统的一些体会这些天重新翻这个库的源码让我想起当年用 VC6 一行行调界面布局的日子。这个库在界面表现力上放在今天看确实朴素但它的架构思路和现在很多渲染引擎是相通的。即便你现在不做老工程读读它的消息分发、双缓冲和控件树体系对理解桌面应用 UI 框架也很有帮助。如果用一句话总结我的经验别把这个库当成品工具要当教材看。它能给你的最大价值是让你吃透“窗口句柄不是必须的、绘制可以全权接管、布局可以用描述性语言定义”这三个现代 UI 框架都在用的基本思路。看完它的实现你再看 XAML、QML 甚至前端 DOM 树都会有豁然开朗的感觉。最后补充一个非常实用的小技巧如果你要维护老工程但不想手动写 XML可以用代码动态创建控件树。也就是不用LoadFromXML而是直接new CButtonUI()设置属性然后Add到容器里。混用可以但别一部分用 XML 一部分用代码写后面会乱到你怀疑人生。最好整体保持一种方式要么全 XML要么全代码别来回换。本文还有配套的精品资源点击获取
返回列表