简介面向使用 VC 进行 Windows 界面编程的开发者这份实例合集围绕 26 个通用控件展示按钮、编辑框、组合框、列表框、对话框、单选与复选等常见控件的实现方式并结合 MFC 框架讲解控件属性、消息映射、动态创建和自定义控件等核心知识点。压缩包共包含 420 个文件其中 h 头文件、cpp 源文件、rc/rc2 资源描述脚本以及 dsp/dsw 工程文件占主要部分同时提供 ico 图标和 txt 说明整体约 1.25MB便于直接加载到工程中查看编译。这些工程并非零散示例而是按对话框程序设计思路组织从界面布局到事件响应均有完整代码还可看到多线程更新、国际化资源文件等进阶用法。目前已有 240 人浏览学习适合刚接触 MFC 的 C 初学者也可作为中级开发者查用控件封装技巧与界面排错思路的参考资料。1. VC界面编程一套控件实例源码能帮你省下多少重复劳动做过 MFC 或 WTL 界面开发的人都懂真正耗时耗力的往往不是业务逻辑而是那些看起来不起眼的控件细节按钮要贴图、列表要排序、编辑框要限制输入、进度条要变色。每一次换肤、每一项交互微调都意味着你又要翻一遍 MSDN或者在一堆老旧代码里翻找可复用的片段。这套 VC 界面编程通用控件 26 个实例源代码正是冲着这个痛点来的。它不是让你按 F5 看一圈特效就完事的演示工程而是一套可以直接往自己项目里搬的控件实现集合。文章会把这 26 个实例按功能拆开讲清楚每个实例背后用到的自绘、消息反射、状态切换等关键机制再给出一份能照着落地的集成路线图适合刚接手 MFC 维护项目的新手也适合手头正缺几个稳定控件实现的老手。2. 26 个实例到底在解决什么问题先看懂这套代码的底层分类逻辑2.1 从资源结构看设计意图不是堆砌 Demo而是按交互场景分组把这份源代码下载解压后你会看到工程文件里并没有按 控件类型 建一堆目录而是按 交互场景 分了组。这个设计本身就透露出资源作者的真实意图他希望你关注的是控件如何服务于业务交互而不是单纯为了展示绘制技巧。我大致归类了一下26 个实例基本落在三个大的类别上。第一类是 视觉反馈类控件 典型代表是带有自绘功能的按钮、进度条、滑块和状态栏。这类控件解决的核心问题是让界面状态变得可感知。比如进度条的平滑动画、按钮按下时的立体凹陷效果、滑块拖动时的刻度吸附这些都是通过重写控件的OnPaint或DrawItem消息来实现的。第二类是 数据交互类控件包括支持排序的列表控件、带下拉自动匹配的组合框、带格式校验的编辑框。这类实例的重点在于消息反射Message Reflection和虚函数的二次封装比如截获LVN_COLUMNCLICK消息来完成点击列表头排序。第三类是 窗口行为控制类控件比如可停靠的面板、可折叠的的分隔条、带透明效果的静态文本。这类实现通常涉及对WM_NCHITTEST或者WM_SIZE的深度处理涉及到窗口样式层面的技巧。提示拿到源码后先别急着编译运行。建议先打开解决方案资源管理器按上面三个维度把 26 个项目文件做一个自己的分类清单这会帮你后面快速定位需要参考的工程。2.2 关键机制之一自绘控件的消息路径与绘制责任转移自绘控件在这 26 个实例中的占比很高也是最基础的一项能力。MFC 框架里控件自绘通常有两个入口一是重写控件类的虚函数二是处理WM_DRAWITEM消息。最常见的组合是声音CButton然后重写DrawItem虚函数因为在BS_OWNERDRAW样式下框架会把绘制责任完全交给你。下面是这套源码里一个典型的自绘按钮实现骨架也是我拆解这份源码时最先看懂的一个文件// 摘自某自绘按钮实例核心是接管 DrawItem把绘制责任转移到自己的绘制函数 void CMySkinButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { // 1. 获取按钮当前的完整矩形区域包括背景和边框 CRect rect lpDrawItemStruct-rcItem; CDC* pDC CDC::FromHandle(lpDrawItemStruct-hDC); // 2. 判断按钮处于什么状态按下、悬停还是正常这决定了绘制不同底图 UINT state lpDrawItemStruct-itemState; BOOL bPressed (state ODS_SELECTED) ? TRUE : FALSE; BOOL bFocused (state ODS_FOCUS) ? TRUE : FALSE; BOOL bDisabled (state ODS_DISABLED) ? TRUE : FALSE; // 3. 根据状态从资源中加载对应的位图这里演示用纯色渐变替代位图 if (bPressed) { // 按下状态整体颜色加深并让文字向右下偏移 1 像素模拟按压 pDC-FillSolidRect(rect, RGB(180, 180, 180)); rect.OffsetRect(1, 1); // 关键按下时的文字偏移是手感的重要来源 } else if (bFocused || bDisabled) { // 悬停或禁用状态禁用状态一般用浅灰色并降低文字对比度 pDC-FillSolidRect(rect, RGB(220, 220, 220)); } else { // 正常状态使用默认背景色 pDC-FillSolidRect(rect, RGB(240, 240, 240)); } // 4. 绘制边框和文字注意这里要恢复 rect 的偏移否则文字会跑偏 pDC-Draw3dRect(rect, RGB(90, 90, 90), RGB(140, 140, 140)); CString strText; GetWindowText(strText); pDC-SetBkMode(TRANSPARENT); pDC-DrawText(strText, rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); }这段代码的逻辑主线很清晰从LPDRAWITEMSTRUCT结构体里取绘制上下文和按钮状态然后针对不同状态分别填充背景、绘制边框和文字。这里有几个参数值得留意。itemState判断里ODS_SELECTED表示鼠标左键正在按钮上按住这和你通常直接处理WM_LBUTTONDOWN是不同的路径。rect.OffsetRect(1, 1)这行是我自己加上去的注释点注意看Draw3dRect和DrawText使用的是偏移后的rect这一步直接决定了按钮按下时有没有立体下沉的反馈。这套源码里好几个按钮实例都有类似处理但有的写在了DrawItem里有的写在了处理WM_MOUSEMOVE的代码里。如果后面你改代码发现按钮按下去没有反馈优先检查的就是这个偏移量有没有被后续绘制逻辑覆盖。2.3 关键机制之二列表控件排序的数据层与界面层解耦26 个实例里列表控件的案例最接近实际生产环境也最值得你花时间读。它解决的不仅仅是 点击列头排序 这个动作而是把界面层和数据层做了正确的解耦。我看到的第一版实现里作者直接在OnLvnColumnclick里操作了CListCtrl的SortItems函数。这个做法即用即走但有个问题SortItems的回调函数参数是DWORD_PTR你用静态成员函数处理数据时无法直接拿到控件指针。这份源码采用了更稳健的方案封装一个排序结构体作为中转站把比较操作剥离到控件外的普通函数中。核心思路如下// 排序回调需要的上下文结构体保存排序列索引和排序方向 struct SortInfo { CListCtrl* pListCtrl; int iColumn; // 当前点击的列索引 BOOL bAscending; // 是否升序 }; // 回调函数通过 lParam 拿到结构体指针比较指定列的两行数据 int CALLBACK ListCompareProc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort) { SortInfo* pInfo (SortInfo*)lParamSort; CListCtrl* pList pInfo-pListCtrl; // 从 lParam 中获取行索引注意这里存的是行号不是数据地址 int nRow1 (int)lParam1; int nRow2 (int)lParam2; // 取出指定列的子项文本进行比较 CString strItem1 pList-GetItemText(nRow1, pInfo-iColumn); CString strItem2 pList-GetItemText(nRow2, pInfo-iColumn); // 根据是否升序决定返回值返回负数表示 nRow1 排前面 if (pInfo-bAscending) return strItem1.CompareNoCase(strItem2); else return -strItem1.CompareNoCase(strItem2); }这个实现的妙处在于lParam1和lParam2被设计成列表行索引而不是某列数据的副本。这样一来比较逻辑就可以统一通过GetItemText来访问任意列的数据让排序代码真正做到了列无关。在使用的时候需要把lParam设置成SortInfo指针调用方式如下先往列表设置整行数据然后在响应LVN_COLUMNCLICK时把当前列号和排序方向填充进SortInfo结构体再调用SortItems(ListCompareProc, (DWORD_PTR)sortInfo)。这里有一个容易踩坑的地方SortItems是异步的SortInfo结构体必须是成员变量或堆变量绝对不能在栈上声明一个局部变量然后传地址给SortItems否则回调触发时这个栈地址很可能已经被回收了排序结果会完全不可预知。这套源码里我看到作者是把SortInfo声明成了类的成员变量。如果你在自己项目里复刻建议同样用成员变量或者用new并在排序完成后delete。3. 把 26 个实例落进你项目的第一步新建工程与核心代码文件的搬迁要领3.1 如何把这些控件文件搬运到你的 MFC 工程而不破坏原有构建这份源码里的 26 个实例不少是独立的小工程但你可能只需要其中几个控件而已。把整个源码工程塞进自己的项目显然是笔糊涂账。正确的做法是挑选单个控件对应的.h和.cpp文件复制进你的工程目录并纳入版本管理。以自绘按钮CMySkinButton为例我建议你这样操作。先在解决方案资源管理器里选中目标工程右键选择 添加 - 现有项 ,定位到源码中对应控件的文件夹把MySkinButton.h和MySkinButton.cpp一起选中加入。注意CMySkinButton类的定义很可能在MySkinButton.h里#include了资源文件头比如Resource.h你需要检查这个头的路径。如果迁移后编译报错提示找不到Resource.h别急着全盘搜索先看MySkinButton.h里#include的具体路径格式。如果是用相对路径且带了..\..\resource.h你必须把它改成绝对工程路径或者把对应的资源文件拷进自己工程目录。注意MFC 控件类通常会调用LoadBitmap或LoadImage加载位图资源。跨工程搬代码时最隐蔽的问题是位图资源 ID 冲突。实例工程里一个按钮加载的位图 ID 可能是IDB_BUTTON1你自己工程里也定义同名字符串资源就会导致链接错误或运行时加载错图。检查Resource.h中的#define值如果项目里已有相同 ID手动改掉这一处#define同步修改代码里引用它的地方。3.2 实例代码的命名空间与类名冲突处理一个容易被忽略的安全隐患把多个实例文件搬进同一个工程时最常见的问题是类名冲突。26 个实例是独立的工程作者可能在不同工程里都定义了一个叫CMyButton的类但实现方式不同。如果你同时搬了 自绘按钮 和 不规则按钮 两个实例编译时就会报重定义错误。处理方法有两个一是给每个控件类加上不同的命名空间用namespace SkinButton { class CMyButton ... }包裹起来调用时写SkinButton::CMyButton。二是快速重命名搬入的类名比如改成CMySkinButton27。我个人更倾向于命名空间方案因为实例源码本身是为了展示控件能力函数内部可能会调用一些同样名称的辅助函数。重命名类名的话构造函数和析构函数也要同步改如果内部有IMPLEMENT_DYNAMIC宏宏参数里的类名也得跟着改。命名空间则不会破坏宏参数的匹配。当然这里也有一个限制MFC 的消息映射宏BEGIN_MESSAGE_MAP里的类名参数是字符串化的如果你用了命名空间ON_NOTIFY_REFLECT这类宏展开时可能会遇到问题。最保险的做法是先在单独的测试工程里编译通过再搬入正式工程。3.3 对话框上动态创建控件静态绑定改为动态创建的完整步骤实例源码里很多控件是在对话框模板上静态拖放然后通过DDX_Control绑定变量。但在真实项目中界面往往需要根据配置动态创建控件。这里我以动态创建一个自绘进度条为例演示如何改造实例代码这是把这套源码用活的关键一步。// 在对话框初始化函数中动态创建自绘进度条控件 BOOL CMyDialog::OnInitDialog() { CDialogEx::OnInitDialog(); // 1. 先实例化自绘控件对象这是成员变量 // m_wndSkinProgress 是 CPaintProgress 类型从 CProgressCtrl 派生 // 2. 创建窗口WS_CHILD 和 WS_VISIBLE 缺一不可 // 注意 rect 的参数如果不给位置和大小控件会显示在左上角且大小为0导致看不见 CRect rect(10, 10, 210, 30); m_wndSkinProgress.Create( WS_CHILD | WS_VISIBLE | PBS_SMOOTH, rect, this, IDC_SKIN_PROGRESS); // 这个 ID 必须和资源文件里定义的常量一致 // 3. 调用实例源码里的扩展接口设置背景色和前景色 // 这一步是实例代码里的 Jersey 方法:如果没调用控件会使用默认丑陋的绿色 m_wndSkinProgress.SetProgressBgColor(RGB(230, 230, 230)); m_wndSkinProgress.SetProgressFgColor(RGB(0, 120, 215)); // 4. 设置范围并初始化位置 m_wndSkinProgress.SetRange(0, 100); m_wndSkinProgress.SetPos(50); return TRUE; }这段动态创建代码有三个参数需要特别留意。PBS_SMOOTH样式是进度条平滑滚动显示的关键不加这个样式进度条会是一格一格的方块状。IDC_SKIN_PROGRESS这个资源 ID 需要在Resource.h里预先定义并且不能和对话框里其他控件 ID 重复。SetProgressBgColor和SetProgressFgColor是实例源码里额外提供的公开方法如果你的搬入工程报错提示这几个函数不存在说明你只拷贝了控件文件还没把配套的实现文件全部找齐。检查一下实例工程的SkinProgress.cpp是否已经添加进工程了。动态创建的好处是布局灵活但代价是你需要自己管理控件的生命周期。对话框析构时MFC 会负责销毁子窗口但m_wndSkinProgress对象本身是成员变量无需手动delete。如果要彻底移除控件调用DestroyWindow而非直接释放对象。4. 避坑排查集成 26 个控件实例时的频发问题和玄学现场4.1 现象按钮自绘后背景全黑或文字消失你按照实例代码把自绘按钮放进对话框一运行按钮区域是一大片黑色或者中文文字不见了。原因在于DrawItem里使用了FillSolidRect填充了不透明的背景色而实例代码为了演示方便用RGB(240,240,240)覆盖了按钮区域如果你的对话框背景色不是这个颜色就会显得异常突兀。文字消失则往往是背景模式没设置透明DrawText默认用不透明背景绘制文字一旦你忘记调用SetBkMode(TRANSPARENT)文字就会和底色糊在一起。解决思路是优先从实例代码保留的状态判断逻辑按下、悬停、禁用但把背景填充部分替换成你的父窗口背景纹理或颜色。常见做法是在DrawItem开头获取父窗口的GetParent()-GetDC()并取样背景色或者干脆在自绘控件里先从GetParent()-SendMessage(WM_CTLCOLORBTN)拿画刷。4.2 现象列表控件排序时完美崩溃或者排序结果错乱这是这套源码里把SortInfo指针用错时的经典故障。初次尝试排序时我看到的现象是程序在点击列头后立即崩溃调试器指向SortItems回调里的GetItemText内存访问冲突。后来发现是使用者在栈上定义SortInfo后直接传给SortItems但SortItems的回调是异步触发的等回调真正执行时栈上的结构体已经失效。还有一个错乱的情况是lParam传参数组数据而我在源码里读到lParam被定义为行索引如果你强行把某列字符串的LPTSTR指针传进去排序比较时会访问非法地址或者得出随机结果。解决方法是严格按照前文代码lParam1传行索引lParamSort指向持久化的SortInfo变量。4.3 现象动态创建的控件不响应鼠标点击像被什么东西挡住了有朋友把自绘按钮动态创建之后发现鼠标移上去没有悬停效果点击也没反应。最初怀疑是自绘代码里DrawItem状态判断不对后来排查发现是控件创建时缺少了WS_TABSTOP样式。没有这个样式控件虽然可见但无法进入 Tab 键的焦点循环鼠标点击不会把焦点设置到按钮上。对于需要键盘操作的自绘按钮WS_TABSTOP必须加上否则DrawItem里的ODS_FOCUS永远不成立。另外一个常见原因是创建时位置和大小有个维度为 0我用CRect(10,10,210,30)创建时宽高各 200 和 20没问题但你若是把rect计算错了导致高度为 0控件就只剩下一条线。用 Spy 查看窗口矩形是最快的排查方式。4.4 现象编译通过但运行时提示 未处理的异常 或 0xC0000005 访问冲突这类问题大多出在没有把支持文件拿全。实例里的自绘控件往往依赖某个公共工具类来加载 PNG 或处理透明位图。比如有一个图片按钮实例static里调用了CImage或GDI封装函数而这个函数定义在另一个独立文件GraphicUtils.cpp里。你只拷贝了按钮的.h和.cpp漏掉了GraphicUtils.cpp导致链接时出现unresolved external symbol。运行时访问冲突则是另外的情况比如实例代码里某个函数返回的是控件内部缓冲区的指针而你直接保存该指针供后续使用但控件重绘时缓冲区被重新分配旧指针变成野指针。解决方法是先给工程添加 现有项 时打开实例工程的 文件夹 仔细检查所有.cpp文件凡是编译该工程时ClCompile项列出来的文件全部搬走。提示遇到 未解决的符号 时先在错误列表里记下符号名比如??0CPaintProgressQAEXZ再用dumpbin /symbols去实例工程对应.obj文件里搜查能快速定位那个符号到底定义在哪个文件。这个命令在 VS 开发人员命令行里就有。4.5 现象实例代码拿过来能编译但字体很难看控件没有跟随对话框字体缩放MFC 对话框默认用系统字体而实例工程里可能把对话框字体设置成了 宋体 9 号 。你把这个自绘控件搬到用 微软雅黑 12 号 的工程中控件里的文字就会明显偏小或偏大。原因是自绘代码在写DrawText时没有显式设置字体而是用pDC-SelectObject选入实例工程里设定的字体。解决方法是在控件类里增加一个public方法SetCustomFont(CFont* pFont)在DrawText之前SelectObject这个字体。这个方法可以沿用实例里字体创建的代码但参数从硬编码字体名改成外部传入。如果你的主对话框里有CFont成员变量并已根据 DPI 缩放创建了对应字体把它的指针传进去即可。5. 把 26 个实例提炼成你自己的界面工具箱封装与双向扩展5.1 抽取公共基类把自绘需要重写的虚函数和公共方法收拢到一起如果你打算把这套实例里的自绘按钮、自绘进度条、自绘滑块都用起来会发现它们都涉及DrawItem、LoadBitmap或者SetColor这类重复代码。更高效的做法是仿照实例代码里隐含的继承关系先定义一个CSkinControlBase公共基类再派生出具体的自绘控件类。这个基类不需要是模板但需要承担三个职责统一管理背景画刷和前景画刷、提供一个便捷的LoadImageFromResource方法、以及保存按钮状态切换时需要重绘的标记。下面是我从这套实例里提炼的基类头文件骨架逻辑注意这是我的整理方案实例源码里并没有这个文件但它能帮你把 26 个实例的公共操作收拢起来// 公共基类处理背景画刷和贴图加载的统一入口 class CSkinControlBase { public: // 设置背景色并触发重绘任何派生控件都可直接调用 virtual void SetBackgroundColor(COLORREF clrColor) { m_clrBgColor clrColor; // 如果控件窗口已经创建必须触发 RedrawWindow 才可见效果 if (GetSafeHwnd()) RedrawWindow(); } protected: // 统一加载位图资源的方法屏蔽了不同控件各自 LoadBitmap 的差异 HBITMAP LoadImageResource(UINT nIDResource) { // 使用全局函数加载LoadImage 返回的位图需要 DeleteObject 释放 return (HBITMAP)::LoadImage( AfxGetInstanceHandle(), MAKEINTRESOURCE(nIDResource), IMAGE_BITMAP, 0, 0, LR_LOADMAP3DCOLORS); // 注意LR_LOADMAP3DCOLORS 会把位图中的深灰映射为按钮表面色 } protected: COLORREF m_clrBgColor RGB(240, 240, 240); // 背景色 COLORREF m_clrTextColor RGB(0, 0, 0); // 文字色 };你会发现派生类里几乎所有绘制代码都用到了m_clrBgColor和m_clrTextColor不再各自定义独立的颜色变量。这样做的好处是你从外部统一调用SetBackgroundColor时所有控件都会保持视觉一致性。LoadImageResource方法里的LR_LOADMAP3DCOLORS是一个很容易被忽略的细节如果不加这个标志位图中纯黑色边缘会变成难看的海关边框加上之后会把位图里的深灰映射成系统按钮表面色让自绘控件在浅色或深色对话框背景下都不那么突兀。这个标志对带圆角按钮贴图尤其重要。5.2 反向扩展从实例源码的写死逻辑中抽象出可配置的参数化模型我阅读这套源码时发现不少实例在收到绘图消息后是调用一连串硬编码的FillSolidRect和MoveToEx也就是说你看到的按钮颜色、进度条宽度都是写死的。如果想让工具箱具备通用性就需要把这些写死的常量替换成实例化时能配置的变量。以进度条为例实例源码里可能写死了 背景是浅灰、前景是蓝色、边框为黑色 。我建议你重构时定义一个结构体承载这些属性// 进度条的可视化属性配置结构体 struct ProgressTheme { COLORREF clrBackground; // 控件背景色 COLORREF clrForeground; // 进度条填充色 COLORREF clrBorder; // 边框色 BOOL bSmooth; // 是否平滑滚动 BOOL bShowPercent; // 是否显示百分比文字 };在实际使用中只需要定义一个ProgressTheme实例并初始化然后在OnPaint里根据结构体里的字段来绘制即可。这样做还有一个隐藏的好处你可以提供不同的主题预设函数比如GetDefaultBlueTheme()、GetDefaultOrangeTheme()在OnInitDialog里根据业务场景选择调用。工具箱的扩展方式就从 改源码里的颜色常量 变成了 使用方传参 这套工具箱才能从一次性代码变成可以长期复用的基础设施。5.3 新增控件实例与既有工程的对接消息映射和 ID 定义顺序当你想在这套源码 26 个实例基础上扩展自己的第 27 个控件时需要注意消息映射的编写规范。MFC 的消息映射里ON_NOTIFY_REFLECT和ON_WM_PAINT这类反射消息是写在控件的.cpp文件里的你需要确保新控件的类声明里DECLARE_MESSAGE_MAP()没有被漏掉。此外如果你不希望新控件依赖对话框模板的静态 ID而是让控件自己发出高层的业务通知比如 进度到达 100%你需要自定义一个ON_MESSAGE或ON_NOTIFY消息。建议在Resource.h里单独定义一块区域比如#define WM_USER_PROGRESS_COMPLETE (WM_USER 100)并由控件主动调用GetParent()-SendMessage(WM_USER_PROGRESS_COMPLETE)通知父窗口。值得提醒的是WM_USER在 MFC 控件中容易与其他控件内部消息冲突。如果是自定义通知更安全的做法是定义在WM_APP到WM_APP 0x7FFF区间内比如#define WM_APP_PROGRESS_COMPLETE (WM_APP 10)。这套源码里有些实例用了WM_USER消息你搬到自己工程时要注意与现有代码的窗口消息区分开。6. 验证工具箱可靠性的两个小技巧排序稳定性测试与自绘重绘压力检查任何控件代码搬进正式工程前都应该过一遍白盒验证。这套 26 个实例源码里自绘控件和排序列表都有现成的测试入口但你需要自己写验证脚本。对于列表排序我会单独建一个无界面的测试函数在里面SendMessage构造 100 行文本数据然后循环触发LVN_COLUMNCLICK每触发一次检查列表首行和末行是否符合预期。对于自绘控件重点验证的是快速切换窗口状态最大化、最小化、遮挡再恢复后内存会不会膨胀也就是重绘是否频繁创建 GDI 对象泄漏。实例代码里有的绘制函数在循环里调用CreateSolidBrush但不DeleteObject这是血泪教训——运行几分钟后整个程序界面就会逐渐变卡。验证方法是在DrawItem入口和出口分别用GetObject检查 GDI 对象计数。如果发现每次重绘都增加对象数赶紧把画笔和画刷改为成员变量窗口销毁时统一清理。第二个技巧是验证 DPI 缩放一致性。把系统显示缩放设置成 150% 运行你的程序观察自绘控件的边框和文字是否出现模糊。实例源码里大部分坐标计算都用的相对坐标但某些贴图按钮可能写死了 32x32 的尺寸。如果出现放大失真你需要把位图从仅支持 32x32 改为支持更高分辨率的多组位图资源。实际操作中我在进度条控件的SetRange方法里加入了 DPI 感知转换让控件高度随着系统缩放值动态调整。从那以后我每次把一份带自绘控件的源码搬进正式工程都会强制先编译一遍x64和x86两个平台再分别跑一遍 GDI 对象泄漏脚本和 150% DPI 截图对比省去了后续界面在用户机器上 变形 的返工。这套 26 个实例源码作为起点完全够用但你要带着 提炼成工具箱 的眼光去拆而不是把某个文件直接糊进项目就跑。希望这份实战拆解能帮到你。本文还有配套的精品资源点击获取