简介:CJ60lib 是一套面向 MFC 框架的经典界面库,适合具备一定 C++ 与 Windows 编程基础、希望快速搭建专业桌面应用界面的开发者。它封装了对话框、工具栏、菜单、状态栏等常用组件,并提供增强控件、自动布局、资源管理、消息映射扩展、多语言支持及托盘图标等功能,可减少重复造轮子的时间,让开发者更专注于核心业务逻辑。资源包共 115 个文件,约 149KB,以 43 个 h 头文件与 41 个 cpp 源文件为主体,另含 bmp、cur、ico 等界面素材,以及 dsp、dsw、clw 等工程与类向导文件,结构完整,便于直接集成到 MFC 项目中编译调试。目前已有 192 人学习下载,可作为学习 MFC 界面封装思路、复用成熟控件与工程配置的参考素材。
1. 从一堆 bmp 和 clw 说起:CJ60lib 到底解决了 MFC 界面开发的什么痛点
如果你手头有一个老 MFC 项目,界面还停留在 Windows 95 那种灰底方块的阶段,而产品经理又天天催着要“现代感”,你大概会经历一段非常难受的时期。自己重绘按钮、写工具栏、处理停靠窗口,代码量翻倍不说,稍不留神就闪屏、错位、消息丢失。CJ60lib 就是在这个场景下值得翻出来的一套 MFC 界面库。它不是什么新框架,而是一组封装好的 C++ 类,配合资源文件(cj_logo.bmp、Toolbar.bmp、button_images.bmp、btn_explorer.bmp、btn_arro.bmp)和工程信息文件 CJ60Lib.clw,直接嵌入到你的 MFC 工程里,帮你把工具栏、菜单、状态栏、对话框这些常见元素快速撑起来。适合谁?适合还在维护 VC6 或 VS 早期版本 MFC 项目的开发者,尤其是那些不想引入庞大第三方 UI 框架、只想在现有代码基础上做增量改进的人。它不解决业务逻辑,只解决“界面看起来专业、写起来不重复”的问题。
2. 把 CJ60lib 挂进 MFC 工程:文件清单、依赖项与编译顺序
2.1 先认清资源文件各自的作用
拿到 CJ60lib 的包,别急着往工程里拖。先看根目录下这几个文件,它们不是随便放的:
| 文件名 | 类型 | 典型用途 |
|---|---|---|
| cj_logo.bmp | 位图 | 关于对话框或启动画面中的 Logo |
| Toolbar.bmp | 位图 | 主工具栏的按钮图像条,通常 16x15 或 24x24 |
| button_images.bmp | 位图 | 扩展按钮控件的状态图(正常/按下/悬停) |
| btn_explorer.bmp | 位图 | 资源管理器风格按钮的图标 |
| btn_arro.bmp | 位图 | 箭头类按钮,用于展开/折叠或滚动 |
| CJ60Lib.clw | 文本 | ClassWizard 的工程数据库,记录类与消息映射关系 |
这些 bmp 不是装饰品,CJ60lib 的工具栏类会按固定顺序从 Toolbar.bmp 里切图。如果你替换了图片但按钮数量对不上,工具栏就会显示错位或空白。CJ60Lib.clw 是给 VC6 的 ClassWizard 用的,如果你用更高版本的 Visual Studio,这个文件可能无法直接识别,需要手动把类加进工程。
2.2 把源文件加入工程的正确顺序
CJ60lib 通常以源码形式提供,包含 .h 和 .cpp。不要一次性把所有文件拖进“Header Files”和“Source Files”,那样容易触发重复包含。我一般按这个顺序操作:
第一步,在工程目录下新建一个子文件夹CJ60Lib,把库的 .h、.cpp 和 .bmp 全部拷进去。第二步,在 Visual Studio 的解决方案资源管理器中右键工程 → 添加 → 现有项,先只添加 .cpp 文件。第三步,打开工程属性 → C/C++ → 常规 → 附加包含目录,把CJ60Lib文件夹路径加进去。第四步,把 .bmp 资源通过“资源视图 → 右键 → 添加资源 → Bitmap → 导入”的方式加入,并记下资源 ID,后面代码里要用。
// 在 stdafx.h 或主头文件中包含 CJ60lib 的核心头 #include "CJ60Lib.h" // 库的主头文件,通常包含所有子类声明 #include "CJ60ToolBar.h" // 工具栏类,具体名称以实际文件为准 #include "CJ60StatusBar.h" // 状态栏类 // 在 CMainFrame 类中声明成员变量 class CMainFrame : public CFrameWnd { // ... 其他成员 CJ60ToolBar m_wndToolBar; // 替换原来的 CToolBar CJ60StatusBar m_wndStatusBar; };上面代码里,CJ60ToolBar和CJ60StatusBar是库提供的扩展类,它们继承自 MFC 的标准类,但重写了绘制和布局逻辑。注意:如果你的工程已经用了CToolBar,不要直接改类名,先把原来的成员变量注释掉,换成 CJ60 的版本,再逐步迁移消息映射。否则ON_COMMAND和ON_UPDATE_COMMAND_UI的 ID 对不上,按钮会变灰。
2.3 初始化工具栏和状态栏的代码模板
在CMainFrame::OnCreate里,原来的CToolBar::Create调用要换成 CJ60 的创建方式。下面这段代码可以直接抄,但资源 ID 要换成你实际导入的:
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; // 创建 CJ60 工具栏,IDR_MAINFRAME 是工具栏资源 ID if (!m_wndToolBar.Create(this, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0("Failed to create toolbar\n"); return -1; } // 设置工具栏按钮的图片列表,使用 button_images.bmp m_wndToolBar.SetImageList(IDB_BUTTON_IMAGES, 16, 1, RGB(192, 192, 192)); // 创建状态栏,IDC_STATUS_BAR 是状态栏资源 ID if (!m_wndStatusBar.Create(this) || !m_wndStatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT))) { TRACE0("Failed to create status bar\n"); return -1; } // 启用停靠功能 m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); EnableDocking(CBRS_ALIGN_ANY); DockControlBar(&m_wndToolBar); return 0; }逻辑说明:Create的第三个参数CBRS_GRIPPER让工具栏左侧出现拖拽手柄,这是 CJ60lib 比标准 MFC 工具栏多出来的细节。SetImageList里的16是按钮宽度,1是位图数量,RGB(192,192,192)是透明色——如果你的 button_images.bmp 背景不是这个灰色,要改成实际背景色,否则按钮边缘会有色块。状态栏的indicators数组通常定义在 MainFrm.cpp 顶部,包含 ID_SEPARATOR 和几个窗格 ID,CJ60lib 的状态栏类会接管绘制,让窗格有立体边框。
3. 对话框、菜单与停靠窗口:CJ60lib 里最容易被忽略的三个参数
3.1 对话框模板的继承与资源 ID 冲突
CJ60lib 提供了一些预定义的对话框模板,比如设置对话框、关于对话框。这些模板通常以 .rc 片段的形式存在,或者需要你手动从示例工程里复制。常见做法是:在资源视图里新建一个 Dialog 资源,然后把 CJ60lib 示例中的控件布局照搬过来,但不要直接复制整个 .rc 文件,因为资源 ID 会冲突。
关键参数是对话框的Style。CJ60lib 的扩展对话框类通常要求WS_CLIPCHILDREN和WS_EX_CONTROLPARENT这两个样式,前者防止子控件重绘时闪烁,后者让 Tab 键在子控件间正常切换。如果你漏了WS_EX_CONTROLPARENT,用户按 Tab 键会直接跳到对话框的默认按钮,而不是在编辑框之间移动。
// 在对话框类的 OnInitDialog 中设置扩展样式 BOOL CMySettingsDlg::OnInitDialog() { CDialog::OnInitDialog(); // 启用子控件透明和父级控制 ModifyStyleEx(0, WS_EX_CONTROLPARENT); ModifyStyle(0, WS_CLIPCHILDREN); // 如果使用了 CJ60 的扩展控件,需要调用初始化 m_btnOK.SetFlatStyle(TRUE); // 扁平按钮样式 m_btnCancel.SetFlatStyle(TRUE); return TRUE; }SetFlatStyle(TRUE)是 CJ60lib 按钮类的常见方法,让按钮在非悬停状态下不显示边框,悬停时才浮现。这个效果在 Windows 10 上看起来有点复古,但在工业软件里很受欢迎,因为减少视觉干扰。
3.2 菜单项动态添加与消息映射的绑定
CJ60lib 的菜单类支持运行时插入菜单项,比如最近打开的文件列表。但很多人卡在消息映射上:动态添加的菜单项 ID 是运行时生成的,ON_COMMAND宏无法预知。解决办法是用ON_COMMAND_RANGE或者手动重写OnCmdMsg。
我一般用ON_COMMAND_RANGE,把一段 ID 范围映射到同一个处理函数,然后在函数里根据nID区分具体项。下面是一个示例:
// 在头文件中定义 ID 范围 #define ID_MRU_FILE_FIRST 0xE000 #define ID_MRU_FILE_LAST 0xE00F // 在消息映射中添加 BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_COMMAND_RANGE(ID_MRU_FILE_FIRST, ID_MRU_FILE_LAST, &CMainFrame::OnMRUFile) END_MESSAGE_MAP() // 处理函数 void CMainFrame::OnMRUFile(UINT nID) { int nIndex = nID - ID_MRU_FILE_FIRST; CString strPath = m_arrMRU[nIndex]; // m_arrMRU 是保存路径的数组 // 打开文件逻辑... }注意:CJ60lib 的菜单类在绘制自绘菜单项时,会读取菜单项的dwItemData。如果你用InsertMenu动态添加,记得给dwItemData赋一个有效值,否则菜单项可能显示为空白或高度异常。
3.3 停靠窗口的尺寸记忆与布局持久化
CJ60lib 的停靠窗口支持保存布局到注册表或 ini 文件。但默认情况下,它只保存工具栏的可见性和位置,不保存宽度。如果你有一个左侧的资源管理器风格停靠窗,用户拖宽了,下次启动又变回默认宽度,体验很差。
需要手动处理WM_CLOSE或OnDestroy,把控制条的尺寸写进CWinApp的WriteProfileInt。下面是一个简化的保存逻辑:
void CMainFrame::OnClose() { // 保存工具栏停靠状态 m_wndToolBar.SaveBarState(_T("ToolBarState")); // 保存自定义停靠窗的宽度 if (m_wndExplorer.IsWindowVisible()) { CRect rect; m_wndExplorer.GetWindowRect(&rect); AfxGetApp()->WriteProfileInt(_T("Layout"), _T("ExplorerWidth"), rect.Width()); } CFrameWnd::OnClose(); }SaveBarState是 MFC 控制条的标准方法,CJ60lib 的工具栏类继承了这个行为。但WriteProfileInt需要你在InitInstance里设置好注册表键或 ini 文件路径,否则数据写到了默认位置,卸载时残留。常见做法是在CWinApp派生类的构造函数里调用SetRegistryKey(_T("MyCompany\\MyApp"))。
4. 避坑与排查:CJ60lib 集成时最容易翻车的五个地方
4.1 工具栏按钮图片错位或全黑
现象:编译通过,工具栏显示出来,但按钮上的图标是乱的,或者整条工具栏全黑。 原因:Toolbar.bmp 的尺寸和按钮数量不匹配。CJ60lib 按固定宽度切图,如果图片总宽度不是按钮宽度的整数倍,最后一个按钮会读到空白区域。另外,如果 bmp 是 24 位色,而SetImageList里指定的透明色是 16 位色的 RGB 值,透明处理会失效。 解决:用画图工具打开 Toolbar.bmp,确认总宽度 = 按钮数 × 单个按钮宽度。把图片另存为 256 色或 24 位色,然后在SetImageList里用GetPixel取左上角像素作为透明色,不要硬编码RGB(192,192,192)。
4.2 状态栏窗格文字不更新
现象:状态栏创建成功,但SetPaneText调用后文字不变。 原因:CJ60lib 的状态栏类可能重写了OnPaint,但没有调用SetPaneText的基类实现,或者窗格 ID 和indicators数组里的顺序不一致。 解决:检查indicators数组,确保每个非分隔符的 ID 都是唯一的。调用SetPaneText时用索引而不是 ID,索引从 0 开始,对应数组位置。如果还不行,在SetPaneText后手动调用m_wndStatusBar.Invalidate()强制重绘。
4.3 对话框上的 CJ60 按钮不响应点击
现象:按钮显示正常,但鼠标点击没反应,或者点击后对话框直接关闭。 原因:CJ60lib 的按钮类可能覆盖了OnLButtonDown但没有调用Default(),导致消息没有传递给父对话框。另外,如果按钮的 ID 和IDOK或IDCANCEL相同,MFC 会默认处理为关闭对话框。 解决:在按钮类的OnLButtonDown里,处理完自绘逻辑后调用CButton::OnLButtonDown(nFlags, point)。如果按钮 ID 是IDOK,但你想自定义行为,需要重写OnOK并留空,或者把按钮 ID 改成自定义值,再用ON_BN_CLICKED映射。
4.4 工程从 VC6 迁移到 VS2019 后编译报错
现象:在 VC6 下正常的 CJ60lib 代码,拿到 VS2019 编译,出现大量C2065未声明标识符或C2440类型转换错误。 原因:VC6 的 C++ 标准支持不严格,很多隐式转换和旧式头文件在 VS2019 里被禁止。CJ60lib 作为老库,可能用了#include <iostream.h>或char*到LPCTSTR的隐式转换。 解决:在工程属性 → C/C++ → 语言 → 符合模式,改为“否”。在预处理器定义里加上_CRT_SECURE_NO_WARNINGS。对于char*到LPCTSTR的转换,用CString显式包装,或者把项目字符集从 Unicode 改成多字节。但注意,改成多字节后,所有_T("")宏仍然有效,但CString的GetBuffer返回类型会变。
4.5 资源文件 CJ60Lib.clw 无法被 ClassWizard 识别
现象:双击 CJ60Lib.clw 没反应,或者提示“无法打开此文件”。 原因:.clw 文件是 VC6 的 ClassWizard 数据库,格式是二进制或特定文本,高版本 Visual Studio 不再支持。 解决:不要试图修复 .clw。直接在解决方案资源管理器里右键对应的 .h 文件 → 类向导,手动添加类。如果类已经存在但消息映射不显示,检查 .h 文件里是否有DECLARE_MESSAGE_MAP()宏,以及 .cpp 里是否有BEGIN_MESSAGE_MAP和END_MESSAGE_MAP。CJ60lib 的类通常已经包含这些,你只需要在派生类里补充自己的映射。
5. 进阶技巧:用 CJ60lib 做一套可换肤的工具栏,并验证内存泄漏
5.1 换肤的本质是替换 ImageList
CJ60lib 的工具栏换肤,不需要重新创建窗口,只需要替换CImageList。我一般把不同风格的按钮图片做成多个 bmp 资源,运行时根据配置加载。下面是一个换肤函数:
void CMainFrame::SwitchToolbarSkin(int nSkin) { // 先分离旧的 ImageList,防止内存泄漏 m_wndToolBar.SetImageList(NULL); // 根据皮肤编号选择资源 ID UINT nBmpID = (nSkin == 0) ? IDB_TOOLBAR_BLUE : IDB_TOOLBAR_GRAY; // 创建新的 ImageList,16x16,透明色用图片左上角 CBitmap bmp; bmp.LoadBitmap(nBmpID); BITMAP bm; bmp.GetBitmap(&bm); COLORREF clrTrans = bmp.GetPixel(0, 0); m_imageList.DeleteImageList(); m_imageList.Create(16, bm.bmHeight, ILC_COLOR24 | ILC_MASK, 0, 1); m_imageList.Add(&bmp, clrTrans); // 重新设置到工具栏 m_wndToolBar.SetImageList(&m_imageList); m_wndToolBar.Invalidate(); }参数说明:ILC_COLOR24表示 24 位色,ILC_MASK启用透明遮罩。Add的第二个参数是透明色,从图片左上角取,避免硬编码。DeleteImageList必须在Create之前调用,否则旧的 ImageList 句柄泄漏。SetImageList(NULL)先断开工具栏和旧列表的关联,再删除,顺序反了会崩溃。
5.2 用 CRT 调试堆验证 CJ60lib 是否泄漏
CJ60lib 作为老库,有些类在OnDestroy里没有释放 GDI 对象。换肤功能频繁调用后,GDI 对象数会飙升。验证方法:在InitInstance里启用_CrtSetDbgFlag,然后在退出时看输出窗口。
// 在 CWinApp::InitInstance 开头添加 #ifdef _DEBUG _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif // 在 CMainFrame::OnDestroy 里手动清理 void CMainFrame::OnDestroy() { // 删除自定义 ImageList m_imageList.DeleteImageList(); // 如果 CJ60lib 的工具栏类有 Cleanup 方法,调用它 m_wndToolBar.Cleanup(); CFrameWnd::OnDestroy(); }运行程序,打开换肤功能切换几次,然后正常关闭。如果输出窗口出现Detected memory leaks!,看大括号里的分配编号,用_CrtSetBreakAlloc(编号)在第二次运行时断下,就能定位到是哪一行new出来的对象没释放。常见的是CJ60ToolBar内部的m_pImageList没有在析构里 delete,需要你在派生类里补一个析构函数。
5.3 一个我踩过的坑:换肤后工具栏按钮热区错位
有一次换肤后,按钮图标显示正常,但鼠标悬停高亮的位置偏了半个按钮。查了半天,发现是SetImageList之后没有调用SetSizes。CJ60lib 的工具栏类在Create时根据初始 ImageList 计算了按钮尺寸,换肤后 ImageList 的bmHeight变了,但按钮的m_sizeButton没更新。解决办法是在SetImageList之后加一行:
m_wndToolBar.SetSizes(CSize(16, bm.bmHeight), CSize(16, bm.bmHeight));第一个CSize是按钮图像尺寸,第二个是按钮整体尺寸。如果按钮有边框,第二个尺寸要比第一个大几个像素。从那以后,我每次换肤都强制走一遍SetImageList→SetSizes→Invalidate三步,再也没出现过热区错位。希望帮到你。
本文还有配套的精品资源,点击获取