ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CJ60lib实战:老MFC工程界面升级与自绘控件避坑指南

CJ60lib实战:老MFC工程界面升级与自绘控件避坑指南 简介CJ60lib是一套专为MFC框架设计的经典界面库主要面向Windows桌面应用开发者用于快速构建具有专业外观和丰富交互的界面减少从零封装控件带来的重复工作。压缩包共包含115个文件以43个h头文件与41个cpp源文件为核心配合bmp位图、cur光标、ico图标及rc资源脚本等素材完整保留了库的源码结构和工程配置压缩后仅149KB便于直接集成或学习。目前已有192人学习下载。包内提供的增强控件、对话框模板、工具栏与菜单类、状态栏及托盘图标管理等功能可帮助开发者快速搭建界面框架同时通过dsp、dsw、clw等工程文件还能了解经典MFC库的组织方式与消息映射扩展思路适合希望提升界面开发效率的MFC中高级开发者。1. CJ60lib是个什么段位的MFC界面库救老工程还是添新麻烦CJ60lib 是老 MFC 工程师手里很常见的一套界面库它解决的问题很直接让默认的 MFC 窗口看起来像专业商业软件而不是入门课作业。提到桌面开发现在很多人先想到 Qt 或者 C#但真要接手一个跑得好好的二十年老工程重写成本太大只能继续用 MFC 缝缝补补。这时候把 CJ60lib 接进去替换掉原生按钮、工具栏、状态栏整体观感立刻就不一样了。这份资源包里带了 button_images.bmp、Toolbar.bmp 这类位图和 CJ60Lib.clw 工程信息说明就是给 MFC 工程界面换皮用的。它适合谁一种是还在维护老 MFC 程序、想低成本提升界面的开发者另一种是刚学 MFC 但不想在界面细节上费劲的新人。不过它也有明显边界库比较老在 VS2019、VS2022 上直接用会碰到字符集和编译兼容问题这些后面都会说到。2. 拆开CJ60lib看模块控件增强、对话框模板、工具栏状态栏为什么值得换2.1 控件增强为什么不用原生控件原生 MFC 控件比如 CButton、CEdit、CListCtrl直接封装了 Windows 标准控件外观由系统主题决定在 Win10/Win11 下显得很单薄。CJ60lib 的核心工作就是替你把默认控件换成自绘控件。比如 CJButton 通常继承 CButton但在内部接管了 WM_PAINT自己画边框、底色、圆角和图标。这样你不需要自己处理双缓冲绘图只要把控件类型换掉剩下的样式由库完成。替换代码很简单我一般在对话框头文件里把成员变量类型从 CButton 改成 CJButton// 原声明CButton m_btnOk; CJButton m_btnOk; // CJButton 同样继承 CButton接口兼容 // OnInitDialog 中的绑定代码完全不用改 m_btnOk.SubclassDlgItem(IDOK, this);第一句说明CJButton 保持了 CButton 的消息接口所以 ON_BN_CLICKED 消息映射仍然有效。第二句说明SubclassDlgItem 把对话框上的 IDOK 按钮实例接管库的自绘逻辑从这里开始生效。要注意的是更换类型后一定要重新编译所有引用该控件的文件否则属性表里还是旧类型编译期不会报错但运行时会出奇怪行为。对于编辑框、列表视图也是一样的套路把 CEdit 换成 CJEdit把 CListCtrl 换成 CJListCtrl原有代码基本可以原样保留。我的经验是每换一个控件就在同一个窗口里放一个原生控件做对比确认样式变化符合预期再继续不要一口气全换否则出了问题很难定位是哪一类引起的。这样换完以后对话框的观感会整体统一因为 CJ60 会给同一族控件做统一的配色和边框风格。2.2 对话框模板与布局管理省掉一半OnInitDialog代码MFC 工程里每个对话框都要在 OnInitDialog 里做控件初始化、设置字体、调整位置代码重复度很高。CJ60lib 会把这一层公共工作提前封装好。常见做法是让对话框继承库提供的对话框基类比如 CJDialog然后在基类的 OnInitDialog 里完成背景色设置、控件排列和默认焦点处理。这样我自己的 OnInitDialog 里只需要写业务相关代码。class CAboutDlg : public CJDialog { public: // 不用手动添加 WM_ERASEBKGND 的重写基类已经处理 virtual BOOL OnInitDialog(); }; BOOL CAboutDlg::OnInitDialog() { CJDialog::OnInitDialog(); // 这里只放本对话框特有的初始化 return TRUE; }逻辑说明CJDialog::OnInitDialog() 内部通常会先调用 CDialog::OnInitDialog()再应用全局默认字体、背景和布局策略所以子类不用重复调用 CDialog::OnInitDialog()。参数说明CJDialog 还支持通过基类成员设置背景颜色比如 SetDialogBkColor(RGB(...))如果你希望不同对话框用不同背景可以在子类里覆盖这个设置。布局管理是另一个省事点。原生 MFC 没有自动布局窗口一缩放控件就挤在一起。CJ60lib 的布局管理器可以记录控件与窗口边缘的相对距离在 WM_SIZE 时自动调整。我一般会在 OnInitDialog 里把需要拉伸的控件注册进布局管理器比如让列表控件随对话框放大。以库内的 CJLayoutManager 类为例// 假设 m_list 是一个 CJListCtrl CJLayoutManager* pLayout CJLayoutManager::GetInstance(); pLayout-AddControl(GetSafeHwnd(), m_list.GetSafeHwnd(), CJ_ANCHOR_LEFT | CJ_ANCHOR_TOP | CJ_ANCHOR_RIGHT | CJ_ANCHOR_BOTTOM);这样设置之后列表会随窗口四个边一起拉伸不用再写一堆 MoveWindow。注意 AddControl 的参数常用的锚定值是 CJ_ANCHOR_LEFT、CJ_ANCHOR_TOP、CJ_ANCHOR_RIGHT、CJ_ANCHOR_BOTTOM组合起来表示控件需要钉住窗口的哪几条边。不同版本库的宏名可能不同直接在头文件里搜 ANCHOR 就能找到。2.3 工具栏、状态栏和托盘图标CJ60lib最出彩的部分工具栏是 CJ60lib 的招牌。原生 CToolBar 在图标只有 16 色时还说得过去一旦项目想用真彩色图标、给按钮加文字、做下拉箭头就得费很大劲。CJToolBar 直接支持高彩色位图、按钮文字、图标和文字同时显示、按钮分组、甚至可拖拽调整位置。资源包里的 Toolbar.bmp 和 button_images.bmp就是给这些工具栏按钮提供不同状态的图片源。用 CJToolBar 替换 CToolBar 的方式和上面类似先把成员变量改成 CJToolBar然后创建时多传一个样式标志// MainFrame.h CJToolBar m_wndToolBar; // OnCreate 中 if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT | TBSTYLE_TRANSPARENT, WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_GRIPPER | CBRS_TOOLTIPS)) { return -1; } m_wndToolBar.LoadToolBar(IDR_MAINFRAME); // 关键是这个用资源包里的Toolbar.bmp替换工具栏默认位图 m_wndToolBar.GetToolBarCtrl().MapBitmap(IDR_MAINFRAME, IDB_TOOLBAR_BITMAP);第一句 CreateEx 里 TBSTYLE_FLAT 让按钮呈平面样式TBSTYLE_TRANSPARENT 让工具栏背景透明CBRS_GRIPPER 显示拖动把手。第二句 LoadToolBar 加载命令 ID 和图标索引的映射关系。第三句把原来的内部位图替换成自己准备的 BMP。这里 IDB_TOOLBAR_BITMAP 要放在工程资源里BMP 格式建议用 24 位色以下因为库的位图解码逻辑比较老32 位带 Alpha 通道的 BMP 很容易画成半透明灰块。状态栏方面原生 CStatusBar 只能显示文本CJStatusBar 可以给窗格加图标、背景色和进度条。托盘图标则由库里的托盘管理类封装Create、SetTooltip、OnNotifyIcon 都处理好了比自己写 Shell_NotifyIcon 省不少事。我接手老工程时一般先换工具栏和状态栏这两个部分对用户感知最强而且改动面比较集中出问题容易回滚。控件增强慢慢来布局管理和托盘作为第二阶段。3. 把CJ60lib接进你的MFC工程环境配置、初始化与第一个换皮案例3.1 拿到资源包后先看什么拿到这套 CJ60lib 资源后很多人急着把文件往工程里拖其实应该先冷静看一下包里有什么。CJ60Lib.clw 是 VC6 时代的 ClassWizard 数据库它记录的是这个库工程本身的类信息不是给你直接用的。真正的使用价值在头文件、实现文件和位图资源。从文件名看cj_logo.bmp 是库厂商 logoToolbar.bmp 是工具栏位图button_images.bmp 是按钮图集btn_explorer.bmp 和 btn_arro.bmp 分别是资源管理器和箭头按钮的图标素材。把这些位图放进工程资源时要勾选导入到 Bitmaps 组而不是 Icon 组否则 LoadBitmap 找不到。然后确认库的交付形式。有的 CJ60lib 是源码包包含一堆 .h 和 .cpp这种最好办直接编进你的工程有的是编译好的 .lib/.dll那么要额外配置库目录和运行库路径。我习惯优先用源码版因为老库在 VS 新版本下编译常有小毛病源码在手可以直接改用 .lib 碰到问题就只能干瞪眼。还有一个容易被忽略的检查版本。CJ60lib 经历了多个版本早期只支持 VC6后来支持 VC2008再后来社区修改版才兼容 VS2015 及以上。如果你拿到的是非常老的版本不要指望原封不动能编译过先拿一个空的 MFC 对话框工程做集成试验比直接改业务工程稳妥得多。这一步决定了后面会不会在编译阶段大量翻车。3.2 包含路径、链接库和初始化设置集成 CJ60lib 的标准步骤我一般分三步配置头文件路径、配置链接库、做全局初始化。第一步把 CJ60lib 的 Include 目录加进工程的附加包含目录把 Lib 目录加进附加库目录。以 VS2019 为例在工程属性中设置// C/C - 常规 - 附加包含目录 $(ProjectDir)..\CJ60lib\Include // 链接器 - 常规 - 附加库目录 $(ProjectDir)..\CJ60lib\Lib // 链接器 - 输入 - 附加依赖项 CJ60Lib.lib这段配置里最容出错的是末尾的反斜杠VS 要求目录分隔符用\写成/会直接报找不到文件。另外附加依赖项里如果已经有一个老的界面库比如 BCGControlBar这里会同时链接两个库这在后续很容易触发符号冲突建议先只保留一个。第二步在 stdafx.h 里引入库头文件// stdafx.h 中放在所有系统头文件之后 #include CJLib.h为什么要放在最后因为老版本头文件可能会重定义 Windows SDK 里已经定义的结构如果放在系统头文件前面编译时会出现一堆 C2011 重定义错误。这个问题在 VS2010 之后特别明显放到最后至少能缓解一部分。第三步全局初始化。CJ60lib 的工具栏、状态栏和控件自绘依赖一些全局钩子和窗口样式必须在创建主窗口之前初始化。在 CWinApp::InitInstance 里加上BOOL CCJ60DemoApp::InitInstance() { // 库初始化必须在第一个窗口创建之前调用 // 函数名以你拿到的头文件为准常见叫 CJ60Initialize() 或 InitCJ60Library() CJ60Initialize(); // 原来的 CWinApp::InitInstance 代码 ... }如果这个函数调用顺序不对典型症状是程序能启动但工具栏按钮全部没有图标或者状态栏第一格不显示任何文本但占着位置像黑匣子一样。遇到这种情况优先检查 InitInstance 层级的调用顺序。3.3 第一个改造案例给原来控件换皮并加事件环境配好之后找一个最小的对话框做改造。拿我常做的按钮和编辑框换皮来说直接在头文件里换类型在 OnInitDialog 里加一行调用。class CDemoDlg : public CJDialog { public: CJButton m_btnQuery; CJEdit m_editKeyword; }; BOOL CDemoDlg::OnInitDialog() { CJDialog::OnInitDialog(); m_btnQuery.SubclassDlgItem(IDC_BTN_QUERY, this); m_editKeyword.SubclassDlgItem(IDC_EDIT_KEYWORD, this); // 给按钮加一个图标资源ID为IDB_QUERY_ICON m_btnQuery.SetBitmap(IDB_QUERY_ICON); return TRUE; }这里 SubclassDlgItem 的作用是把对话框里已有的控件实例交给 CJButton 管理不改变控件 ID 和消息关联。SetBitmap 是 CJButton 额外的接口用来显示图标如果库版本不支持 SetBitmap可以改用 SetImageList 加图标索引效果一样只是参数不同。事件处理完全不用动在消息映射里照写 ON_BN_CLICKED(IDC_BTN_QUERY, CDemoDlg::OnBtnQuery)。用一个小对话框验证比直接改主界面风险低得多。如果换皮后编译通过但按钮没有变化可能是库的自绘函数只在系统主题是经典模式时生效检查一下工程是否定义了 _WIN32_WINNT0x0601如果定义高了需要降下来。这是我踩过最深的坑之一界面库的代码还是按照 XP 时代的窗口样式写的强行适配 Win11 的结果是自绘全部失效按钮恢复成系统样式看起来像是没集成功。4. CJ60lib避坑排查手册编译、链接、运行时、位图的五个血泪记录4.1 编译错误头文件顺序和宏定义冲突现象把 CJ60 头文件加进 stdafx.h 后编译突然出现几百个 C2011“X : struct type redefinition”错误很多还出现在 Windows 头文件内部。原因CJ60 老版本为了兼容 VC6自己定义了部分原本由 Windows SDK 定义的结构或宏比如 TCHAR 相关处理、BITMAP 的结构别名等。当它被放在系统头文件前面时SDK 再定义一遍就会冲突。解决把 CJ60 的头文件全部移动到所有系统头文件之后并且保证整个工程只包含一份 CJ60 头文件。如果仍然冲突可以尝试在包含 CJ60 头文件之前定义 WIN32_LEAN_AND_MEAN把系统头文件裁剪掉一部分减少重复定义的概率。4.2 链接冲突无法解析的外部命令LNK2001现象编译通过但链接时报 LNK2001符号是 CJ60 类里某个函数比如public: __thiscall CJToolBar::CreateEx(...)有时候还会同时出现 LNK2019。原因常见两种情况一是头文件声明的类使用了 __declspec(dllexport) 但对应的 .lib 没加到链接项里或者 .lib 路径错误二是 CJ60 的类是在 DLL 中导出的而你的工程忘记定义导出相关的宏导致导入导出不一致。解决先确认附加依赖项里有 CJ60Lib.lib且库的位数和工程一致32 位工程配 32 位库。然后把项目的预处理定义和库头文件里要求的宏对照按说明在工程预处理定义里加上对应的宏。4.3 运行时断言失败Assertion in dlgcore.cpp现象程序启动时弹出一个断言框定位到 dlgcore.cpp 或 wincore.cpp点击重试后发现窗口没有完全创建。原因CJ60 的初始化函数在 InitInstance 里没有被调用或者被调用得太晚。因为库会钩住某些窗口消息来维护自绘状态钩子未安装时控件消息链断裂MFC 内部的状态检查就会触发断言。解决在 CWinApp::InitInstance 里第一步调用初始化接口。如果已经调用确认是不是创建了多个 CWinApp 实例或者用 SetCurrentHandles 之类的操作干扰到了 MFC 的状态。我的血泪经验是别把初始化放在 OnCreate 里那太晚了主窗口的创建过程在 OnCreate 之前就会用到库的钩子。4.4 位图加载失败工具栏上的图标全是灰块或空白现象按钮能点但图标区域显示灰度马赛克或完全空白文字正常。原因CJ60 工具栏的位图加载逻辑比较古老它要求 BMP 资源在资源文件中是 Bitmaps 类型并且颜色格式是 4 位或 8 位依赖调色板真彩色 BMP 反而不识别。另一个常见错误是 LoadBitmap 直接传了资源 ID 数字导致被当成字符串 ID 解析失败。解决把工具栏 BMP 导入资源时选“Bitmaps”用 MAKEINTRESOURCE 包裹 ID比如 SetBitmap(MAKEINTRESOURCE(IDB_TOOLBAR))。如果手头 BMP 是 32 位带 Alpha 的先转成 24 位把透明信息去掉否则库会按调色板索引去读结果就是灰块。4.5 VS高版本Unicode和MBCS问题中文乱码和编译警告现象同样代码在 VC6 里好好的在 VS2019 里编译通过但是界面中文变乱码或者编译时警告 C4566。有些模块在资源编辑器中显示正常运行时显示问号。原因CJ60 老库是按 MBCS 多字节字符集写的字符串处理用的是 char* 和某些直接索引操作。VS2010 之后新建 MFC 工程默认是 Unicode老库内部没有把 char* 转成 TCHAR*两种字符集混用导致解析错误。解决最简单的是把整个工程的“字符集”设为“使用多字节字符集”。如果项目必须用 Unicode就得把库源码里所有 CString 的格式化、字符数组访问都用 _T 和 CStringW 适配这个工作量大建议优先改工程字符集而不是改库源码。需要注意如果同时掺入了其他要求 Unicode 的第三方库那得多花时间做桥接层。5. 用CJ60lib做状态栏实时信息和动态菜单两个高频场景的落地代码5.1 状态栏显示实时数据从SetIndicators到SetPaneTextMFC 默认状态栏是 CStatusBarCJ60 把它升级成 CJStatusBar除了显示固定文本还能放进度条、图标并且文本可以随时刷新。热搜里常有人问“MFC 状态栏怎么显示”其实就是两个动作创建状态栏、更新指定窗格文本。先把框架窗口的状态栏成员换成 CJStatusBar创建方式保持不变// MainFrame.h CJStatusBar m_wndStatusBar; // MainFrame.cpp OnCreate if (!m_wndStatusBar.Create(this, WS_CHILD | WS_VISIBLE | CBRS_BOTTOM)) { return -1; } static UINT indicators[] { ID_SEPARATOR, ID_INDICATOR_CAPS, ID_INDICATOR_NUM }; m_wndStatusBar.SetIndicators(indicators, sizeof(indicators) / sizeof(UINT)); // 第一个分隔窗格专门留给实时信息 m_wndStatusBar.SetPaneText(0, _T(等待数据...), TRUE);这里 SetIndicators 确定了状态栏分几格第一格 ID_SEPARATOR 是可变宽度文本格后两格是标准的 Caps Lock 和 Num Lock 指示器。SetPaneText 的第三个参数 TRUE 表示立即重绘如果更新频率很高比如每秒多次建议改成 FALSE在定时器批量更新完后再单独 Refresh。接下来是实时数据的推送。我用定时器每秒钟读一次数据并更新状态栏#define TIMER_STATUS 1001 void CMainFrame::OnCreate(...) // 创建状态栏之后 { SetTimer(TIMER_STATUS, 1000, NULL); } void CMainFrame::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent TIMER_STATUS) { CString strInfo; strInfo.Format(_T(当前CPU占用%d%% 可用内存%d MB), GetCpuUsage(), GetFreeMemory()); m_wndStatusBar.SetPaneText(0, strInfo, TRUE); } CFrameWnd::OnTimer(nIDEvent); }SetPaneText 只改变指定窗格内容不会影响其他格子。参数说明0 表示第一个可变量宽窗格GetCpuUsage 和 GetFreeMemory 是我自己封装的两个采集函数返回值分别是 int 和 long你写业务代码时替换成实际数据源比如从串口或网络接收的数据。这里有一个容易翻车的地方CJStatusBar 在某些版本里 SetPaneText 之前必须先调用 SetIndicators否则内部窗格数组为空直接访问 0 号窗格会崩溃。所以顺序要固定Create → SetIndicators → SetPaneText。如果界面出现状态栏第一格是空白但大小占着十有八九是 SetIndicators 没执行成功检查 indicators 数组里是否有无效资源 ID。5.2 动态添加工具栏按钮和菜单项把功能入口做成活的老工程里经常有这种需求根据当前用户权限决定工具栏上有哪些按钮、菜单里有哪些项。原生做法是预先放好所有按钮再设置隐藏或禁用但这样不仅切换时不流畅代码也很碎。CJToolBar 和菜单类都支持运行时插入可以做成一个权限配置刷新的函数。先看工具栏动态插入。假设在 OnCreate 之后根据权限状态追加一个“查询”按钮// 在工具栏末尾插入按钮 int nCount m_wndToolBar.GetButtonCount(); m_wndToolBar.InsertButton(nCount, ID_TOOL_QUERY, _T(查询), 2);InsertButton 的参数从左到右分别是插入位置、命令 ID、按钮文字、图标索引。图标索引对应工具栏位图里第几个图标2 表示第三张因为索引从 0 开始。注意插入后工具栏会自动重新排列但原本的按钮分隔符位置会被打乱所以如果工具栏有分隔符最好在 InsertButton 之后调用一次 RefreshState() 重排。动态菜单更简单。用 CMenu 的 AppendMenu 就能往菜单里加项CJ60 的菜单类只是增强绘制接口还是标准 CMenuCMenu* pTopMenu m_wndMenuBar.GetMenu(); if (pTopMenu) { CMenu* pOpMenu pTopMenu-GetSubMenu(2); // 拿到“操作”菜单 pOpMenu-AppendMenu(MF_STRING, ID_TOOL_QUERY, _T(查询实时数据)); }AppendMenu 第二个参数是标识位MF_STRING 表示按文本方式添加命令 ID 是 ID_TOOL_QUERY。这样添加的菜单项在点击后会进入 OnCmdMsg 的消息路由和资源编辑器里拖出来的菜单完全一样所以消息映射照常写ON_COMMAND(ID_TOOL_QUERY, CMainFrame::OnToolQuery)在 OnToolQuery 里弹一个对话框来展示实时数据图表。这里用 CJDialog 作为基类放一个自绘控件画曲线图对话框用 DoModal 方式弹出。如果你要的是非模态实时窗口记得在关闭时 Delete 掉指针否则内存泄漏很难查。void CMainFrame::OnToolQuery() { CQueryDlg dlg(this); dlg.DoModal(); }这样把按钮、菜单、状态栏连起来整个界面就有了响应感。动态菜单和工具栏最典型的坑是插入按钮时图标索引越界InsertButton 会静默失败界面没反应但 GetLastError 不一定有输出。我一般会在调试版本里把索引值用 TRACE 打印出来确认在位图范围内。6. 验证CJ60lib接得值不值集成检查清单和两个维护技巧6.1 集成后的验证清单集成不是编译通过就完了按这个清单过一遍能省很多上线后的麻烦检查项验证方法预期结果控件自绘新对话框用 CJButton悬停按钮看边框按钮样式明显与系统按钮不同工具栏拖拽工具栏到窗口右侧再拖回来可停靠和自动换行正常图标不花状态栏定时更新第一格文本观察 1 分钟无闪烁、无空白Caps 指示器同步字符集查看中文菜单、中文状态栏文本乱码零出现内存连续开关动态菜单对话框 20 次内峰峰值不持续增长6.2 老代码兼容性保留原消息映射的同时加CJ60处理很多老工程消息映射已经写到几千行不需要为了 CJ60 把所有消息重写。CJ60 的类如果继承自 MFC 原类那么 ON_BN_CLICKED、ON_COMMAND 这些映射全部保留。如果库要求你在窗口过程里额外处理某个通知可以用 ON_WM_CTLCOLOR 等原有映射配合。原则是不删原有映射只在需要增强的通知函数里增加对 CJ60 扩展接口的调用。6.3 一个提高维护性的习惯把CJ60初始化封装到基类最后给一个实际习惯。我一般在项目里加一个中间基类比如 CBaseCJ60Dialog所有业务对话框都继承它class CBaseCJ60Dialog : public CJDialog { public: BOOL OnInitDialog() override { CJDialog::OnInitDialog(); SetDialogBkColor(RGB(240, 240, 240)); return TRUE; } };这样整个项目的对话框背景、字体风格统一以后想升级 CJ60 或者替换成别的界面库只需要改这一个基类不用每个对话框逐个遍历。同样CMainFrame 也可以抽一个基类统一创建 CJToolBar 和 CJStatusBar避免框架窗口到处重复代码。我从那以后每次接 CJ60 老工程都会先建一个空项目把库集成、初始化、一个按钮换皮、状态栏刷一条数据都验证通过再往业务项目里迁移。这套 CJ60lib 资源如果你正打算用建议也按这个顺序走一遍能避开很多上线前才发现的自绘失效问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表