DirectUI for VC6.0实战:老工程界面换肤与性能优化
简介DirectUI for VC6.0 是一套开源的图形界面库主要服务于使用 VC6.0 及后续版本的桌面软件开发者解决传统窗口控件界面风格单一、绘制效率不高的问题面向有一定 C 基础的中高级开发者帮助快速构建美观且交互友好的用户界面。压缩包共包含453个文件大小约8.35MB内容涵盖大量源代码、头文件、库文件、位图与PNG图片、XML配置以及编译好的DLL等目录划分明确便于按需查阅。库内组件化设计、事件驱动、GDI绘图、布局管理、皮肤切换和性能优化等特性一应俱全开发者可以直接链接到旧版工程中快速搭建界面也可以通过阅读源码理解设计思路并自行扩展控件。目前已有345人浏览学习适合有一定 C 基础、希望提升桌面程序界面品质的开发者参考与使用。1. 拿到这个 DirectUI for VC6.0(开源).rar先别急着删文件我猜你点进这篇笔记十有八九是手头有个老工程要么是某工控上位机要么是某图像处理 Demo 的配套界面项目还锁在 VC6.0 里窗户纸还是那套 MFC 对话框加一排原生控件。客户突然提了个换肤需求你改自绘改到怀疑人生才想起来搜 DirectUI for VC6.0(开源) 这种东西。别急着否定这个包解决的核心问题就一句话用单个宿主窗口承载全部控件把几十个 HWND 换成一棵 C 对象树绘制、命中、消息全由自己说了算。代价是要自己补一套布局和分发逻辑换来的是换肤自由、控件样式自由以及把界面卡顿按在地上摩擦的空间。它适合两类人一类是想在老 VC6.0 工程里快速做出现代化界面的维护者另一类是故意想用最老的编译器学 DirectUI 原理的进阶学习者。这篇笔记就按“原理 → 编译落地 → 布局扩展 → 踩坑 → 性能优化”的顺序把它讲成一份可复现的作业。2. 认识 DirectUI 的两条主线控件对象树与单窗口消息分发2.1 传统控件树 vs DirectUI 对象树为什么界面会卡顿传统 Win32/MFC 界面里每个按钮、编辑框、静态文本都是一个独立的顶层窗口都有自己的 HWND 和窗口过程。窗口一多系统要维护的消息队列、Z序、焦点链就跟着膨胀更致命的是样式定制路径极其别扭——你想让某个按钮背景换成渐变色得处理 WM_CTLCOLORBTN、自绘或者 Owner Draw风格不统一就得逐个控件做子类化。换肤需求一来几十个控件挨个改改到后面自己都记不住哪些改过。DirectUI 的思路是彻底反过来整个界面只有一个真正意义上的窗口其余“控件”是普通 C 对象存在一棵树里。根节点是宿主窗口子节点是容器、按钮、标签、编辑框。系统消息只发给宿主窗口宿主根据鼠标坐标做命中测试再把消息转交给命中的控件对象。控件对象之间通过父子关系、兄弟顺序管理 Z序绘制时从根节点开始深度遍历。这种做法带来的直接好处有两个。第一控件数量不直接影响系统窗口资源占用窗口过程调用次数大幅下降第二样式不再绑定某个系统控件规范你可以给按钮定义普通、悬停、按下、禁用四套颜色绘制函数里自己选一套。对一个老 VC6.0 工程来说不用升级编译器不用换 UI 框架就能把界面外观和交互模式重写一遍这是它最大的现实价值。2.2 宿主窗口承担全部消息一个可运行的消息分发骨架DirectUI 宿主窗口的窗口过程通常写得极其统一。核心逻辑就三件事保存根对象指针、把客户区坐标换算成逻辑点、调用根节点命中测试后分发。我习惯把指针存在 GWL_USERDATA窗口创建时用 SetWindowLong 塞进去之后每次消息进来先取出来。LRESULT CALLBACK DuiWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { CDuiRoot* root (CDuiRoot*)GetWindowLong(hWnd, GWL_USERDATA); if (root NULL) return DefWindowProc(hWnd, msg, wParam, lParam); switch (msg) { case WM_PAINT: { PAINTSTRUCT ps; BeginPaint(hWnd, ps); // 双缓冲内存DC画完后一次性拷回屏幕避免闪烁 HDC memDC CreateCompatibleDC(ps.hdc); RECT rc; GetClientRect(hWnd, rc); HBITMAP bmp CreateCompatibleBitmap(ps.hdc, rc.right - rc.left, rc.bottom - rc.top); HBITMAP oldBmp (HBITMAP)SelectObject(memDC, bmp); root-Draw(memDC); BitBlt(ps.hdc, 0, 0, rc.right, rc.bottom, memDC, 0, 0, SRCCOPY); SelectObject(memDC, oldBmp); DeleteObject(bmp); DeleteDC(memDC); EndPaint(hWnd, ps); return 0; } case WM_LBUTTONDOWN: { POINT pt; pt.x (short)LOWORD(lParam); pt.y (short)HIWORD(lParam); CDuiControl* ctl root-HitTest(pt); if (ctl) ctl-OnLButtonDown(pt); return 0; } case WM_SIZE: { RECT rc; GetClientRect(hWnd, rc); root-SetPos(rc); // 根容器铺满客户区 InvalidateRect(hWnd, NULL, FALSE); return 0; } default: return DefWindowProc(hWnd, msg, wParam, lParam); } }这里几个参数值得说清楚。HitTest 用的坐标必须是客户区坐标WM_LBUTTONDOWN 的 lParam 正好是客户区坐标但如果你在 WM_MOUSEMOVE 里用 GetCursorPos 拿坐标就得多做一次 ScreenToClient否则命中永远偏一截。InvalidateRect 的第三个参数 FALSE 是指不清除背景DirectUI 自己会把整个客户区画满没必要让系统先刷一层背景色。双缓冲里 CreateCompatibleBitmap 的尺寸必须匹配客户区否则 BitBlt 会丢弃一部分内容。2.3 控件树的构建与绘制调度从根到叶的一次遍历控件对象树本身倒不复杂关键是几个约定。每个控件持有自己的矩形区域 m_rc、可见标志、启用状态、子控件列表容器类多了一个子列表非容器类则没有。命中测试时从最上层兄弟开始倒序查找因为视觉上后绘制的控件在上面鼠标也优先点中它。CDuiControl* CDuiContainer::HitTest(POINT pt) { // 子控件倒序遍历保证最上层控件优先响应 for (int i (int)m_children.size() - 1; i 0; i--) { CDuiControl* pChild m_children[i]; if (!pChild-IsVisible()) continue; if (PtInRect(pChild-m_rc, pt)) { if (pChild-IsContainer()) { CDuiControl* pInner ((CDuiContainer*)pChild)-HitTest(pt); if (pInner) return pInner; } return pChild; } } return NULL; }绘制顺序和命中顺序相反从根开始先画父容器背景再依次画每个子控件子控件递归自己的子节点。这样后加入列表的子控件就画在兄弟上面Z序天然等同于添加次序。绘制时必须做两件事一是保存并设置裁剪区避免子控件画到兄弟区域外面二是控件内部要画满自己的矩形背景否则透明区域会漏出下层内容。void CDuiRoot::Draw(HDC hDC) { // 第一步整根清底 HBRUSH bg CreateSolidBrush(m_bgColor); FillRect(hDC, m_rc, bg); DeleteObject(bg); DrawChild(hDC, this); } void CDuiRoot::DrawChild(HDC hDC, CDuiControl* pParent) { for (int i 0; i (int)pParent-GetChildCount(); i) { CDuiControl* pChild pParent-GetChildAt(i); if (!pChild-IsVisible()) continue; int saved SaveDC(hDC); IntersectClipRect(hDC, pChild-m_rc.left, pChild-m_rc.top, pChild-m_rc.right, pChild-m_rc.bottom); pChild-Draw(hDC); if (pChild-IsContainer()) DrawChild(hDC, pChild); RestoreDC(hDC, saved); } }SaveDC / RestoreDC 必须成对不然裁剪区会越积越小最底层控件画不全。VC6.0 的 GDI 不支持硬件加速这种全量遍历在高分辨率窗口下确实有压力所以后面我会专门讲脏矩形和图层缓存。先把这套逻辑跑通再谈优化。3. 把开源包接进 VC6.0 工程解压、集成到弹出一个淡蓝窗口3.1 先看包内文件与项目组织哪些文件必须进工程解压这个 rar 后典型的目录结构是 include、src、demo 三块外加一个工作区文件和一个说明文档。具体文件名会因包而异但核心内容基本跑不掉这几类控件基类、容器类、布局类、窗口宿主类以及一套最简单的控件集按钮、标签、编辑框。demo 工程一般是用来验证库能跑的不一定要直接参考它往往把按钮和进度条的用法摆得很完整建议先从 demo 里抄初始化顺序。集成方式我推荐“源码直接进工程”而不是先编成静态库。原因有三个一是 VC6.0 的调试器对符号路径比较挑剔源码直接编进去断点好踩二是老库多少会依赖一些编译选项比如 _MBCS直接编库容易在链接阶段对不上三是你大概率要改库的绘制代码进工程后改一处立刻生效。需要手动加入工程的源文件大致包括文件分类建议说明控件基类必须加定义自绘虚函数、命中测试、状态容器/布局类必须加管理子控件排列与递归绘制窗口宿主类必须加封装窗口注册、消息分发XML 解析器若有视情况只用代码布局可以先不编demo 里的对话框代码不加仅作参考加入方式是在 Project 菜单里选择 Add to Project把 src 目录下的 .cpp 全选进去。如果编译时报一堆重复定义多半是某些公共源文件被同时加进了两个项目。3.2 最小 MFC 宿主对话框只留一个空白客户区我习惯的做法是Dialog 模板里不放任何原生控件只留一个空客户区。对话框本身保留标题栏和边框DirectUI 根容器直接铺满客户区。这样做的好处是窗口拖动、最小化、任务栏行为全部由系统接管不用自己实现非客户区逻辑。在 OnInitDialog 里创建根容器设置背景色把客户区矩形传进去。BOOL CDuiDlg::OnInitDialog() { CDialog::OnInitDialog(); m_pRoot new CDuiRoot; // 根容器内部持有子控件列表 CRect rc; GetClientRect(rc); m_pRoot-SetPos(rc); // 铺满整个客户区 m_pRoot-SetBkColor(RGB(240, 248, 255)); // 淡蓝背景 // 加一个测试按钮 m_pBtn new CDuiButton; m_pBtn-SetText(点我测试); m_pBtn-SetRect(20, 20, 140, 60); m_pRoot-AddChild(m_pBtn); return TRUE; }SetPos 传入的是根容器将要覆盖的矩形它内部会把这个矩形再分配给子控件做布局参考。SetRect 的四个参数分别是左、上、右、下注意这是控件本身的边界不是宽高。按钮加到根容器后绘制和命中测试都会自动走根节点的遍历逻辑。此时如果你直接运行窗口还是传统画法鼠标也点不动按钮因为对话框的默认消息循环还没把消息转给 DirectUI。3.3 把鼠标消息喂给 DirectUIPreTranslateMessage 的关键改动MFC 对话框有一个天然的分发入口 PreTranslateMessage所有消息在进入窗口过程之前都会经过它。利用这一点可以把鼠标消息截下来转交给控件树。这是我落地的核心一步也是最容易写错的一步。BOOL CDuiDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg-hwnd m_hWnd) { UINT nMsg pMsg-message; if (nMsg WM_MOUSEFIRST nMsg WM_MOUSELAST) { POINT pt pMsg-pt; ScreenToClient(pt); CDuiControl* pCtl m_pRoot-HitTest(pt); if (pCtl !pCtl-IsTransparent()) { pCtl-OnMouseMessage(nMsg, pt, pMsg-wParam); return TRUE; // 消息已被 DirectUI 消费 } } } return CDialog::PreTranslateMessage(pMsg); }这里出现了一个常见误区pMsg-pt 是屏幕坐标必须转成客户区坐标再给 HitTest否则点击偏差正好是窗口在屏幕上的偏移量。另一个坑是透明区域判断如果按钮是圆角或者图标周围有大量透明像素不判断 IsTransparent 会导致这些区域的点击被吞掉对话框的右键菜单、系统行为全部失效。消息区间 WM_MOUSEFIRST 到 WM_MOUSELAST 覆盖了移动、按下、抬起、双击但不包含 WM_MOUSEWHEEL。滚轮消息要走 WM_MOUSEWHEEL 单独转发而且坐标来源是屏幕坐标同样要先转换。如果省略这一步列表滚动条永远滚不动。3.4 编译选项与首次运行注意事项VC6.0 下编译 DirectUI 老包最常见的编译失败来自头文件兼容和预处理器定义。建议在 Project Settings 里做三件事C/C 标签页的 Preprocessor 定义里加上 _MBCS不要用 _UNICODE因为老库的字符串接口大多按 char* 实现C Language 里把 Enable RTTI 打开优化选项 Release 用 Maximize SpeedDebug 用默认即可。首次编译通过后先别急着加业务逻辑。跑一次 demo 或者最小对话框确认三件事窗口能拖动、按钮有悬停和按下反馈、拉伸窗口时按钮位置跟随。如果按钮位置不动去查根容器的 WM_SIZE 处理看 SetPos 是否重新触发布局。如果悬停状态没反应检查是否有 WM_MOUSEMOVE 的重复触发TrackMouseEvent 需要自己调用否则系统不会持续汇报鼠标离开事件。4. 布局、换肤与自定义控件做一个纵向按钮列表4.1 纵向布局算法Arrange 时先算 Y 再递归算子级有了根容器和基本按钮下一步就是把控件组织成真正的界面。最常用的容器是纵向布局它负责把子控件按顺序从上到下排列每个子控件的宽度撑满容器客户区高度由子控件自己声明纵向间距由布局参数控制。void CDuiVerticalLayout::Arrange(CDuiControl* pParent) { RECT rc pParent-GetClientRect(); int y rc.top m_nPaddingTop; for (int i 0; i (int)m_children.size(); i) { CDuiControl* pChild m_children[i]; if (!pChild-IsVisible()) continue; int nHeight pChild-GetFixedHeight(); if (nHeight 0) { pChild-Measure(); // 按文本内容估算高度 nHeight pChild-GetDesiredHeight(); } pChild-SetRect(rc.left m_nPaddingLeft, y, rc.right - m_nPaddingRight, y nHeight); y nHeight m_nSpacing; } }这段逻辑里三个参数决定了最终观感m_nPaddingTop 控制整体距容器顶部的空白m_nPaddingLeft / Right 控制子控件的左右边距m_nSpacing 控制相邻控件的间距。SetRect 的右值用 rc.right 减去右边距意味着子控件宽度自动撑满。如果你想要固定宽度的按钮就得在 Measure 里修改 desired width或者给布局增加对齐模式这部分不同包实现方式不太一样但入口都是 Arrange。容器变化时一定要触发重排否则窗口拉伸后按钮还在原地。常见做法是在容器的 SetPos 里调用 Arrange再传递给子容器做递归。我习惯把 Arrange 和 Draw 分开Draw 每帧都可能执行Arrange 只在位置变化、子控件增删、显隐变化时执行省下的 CPU 在列表滚动时会很明显。4.2 按钮状态与换肤按下时颜色怎么变按钮的交互状态是 DirectUI 拿手好戏。普通态、悬停态、按下态、禁用态每种状态对应一组前景色和背景色绘制函数里先查状态再取对应颜色全部在内存里完成不需要系统帮你重建窗口。void CDuiButton::Draw(HDC hDC) { COLORREF clrBg, clrText; switch (m_nState) { case BTN_NORMAL: clrBg m_clrNormalBg; clrText m_clrNormalText; break; case BTN_HOVER: clrBg m_clrHoverBg; clrText m_clrHoverText; break; case BTN_PRESSED: clrBg m_clrPressedBg; clrText m_clrPressedText; break; default: clrBg m_clrDisabledBg; clrText m_clrDisabledText; break; } HBRUSH brush CreateSolidBrush(clrBg); FillRect(hDC, m_rc, brush); DeleteObject(brush); SetBkMode(hDC, TRANSPARENT); SetTextColor(hDC, clrText); DrawText(hDC, m_strText.c_str(), -1, m_rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); }状态更新发生在鼠标消息里WM_MOUSEMOVE 时先判定点是否仍落在本控件内落到外部就切回 normal按下去那一刻切 pressed释放时恢复 normal。连续快速移动会产生大量 WM_MOUSEMOVE状态切换不需要每次都重绘一整个窗口调 InvalidateRect 时把矩形收缩到按钮区域即可。换肤的本质就是对这组颜色的统一替换。最简单的做法是给控件增加 SetSkinColor 接口把 normal / hover / pressed / disabled 四个颜色值一次传入更工程化的做法是做一个全局皮肤管理器控件绘制时从皮肤管理器取色这样切换主题只需要改一份配置不用遍历整棵控件树。4.3 添加自定义控件而不碰库源码老库自带的控件数量有限遇到开关、滑块、图表这类组件就要自己扩展。扩展方式很固定继承控件基类实现 Draw、HitTest、OnMouseMessage 三个核心方法必要时加一个新接口给外部设置数据。class CDuiSwitch : public CDuiControl { public: virtual void Draw(HDC hDC); virtual BOOL HitTest(POINT pt) { return PtInRect(m_rc, pt); } virtual void OnMouseMessage(UINT nMsg, POINT pt, WPARAM wParam) { if (nMsg WM_LBUTTONDOWN) { m_bOn !m_bOn; // 点击切换开关状态 InvalidateRect(GetHostHWND(), m_rc, FALSE); } } void SetOn(BOOL bOn) { m_bOn bOn; } BOOL m_bOn; };自定义控件不要直接调用系统 InvalidateRect应该通过宿主窗口句柄去失效或者给控件基类封装一个 Invalidate 方法统一处理。这样能保证双缓冲绘制流程完整走一遍。绘制函数里注意别在每次 Draw 里创建字体和画刷创建 GDI 对象有成本频繁创建还会造成句柄泄露建议在控件构造时把固定笔刷缓存下来。4.4 别把 DirectUI 当万金油何时退回到真实窗口DirectUI 不是全场景通吃。以下四种情况我一般会保留原生窗口输入法合成窗口、系统右键菜单、可访问性辅助、嵌入浏览器或 ActiveX 控件。输入法需要窗口消息配合候选框定位DirectUI 控件没有真实 HWNDIME 不知道该把候选框放在哪里系统菜单和右键菜单用 TrackPopupMenu 需要宿主窗口坐标处理起来格外绕。场景推荐方案原因文本输入框保留真实 Edit 或者用 DirectUI 控件手写 IME 适配IME 候选框需要真实窗口右键菜单TrackPopupMenu 宿主窗口做 TPM_RETURNCMD菜单交互本身依赖 HWNDActiveX / 浏览器内核嵌入真实子窗口DirectUI 无法渲染外部窗口内容屏幕阅读器退回传统控件或在 DirectUI 上实现 MSAA无障碍接口依赖 HWND换肤再漂亮输入法乱跑这类的体验问题很容易让一个项目全部白干。我的建议是提前把边界画清楚DirectUI 负责视觉和常规交互系统能力仍然借用宿主窗口真实窗口作为特殊区域嵌入到布局树的叶子节点。5. DirectUI for VC6.0 常见问题与排查五个翻车现场5.1 点击没有反应消息在 PreTranslateMessage 被吞现象按钮画出来了鼠标移上去也有悬停反馈但按下毫无反应连焦点框都不出现。原因我排查过的大部分情况都在 PreTranslateMessage 上。要么只转了 WM_LBUTTONDOWN漏了 WM_LBUTTONUP控件只在按下瞬间记录状态抬起时没人告诉它该触发 Click要么返回 TRUE 的条件判断写反了透明区域连系统默认行为也拦截掉了对话框本身收不到任何点击。解决按第 3.3 节的写法把 WM_MOUSEFIRST 到 WM_MOUSELAST 整段区间都转发并且用 IsTransparent 做统一出口。调试时可以在 HitTest 后加一个 OutputDebugString 打印控件 ID确认命中目标是否预期。如果某个区域点了以后既没有控件响应也没有系统行为就去查是不是把透明区域判定成了可点击。5.2 界面闪烁、黑屏或一块一块地花现象窗口拖动时整个客户区像曝光过度一样闪或者拉动窗口边缘后出现大块黑斑要等下一次重绘才恢复。原因几乎都是绘制配对问题。第一种是 WM_ERASEBKGND 没处理系统先按对话框背景色擦一遍DirectUI 紧接着再画一遍双重绘制在低频刷新率下就闪。第二种是 InvalidateRect 第三个参数传了 TRUE背景清除和自绘交叠。第三种是 WM_PAINT 里用了 GetDC 而不是 BeginPaint导致重绘标志没被清除系统认为窗口永远需要刷新CPU 直接被拉满界面卡到像花屏。解决在消息映射里明确处理 WM_ERASEBKGND 并返回 TRUE禁止系统擦背景所有 InvalidateRect 统一用 FALSE绘制统一走 BeginPaint/EndPaint。双缓冲不是可选项是必须项。内存 DC 用完立即 DeleteDC位图用完立即 DeleteObject不然 GDI 对象也会成为下一个坑。5.3 GDI 对象只增不减画刷、位图、字体的清理不当现象窗口运行十几分钟任务管理器里 GDI 对象数量一路从两百涨到上千最终绘图花掉、点击失效整个界面像被人泼了墨。原因绘制函数里每次创建画刷、字体、位图但没有及时释放。最常见的是 CreateSolidBrush 之后忘记 DeleteObject或者 SelectObject 把新字体选进 DC 后没有把旧对象选回来导致 DC 的默认对象被覆盖之后 DeleteObject 删除的又是错误对象真正的泄露对象反而留在 DC 里无法释放。解决养成三个习惯。局部用的 GDI 对象函数结束前必须 DeleteObject长期复用的对象作为控件成员变量在析构函数里统一释放SelectObject 的返回值保存成旧句柄画完立刻选回去再删除本地创建的对象。void CDuiButton::Draw(HDC hDC) { HBRUSH brush CreateSolidBrush(m_clrNormalBg); HBRUSH oldBrush (HBRUSH)SelectObject(hDC, brush); // 绘制逻辑 SelectObject(hDC, oldBrush); DeleteObject(brush); // 必须删除不是选回旧对象就完事 }凡是创建型 GDI 函数GetStockObject 返回的对象不需要删除其他一概需要。排查时可以在 OnPaint 末尾加一个 GetGuiResources 调用跟踪当前 GDI 对象数连续刷新十帧数字不回落就说明有泄露。5.4 点击偶尔崩溃、窗口拖动时直接退出现象程序大部分时间正常但快速拉伸窗口或者连续快速点击不同按钮时偶尔崩在窗口过程里VC6.0 调试器停在某个空指针调用位置。原因根容器指针或控件指针存的位置不对或者生命周期管理出了问题。最常见的是把指针存在 GWL_USERDATA但窗口销毁时的消息顺序导致取出来的指针已经释放另一种情况是控件对象在堆上创建对话框析构时没有先删控件树再销毁窗口悬空指针被窗口过程的到下一条消息再次调用。解决窗口过程头里做空指针保护消息分发严格按 WM_NCCREATE 保存指针、WM_NCDESTROY 清理指针的顺序执行销毁对话框时先 delete 根容器再调用 CDialog 的析构。控件树所有子控件统一由根容器持有并负责释放外部不要单独 delete 子控件避免二次释放。case WM_NCDESTROY: { CDuiRoot* root (CDuiRoot*)GetWindowLong(hWnd, GWL_USERDATA); if (root) delete root; SetWindowLong(hWnd, GWL_USERDATA, 0); break; }如果你把这段代码迁移到新平台 SDK习惯性用 GetWindowLongPtr 和 GWLP_USERDATA因为 GetWindowLong 在 64 位下会截断指针。VC6.0 时代只有 32 位程序没出过问题但代码一旦被拷到新版编译器里编译 64 位这就是定时炸弹。5.5 中文乱码、字体重叠和文字跑出控件边界现象按钮文字显示成“口口口”或者中文标题和英文混排时行高不对、上下重叠字体比控件本身还大一圈。原因编码和字体参数各占一半。VC6.0 默认走 MBCS字符串是 char*如果包内 XML 文件是 UTF-8 编码解析出来的中文就会错位CreateFont 创建字体时没有指定字符集系统用一个默认字体替代遇到中文直接变方块高度参数如果传正值指的是字体总高度英文字符和汉字的基线计算规则不同行高容易被撑乱。解决编码统一用 GB2312 或 GBKXML 声明里明确写 encodinggb2312字体创建时显式指定字符集和字体名高度传负值让系统按字符高度换算文字绘制前先测量字符串宽度超出控件矩形就做省略号或换行处理。CreateFont 参数推荐值说明nHeight-14 或按 DPI 换算负值表示字符高度避免字体撑乱lprfFaceName“宋体”或“Microsoft YaHei”中文字体名必须与系统匹配nCharSetGB2312_CHARSET缺省会退回默认字符集中文乱码nOutPrecisionOUT_DEFAULT_PRECIS保持默认即可字体创建后在析构函数里 DeleteObject和画刷同等级对待。如果项目里大量文字需要自适应宽度给控件基类加一个 AutoSize 标志Draw 前调用 GetTextExtentPoint32 重新计算控件矩形再触发重排比手工调坐标省心得多。6. 把 DirectUI 重绘成本打下来脏矩形、图层缓存与计时验证界面一复杂全量遍历绘制就成了瓶颈。拿一个包含几百个控件的窗口来说每帧把整棵树都画一遍GDI 在 1024×768 下还能撑住上到 1920×1080 就开始发飘。我一般会先上两个技巧效果立竿见影。第一个是脏矩形。在控件基类里加一个 BOOL m_bDirtySetText、SetColor、SetRect 时把脏标记置上同时把控件矩形合并进宿主窗口的待重绘列表。绘制时只遍历脏矩形覆盖到的控件干净的叶子节点直接跳过。列表里可以用粗粒度合并相邻控件合并成一个矩形避免一个按钮刷新导致半屏重绘。void CDuiRoot::InvalidateRect(CDuiControl* pCtl) { if (!pCtl-m_bDirty) { pCtl-m_bDirty TRUE; // 合并到全局脏矩形多个控件相邻时就拼成大块 UnionRect(m_rcDirty, m_rcDirty, pCtl-m_rc); } }第二个是图层缓存。把完全静止的背景、Logo、表格线画到一张离屏位图上每帧先 BitBlt 这张背景位图再绘制动态控件。这样动态区域的绘制量被砍到只剩真正需要变化的控件。实现时在根容器里保存一张 m_bgBmp窗口首次创建和主题切换时重建平时只负责整张拷贝。// 每帧绘制前先铺背景位图 BitBlt(hDC, 0, 0, rc.right, rc.bottom, m_bgDC, 0, 0, SRCCOPY); // 再只画动态控件和脏矩形内的变化区域 DrawDirtyItems(hDC);验证优化有没有效果不要靠肉眼感受。我在每个宿主窗口的 OnPaint 里包一段 GetTickCount 计时连续记录三十帧的耗时取平均值同时用 GetGuiResources 观察 GDI 对象数是否平稳。全量重绘时平均耗时在几十毫秒脏矩形和图层缓存做完之后正常交互一般能压到十毫秒以内GDI 对象数保持在一个稳定值不再上涨。最后说一个我自己的教训某工控项目里第一次上 DirectUI 时我图省事所有状态变化都直接 InvalidateRect 整个客户区界面在 1280×1024 下拖一次要响应将近两秒。后来老老实实把脏矩形的计算补上又给静态背景加了图层缓存拖拽才回到顺滑。这个方向值不值得投入我的判断标准很简单只要你的项目需要在老编译器里做出自定义程度高的界面DirectUI 就比逐个控件自绘可持续得多。先跑通最小宿主再逐个加控件坑基本都能在这篇文章里对号入座希望帮到你。本文还有配套的精品资源点击获取

相关新闻

高并发网络编程必知:IO多路复用与select、poll、epoll深度解析

高并发网络编程必知:IO多路复用与select、poll、epoll深度解析

1. 一个连接撑爆了的下午:从阻塞IO说起如果你做过高并发服务端开发,大概率遇到过类似的场景:某天下午,线上告警突然响起来,某服务的连接数一路飙升,CPU占用率跟着往上冲,紧接着就是一连串的&quo…

2026/10/11 2:38:09 阅读更多 →
Modbus地址规则详解:从PLC地址到协议地址的调试避坑指南

Modbus地址规则详解:从PLC地址到协议地址的调试避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 2:38:09 阅读更多 →
小电流接地系统故障选线:基于MATLAB与Simulink的暂态分析实现

小电流接地系统故障选线:基于MATLAB与Simulink的暂态分析实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 2:38:09 阅读更多 →

最新新闻

深度学习OCR系统实战:基于PyTorch的CRNN+CTC文字识别

深度学习OCR系统实战:基于PyTorch的CRNN+CTC文字识别

简介:基于深度学习的文字识别系统完整项目包,面向毕业设计、课程设计与期末大作业场景,适合需要快速搭建OCR系统的计算机相关专业学生。项目采用CNN与RNN结合实现文字检测与识别,覆盖图像预处理、模型训练、后端接口与移动端展示全…

2026/10/11 10:25:10 阅读更多 →
使用 claude-howto 的 blog-draft 技能与草稿模板,系统化产出高质量技术博客

使用 claude-howto 的 blog-draft 技能与草稿模板,系统化产出高质量技术博客

教程文档 【免费下载链接】claude-howto A visual, example-driven guide to Claude Code — from basic concepts to advanced agents, with copy-paste templates that bring immediate value. 项目地址: https://gitcode.com/GitHub_Trending/cl/claude-howto 点…

2026/10/11 10:25:10 阅读更多 →
Excel自动评分:用LOOKUP和IF函数实现体育成绩折算自动化

Excel自动评分:用LOOKUP和IF函数实现体育成绩折算自动化

简介:这份资源是一份面向体育教师及学校教务人员的Excel实用教程文档,聚焦体育测试成绩换算这一高频痛点,帮助读者用公式与函数替代人工比对,降低错漏率。文档围绕学生成绩空表搭建、跳远与跳绳评分标准表制作、LOOKUP近似匹配与I…

2026/10/11 10:25:10 阅读更多 →
Legendary OSINT 海事篇:AIS 船舶追踪工具全景对比

Legendary OSINT 海事篇:AIS 船舶追踪工具全景对比

Legendary OSINT 海事篇:AIS 船舶追踪工具全景对比 【免费下载链接】Legendary_OSINT A list of OSINT tools & resources for (fraud-)investigators, CTI-analysts, KYC, AML and more. 项目地址: https://gitcode.com/GitHub_Trending/le/Legendary_OSINT…

2026/10/11 10:25:10 阅读更多 →
ProxCenter热迁移深度解析:VDDK+nbdkit实现虚拟机零停机迁移的原理与调优

ProxCenter热迁移深度解析:VDDK+nbdkit实现虚拟机零停机迁移的原理与调优

【免费下载链接】proxcenter-ui ProxCenter is an alternative to VMware vCenter for Proxmox environments. It provides a modern, intuitive web interface to manage multiple Proxmox VE clusters and Proxmox Backup Server instances from a single pane of glass. 项目…

2026/10/11 10:25:10 阅读更多 →
2025年AI IDE实战测评榜:从个人开发到企业部署的完整选型攻略(TaoToken统一API接入篇)

2025年AI IDE实战测评榜:从个人开发到企业部署的完整选型攻略(TaoToken统一API接入篇)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:24:10 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 10:38:42 阅读更多 →