简介:一款基于微软基础类库(MFC)的Windows取色器示例工程,面向初学MFC的C++开发者,演示屏幕取色、颜色面板与自定义控件交互的完整实现思路。工程以NewPicker命名,围绕取色与调色功能拆分为多个独立控件模块:自定义颜色按钮、下拉取色按钮、弹出调色板窗口、颜色对话框,以及用于状态反馈的自定义静态图片框等,覆盖了控件封装、消息映射到对话框事件响应的常见MFC开发场景,对理解MFC自定义控件绘制与资源管理很有帮助。资源包共80个文件,压缩后约57.61MB,以头文件、实现文件和编译中间文件为主,同时包含Visual Studio工程配置、光标、位图、图标等资源文件以及可直接运行的exe程序,既适合编译运行验证效果,也适合对照源码逐行研读,从目录结构可清晰区分源码与编译产物。已有173人学习下载,适合正在苦学MFC Windows程序设计的小伙伴边读源码边验证效果,能少走不少弯路。
1. 从屏幕取色到一键复制:MFC 取色器到底在解决什么问题
写代码的人桌面常备一个取色器。前端调一个色值、给图标配色、提取某个页面里的强调色,都要先把鼠标移到目标像素上,再看取色器读出的数值。MFC Windows 程序设计里的新款取色器,就是用 C++ 和 Win32 API 把这条路走通:光标移到哪,颜色就采到哪,RGB、HEX、HSV 三套值实时可见,空格锁定、Ctrl+C 复制,配合放大镜还能精确到一个像素。它解决的不只是「取一个颜色」这个动作,而是把屏幕采样、窗口消息、DC 绘制、剪贴板这些桌面开发基本功串成一个完整工具。适合刚学完 MFC 基本控件、想做一个能天天自用的桌面工具的人,也适合想看看 Win32 底层 API 怎么在一百行代码里干活的老手。
2. 搭建取色器对话框骨架:从工程类型到定时器驱动的采样循环
取色器的界面不复杂,但工程类型、消息循环、绘制路径这三件事在动工前就要定下来。这一章先把对话框骨架和采样循环搭起来,让程序先能跑、能看到颜色在跟着光标变,后面再谈数据换算和功能增强。
2.1 选对话框还是单文档:为什么取色器要的是一个无文档的窗口
很多人在计划一个 MFC 工具时,第一反应是找单文档工程模板。但对取色器来说,单文档是杀鸡用牛刀——它没有数据要存盘、没有文档/视图分离的需求,真正需要的只是一个能吸附在屏幕上、随光标刷新内容的顶层窗口。这个问题放到「桌面软件开发 用 mfc 还是 qt」的讨论里看也一样:取色器要的是直接操作 Win32 消息循环和屏幕 DC 的能力,MFC 恰好把 GetPixel、SetTimer、消息映射这几样都摆在手边;如果换成 Qt,你最终还是要绕回 QScreen::grabWindow 或 Win32 钩子,中间多一层抽象反而容易在坐标换算上翻车。所以对话框工程(CDialogEx)在这个场景下是最轻量的选择:资源编辑器里拖几个控件、绑定几个成员变量,剩下的工作全部集中在消息处理函数里就够用了。这个选型也直接影响后续交互——对话框没有文档框架的负担,做置顶、半透明、自绘小控件都自由得多。
2.2 先做最小可运行的采样循环:DPI 感知 + SetTimer + GetPixel
取色器最核心的循环是:定时器的每个 tick 里拿一次光标位置,从屏幕 DC 上取这个点的颜色,然后刷新界面。这三个动作拆开都不难,但组合起来有一个隐蔽前提——DPI 感知。如果你的程序没有声明 DPI 感知,系统会按缩放兼容模式处理:GetCursorPos 返回的是物理坐标,而屏幕 DC 的逻辑尺寸还停留在 96dpi 的虚拟坐标系里。100% 缩放下两者一样,到了 125%、150% 缩放的显示器上,你拿物理坐标去 DC 里取像素,实际命中点是光标偏移后的位置,颜色自然就偏了。所以第一步是在 InitInstance 里把 DPI 感知打开:
BOOL CColorPickerApp::InitInstance() { // 必须在创建窗口之前调用,否则不会生效 SetProcessDPIAware(); CColorPickerDlg dlg; m_pMainWnd = &dlg; dlg.DoModal(); return FALSE; }SetProcessDPIAware 的作用是告诉系统「这个程序自己处理缩放,别用虚拟化坐标糊弄我」。这段代码解决了老工程在高分屏下取色坐标漂移的问题。如果你的目标系统是 Windows 10 以上的新版本,更彻底的做法是用 manifest 声明 Per-Monitor V2 DPI awareness,这样多显示器不同缩放比例的场景也不会出问题,但这一行 API 对大多数取色器场景已经够用。
然后是对话框初始化里开启定时器:
BOOL CColorPickerDlg::OnInitDialog() { CDialogEx::OnInitDialog(); // 30ms 一个采样周期,60Hz 屏上无延迟感,CPU 占用也可接受 SetTimer(TIMER_PICK, 30, nullptr); // 首次显示给个白色,避免控件还没取到色就画黑块 m_color = RGB(255, 255, 255); return TRUE; }定时器周期不要盲目调小。20ms 是很多取色器默认值,但实测 30ms 已经能满足每次光标移动都即时刷新,CPU 占用也更低。如果你后面开了放大镜绘制,这个值还要再权衡,第 5 章会说到。
采样主体在 OnTimer 里完成:
void CColorPickerDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == TIMER_PICK) { POINT pt; GetCursorPos(&pt); // 物理屏幕坐标 HDC hScreen = ::GetDC(nullptr); // 整个虚拟屏幕的 DC m_color = ::GetPixel(hScreen, pt.x, pt.y); ::ReleaseDC(nullptr, hScreen); if (m_color == CLR_INVALID) m_color = RGB(255, 0, 255); // 取不到时显示品红,一眼可辨 UpdateColorDisplay(); Invalidate(FALSE); // FALSE 表示不擦背景 } CDialogEx::OnTimer(nIDEvent); }这里有三个容易忽略的细节。第一,GetDC(nullptr) 拿到的是整个虚拟屏幕的 DC,多显示器环境下鼠标在副屏时 GetCursorPos 返回的坐标可能带负数,但虚拟屏幕 DC 的坐标范围是覆盖所有显示器的,所以行为仍然正确。第二,GetPixel 在取色失败时返回 CLR_INVALID,它的值等于 32 位的 0xFFFFFFFF,如果直接当正常颜色用,三个通道都会解出 0xFF,看起来就是个刺眼的白块。第三,每次 OnTimer 都 GetDC/ReleaseDC 有性能开销,虽然量级很小,但如果后面放大镜绘制变重,可以考虑把屏幕 DC 缓存起来,只在分辨率变化时重建。
2.3 用双缓冲 OnPaint 画颜色预览:告别画面闪烁
在 OnTimer 里调用了 Invalidate(FALSE),接下来的 OnPaint 就要把当前颜色可视化。新手最常见的做法是直接在 OnPaint 里往窗口 DC 上 FillRect,结果是鼠标一挪,预览区域疯狂闪烁。原因在于 WM_PAINT 的擦背景和绘制不是原子的:系统先擦除背景再执行 OnPaint,两次操作之间的间隙用户能看见。解决方式就是双缓冲:先在一个内存位图上画完所有内容,再一次 BitBlt 到屏幕。
void CColorPickerDlg::OnPaint() { CPaintDC dc(this); CDC memDC; memDC.CreateCompatibleDC(&dc); CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, PREVIEW_W, PREVIEW_H); CBitmap* pOldBmp = memDC.SelectObject(&bmp); // 左边 50x50 颜色块,右边跟上 HEX 文本 memDC.FillSolidRect(10, 10, 50, 50, m_color); memDC.SetBkMode(TRANSPARENT); memDC.TextOut(70, 25, RGBToHex(m_color)); // 一次性拷贝到窗口,避免逐像素刷新闪烁 dc.BitBlt(0, 0, PREVIEW_W, PREVIEW_H, &memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBmp); bmp.DeleteObject(); }要点是绘制期间尽量把画图指令都发向 memDC,只在最后一步用 BitBlt 汇入客户 DC。还有一个容易漏的小动作:SelectObject 的返回值 pOldBmp 一定要存下来,画完恢复回去,否则位图对象释放时可能触发 GDI 对象泄漏。取色器是长时间挂机的进程,GDI 对象数涨上去之后,界面绘制会肉眼可见地变卡,最后甚至 crash,这是血泪教训。
3. 把屏幕像素变成可丢给设计的参数:RGB/HEX/HSV 换算与输出
采样循环只产出一个 COLORREF,但它还不是人能直接用的颜色值。这一章处理数据的后半程:把 COLORREF 解成三通道,再换算成 HEX 和 HSV,最后送进编辑框、剪贴板和自绘颜色块。
3.1 从 COLORREF 说起:GetPixel 拿回来的 32 位整数是什么
GetPixel 返回的是一个 COLORREF。别被名字骗了,COLORREF 的字节序是 BGR 而不是望文生义的 RGB:从低位到高位,第 1 字节是蓝、第 2 字节是绿、第 3 字节是红、第 4 字节恒为零。务必用 GetRValue/GetGValue/GetBValue 这三个宏去解分量,手写移位不仅可读性差,将来一旦遇到带 alpha 的扩展颜色格式,自以为是的移位会直接翻车。取色器场景里,COLORREF 就是屏幕 DC 的原始返回类型,后续所有换算、显示、复制都从它出发。理解这一点,整个颜色处理模块就稳了。
3.2 RGB 到 HEX/HSV 的换算函数:公式与边界值处理
界面上至少需要三套数据:HEX 是设计师和前端最习惯的,HSV 用来判断颜色明度和纯度,RGB 三个分量则是底层基础。HEX 的转换是纯格式化:
CString RGBToHex(COLORREF color) { CString str; // %02X 保证单通道小于 0x10 时补齐两位 str.Format(_T("#%02X%02X%02X"), GetRValue(color), GetGValue(color), GetBValue(color)); return str; }注意这里用的是%02X。漏掉02的话,红色分量0x0F会输出成#F…,颜色值残缺,前端拿到手也认不出。
HSV 的换算稍微绕一些,套数学公式时要特别处理边界:
void RGBToHSV(COLORREF color, double* h, double* s, double* v) { double r = GetRValue(color) / 255.0; double g = GetGValue(color) / 255.0; double b = GetBValue(color) / 255.0; double maxC = max(r, max(g, b)); double minC = min(r, min(g, b)); double delta = maxC - minC; *v = maxC; if (delta == 0.0) { *h = 0.0; // 灰色与黑色没有色相 *s = 0.0; } else { *s = delta / maxC; if (maxC == r) *h = 60.0 * (g - b) / delta; else if (maxC == g) *h = 60.0 * (2.0 + (b - r) / delta); else *h = 60.0 * (4.0 + (r - g) / delta); if (*h < 0.0) *h += 360.0; // 负角归入 0~360 } }这段函数有两个边界必须守住。一是纯黑时 maxC 等于 0,此时 delta 也为 0,好在delta == 0.0分支先拦住了,不会走进delta / maxC除零。二是色相计算出来可能是负角度,必须加 360 度归位,否则界面上会出现负数和 NaN 的诡异字符。从 COLORREF 取分量还有一个隐性坑:调试时直接看裸 int 会看到类似0x00FF8040的值,它其实对应 RGB(64,128,255)——低字节是蓝。手动比对颜色时很容易自我怀疑,建议把三色分量封装成固定的调试输出函数,不要直接看内存。
3.3 输出到编辑框、剪贴板和颜色块:mfc 状态栏显示这类需求的取舍
颜色算出来以后,界面要做三件事:文本显示、颜色块同步、剪贴板复制。文本显示这里有一个绕不开的问题:到底用状态栏还是编辑框。搜索里「mfc 状态栏怎么显示」这类问题经久不衰,但对话框程序里手动创建 CStatusBar 要额外管理布局和刷新,对取色器这种高频刷新文本的场景有点笨重。用编辑框配合 SetDlgItemText 反而轻快,30ms 一刷毫无压力。
void CColorPickerDlg::UpdateColorDisplay() { CString hex = RGBToHex(m_color); CString rgb; rgb.Format(_T("RGB(%d, %d, %d)"), GetRValue(m_color), GetGValue(m_color), GetBValue(m_color)); double h, s, v; RGBToHSV(m_color, &h, &s, &v); CString hsv; hsv.Format(_T("H %.0f S %.1f%% V %.1f%%"), h, s * 100, v * 100); SetDlgItemText(IDC_EDIT_HEX, hex); SetDlgItemText(IDC_EDIT_RGB, rgb); SetDlgItemText(IDC_EDIT_HSV, hsv); if (m_ctrlColorBlock.GetSafeHwnd()) m_ctrlColorBlock.Invalidate(FALSE); // 颜色块自绘刷新 }剪贴板复制是取色器的高频操作,必须做到能复制就绝不失败:
void CColorPickerDlg::CopyHexToClipboard() { CString hex = RGBToHex(m_color); if (!OpenClipboard(GetSafeHwnd())) return; EmptyClipboard(); HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, (hex.GetLength() + 1) * sizeof(TCHAR)); if (hMem != nullptr) { LPTSTR pBuf = (LPTSTR)GlobalLock(hMem); if (pBuf != nullptr) { lstrcpy(pBuf, hex); GlobalUnlock(hMem); // 成功时系统接管内存,失败时必须自己释放 if (SetClipboardData(CF_UNICODETEXT, hMem) == nullptr) GlobalFree(hMem); } } CloseClipboard(); }剪贴板这段有三个坑。第一,OpenClipboard 失败不能直接 return 了事,至少留一条日志,否则用户会以为取色器坏了,其实是别的程序正占着剪贴板。第二,用 CF_UNICODETEXT 就必须按宽字符分配内存,(len + 1) * sizeof(TCHAR)是刚需,漏乘 sizeof(TCHAR) 必然越界。第三是句柄归属:SetClipboardData 成功,系统接管;失败,必须自己 GlobalFree,代码里已经把这个分支写上了。颜色块控件我倾向于不单独造类,直接在对话框 OnPaint 里固定一块矩形 FillSolidRect 填当前色,再叠 1 像素对比色边框,省掉一份消息映射,代码量也更少。
4. 取色器避坑指南:DPI、失焦、管理员权限与闪烁排查
取色器看起来简单,实际跑起来翻车点一个接一个。这一章把最常见的五类问题按「现象 → 原因 → 解决」写清楚,每一类都是实际开发里会被用户骂的场景。
4.1 DPI 缩放让取色偏了:SetProcessDPIAware 的调用时机与清单声明
现象:在 125% 缩放的笔记本上,鼠标指着一个红色像素,取色器显示的是它旁边几像素的混色。原因:程序没有声明 DPI 感知,GetCursorPos 返回物理坐标,屏幕 DC 却被虚拟化成 96dpi 逻辑坐标,两者在非 100% 缩放下不相等。解决:InitInstance 最前面调用 SetProcessDPIAware,而且必须在窗口创建前。这个坑的麻烦在于它「偶尔发生」——家里电脑 100% 缩放完全正常,一到公司接上 2K 外接屏就偏,属于最隐蔽的一类 bug。发布前一定在 125% 和 150% 两种缩放下各跑一遍。
4.2 一取色就变灰按钮:失焦与鼠标点击的时序问题
现象:鼠标移到一个按钮上,取色器显示的不是按钮的蓝灰色,而是它周围一圈白色或父窗口底色。原因:鼠标按下的瞬间,焦点切换、按钮所属窗口正在处理 WM_LBUTTONDOWN,此刻 GetPixel 读到的可能是按钮阴影或下层窗口的内容。这条尤其容易在使用「按下取色」交互时触发。解决:不要在按下瞬间采样,把取色动作推迟到下一帧——OnLButtonDown 里只置一个 pending 标志,等下一个 OnTimer 才真正读像素;或者干脆用悬停采样,鼠标不用点下去。取色器的设计原则是「所见即所得」,用户看到什么就取什么,点击造成的时序差会让结果和看到的对不上。
4.3 管理员窗口取不到颜色:完整性级别与硬件加速渲染
现象:对一个以管理员权限运行的窗口(比如某些安装向导)取色,GetPixel 返回 CLR_INVALID,界面弹出一个刺眼的品红块。原因有两类:一是完整性级别不同,系统对提权窗口的跨级别 GDI 采样有限制;二是现代 Windows 上大量窗口走 DirectComposition 硬件合成,根本不经过传统屏幕 DC,GDI 的 GetPixel 读不到合成器里的内容。解决:取色器以管理员身份运行能解决第一类,第二类只能换 PrintWindow 抓取目标窗口位图,再从位图里取像素,代价是 CPU 占用和窗口遮挡管理都复杂不少。如果只是内部工具,在说明文档里写一句「对管理员窗口请右键以管理员身份启动本程序」就够了。
4.4 放大区域疯狂闪烁:双缓冲失效的三个可能原因
现象:开启放大镜后,预览区域高速闪烁,肉眼几乎无法辨认像素格子。原因通常有三个:一是 OnTimer 里 Invalidate 写成了 TRUE,导致每个 tick 都先擦背景再重绘;二是 OnPaint 里绕过内存位图,直接往客户 DC 上逐格画画,绘制时间拉长到几十毫秒;三是定时器周期压得太短,比如 5ms,WM_PAINT 处理不过来,消息队列里堆了一串绘制请求。解决:Invalidate 参数一律 FALSE,背景交给双缓冲的 BitBlt 整体覆盖;用 QueryPerformanceCounter 实测一次完整绘制耗时,再反推定时器周期——如果一次绘制要 15ms,定时器设 20ms 就是底线,设 10ms 只会让消息队列爆炸。
4.5 最小化后再恢复取不了色:定时器与消息循环的坑
现象:把取色器最小化到任务栏几分钟,恢复后界面冻结,颜色不再刷新。原因:最小化期间窗口不重绘,OnTimer 虽然还在触发,但 Invalidate 的绘制请求被系统合并或延迟;恢复时如果 OnPaint 里依赖了某些早已失效的 GDI 对象或剪贴板句柄,还可能出现断言。解决:在 OnSize 里判断最小化状态,最小化时 KillTimer(TIMER_PICK),恢复时重新 SetTimer。取色器属于「挂后台一整天」的工具,这个防护必须做,否则用户最小化一下再拉起来,第一反应就是程序死了。
5. 把取色器做成新款:放大镜、微调锁定与历史颜色
到这里基础取色器已经能用了,但它和十年前的版本没有区别。新款取色器的价值在三个交互升级:放大镜让用户看清像素边界,键盘微调解决手抖,历史颜色解决连续取色的记忆负担。
5.1 7x7 采样窗放大镜:在 OnPaint 里画像素格子
基础取色器只能看到当前这一个点,但界面元素几乎都有抗锯齿、渐变和 1 像素描边,用户需要看到周围一圈才能确定自己选的是不是想要的那条边。新款取色器的关键升级就是放大镜:把光标周围 7x7 个像素按每像素放大成 8x8 的小格子画在对话框里,再叠一个十字准线标出中心像素。
先在 OnTimer 里多采一个 7x7 窗口:
void CColorPickerDlg::CaptureSurroundingPixels(const POINT& pt) { HDC hScreen = ::GetDC(nullptr); for (int dy = -3; dy <= 3; dy++) { for (int dx = -3; dx <= 3; dx++) { POINT sample = { pt.x + dx, pt.y + dy }; COLORREF c = ::GetPixel(hScreen, sample.x, sample.y); m_sampleGrid[(dy + 3) * 7 + (dx + 3)] = (c == CLR_INVALID) ? RGB(255, 0, 255) : c; } } ::ReleaseDC(nullptr, hScreen); }然后 OnPaint 里逐格放大绘制:
// 每个像素放大为 8x8 的小格子,7x7 窗变成 56x56 预览区 const int kZoomScale = 8; const int kGridSize = 7; for (int row = 0; row < kGridSize; row++) { for (int col = 0; col < kGridSize; col++) { COLORREF c = m_sampleGrid[row * kGridSize + col]; CBrush brush(c); CRect rc( kZoomLeft + col * kZoomScale, kZoomTop + row * kZoomScale, kZoomLeft + (col + 1) * kZoomScale, kZoomTop + (row + 1) * kZoomScale); memDC.FillRect(&rc, &brush); } } // 中心像素用反色十字线标记,任何背景下都可见 COLORREF center = m_sampleGrid[3 * kGridSize + 3]; COLORREF lineColor = (GetRValue(center) > 128) ? RGB(0, 0, 0) : RGB(255, 255, 255);这里有几个参数调节经验。7x7 窗放大 8 倍后是 56x56 像素,视觉比例舒适;如果只需要定位边缘,5x5 放大 10 倍更锐利。中心像素一定要用反色十字线标记,否则黑背景上的黑十字完全看不见。越界像素统一填品红,让用户一眼知道光标已经跑到屏幕边缘外了。放大镜的绘制成本主要花在 49 次 FillRect 上,这也是为什么第 2 章说要先做双缓冲——如果每次直接往客户 DC 画,这 49 个矩形会闪得人眼晕。
5.2 方向键微调、空格锁定、Ctrl+C 复制:键盘工作流
鼠标取色最大的问题是手抖。即使有放大镜,菜单栏上 1 像素宽的描边依旧极难精确点中。新款取色器的第二个特征是键盘微调:按方向键让采样点向对应方向移动 1 像素,按空格锁定当前颜色并复制 HEX,Ctrl+C 复制 RGB 三通道文本。这套工作流配合放大镜,取精度要求极高的色时手完全不用碰鼠标。
实现时直接拦对话框的键盘消息:
BOOL CColorPickerDlg::PreTranslateMessage(MSG* pMsg) { if (pMsg->message == WM_KEYDOWN) { switch (pMsg->wParam) { case VK_LEFT: m_pickPos.x -= 1; // 采样点向左微调 1px return TRUE; case VK_RIGHT: m_pickPos.x += 1; return TRUE; case VK_UP: m_pickPos.y -= 1; return TRUE; case VK_DOWN: m_pickPos.y += 1; return TRUE; case VK_SPACE: CopyHexToClipboard(); SetWindowText(_T("HEX 已复制")); return TRUE; } } return CDialogEx::PreTranslateMessage(pMsg); }m_pickPos 是 POINT 成员变量,初始值来自当前光标位置;一旦按过方向键,采样点就和鼠标解耦。这时界面上最好把十字线加粗或换颜色,明确提示用户处于微调模式,否则用户会疑惑光标没动、颜色怎么在变。空格锁定后一定要给可视反馈——改窗口标题或者让颜色块闪一下都行,没有任何反馈的复制会让人连续按好几下空格,又不敢确定到底成没成功。
5.3 用 CListBox 存最近 20 个颜色:双击回调到主预览
连续取色时最大的痛点不是取不到,而是「上一种颜色还没记下来就取了下一个」。历史颜色列表就是为这个场景准备的:每次锁定或复制时把 HEX 文本追加到一个只读 CListBox,最多保留 20 条;双击某条时,把那个颜色回填到主预览区域,方便和当前颜色对比。
void CColorPickerDlg::OnLbnDblclkHistory() { int idx = m_ctrlListHistory.GetCurSel(); if (idx == LB_ERR) return; CString hex; m_ctrlListHistory.GetText(idx, hex); // 从 "#RRGGBB" 反解出 COLORREF COLORREF color = RGB( _tcstol(hex.Mid(1, 2), nullptr, 16), _tcstol(hex.Mid(3, 2), nullptr, 16), _tcstol(hex.Mid(5, 2), nullptr, 16)); m_color = color; UpdateColorDisplay(); Invalidate(FALSE); }CListBox 的管理有两个注意点。一是在资源编辑器里给列表加上只读样式,并在初始化时用AddString逐条加入;超过 20 条就DeleteString(0)把最老的顶掉。二是双击事件的通知码是 LBN_DBLCLK,通过 WM_COMMAND 发给父窗口,记得在消息映射里补上 ON_LBN_DBLCLK,否则双击毫无反应,这是 CListBox 新手最常见的「为什么我双击没效果」来源。
6. 取色器的验证方法:从行为测试到发布前检查
取色器是高频使用的桌面工具,验证的核心就一句话:长时间挂机不崩、各种像素环境都取得到。我自己的习惯是拿一张包含纯黑、纯白、渐变、抗锯齿文字、彩色按钮的测试图跑下面的清单:
- 在 100%、125%、150% 缩放的三台机器上分别取色,确认坐标无偏移。
- 将窗口拖到两个显示器交界处取色,确认副屏负坐标采样正确。
- 锁定纯黑和纯白两个极端色,确认 HSV 不会出现 NaN。
- 连续取色 200 次后用任务管理器观察 GDI 对象数,确认没有增长。
- 最小化半小时后恢复,确认定时器重启、颜色能继续刷新。
- 开启放大镜后把鼠标快速扫过屏幕边缘,确认品红越界标记出现且界面不闪。
发布前还有两个小技巧。一是用 Release 版而不是 Debug 版跑测试——Debug 的 STL 断言会主动报警,但 Release 版的崩溃更隐蔽,很多问题在 Debug 下根本复现不了;二是给程序加一个「取色频率」菜单项,方便在测出定时器周期对 CPU 的占用曲线,再决定要不要在性能弱的机器上自动降频。大多数 MFC 教程讲到控件和消息循环就收场,但取色器这种项目能把 DC、定时器、剪贴板、自绘全部串起来,是检验基本功的好练习。当年我第一次做取色器就因为没有在 Release 下测,发布后用户一开放大镜就闪退,查了半天是 STL 迭代器断言在 Release 编译下静默失效,从此养成了 Release 优先的习惯。希望这份 MFC 取色器的设计与踩坑记录能帮到你,把它做进你自己的工具箱。
本文还有配套的精品资源,点击获取