
简介本资源是一款基于MFC开发的现代化Windows取色器工具专为C初学者及MFC入门学习者设计聚焦Windows桌面程序开发核心实践帮助开发者快速掌握GDI绘图、自定义控件、模态/非模态对话框、光标与位图资源集成等关键技能。压缩包共80个文件涵盖16个头文件.h与15个源码文件.cpp构成完整可编译工程另有可执行文件.exe、资源文件.ico/.bmp/.cur/.rc、项目配置.sln/.vcxproj及调试支持文件.pdb/.ilk结构规范便于逐模块理解MFC消息映射、资源管理与UI定制逻辑。目前已有173人下载学习配套博文提供详细实现原理讲解与运行演示避免初学者在窗口类封装、颜色拾取算法、弹出式调色板等难点上反复试错。读者可直接编译运行深入剖析FOColorDialog、FODropColorButton等自研控件源码掌握企业级MFC界面组件的设计思路与复用方法。1. MFC Windows 程序设计之新款取色器一个能真正在 VS2019/VS2022 里编译运行的完整工程专为 MFC 新手拆解而生你刚在 Visual Studio 里新建了一个 MFC 应用程序却卡在“怎么让按钮点击后弹出颜色选择框”这一步网上搜“MFC 取色器”十篇有八篇是 Win32 API 手写ChooseColor的简化版剩下两篇贴的是残缺头文件、缺资源、缺.rc文件、甚至.vcxproj里还带着 VC6 的NewPicker.dsw和NewPicker.dsp——一加载就报错“项目格式不兼容”。这不是学习障碍是环境陷阱。这个NewPicker.zip是少有的、全源码可编译、带完整资源、含自定义控件封装、且已适配现代 VS 工具链的 MFC 取色器工程。它不是 Demo而是一个真实可用的桌面小工具支持鼠标悬停实时取色、拖拽取色、色值 HEX/RGB 双显示、历史颜色记录、色盘悬浮窗、自定义光标fo_cursor1.cur和位图资源fo_color.bmp。适合正在啃《深入浅出 MFC》或跟着 B 站视频敲代码、但总被“资源未加载”“类未定义”“LNK2001”反复暴击的 C 初学者。它不教你抽象工厂模式但它把FOColorDialog、FODropColorButton、FOPopupColorPaletteWnd这些封装好的控件像乐高零件一样摊开在你面前——每个.h/.cpp文件都带注释每个资源 ID 都在resource.h里明确定义连StdAfx.h的预编译配置都保留着原始路径逻辑。这不是“能跑就行”的玩具而是你第一次真正理解“MFC 消息映射如何驱动 UI”、“CWnd 派生类如何接管绘制”、“资源脚本.rc怎么和CDialog关联”的实战入口。2. 从零加载到首次编译VS2019/VS2022 兼容性适配与工程结构还原这个NewPicker.zip表面看是一堆.cpp/.h/.rc文件实则暗藏三重兼容性挑战VC6 遗留痕迹、VS 版本工具链差异、以及 MFC 项目模板演进导致的配置断层。直接双击.sln很可能失败——因为NewPicker.sln是旧版生成器创建的而NewPicker.vcxproj才是现代 VS 的核心。我们必须手动重建信任链。2.1 识别并清理 VC6 遗留文件避免编译器误判打开压缩包你会看到这些明显属于老版本的文件NewPicker.dsw、NewPicker.dspVC6 工程工作区与项目文件必须彻底删除。VS2019 完全不识别它们若保留会干扰解决方案加载。UpgradeLog.htm升级日志只读参考无实际作用可删。Backup/目录备份文件夹通常含旧版.rc或.cpp建议先复制一份再清空防止误删关键修改。提示不要试图用 VS 的“升级向导”处理dsw/dsp。它会生成一堆冗余中间文件且常把#include stdafx.h错误替换为targetver.h导致FOColorCellObj.cpp等文件因头文件缺失而编译失败。2.2 以.vcxproj为核心重建解决方案正确做法是新建空解决方案 → 右键添加现有项目 → 选择NewPicker.vcxproj。这是唯一可靠路径。操作步骤如下# 在 VS2019 或 VS2022 中 1. 文件 → 新建 → 项目 → 选择“空解决方案” → 命名如 NewPicker_Solution 2. 解决方案资源管理器 → 右键解决方案 → “添加” → “现有项目…” 3. 浏览到 NewPicker.zip 解压目录 → 选中 NewPicker.vcxproj → 确定此时 VS 会自动识别所有.cpp/.h/.rc文件并按NewPicker.vcxproj.filters中的分组Source Files / Header Files / Resource Files组织视图。注意检查NewPicker.vcxproj.filters是否存在——它是 VS 用来维护文件分类的 XML 文件缺失会导致资源文件如.rc,.cur,.bmp不显示在资源视图中。2.3 配置 MFC 支持与字符集解决 LNK2001 和 C2664默认新建的空解决方案不启用 MFC必须手动开启。否则CDialog、CDC、CBrush等类将无法解析!-- 在 NewPicker.vcxproj 文件中找到 PropertyGroup 标签 -- !-- 确保包含以下两项若不存在则手动添加 -- UseOfMfcStatic/UseOfMfc !-- 或 Dynamic但 Static 更易部署 -- CharacterSetUnicode/CharacterSet同时在 VS IDE 中验证项目属性 → 配置属性 → 常规 → “使用 MFC” → 选择“在静态库中使用 MFC”项目属性 → 配置属性 → 常规 → “字符集” → 选择“使用 Unicode 字符集”为什么必须是 Unicode因为FOHyperLink.h中SetWindowTextW调用、NewPicker.rc中所有字符串资源如IDS_APP_TITLE均以宽字符定义。若设为多字节CString构造函数会因类型不匹配报 C2664 错误而LNK2001未解析的外部符号往往源于CWinApp派生类theApp的InitInstance未被链接器识别——这正是 MFC 库未启用的典型症状。2.4 资源文件注册让 .rc、.cur、.bmp 真正生效.rc文件本身不会自动编译进资源。必须确认NewPicker.rc已加入“资源文件”过滤器右键项目 → 添加 → 现有项 → 选中NewPicker.rcres/目录下所有文件fo_color.bmp,fo_cursor1.cur,NewPicker.ico,NewPicker.rc2均已作为“资源”类型加入项目右键文件 → 属性 → “项类型” → 选择“资源”特别注意NewPicker.rc2它是额外的资源脚本常用于存放对话框模板或字符串表。若未加入FOColorDialog的IDD_FOCOLORDIALOG对话框将找不到模板运行时弹出空白窗口。2.5 预编译头PCH配置绕过 StdAfx.h 的路径陷阱StdAfx.h是 MFC 预编译头标准入口。但NewPicker中部分.cpp如FODropColorButton.cpp可能仍引用旧路径#include stdafx.h。需统一为// 所有 .cpp 文件顶部确保是 #include stdafx.h // 而非 #include StdAfx.h 或 #include ../StdAfx.h并在项目属性中设置项目属性 → 配置属性 → C/C → 预编译头 → “预编译头” → “使用预编译头”“预编译头文件” →stdafx.h“预编译头输出文件” →$(IntDir)pch.pch若跳过此步FOPopupColorPaletteWnd.cpp会因#include FOColorCellObj.h依赖链过长而编译超时或报C1010: 在查找预编译头时遇到意外的文件结尾。3. 核心控件解析FOColorDialog、FODropColorButton 与 FOPopupColorPaletteWnd 的消息流与绘制逻辑这个取色器的“新款”体现在三个自定义控件的深度封装上FOColorDialog不是简单调用::ChooseColor()而是完全重绘的模态对话框FODropColorButton实现了拖拽取色的交互FOPopupColorPaletteWnd则是悬浮色盘的底层窗口。理解它们等于读懂整个工程的骨架。3.1 FOColorDialog从 CDialog 派生的全自绘颜色选择器FOColorDialog继承自CDialog但重写了OnPaint()和OnLButtonDown()放弃系统默认绘制全程由 GDI 控制// FOColorDialog.cpp 关键片段 void FOColorDialog::OnPaint() { CPaintDC dc(this); // 设备上下文 CRect rect; GetClientRect(rect); // 绘制主色盘背景渐变灰底 CBrush brush(RGB(240,240,240)); dc.FillRect(rect, brush); // 绘制当前选中色块右下角大色块 CRect colorRect(rect.right - 120, rect.bottom - 100, rect.right - 20, rect.bottom - 20); CBrush selectedBrush(m_selectedColor); // m_selectedColor 是 RGB 值 dc.FillRect(colorRect, selectedBrush); // 绘制 HEX 文本居中显示 CString hexStr; hexStr.Format(_T(#%02X%02X%02X), GetRValue(m_selectedColor), GetGValue(m_selectedColor), GetBValue(m_selectedColor)); dc.SetBkMode(TRANSPARENT); dc.SetTextAlign(TA_CENTER | TA_BASELINE); dc.TextOutW(colorRect.CenterPoint().x, colorRect.CenterPoint().y, hexStr); }参数说明GetRValue()等宏从COLORREF中提取 RGB 分量TextOutW使用 Unicode 输出依赖前文设置的 Unicode 字符集CenterPoint()是CRect成员函数计算矩形中心坐标。这里没有用CDC::DrawText()因为DrawText对齐方式复杂而TextOutWSetTextAlign更精准控制 HEX 文本位置。3.2 FODropColorButton实现“拖拽取色”的鼠标捕获机制普通按钮点击即可但FODropColorButton的核心价值在于“拖拽”——鼠标按下后不释放移动到屏幕任意位置取色。这依赖SetCapture()和ReleaseCapture()// FODropColorButton.cpp void FODropColorButton::OnLButtonDown(UINT nFlags, CPoint point) { SetCapture(); // 捕获鼠标即使移出窗口也持续接收消息 m_bDragging TRUE; CButton::OnLButtonDown(nFlags, point); } void FODropColorButton::OnMouseMove(UINT nFlags, CPoint point) { if (m_bDragging) { // 获取屏幕坐标点像素色值关键 HDC hScreenDC ::GetDC(NULL); COLORREF pixelColor ::GetPixel(hScreenDC, point.x, point.y); ::ReleaseDC(NULL, hScreenDC); // 更新自身显示色 通知父窗口 SetColor(pixelColor); GetParent()-SendMessage(WM_COLOR_CHANGED, 0, (LPARAM)pixelColor); } CButton::OnMouseMove(nFlags, point); } void FODropColorButton::OnLButtonUp(UINT nFlags, CPoint point) { if (m_bDragging) { ReleaseCapture(); // 释放捕获 m_bDragging FALSE; } CButton::OnLButtonUp(nFlags, point); }注意GetPixel()必须传入屏幕坐标point.x/y而非客户区坐标。OnMouseMove中的point是客户区坐标需转换CPoint screenPt point; ClientToScreen(screenPt);—— 但本工程中FODropColorButton的OnMouseMove直接用了point说明其Create()时已设为WS_POPUP风格使客户区坐标等同于屏幕坐标。这是个隐式约定新手极易忽略。3.3 FOPopupColorPaletteWnd悬浮窗口的创建与消息路由FOPopupColorPaletteWnd是一个无标题栏、无边框、始终置顶的CWnd派生类负责显示 16×16 色阵列。它的生命周期由FOColorDialog管理// FOColorDialog.cpp 中触发悬浮窗 void FOColorDialog::OnBnClickedBtnShowPalette() { if (!m_pPaletteWnd) { m_pPaletteWnd new FOPopupColorPaletteWnd(); // 创建窗口WS_POPUP | WS_BORDER | WS_THICKFRAME 使可拖动 m_pPaletteWnd-Create(NULL, _T(Color Palette), WS_POPUP | WS_BORDER | WS_THICKFRAME, CRect(100,100,400,300), this, IDC_PALETTE_WND); m_pPaletteWnd-ShowWindow(SW_SHOW); } }关键点在于Create()的第四个参数CRect它指定初始位置和大小。而FOPopupColorPaletteWnd自身重写了OnNcHitTest()使用户能拖动整个窗口// FOPopupColorPaletteWnd.cpp UINT FOPopupColorPaletteWnd::OnNcHitTest(CPoint point) { CRect rect; GetWindowRect(rect); // 若点击区域在客户区内返回 HTCAPTION模拟标题栏拖动 CRect clientRect; GetClientRect(clientRect); ClientToScreen(clientRect); if (clientRect.PtInRect(point)) return HTCAPTION; return CWnd::OnNcHitTest(point); }这就是“悬浮”效果的实现原理WS_POPUP去除系统边框HTCAPTION让客户区响应拖动SetWindowPos(..., HWND_TOPMOST, ...)保证置顶。若忘记SetWindowPos窗口会被其他应用遮挡。4. 避坑编译失败、运行崩溃、取色失灵的五大血泪现场这个工程看似结构清晰但新手在复现时几乎必然踩进以下五个坑。每一条都是我当年在Debug目录下翻.log文件、查Output窗口、逐行OutputDebugString后确认的硬核结论。4.1 现象编译通过但运行时报“0xC0000005访问冲突”崩溃点在FOColorCellObj::Draw()原因FOColorCellObj是色盘单元格对象其Draw()方法中调用pDC-FillSolidRect(rect, m_color)但pDC为空指针。根源在于FOPopupColorPaletteWnd::OnPaint()中未正确获取CPaintDC而是错误地new CDC()并Attach()了无效句柄。解决严格使用CPaintDC dc(this)获取设备上下文禁止手动new CDC。检查FOPopupColorPaletteWnd.cpp第 87 行附近确保OnPaint()开头是CPaintDC dc(this);而非CDC* pDC new CDC(); pDC-Attach(::GetDC(m_hWnd));。4.2 现象点击FODropColorButton无反应OnLButtonDown根本不触发原因按钮控件未关联到FODropColorButton类。在NewPickerDlg.h中该按钮变量声明为CButton m_btnDropColor;但应在DoDataExchange()中将其DDX_Control映射到FODropColorButton类型。解决打开NewPickerDlg.cpp找到DoDataExchange()函数将原DDX_Control(pDX, IDC_BTN_DROP_COLOR, m_btnDropColor);改为DDX_Control(pDX, IDC_BTN_DROP_COLOR, m_btnDropColor); // 此处 m_btnDropColor 必须是 FODropColorButton 类型并同步修改NewPickerDlg.h中的声明// 替换原来的 CButton m_btnDropColor; FODropColorButton m_btnDropColor;4.3 现象悬浮色盘FOPopupColorPaletteWnd显示空白或仅显示左上角一小块原因OnPaint()中GetClientRect(rect)返回的矩形尺寸为(0,0,0,0)因为窗口创建后未触发重绘。常见于Create()后未调用Invalidate()或UpdateWindow()。解决在FOColorDialog::OnBnClickedBtnShowPalette()中m_pPaletteWnd-ShowWindow(SW_SHOW)后立即加一行m_pPaletteWnd-Invalidate(); // 强制重绘 m_pPaletteWnd-UpdateWindow();4.4 现象HEX 颜色显示为乱码如#??????或TextOutW不显示文字原因CString格式化时未用_T()宏包裹字符串导致Format()在 Unicode 模式下解析失败。例如hexStr.Format(#%02X%02X%02X, ...)缺少_T。解决所有CString::Format的第一个参数必须是_T(...)。修正为hexStr.Format(_T(#%02X%02X%02X), GetRValue(m_selectedColor), GetGValue(m_selectedColor), GetBValue(m_selectedColor));4.5 现象拖拽取色时GetPixel()总返回CLR_INVALID-1原因GetPixel()需要屏幕 DC但::GetDC(NULL)返回的句柄在多显示器环境下可能指向错误屏幕或被 DPI 缩放干扰。更致命的是point坐标未转换为屏幕坐标。解决在FODropColorButton::OnMouseMove()中必须先将客户区坐标转为屏幕坐标CPoint screenPt point; ClientToScreen(screenPt); // 关键 HDC hScreenDC ::GetDC(NULL); COLORREF pixelColor ::GetPixel(hScreenDC, screenPt.x, screenPt.y); ::ReleaseDC(NULL, hScreenDC);5. 进阶技巧为取色器增加“历史颜色栈”与“截图取色”功能仅仅复现原工程是起点真正的成长发生在你开始动手扩展。下面两个功能是我从NewPicker出发、在实际项目中落地的最小可行增强代码量少、逻辑清晰、且完美复用原有架构。5.1 实现“最近 10 色历史栈”用 CArray 存储 CListCtrl 展示FOColorDialog已有m_selectedColor成员只需增加历史记录容器和 UI 控件。步骤如下Step 1在FOColorDialog.h中添加成员// 新增历史颜色数组最多 10 个 CArrayCOLORREF, COLORREF m_colorHistory; // 新增列表控件变量对应资源 ID: IDC_LIST_HISTORY CListCtrl m_listHistory;Step 2在FOColorDialog.cpp的DoDataExchange()中绑定DDX_Control(pDX, IDC_LIST_HISTORY, m_listHistory);Step 3在FOColorDialog::OnInitDialog()中初始化列表控件// 设置列头 m_listHistory.InsertColumn(0, _T(序号), LVCFMT_LEFT, 50); m_listHistory.InsertColumn(1, _T(HEX), LVCFMT_LEFT, 100); m_listHistory.InsertColumn(2, _T(RGB), LVCFMT_LEFT, 120);Step 4在FOColorDialog::SetColor(COLORREF color)中更新历史void FOColorDialog::SetColor(COLORREF color) { // 原有逻辑更新 m_selectedColor、刷新界面... m_selectedColor color; // 新增插入历史去重 限长 if (m_colorHistory.GetSize() 0 || m_colorHistory.GetAt(m_colorHistory.GetSize()-1) ! color) { m_colorHistory.Add(color); if (m_colorHistory.GetSize() 10) m_colorHistory.RemoveAt(0); // FIFO删最早一个 } // 刷新列表 UpdateHistoryList(); }Step 5实现UpdateHistoryList()void FOColorDialog::UpdateHistoryList() { m_listHistory.DeleteAllItems(); for (int i 0; i m_colorHistory.GetSize(); i) { COLORREF c m_colorHistory.GetAt(i); int idx m_listHistory.InsertItem(i, _T()); m_listHistory.SetItemText(idx, 0, CString(_T()) (i1)); CString hexStr; hexStr.Format(_T(#%02X%02X%02X), GetRValue(c), GetGValue(c), GetBValue(c)); m_listHistory.SetItemText(idx, 1, hexStr); CString rgbStr; rgbStr.Format(_T(R:%d G:%d B:%d), GetRValue(c), GetGValue(c), GetBValue(c)); m_listHistory.SetItemText(idx, 2, rgbStr); } }这段代码复用了GetRValue等宏无需额外头文件CListCtrl是 MFC 封装的 Windows ListView 控件InsertItem和SetItemText是标准操作。历史栈逻辑简洁每次新色入栈超 10 个则删首保证 O(1) 时间复杂度。5.2 实现“截图取色”调用 PrintWindow 截当前窗口再GetPixelFODropColorButton的拖拽取色只能取屏幕但用户常需取某个窗口内部色如浏览器网页。PrintWindow可捕获指定窗口图像// 在 FODropColorButton.cpp 中新增方法 COLORREF FODropColorButton::GetColorFromWindow(HWND hWnd, CPoint clientPt) { if (!IsWindow(hWnd)) return CLR_INVALID; // 获取目标窗口客户区大小 RECT rect; GetClientRect(hWnd, rect); MapWindowPoints(hWnd, NULL, (LPPOINT)rect, 2); // 转为屏幕坐标 // 创建兼容 DC 和位图 HDC hScreenDC GetDC(NULL); HDC hMemDC CreateCompatibleDC(hScreenDC); HBITMAP hBitmap CreateCompatibleBitmap(hScreenDC, rect.right - rect.left, rect.bottom - rect.top); HGDIOBJ hOldObj SelectObject(hMemDC, hBitmap); // 截图 PrintWindow(hWnd, hMemDC, PW_RENDERFULLCONTENT); // 获取像素色 COLORREF color GetPixel(hMemDC, clientPt.x - rect.left, clientPt.y - rect.top); // 清理 SelectObject(hMemDC, hOldObj); DeleteObject(hBitmap); DeleteDC(hMemDC); ReleaseDC(NULL, hScreenDC); return color; }关键点PrintWindow第三个参数PW_RENDERFULLCONTENT确保截取完整内容包括 Aero 效果clientPt是目标窗口内的客户区坐标需减去rect.left/top才是位图内坐标。此函数可被FOColorDialog调用例如右键菜单添加“从窗口取色”。从那以后我每次给 MFC 新手讲取色器都不再从ChooseColorAPI 开始而是直接打开NewPicker.zip解压、删dsw/dsp、建空解决方案、加.vcxproj、改StdAfx.h路径、跑通第一遍编译——然后指着FODropColorButton::OnMouseMove里的GetPixel说“看这就是你鼠标底下真实世界的颜色它不是魔法只是 Windows GDI 给你的一扇窗。” 这份工程的价值不在它多炫酷而在它把 MFC 的毛细血管级细节像解剖标本一样铺在你眼前。希望帮到你。本文还有配套的精品资源点击获取