简介:CJ60lib 是一套面向 MFC 框架的经典界面库,适合具备一定 C++ 与 Windows 编程基础、希望快速搭建专业桌面应用界面的开发者。它封装了对话框、工具栏、菜单、状态栏等常用组件,并提供增强控件、布局管理、资源加载、消息映射扩展、托盘图标与日志调试等能力,可减少重复造轮子的时间,让开发者更专注于核心业务逻辑。资源包共 115 个文件,约 149KB,以 43 个 h 头文件与 41 个 cpp 源文件为主体,另含 bmp、cur、ico 等界面位图与图标资源,以及 dsp、dsw、clw 等工程与类向导文件,便于直接编译研究。目前已有 192 人学习下载。通过阅读源码与示例,读者可理解 MFC 界面库的组织方式、控件扩展思路与消息处理机制,并将其集成到自己的项目中,提升界面一致性与开发效率。
1. CJ60lib 到底是什么:一个被时间尘封的 MFC 界面增强库
如果你在维护一套跑了十几年的 MFC 老系统,客户突然要求界面“现代化一点”,你大概会经历这样的心路:重写不现实,换 Qt 成本太高,硬啃 MFC 自绘又太费时间。这时候有人提到 CJ60lib,一个在 MFC 圈子里流传很久的界面库,你搜了一圈发现资料零散、示例老旧、编译报错一堆,于是陷入“这东西到底还能不能用”的犹豫。
CJ60lib 本质上是一套基于 MFC 的界面增强类库,核心解决的是原生 MFC 控件外观陈旧、缺乏现代交互反馈的问题。它提供了自绘菜单、工具栏、停靠面板、Outlook 风格导航栏、可换肤按钮等一整套控件封装,让开发者不用从 WM_DRAWITEM 开始手写绘制逻辑,就能把对话框和主框架窗口的视觉质感拉高一个档次。适合的人群很明确:手上有一到多个 MFC 项目需要做界面改造,但又不具备全面迁移到新框架条件的 C++ 桌面开发者。这篇文章不讲空泛的历史,只讲怎么把它跑起来、怎么用对、以及哪些地方容易翻车。
2. 把 CJ60lib 集成进 MFC 工程:从编译到第一个自绘控件
2.1 源码组织方式与工程配置选择
CJ60lib 常见的分发形式是源码包,里面通常包含若干 .h 和 .cpp 文件,按功能模块划分,比如菜单、工具栏、停靠窗口各成一组。拿到源码后第一件事不是急着往工程里加,而是先判断你的项目类型:是单文档、多文档还是基于对话框。不同类型的 MFC 工程,CJ60lib 的初始化入口和消息链挂载点不一样,搞错了就会出现控件不刷新或者点击无响应。
我一般会先在解决方案里新建一个静态库工程,把 CJ60lib 的源码全部加进去编译一遍。这样做的好处是隔离编译错误,避免和主工程的预编译头、字符集设置互相干扰。编译通过后再把生成的 .lib 和头文件目录挂到主工程,比直接把源码混进主工程要干净得多。如果你的项目是 Unicode 字符集,需要确认源码里有没有硬编码的 ANSI 字符串,有的话要么改工程字符集,要么在包含头文件前做转换处理。
2.2 最小集成步骤与初始化代码
假设你已经把 CJ60lib 编译成了静态库,接下来在主工程的 stdafx.h 或 pch.h 里包含核心头文件,然后在应用程序类的 InitInstance 里做初始化。下面是一个最小可运行的集成片段:
// 在应用程序类的 InitInstance 中,创建主窗口之前调用 #include "CJ60Lib.h" // 核心头文件,具体名称以你拿到的源码为准 BOOL CMyApp::InitInstance() { // 初始化 CJ60lib 的内部状态,必须在任何 CJ60 控件创建之前调用 // 参数传 NULL 表示使用默认资源句柄,如果你的资源在独立 DLL 里需要另行指定 CJ60LibInitialize(NULL); CWinApp::InitInstance(); // 创建主框架窗口,后续在窗口内使用 CJ60 控件 CMainFrame* pFrame = new CMainFrame; m_pMainWnd = pFrame; pFrame->LoadFrame(IDR_MAINFRAME); pFrame->ShowWindow(SW_SHOW); pFrame->UpdateWindow(); return TRUE; }这段代码的关键点在于初始化时机。CJ60LibInitialize 必须在任何 CJ60 控件对象构造之前执行,否则控件内部会访问未初始化的全局状态,表现为随机崩溃或者绘制出黑块。参数传 NULL 是常见做法,表示使用当前进程的默认资源句柄;如果你的界面资源放在单独的 MFC 扩展 DLL 里,就需要把 DLL 的 HINSTANCE 传进去,否则控件找不到位图、图标等资源。
2.3 在对话框里替换标准控件为 CJ60 控件
集成完成后,下一步是把对话框上的标准 MFC 控件替换成 CJ60 对应控件。以按钮为例,标准 CButton 换成 CJ60 的自绘按钮类,通常做法是在对话框头文件里声明成员变量时改类型,然后在 DoDataExchange 里绑定控件 ID。
// 对话框头文件中,把 CButton 替换为 CJ60 的按钮类 // 具体类名以源码为准,这里用 CCJ60Button 代指 class CMyDlg : public CDialogEx { // ... 其他成员 CCJ60Button m_btnConfirm; // 替换原来的 CButton }; // DoDataExchange 中正常绑定 void CMyDlg::DoDataExchange(CDataExchange* pDX) { CDialogEx::DoDataExchange(pDX); DDX_Control(pDX, IDC_BTN_CONFIRM, m_btnConfirm); }替换之后按钮的外观会由 CJ60 库接管,你可以在属性里设置按钮的普通、悬停、按下三种状态的位图或颜色。这里有个容易忽略的点:如果你在资源编辑器里给按钮设置了 Owner Draw 属性,需要去掉,否则 CJ60 的自绘逻辑和 MFC 的 Owner Draw 机制会打架,表现为按钮闪烁或者只绘制一半。另外,批量替换控件时建议一个一个来,替换一个编译运行一次,确认没问题再换下一个,一次性全换完出问题很难定位是哪个控件引起的。
3. 自绘菜单与停靠面板:CJ60lib 里最值得用的两个模块
3.1 自绘菜单的挂载方式与消息映射
CJ60lib 的自绘菜单模块是很多人用这个库的主要原因。原生 MFC 菜单在 Windows 高版本系统上虽然跟着系统主题走,但样式单一,没法做图标加文字混排、渐变背景、自定义高亮色这些效果。CJ60 的菜单类通过接管 WM_MEASUREITEM 和 WM_DRAWITEM 消息,把菜单项的绘制完全接管过来。
挂载方式通常是在主框架窗口的 OnCreate 里替换默认菜单:
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; // 获取当前菜单并交给 CJ60 菜单类接管 // m_wndMenu 是 CJ60 菜单类的成员变量 CMenu* pMenu = GetMenu(); if (pMenu != NULL) { m_wndMenu.AttachMenu(pMenu->GetSafeHmenu()); // 设置菜单项的高亮颜色和文字颜色,参数为 COLORREF m_wndMenu.SetHighlightColor(RGB(0, 120, 215)); m_wndMenu.SetTextColor(RGB(50, 50, 50)); } return 0; }AttachMenu 的作用是把标准 HMENU 交给 CJ60 菜单对象托管,后续菜单的测量和绘制都会走 CJ60 的逻辑。SetHighlightColor 和 SetTextColor 是常用的两个外观参数,前者控制鼠标悬停时的背景色,后者控制菜单项文字颜色。需要注意的是,如果你的菜单项里有动态添加或删除的项,在修改菜单结构后要调用一次刷新方法,否则新加的项可能还是按系统默认样式绘制。
3.2 停靠面板的创建与布局参数
CJ60lib 的停靠面板模块提供了类似 Visual Studio 那种可拖拽、可停靠、可自动隐藏的侧边栏。创建停靠面板一般分两步:先创建面板对象,再把它停靠到主框架的某个位置。
// 在主框架的 OnCreate 中创建停靠面板 // m_wndDockLeft 是 CJ60 停靠面板类的成员变量 if (!m_wndDockLeft.Create(_T("属性面板"), this, CRect(0, 0, 200, 400), TRUE, IDD_DOCK_LEFT)) { TRACE0("Failed to create dock pane\n"); return -1; } // 设置面板允许停靠的位置:左侧、右侧、浮动 m_wndDockLeft.EnableDocking(CBRS_ALIGN_LEFT | CBRS_ALIGN_RIGHT | CBRS_FLOAT_MULTI); // 把面板停靠到主框架左侧,宽度 200 像素 EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndDockLeft, AFX_IDW_DOCKBAR_LEFT);Create 方法的参数依次是面板标题、父窗口、初始矩形、是否可见、面板内容的对话框资源 ID。EnableDocking 的参数决定这个面板能被拖到哪些位置,CBRS_ALIGN_LEFT 和 CBRS_ALIGN_RIGHT 表示允许停靠左右两侧,CBRS_FLOAT_MULTI 表示允许多个面板浮动在一起。DockControlBar 的第二个参数指定初始停靠位置,AFX_IDW_DOCKBAR_LEFT 是 MFC 预定义的左侧停靠栏 ID。
实际使用中,面板内容的对话框资源需要设置成 Child 风格,去掉标题栏和边框,否则嵌进去之后会出现双层边框。面板的宽度可以在 Create 的矩形参数里指定,但最终宽度会受停靠栏的布局影响,如果发现宽度不对,检查一下主框架有没有设置过大的最小宽度限制。
3.3 工具栏与菜单的联动更新
CJ60lib 的工具栏类支持在工具栏按钮上显示文字、下拉箭头、以及和菜单项联动的选中状态。联动更新的关键在于 ON_UPDATE_COMMAND_UI 消息的处理。当菜单项状态变化时,工具栏按钮的状态需要同步刷新。
// 在消息映射中声明更新处理函数 BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_UPDATE_COMMAND_UI(ID_VIEW_GRID, &CMainFrame::OnUpdateViewGrid) END_MESSAGE_MAP() void CMainFrame::OnUpdateViewGrid(CCmdUI* pCmdUI) { // pCmdUI 同时关联菜单项和工具栏按钮 // 设置选中状态,菜单项前面会出现勾选标记,工具栏按钮会呈现按下状态 pCmdUI->SetCheck(m_bGridVisible ? 1 : 0); // 设置是否可用 pCmdUI->Enable(TRUE); }这段代码里 pCmdUI 是 MFC 的命令更新机制自动传入的,它同时指向触发更新的菜单项和工具栏按钮。SetCheck 传 1 表示选中,传 0 表示未选中。CJ60 的工具栏类会响应这个状态,把按钮绘制成按下或弹起的样子。如果发现工具栏按钮状态没跟着菜单变,先检查消息映射里有没有漏掉 ON_UPDATE_COMMAND_UI 条目,再检查工具栏按钮的 ID 和菜单项 ID 是否一致。
4. 避坑与排查:CJ60lib 集成过程中最容易翻车的五个地方
4.1 编译报错 “无法解析的外部符号”
现象:把 CJ60lib 源码加入工程后编译,链接阶段报大量 LNK2019 错误,提示某个 CJ60 类的成员函数无法解析。
原因:最常见的情况是源码文件没有全部加入编译,或者静态库工程和主工程的运行库设置不一致。比如静态库用了 /MT,主工程用了 /MD,链接时就会找不到符号。另一个原因是某些 CJ60 源文件依赖 MFC 的特定版本宏,如果你的工程定义的 _AFXDLL 和源码预期的不一样,部分代码会被条件编译掉。
解决:先确认所有 .cpp 文件都参与了编译,在解决方案资源管理器里检查每个文件的“从生成中排除”属性是否为“否”。然后统一运行库设置,在项目属性 → C/C++ → 代码生成 → 运行库中,确保静态库和主工程选同一项。最后检查 MFC 的使用方式,在项目属性 → 常规 → MFC 的使用中,确认是“在共享 DLL 中使用 MFC”还是“在静态库中使用 MFC”,这个选项要和 CJ60lib 源码的预期一致。
4.2 控件绘制出现黑块或闪烁
现象:CJ60 控件显示出来后,背景是黑色或者拖动窗口时控件区域闪烁严重。
原因:黑块通常是因为控件的绘制消息没有被正确处理,系统用默认背景刷填充了区域。闪烁则是因为没有开启双缓冲,每次重绘都先擦背景再画内容。
解决:检查控件类有没有正确响应 WM_ERASEBKGND 消息,CJ60 的控件一般会在这个消息里返回 TRUE 表示不擦背景。如果没有,可以在控件类的消息映射里加上 ON_WM_ERASEBKGND,处理函数直接返回 TRUE。对于闪烁,确认 CJ60 控件是否支持双缓冲,如果支持,检查有没有调用开启双缓冲的方法;如果不支持,可以在父窗口的 OnEraseBkgnd 里做处理,或者给控件加上 WS_CLIPCHILDREN 样式减少重绘区域。
4.3 菜单项文字显示为乱码
现象:自绘菜单挂载后,菜单项文字变成方块或乱码。
原因:字符集不匹配。CJ60lib 的早期版本很多是基于 ANSI 编写的,内部用 char 处理字符串。如果你的工程是 Unicode 字符集,直接把宽字符串传给 ANSI 接口就会乱码。
解决:先确认源码的字符集。如果源码是 ANSI 的,而你的工程是 Unicode,有两个选择:一是把工程改成多字节字符集,但这会影响工程里其他代码;二是找到 CJ60 菜单类里处理文字的地方,把 ANSI 接口替换成宽字符版本,或者用 CString 的转换宏做中转。我一般倾向于后者,虽然改动多一点,但不用动整个工程的字符集设置。具体做法是在调用菜单文字设置方法前,用 CT2W 或 CA2W 宏把字符串转成宽字符。
4.4 停靠面板拖拽后布局错乱
现象:把停靠面板拖到新位置后,面板大小异常,或者和其他面板重叠。
原因:CJ60 的停靠面板在拖拽结束后会重新计算布局,如果主框架的 RecalcLayout 没有被正确调用,或者面板的最小尺寸、最大尺寸设置不合理,就会出现布局错乱。
解决:在面板拖拽结束的消息处理里手动调用一次 RecalcLayout。另外检查面板的 SetMinSize 和 SetMaxSize 有没有设置,如果最小尺寸设得比停靠栏还大,拖拽后就会溢出。还有一个容易忽略的点是主框架的 EnableDocking 参数,如果设成了 CBRS_ALIGN_ANY,但实际只想让面板停靠在左右两侧,拖到上下就会出问题,建议按实际需求精确设置允许的停靠方向。
4.5 程序退出时崩溃
现象:关闭主窗口后,程序在退出过程中崩溃,调试器指向 CJ60 的某个析构函数。
原因:CJ60 的全局状态在程序退出时被提前释放,而某些控件对象还在析构中访问这些全局状态。常见于 CJ60LibInitialize 传了资源句柄,但退出时没有对应的清理调用。
解决:在应用程序类的 ExitInstance 里调用 CJ60 库提供的清理函数,确保全局状态在控件对象全部析构之后再释放。如果库没有提供清理函数,检查一下是不是有全局的 CJ60 对象在静态存储区,它们的析构顺序可能和 MFC 的清理顺序冲突。一个实用的做法是把 CJ60 的全局对象改成指针,在 InitInstance 里 new,在 ExitInstance 里 delete,这样能精确控制生命周期。
5. 让 CJ60lib 在现代 Windows 上跑得更稳的几个技巧
5.1 高 DPI 下的缩放适配
CJ60lib 诞生的时候高 DPI 显示器还不普及,很多尺寸是写死的像素值。在 4K 屏上直接跑,控件会小得看不清。我一般会在应用程序初始化时调用 SetProcessDPIAware 让进程感知 DPI,然后在创建 CJ60 控件之前,根据当前 DPI 缩放比例调整控件的尺寸参数。
// 在 InitInstance 最开始调用,让进程感知 DPI // 注意:这个调用要在任何窗口创建之前 SetProcessDPIAware(); // 获取当前 DPI 缩放比例 HDC hdc = GetDC(NULL); int dpiX = GetDeviceCaps(hdc, LOGPIXELSX); ReleaseDC(NULL, hdc); float scale = dpiX / 96.0f; // 96 是标准 DPI // 创建 CJ60 控件时,把像素尺寸乘以 scale int nWidth = static_cast<int>(200 * scale); int nHeight = static_cast<int>(400 * scale);这段代码的关键是 SetProcessDPIAware 的调用时机,必须在任何窗口创建之前,否则不生效。scale 计算出来后,所有传给 CJ60 控件的尺寸参数都要乘这个系数。字体大小也需要相应调整,可以通过 CreateFont 指定高度时乘以 scale。如果发现缩放后控件位置偏移,检查一下对话框资源的布局是不是用了固定坐标,固定坐标在高 DPI 下不会自动缩放,需要手动调整。
5.2 用主题色统一管理外观
CJ60lib 的各个控件外观是分散设置的,按钮一个颜色、菜单一个颜色、停靠面板又一个颜色,改起来很麻烦。我的习惯是定义一个全局的主题结构体,把所有颜色集中管理,初始化时统一应用到各个控件。
// 定义主题颜色结构 struct CJ60Theme { COLORREF clrHighlight; // 高亮色 COLORREF clrText; // 文字色 COLORREF clrBackground; // 背景色 COLORREF clrBorder; // 边框色 }; // 全局主题实例 CJ60Theme g_theme = { RGB(0, 120, 215), // 高亮色 RGB(50, 50, 50), // 文字色 RGB(245, 245, 245), // 背景色 RGB(200, 200, 200) // 边框色 }; // 在初始化时统一应用 void ApplyTheme() { m_wndMenu.SetHighlightColor(g_theme.clrHighlight); m_wndMenu.SetTextColor(g_theme.clrText); m_btnConfirm.SetHighlightColor(g_theme.clrHighlight); m_wndDockLeft.SetBackgroundColor(g_theme.clrBackground); // ... 其他控件 }这样做的价值在于,当客户说“把蓝色换成绿色”的时候,你只需要改 g_theme 里的一个值,所有控件同步生效。如果后续要做多套皮肤切换,也只需要准备多个 CJ60Theme 实例,切换时重新调用 ApplyTheme 即可。注意有些 CJ60 控件的颜色设置方法名称可能不一样,需要根据实际源码里的接口来调整,但思路是一样的。
5.3 用版本控制管理对 CJ60lib 的修改
CJ60lib 的源码拿过来之后,你大概率会改一些地方:修 bug、适配字符集、调整默认参数。这些修改如果没有版本控制,下次换一台机器或者重新拉一份源码,改动就丢了。我的做法是把 CJ60lib 的源码单独放在一个仓库里,主工程通过子模块或者包管理的方式引用。
具体操作上,我会在 CJ60lib 源码仓库里建一个 patches 目录,把每一处修改做成独立的补丁文件,补丁文件里写清楚修改原因和影响范围。主工程构建时,先拉取原始源码,再按顺序应用补丁。这样做的好处是升级 CJ60lib 版本时,能清楚知道哪些改动需要重新适配,而不是在一堆修改过的文件里大海捞针。
5.4 一个验证集成是否成功的检查清单
集成完成后,跑一遍下面这个检查清单,能提前发现大部分问题:
| 检查项 | 预期结果 | 失败时优先排查 |
|---|---|---|
| 主窗口菜单显示 | 菜单项有自绘高亮效果 | 菜单挂载时机、字符集 |
| 工具栏按钮悬停 | 按钮有悬停变色 | 工具栏类是否替换、消息映射 |
| 停靠面板拖拽 | 面板能停靠到指定位置 | EnableDocking 参数、RecalcLayout |
| 对话框按钮点击 | 按钮响应正常且外观自绘 | 控件类型替换、Owner Draw 属性 |
| 程序退出 | 无崩溃、无内存泄漏提示 | 全局对象析构顺序、清理函数调用 |
| 高 DPI 显示 | 控件大小随 DPI 缩放 | SetProcessDPIAware 调用时机 |
这个清单是我自己在多个 MFC 项目里集成 CJ60lib 之后总结出来的,每次集成完跑一遍,基本能覆盖八成以上的常见问题。清单里的检查项按优先级排列,前面的不通过,后面的不用急着测。
5.5 什么时候该放弃 CJ60lib
CJ60lib 虽然能解决 MFC 界面老旧的燃眉之急,但它也有明确的边界。如果你的项目需要支持触摸手势、需要做复杂的动画过渡、或者需要适配 Windows 11 的圆角和云母材质,CJ60lib 是做不到的,硬改的成本可能比重写界面还高。另一个判断标准是团队的技术栈:如果团队里已经没有人熟悉 MFC 的消息机制和 GDI 绘制,维护 CJ60lib 的定制修改会变成负担。
我自己的习惯是,在决定用 CJ60lib 之前,先花半天时间做一个最小验证:建一个空白的 MFC 对话框工程,把 CJ60lib 集成进去,替换一个按钮和一个菜单,跑起来看看效果。如果半天内能跑通,说明集成路径是通的,后续投入可控;如果半天还在跟编译错误较劲,就要重新评估这个方向了。这个习惯帮我省过好几次后悔药,希望也能帮到你。
本文还有配套的精品资源,点击获取