☰
MFC自绘取色控件实战:从FOColorButton到弹出调色板源码解析
2026/10/22 3:58:50 网站建设 项目流程

简介:这份资源是面向MFC Windows程序设计初学者与进阶学习者的实战型取色器项目源码,围绕对话框、控件自绘与颜色选择交互展开,适合正在啃MFC框架、希望理解Windows消息机制与界面定制的人群。压缩包共80个文件,约57.61MB,以h头文件与cpp源文件为核心,辅以obj、tlog、pdb等编译中间产物,以及rc资源脚本、ico与bmp图像素材、sln与vcxproj工程文件,完整保留了从源码到可执行程序的构建链路。项目包含颜色按钮、调色板窗口、弹出式取色面板、自定义静态控件与超链接等模块,可帮助读者掌握控件派生、绘制响应与资源组织方式。目前已有173人学习研究,配套博客讲解与演示,能减少自行摸索的时间,适合作为课程设计或自学练手参考。

1. 从一块取色按钮说起:这套 MFC 源码到底能跑出什么

很多人学 MFC 卡在同一个地方:对话框、控件、消息映射都看懂了,但真让他做一个「点一下弹出调色板、选完颜色实时回填到按钮上」的功能,就不知道从哪下手。这套 NewPicker 源码解决的正是这个问题——它把 Windows 取色器拆成了一组可复用的自绘控件,包括颜色按钮、下拉调色板、弹出式调色窗口、颜色单元格对象、自定义静态框和超链接控件,全部用 C++ 和 MFC 写成,工程文件从 .dsw 到 .sln 一路齐全,Debug 目录里连编译产物都在。

它适合两类人:一类是刚学完 MFC 基础、想找一个完整小项目练手的初学者,另一类是需要在老系统里嵌入取色功能、又不想引入第三方 UI 库的工程师。源码里每个控件都独立成对(.h + .cpp),你可以只拿 FOColorButton 和 FOColorDialog 两个文件塞进自己的工程,也可以整套跑起来看它们怎么协作。下面按「先跑通、再拆控件、最后避坑」的顺序往下走。

2. 把工程跑起来:环境、编译与第一次取色

2.1 先确认你的编译环境对不对

这套代码的工程文件里出现了 vc141.idb、vc141.pdb,说明它至少被 Visual Studio 2017(工具集 v141)打开过。但工程同时保留了 .dsw 和 .dsp,这是 VC6 时代的工程格式,意味着源码本身对老版本 MFC 是兼容的。我一般会这样处理:

  • 用 VS2017 或更高版本打开 NewPicker.sln,让 IDE 自动做一次工程升级;
  • 如果提示「Windows SDK 版本」不匹配,在项目属性里把目标 SDK 改成你本机已安装的版本;
  • 字符集保持「使用 Unicode 字符集」,源码里的 CString、TCHAR 都是按 Unicode 写的,改成多字节会有一堆类型不匹配。

提示:升级工程前先把整个目录复制一份,.sln 一旦被新版本改写,回退到老 IDE 就打不开了。

2.2 编译命令与常见报错

如果你习惯命令行,可以用 MSBuild 直接编:

:: 在 Developer Command Prompt 里执行 msbuild NewPicker.sln /p:Configuration=Debug /p:Platform=Win32

这条命令的含义:Configuration=Debug指定调试配置,Platform=Win32指定 32 位平台。源码里的 Debug 目录就是 Win32 平台的产物,如果你改成 x64,需要确认所有 .cpp 里没有硬编码的 32 位假设(这套代码里没有指针截断问题,但养成检查习惯)。

编译时最容易撞上的三个错:

  1. fatal error C1083: 无法打开包括文件: "StdAfx.h"——预编译头没配好。检查 NewPicker.cpp 里#include "StdAfx.h"是不是第一行,且项目属性里「预编译头」设为「使用 (/Yu)」,StdAfx.cpp 单独设为「创建 (/Yc)」。
  2. error LNK2019: 无法解析的外部符号——通常是某个 .cpp 没被加入工程。NewPicker.vcxproj.filters 里列了所有源文件,对照一下有没有漏。
  3. error RC1015: cannot open include file 'res\NewPicker.rc2'——资源路径问题,确认 res 目录和 .rc 文件在同一级。

2.3 跑起来之后先做一次取色验证

程序启动后是一个对话框,上面摆着几个自绘按钮。点带下拉箭头的颜色按钮,会弹出 FOPopupColorPaletteWnd 或 FODropPaletteWnd;点普通颜色按钮,走的是 FOColorDialog。选一个颜色,按钮背景会立刻刷新。

验证时重点看三件事:弹出窗口的位置是不是贴着按钮(这是 FODropColorPaletteControl 在算坐标)、颜色回填后按钮文字有没有被盖住(自绘的 DrawItem 逻辑)、连续打开关闭十次会不会有 GDI 对象泄漏(任务管理器看 GDI 对象数)。这三条过了,说明工程本身是健康的,可以开始拆控件。

3. 拆开看自绘按钮:FOColorButton 与消息映射

3.1 自绘按钮的核心:DrawItem 与状态位

MFC 的标准 CButton 只会画系统灰按钮,要让它显示颜色,必须走自绘(Owner Draw)。FOColorButton 继承自 CButton,在 PreSubclassWindow 里把按钮样式加上 BS_OWNERDRAW:

void FOColorButton::PreSubclassWindow() { // 强制加上自绘样式,否则 DrawItem 不会被调用 ModifyStyle(0, BS_OWNERDRAW); CButton::PreSubclassWindow(); }

ModifyStyle(0, BS_OWNERDRAW)的意思是「移除 0 个样式,添加 BS_OWNERDRAW」。这一步是自绘按钮的开关,漏了它 DrawItem 永远不会触发,按钮还是灰的——这是初学者最常翻车的地方。

DrawItem 里拿到 LPDRAWITEMSTRUCT 后,先判断itemState & ODS_SELECTED决定按下态,再用CDC::FromHandle包一层,填充背景色、画边框、画文字。颜色值存在成员变量 m_clr 里,外部通过 SetColor/GetColor 读写。

3.2 消息映射:把点击和下拉分开

普通点击和下拉箭头点击要区分开,否则点箭头也会触发颜色选择。常见做法是在 OnLButtonDown 里判断点击位置:

void FOColorButton::OnLButtonDown(UINT nFlags, CPoint point) { CRect rc; GetClientRect(&rc); // 右侧 16 像素算下拉区域 if (m_bDropDown && point.x > rc.right - 16) { // 走下拉分支,通知父窗口弹调色板 GetParent()->SendMessage(WM_COMMAND, MAKEWPARAM(GetDlgCtrlID(), FOBN_DROPDOWN), (LPARAM)m_hWnd); } else { CButton::OnLButtonDown(nFlags, point); } }

参数说明:m_bDropDown控制这个按钮是不是下拉型;FOBN_DROPDOWN是自定义通知码,父对话框在 ON_COMMAND 或 ON_NOTIFY 里接住它,再决定弹哪个窗口。这样设计的好处是按钮本身不关心弹窗长什么样,职责单一,换调色板不用改按钮代码。

3.3 颜色回填与重绘

选完颜色后,父窗口调用按钮的 SetColor,内部更新 m_clr 并 Invalidate。注意 Invalidate 之后要 UpdateWindow 才会立刻重绘,否则要等消息队列空闲。连续快速取色时如果不加 UpdateWindow,会看到颜色「慢半拍」。

注意:自绘控件里所有 GDI 对象(画笔、画刷)用完必须 DeleteObject,或者用 CDC 的 SelectObject 保存旧对象再还原。这套源码里 FOColorCellObj 就是专门管颜色单元格绘制的,把 GDI 生命周期收在一处,比散落在 DrawItem 里安全。

4. 调色板家族:从 FOColorDialog 到弹出式调色窗口

4.1 三种调色入口的分工

源码里跟调色相关的类不止一个,容易看晕,先理清分工:

类名形态适用场景
FOColorDialog模态对话框需要精确输入 RGB/HSV 时
FOPopupColorPaletteWnd弹出窗口点按钮后就近弹出,选完即关
FODropPaletteWnd / FODropColorPaletteWnd下拉面板嵌在下拉按钮下方,带预设色块
FOColorCellObj单元格对象被上面几个窗口复用,负责单个色块绘制与命中

FOColorCellObj 是关键复用点:它把「一个色块的位置、颜色、选中态、绘制、命中测试」封在一起。调色窗口里排一堆 FOColorCellObj,鼠标移动时遍历判断命中哪个,比每次重算坐标可靠得多。

4.2 弹出窗口的定位与失焦关闭

弹出式调色窗口最难的不是画色块,是定位和关闭。定位一般这样算:

void FOPopupColorPaletteWnd::ShowAt(CWnd* pOwner, CRect rcAnchor) { CRect rcWnd; GetWindowRect(&rcWnd); // 默认贴在锚点下方左对齐 int x = rcAnchor.left; int y = rcAnchor.bottom; // 超出屏幕右边界就左移 int cxScreen = GetSystemMetrics(SM_CXSCREEN); if (x + rcWnd.Width() > cxScreen) x = cxScreen - rcWnd.Width(); SetWindowPos(&wndTop, x, y, 0, 0, SWP_NOSIZE | SWP_SHOWWINDOW); SetCapture(); // 捕获鼠标,点外面能收到消息 }

SetCapture是为了在点窗口外面时也能收到 WM_LBUTTONDOWN,从而关闭自己。但 SetCapture 有个坑:如果窗口在捕获期间被销毁而没 ReleaseCapture,鼠标消息会一直发给一个不存在的窗口,表现为整个程序点哪都没反应。所以 OnDestroy 或关闭分支里必须 ReleaseCapture。

4.3 颜色模型转换:RGB 与 HSV 的取舍

FOColorDialog 里如果带色相环或饱和度方块,就涉及 RGB 与 HSV 互转。MFC 没有内置转换函数,源码里是自己写的。常见做法是:

// RGB -> HSV,h∈[0,360), s,v∈[0,1] void RGBtoHSV(COLORREF cr, float& h, float& s, float& v) { float r = GetRValue(cr) / 255.0f; float g = GetGValue(cr) / 255.0f; float b = GetBValue(cr) / 255.0f; float mx = max(r, max(g, b)); float mn = min(r, min(g, b)); float d = mx - mn; v = mx; s = (mx == 0) ? 0 : d / mx; if (d == 0) h = 0; else if (mx == r) h = 60 * fmod((g - b) / d, 6); else if (mx == g) h = 60 * ((b - r) / d + 2); else h = 60 * ((r - g) / d + 4); if (h < 0) h += 360; }

参数说明:输入 COLORREF 是 0x00BBGGRR 排列,注意 GetRValue/GetGValue/GetBValue 的顺序别搞反。输出 h 是角度,s、v 是 0 到 1 的浮点。反向转换 HSVtoRGB 时,h 要先对 360 取模,否则负角度会算出越界颜色。

提示:如果只是做预设色块选择,不需要 HSV;只有要画色相条、饱和度方块时才引入转换。多写一套转换就多一处精度和边界问题,按需上。

5. 避坑与排查:自绘控件最容易翻车的五件事

5.1 现象:按钮点了没反应,颜色不更新

原因:PreSubclassWindow 里没加 BS_OWNERDRAW,或者加了但 DrawItem 里没处理 ODS_SELECTED 之外的状态,导致视觉上没变化。另一种可能是 SetColor 后只改了成员变量没 Invalidate。

解决:确认 ModifyStyle 调用在 PreSubclassWindow 里且基类调用在后;SetColor 末尾统一加Invalidate(); UpdateWindow();。

5.2 现象:弹出调色板关不掉,点哪都没反应

原因:SetCapture 之后窗口被隐藏或销毁,但没有 ReleaseCapture,鼠标被「锁」在已失效的窗口上。

解决:在所有关闭路径(选色完成、点外部、按 ESC、父窗口移动)里统一走一个 ClosePopup 函数,函数内先 ReleaseCapture 再 ShowWindow(SW_HIDE)。别在多个分支里各写一遍。

5.3 现象:连续取色几十次后程序变卡,GDI 对象数一直涨

原因:DrawItem 或色块绘制里每次 new 画笔/画刷,用完没 DeleteObject。MFC 的 CDC 包装不会自动帮你回收。

解决:把画笔画刷做成类成员,构造时创建、析构时删除;或者用 CPen pen; CDC::SelectObject(&pen); 这种栈对象,但要注意 SelectObject 返回的旧对象要还原。FOColorCellObj 里集中管理就是这个思路。

5.4 现象:Release 下正常,Debug 下断言失败

原因:MFC 的 ASSERT 在 Debug 下会检查 HWND 有效性、CDC 是否为空等。常见于在窗口还没创建时就调用 GetDC,或者消息映射里用了已销毁的控件指针。

解决:看断言弹出的文件和行号,多半是 CWnd* 为空。在调用前加if (GetSafeHwnd())判断,别用if (this),this 永远非空。

5.5 现象:换到高 DPI 屏幕后,弹出窗口位置偏移、色块模糊

原因:代码里用了硬编码像素值,且没处理 DPI 缩放。MFC 老工程默认是 DPI 不感知的,系统会做位图拉伸,导致模糊。

解决:在项目属性里开启「DPI 感知」,把 16 像素下拉区、色块尺寸改成按GetDpiForWindow换算。如果不想大改,至少在清单文件里声明 dpiAware,让系统别拉伸。

6. 进阶用法:把取色控件塞进自己的工程

6.1 最小移植清单

如果你不想整套搬,只想要一个能取色的按钮,最少需要这几个文件:

  • FOColorButton.h / FOColorButton.cpp
  • FOColorDialog.h / FOColorDialog.cpp
  • FOColorCellObj.h / FOColorCellObj.cpp(FOColorDialog 依赖它画色块)

把这六个文件加入你的工程,在对话框上放一个普通按钮,用类向导给它绑定 FOColorButton 类型变量(或者手动 SubclassDlgItem),然后处理 FOBN_DROPDOWN 或直接响应点击弹 FOColorDialog。资源文件里的 fo_color.bmp、fo_cursor1.cur 按需拷贝,不用可以删。

6.2 自定义预设色板

FODropPaletteControl 里维护了一组预设颜色。想换成自己公司的品牌色,找到初始化色板的地方,改颜色数组即可。常见做法是把颜色存成 COLORREF 数组:

static const COLORREF g_brandColors[] = { RGB(0x1F, 0x4E, 0x79), // 主色 RGB(0x2E, 0x86, 0xC1), // 辅助色 RGB(0xA9, 0xCC, 0xE5), // 浅色 RGB(0xF7, 0x9A, 0x1E), // 强调色 };

数组长度变化时,记得同步改遍历上限,别用魔数 4 写死在循环里。我一般会加一个_countof(g_brandColors),避免以后加颜色忘了改循环。

6.3 验证移植是否干净

移植完做三个检查:一是把原工程删掉,只留你的工程,看能不能独立编译;二是用 Dependency Walker 或 VS 的「生成依赖项」看有没有意外依赖原工程的 .obj;三是在一台没装过 VS 的干净机器上跑 Release 版,确认没有缺 DLL。这三步走完,才算真正把控件拿出来了。

从那以后我每次移植 MFC 控件,都会先建一个空工程只放目标文件,编译通过再往正式工程里合。这样出问题能立刻定位是控件本身还是工程配置。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询