ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CJ60lib老MFC界面维护指南:停靠工具条、自绘与状态恢复实战

CJ60lib老MFC界面维护指南:停靠工具条、自绘与状态恢复实战 简介CJ60lib是一套专为MFC框架设计的经典界面库资源包面向Windows桌面应用开发者旨在解决UI控件单一、自绘复杂度高等问题。包内共收录115个文件以Visual C工程为主体43个h头文件与41个cpp源文件构成核心代码bmp、cur、ico等位图与图标文件提供界面素材dsp、dsw、clw、rc等文件则承载工程配置、类向导与资源脚本信息。整体压缩包仅149KB却涵盖了MFC界面开发中常用的按钮增强、列表视图、对话框模板、工具栏菜单、状态栏托盘、布局管理、消息映射扩展、国际化支持与日志调试等模块配套代码结构清晰适合直接研究或复用。开发者可以借此避开重复造轮子快速搭建风格统一的专业UI尤其适合具备MFC基础、希望提升界面质量与开发效率的中高级程序员。目前已有192人学习下载是一份理解经典MFC界面库内部机制的高性价比参考资料。1. 老工程里那套“VC6同款”停靠界面十有八九是CJ60lib在撑着接手一个2005年前后的MFC桌面工具主窗口四周能拉出停靠面板工具条能拖出来悬浮按钮还带热跟踪效果——这不是VC6自带的本事。你去源码里翻主框架的基类十有八九撞见CJ60lib。CJ60lib是跟着MFC 6.0时代起来的第三方扩展界面库专门解决当时MFC原生界面“能用但不好看、不好用”的问题可停靠工具条、可漂浮窗格、布局保存恢复全是它给出的标准答案。现在它最大的价值是大量还在服役的工业软件、内部工具跑在它上面。接这种老工程的人要回答的不是“MFC还是Qt”而是“怎么不动业务代码把老界面继续磨得顺手”——这篇就讲这条路怎么走。2. CJ60lib为什么能撑起老MFC程序的界面从名字来历到子类化与自绘机制2.1 CJ60名字里的“60”就是MFC 6.0它当年填了哪些原生坑CJ60这个名字里的60指的就是MFC 6.0、VC6那个时代。1998年前后VC6是Windows桌面开发的主流工具MFC 6.0是它的类库底座。MFC原生不是不能做停靠CFrameWnd提供了DockControlBar但做出来的东西离“专业工具软件”差得远工具条按钮是扁平的方块没有热跟踪没有鼠标停留提示的动态效果窗格能不能浮动、能不能对接全要自己写更不用谈布局保存。CJ60lib这类第三方扩展库就是在那个背景下出现的。它把Visual C 6.0集成开发环境里那套“专业感”搬到了普通MFC程序里窗格可以停靠到父窗口四边、可以拖出来变成独立小窗、工具条拖动时有参考框线、按钮有热跟踪。放在今天看这些功能平平无奇但在当时这是自己手写几百行的活。能力MFC 6.0 原生CJ60libVS2008 之后 MFC Feature Pack工具条可停靠可以但外观简陋可以支持热跟踪与动态尺寸可以且自带现代外观窗格可漂浮/对接基本靠手写开箱即用开箱即用布局保存与恢复无原生支持提供 SaveBarState / LoadBarState提供按钮热跟踪与提示无有有状态栏增强基础增强增强典型年代1998 前后2000 到 2008 前后2008 之后这张表也解释了一个现实为什么2025年还有人要跟CJ60lib打交道。因为大量2000年代写的老工具还跑得好好的业务逻辑几十万行重写不现实。界面库能退则退但停靠框架这种深入主窗口创建流程的东西退不掉只能接着用。2.2 子类化、消息钩子与自绘CJ60lib接管界面的底层套路CJ60lib不是控件库它是一整套界面框架。它最核心的机制有三个MFC扩展DLL、窗口子类化、自绘。理解这三个词比背十个类的构造函数有用得多。MFC扩展DLL是CJ60lib的载体形态。它导出的不是普通函数而是MFC的类。官方一点的说法是“只能被MFC程序加载、并且使用共享MFC DLL时才能用的DLL”通俗讲就是它和你的程序共用同一份MFC运行时类指针、消息映射可以直接跨模块传递。这就是为什么CJ60lib能让你在主框架里直接把基类换成它的类而不是通过一组C接口来回传句柄。窗口子类化是它“接管”老控件的手段。一个窗口已经创建MFC的CWnd对象已经建立CJ60lib不要求你推翻重来而是通过SubclassWindow或类似的机制替换窗口过程。替换之后WM_NCPAINT、WM_ERASEBKGND、WM_NCLBUTTONDOWN这些消息先经过CJ60lib自己的处理逻辑边框的凸凹斜边是画出来的按钮的鼠标悬停高亮是画出来的停靠时的预览框也是自己画的。你在老MFC程序里看到的那种“忽然变专业”的感觉本质上是别人把Windows非客户区绘制接管了。消息钩子解决的是“停靠过程里鼠标在哪、窗格该往哪走”的问题。当用户拖住工具条的空白区域CJ60lib需要持续跟踪鼠标位置判断是重新停靠、是浮动、还是停在原地。常见做法是在拖拽开始时捕捉鼠标在鼠标移动消息里计算目标停靠区域再画一个矩形框给用户预览。这也是前面说的“拖动时有参考框线”效果的来源。至于“桌面软件开发用MFC还是Qt”——那是新项目该纠结的事。老工程面对的是几十万行基于MFC的业务代码和一个还在稳定运行的界面框架用CJ60lib继续维护是把界面风险控制在局部你只改窗口类基类和控件类不碰业务逻辑回报率远高于推倒重来。2.3 三条命令确认老工程里到底有没有CJ60lib接一个陌生工程先确认它到底是不是CJ60lib写的别靠自己猜。我在命令行里跑这样的检索grep -rEn CJToolBar|CJControlBar|CJMDIFrameWnd|CJStatusBar --include*.h --include*.cpp .-r是递归子目录-E按扩展正则匹配-n显示行号--include限定只看头文件和源文件避免去匹配编译输出目录。这四个类名是CJ60lib里出现频率很高的标识符命中任何一个基本可以确认这个工程用了CJ60lib。第二步看主框架的头文件。打开MainFrm.h看CMainFrame的基类class CMainFrame : public CJMDIFrameWnd如果是CJMDIFrameWnd而不是CFrameWnd或CMDIFrameWnd那已经实锤了。第三步去项目属性里看链接器依赖搜索文件名里带CJ字样的.lib或.dll。有些工程把依赖写死在stdafx.h的#pragma comment(lib, ...)里直接在工程里搜#pragma comment(lib也快。这三步做完你至少知道了两件事这个工程的界面框架是谁以及它在哪些类上做了扩展。接下来才是动手阶段。3. 把CJ60lib接进现存VS工程链接方式、字符集与一个最小可运行框架3.1 静态库还是扩展DLL先选对链接方式再谈移植老工程编译CJ60lib通常有两种形态静态库和MFC扩展DLL。静态库把代码直接编进exe部署时少一个依赖文件但多个模块都用同一套CJ60类时容易撞符号。扩展DLL则要求你的程序用“在共享DLL中使用MFC”的方式编译exe运行时旁边得放着对应的DLL少一个就启动失败。我一般会先看老工程原来是什么用法不动这个约定。原来用扩展DLL就继续用扩展DLL原来用静态库就继续用静态库。CJ60lib这种老库它的调试版和发布版内部实现差异不小混着来会在链接阶段冒出一堆LNK2005重复定义。别问为什么直接把整个解决方案的配置统一成同一种模式最省事。其次是字符集这是老库移植到新VS的头号卡点。CJ60lib活跃的年代VC6默认是DBCS多字节字符集那时候TCHAR还是一个能退化成char的宏。VS2019之后新工程默认Unicode老库内部大量窄字符串处理的代码在Unicode工程下会出现两种病链接器找不到符号或者界面上中文乱码。我常用的处理方式是让老工程口服老库的约束项目属性推荐值说明字符集使用多字节字符集老库内部多按窄字符实现切Unicode代价大使用MFC在共享DLL中使用MFC与扩展DLL形态匹配C语言标准C14老代码在C17下的异常处理差异可能引入怪问题SDL检查否老代码告警多SDL会拦第三方头文件Windows SDK版本10.0最近已安装不必刻意选老SDK“使用MFC”这一项如果老库是静态编译的就选“使用静态MFC库”。一句话库怎么编工程就怎么配。3.2 替换主框架基类把CMainFrame从CFrameWnd换到CJ60的Frame类最小改动方案不是去换整个CWinApp而是只换主框架和子窗口框架的基类。多文档程序典型做法是把CMainFrame的基类从CMDIFrameWnd换成CJMDIFrameWnd。单文档程序则换成CJ60lib对应的单文档Frame基类命名风格一般是CJFrameWnd或CJSDIFrameWnd之类具体以你手上头文件里的声明为准。来看一个最小骨架。首先是头文件里的类声明// MainFrm.h class CMainFrame : public CJMDIFrameWnd { DECLARE_DYNAMIC(CMainFrame) public: CMainFrame(); virtual ~CMainFrame(); protected: afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); DECLARE_MESSAGE_MAP() };关键就是把基类换成CJMDIFrameWnd。MFC的消息映射、OnCreate的调用流程都不变CJ60对它们的处理是通过基类内部的虚函数和消息映射完成的。你原来在OnCreate里写的CreateObject、菜单加载、状态栏创建都可以保留但要留意CJMDIFrameWnd创建停靠窗格的时机一般要在基类OnCreate返回之后再调用自己的窗格创建函数。顺序反了会出现“基类还没建立停靠上下文子窗口就要求停靠”的崩溃。配合的cpp文件是最小实现// MainFrm.cpp int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CJMDIFrameWnd::OnCreate(lpCreateStruct) -1) return -1; EnableDocking(CBRS_ALIGN_ANY); // 创建工具条和停靠窗格的代码放这里 // 具体写法见第4章 return 0; }EnableDocking(CBRS_ALIGN_ANY)表示允许停靠在父窗口的任意一边。CBRS_ALIGN_ANY是顶、底、左、右四个方向的组合如果只允许顶和底就写CBRS_ALIGN_TOP | CBRS_ALIGN_BOTTOM。这一步不做后面所有窗格的停靠调用都不会生效工具条拖出来就再也回不去。替换基类之后第一轮编译大概率会报一些符号找不到的错误。多是CJ60lib自身的头文件路径没配好或者stdafx.h里CJ60的头文件包含顺序有问题。老库的头文件对包含顺序敏感常见要求是MFC头文件在前、CJ60头文件在后反过来会出现宏重定义。3.3 最小可运行验证编译、启动与停靠布局的首次保存改完基类别急着加功能先验证“能不能编译、能不能启动、停靠系统是否活着”。我把这个叫CJ60工程的最小可运行验证。编译通过后运行程序如果主窗口外框出现VC6时代那种凸起或平面的非客户区绘制风格说明CJ60的子类化已经接管了窗口。再随便创建一个空窗格比如把一个对话框模板用CJControlBar包起来试一下能不能拖出主窗口变成独立小窗。能拖出来、能拖回去说明停靠系统是通的。第一次验证时顺手把布局保存接上这一步经常被忽略但后续所有界面调整都依赖它。在OnCreate里创建完所有窗格后加一行LoadBarState(_T(MainLayout));在OnClose里加对应的一行SaveBarState(_T(MainLayout));LoadBarState的作用是从注册表或INI读取上次保存的窗格位置与停靠状态SaveBarState在窗口关闭前把当前布局写回去。第一次跑的时候这些函数可能什么都没做但等你把窗格拖乱关了再开就能体会到什么叫后悔药。没有这组函数CJ60窗格被拖到屏幕外之后只能靠改注册表或者重置主窗口状态救回来费时费力。4. 用CJ60lib做三类高频界面改造可停靠工具条、弹窗实时图表、状态栏信息4.1 把旧工具条升级成可停靠工具条CJToolBar的创建与停靠参数在CJ60lib里工具条对应的类是CJToolBar。在CMainFrame里声明一个成员变量CJToolBar m_wndToolBar;然后在OnCreate里创建并让它可停靠。先把它挂到主框架上再加载工具条资源最后调用停靠相关函数if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, CBRS_TOP | CBRS_FLYBY | CBRS_TOOLTIPS | CBRS_SIZE_DYNAMIC | CBRS_HIDE_INRESTORE) || !m_wndToolBar.LoadToolBar(IDR_MAINFRAME)) { TRACE0(Failed to create toolbar\n); return -1; } m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY); DockControlBar(m_wndToolBar, AFX_IDW_DOCKBAR_TOP);逐项说明几个关键参数。CBRS_FLYBY是CJ60lib的标志性能力它让鼠标停在按钮上时弹出文字提示并且带热跟踪效果算是那年代“界面高级感”的出处之一。CBRS_SIZE_DYNAMIC允许用户拖动调整工具条的行宽配合CBRS_TOOLTIPS让按钮提示在状态栏和浮动提示之间切换。CBRS_HIDE_INRESTORE让主窗口还原时隐藏工具条避免小窗口下工具条挤占空间。DockControlBar(m_wndToolBar, AFX_IDW_DOCKBAR_TOP)里的AFX_IDW_DOCKBAR_TOP指定初始停靠到主窗口顶部。CJ60lib的DockControlBar与MFC原生同名函数行为基本一致但内部多了布局记录逻辑LoadBarState时对错位置的窗格能自动纠偏。这里常见的问题是工具条拖出来后停靠不回去。多数原因是EnableDocking的参数和DockControlBar的初始方向不一致比如只允许CBRS_ALIGN_BOTTOM却把它DockControlBar到顶部。两边方向必须对齐否则停靠操作在内部校验阶段就被拒了。4.2 按钮弹对话框显示实时数据图表双缓冲自绘与定时器参数先说结论弹对话框显示实时图表不需要让对话框继承CJ60的类。用标准CDialog就够了CJ60只负责主框架和停靠窗格这层“壳”数据展示自己做反而更可控。一套常见做法是在工具条上加一个按钮响应函数里创建或显示一个图表对话框对话框里放一个静态控件作为绘图区启动定时器周期性刷新。按钮响应用标准命令传递void CMainFrame::OnChartButton() { if (!m_pChartDlg) { m_pChartDlg new CChartDlg(this); m_pChartDlg-Create(IDD_CHART_DLG, this); } m_pChartDlg-ShowWindow(SW_SHOW); m_pChartDlg-SetTimer(1, 100, NULL); }SetTimer的三个参数含义定时器ID为1触发间隔100毫秒回调函数为空表示使用WM_TIMER消息。100毫秒即10帧每秒对工业数据曲线足够也不会把CPU烧在绘图上。CChartDlg的OnPaint里做双缓冲自绘。这个技巧在MFC里是老生常谈但CJ60工程里经常被忽略导致窗格拖动时图表区域一片片闪白。双缓冲的骨架长这样void CChartDlg::OnPaint() { CPaintDC dc(this); CRect rc; GetClientRect(rc); CDC memDC; memDC.CreateCompatibleDC(dc); CBitmap bmp; bmp.CreateCompatibleBitmap(dc, rc.Width(), rc.Height()); CBitmap* pOld memDC.SelectObject(bmp); // 1. 填充白色背景画网格线 // 2. 把最新一段数据画成折线 // 3. 画坐标轴文字 dc.BitBlt(0, 0, rc.Width(), rc.Height(), memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOld); bmp.DeleteObject(); }CreateCompatibleDC创建与屏幕DC兼容的内存画布CreateCompatibleBitmap分配同样尺寸的位图所有绘制先在这块内存上完成最后BitBlt一次拷贝上屏。这样WM_ERASEBKGND和WM_PAINT不会在屏幕上交替擦白。注意SelectObject(pOld)要在bmp.DeleteObject()之前否则释放一个仍然被DC选中的位图对象会触发句柄错误。定时器驱动的刷新要控制节奏。Invalidate(FALSE)只使客户区失效不擦背景比默认的Invalidate(TRUE)省一次背景重绘。但前提是OnPaint里已经自己画了背景如果你依赖系统擦背景这里就改成Invalidate()标准版本。4.3 把运行信息实时显示到状态栏SetPaneText的刷新时机MFC程序在状态栏上显示信息绕不开CStatusBar::SetPaneText。CJ60lib如果提供了自己的状态栏类做法一致只是类名不同。很多人第一次写状态栏显示都踩过“调用了SetPaneText但界面没动静”的坑。原因在于MFC状态栏的刷新依赖命令路由机制。SetPaneText只是改了内部字符串状态栏真正重绘要等WM_PAINT或者OnUpdateCmdUI机制触发。实时性要求高的场景比如采集线程的状态不能只调一句SetPaneText后面要跟上强制刷新m_wndStatusBar.SetPaneText(3, _T(采集线程已启动), TRUE); m_wndStatusBar.UpdateWindow();SetPaneText第三个参数TRUE表示立即刷新该窗格UpdateWindow是确保WM_PAINT立刻被处理而不是排队等消息循环。只写前半句、漏掉UpdateWindow就是“信息没显示出来”的典型原因。更隐蔽的问题是跨线程调用。如果采集线程在工作线程里直接调SetPaneTextMFC的CWnd对象不是线程安全的轻则刷新失灵重则直接崩溃。跨线程更新状态栏的标准做法是PostMessage回主线程。假设自定义消息WM_USER_STATUS_UPDATEvoid CMainFrame::OnStatusUpdate(WPARAM wParam, LPARAM lParam) { // wParam 传状态码lParam 传字符串指针的替代方案是传索引 m_wndStatusBar.SetPaneText(3, g_strStatus[wParam], TRUE); m_wndStatusBar.UpdateWindow(); }发消息的一侧用::PostMessage(GetSafeHwnd(), WM_USER_STATUS_UPDATE, (WPARAM)statusCode, 0);PostMessage不会等待接收方处理适合高频状态更新。如果你用SendMessage工作线程会阻塞等待主线程处理完才继续跑采集线程反而被拖慢。5. CJ60lib避坑手册编译错、崩溃、闪烁和焦点丢失的四类经典翻车现场5.1 现象→原因→解决多字节/Unicode混编导致链接失败和中文乱码现象VS2019工程里引入CJ60lib编译时冒出一堆unresolved external symbol错误定位到CJ60lib内部函数。运行起来又发现界面上中文全变成乱码或问号。原因老库内部处理字符串用的是char*窄字符工程却是Unicode字符集。Unicode下TCHAR展开为wchar_tCString也变成宽字符实现两边的符号修饰名不同链接器自然找不到。乱码则是源文件编码和编译器默认编码不一致导致。解决项目属性 → 常规 → 字符集改为“使用多字节字符集”。源码文件统一保存为UTF-8带BOM不要混用GB2312和UTF-8。代码里不要裸写char*转CString统一走_T()和CString让宏在两种字符集下都能正确展开。5.2 现象→原因→解决CJ60lib与MFC新控件栏混用导致启动崩溃现象工程里既有CJ60lib的停靠窗格又用了VS2008之后MFC新带的CMFCToolBar或CMFCStatusBar启动到一半断言失败或者直接0xC0000005。原因两套界面库都实现了窗口子类化都在自己的消息循环里拦截WM_NCPAINT、WM_ERASEBKGND、WM_NCLBUTTONDOWN并且都维护各自的停靠状态。它们同时对同一个父窗口的消息链动手顺序稍有不一致就把对方保存的消息结果覆盖掉崩溃很难复现属于典型的“玄学问题”。更麻烦的是这种崩溃往往在特定窗格创建顺序下才出现Debug版可能跑半小时才触发。解决界面层统一不要混用。CJ60lib工程里不要引入CMFC*系列控件栏。如果只是因为某个按钮想要现代风格自己用CJ60的工具条自绘实现或者接受原生按钮。两套界面框架共存收益远小于风险。注意即使只是在一个对话框里放一个CMFCButton也不建议和CJ60共存。CJ60的子类化机制会扫描控件的继承树混用会让部分鼠标消息被提前吞掉。5.3 现象→原因→解决停靠窗格拖动时重绘闪烁与残影现象拖动CJ60窗格时窗格内部出现一块块白斑松手后窗口边缘残留上一帧的影像要等窗口重绘才消失。原因WM_ERASEBKGND和WM_PAINT都在画背景。拖动过程中系统不断发WM_MOVE和WM_PAINT默认的擦背景操作擦掉旧内容新的绘制又没跟上屏幕上就出现闪烁和残影。CJ60lib的窗格在拖动过程中自身也会触发布局重算背景绘制被重复触发。解决把背景绘制全部挪到OnPaint的双缓冲里重写OnEraseBkgnd让它什么都不做BOOL CMyDockPane::OnEraseBkgnd(CDC* pDC) { return TRUE; // 背景交给OnPaint里的双缓冲处理 }return TRUE告诉系统“背景已经擦了”实际没擦这样系统不会触发默认擦白。代价是OnPaint必须保证完整绘制背景否则会出现黑色残留。配合上一章讲的双缓冲BitBlt拖动的闪烁能压下去一大部分。另外注意不要在OnSize里频繁创建和销毁CJ子窗口RecalcLayout和OnSize的调用顺序确定之后尽量保持稳定不要随意调整。5.4 现象→原因→解决焦点在停靠窗格时快捷键与Tab连续失灵现象焦点停在CJ60停靠窗格内的编辑框里CtrlC复制失效Tab不能切换到下一个控件但用鼠标点主框架菜单又一切正常。原因MFC的命令路由依赖焦点窗口所在的框架。焦点在停靠窗格里时键盘消息先经过窗格的PreTranslateMessage它没把消息交给主框架的加速键表处理而是直接返回给内部了。加速键挂在主框架自然收不到消息。这是窗口子类化带来的附带影响CJ60把窗格变成了独立的消息处理节点但加速键路由还停留在主框架。解决在停靠窗格类里重写PreTranslateMessage让键盘消息先交给父框架查加速键BOOL CMyDockPane::PreTranslateMessage(MSG* pMsg) { if (pMsg-message WM_KEYFIRST pMsg-message WM_KEYLAST) { CFrameWnd* pFrame GetParentFrame(); if (pFrame pFrame-PreTranslateMessage(pMsg)) return TRUE; } return CJControlBar::PreTranslateMessage(pMsg); }WM_KEYFIRST到WM_KEYLAST覆盖了所有键盘消息。GetParentFrame从当前窗格向上找到所属主框架先让它处理加速键处理返回TRUE就说明消息被消费了不再往下传。注意要避免递归父框架的PreTranslateMessage最终不会回到本类所以这里递归深度是安全的。如果窗格是对话框形态pFrame可能是模态对话框的父框逻辑要按工程实际结构调整。5.5 现象→原因→解决高分屏DPI缩放让旧停靠条错位现象4K显示器上CJ60界面错位工具条高度不够字体偏偏很大停靠窗格之间的缝隙对不齐窗口一缩放布局就乱。原因CJ60lib内部按96 DPI写死了像素来计算停靠区域和控件尺寸。Windows的DPI缩放虽然会拉伸窗口整体但CJ60自绘的部分是按逻辑像素计算后自己画的系统缩放一介入自绘内容和系统拉伸之间出现错位典型的例子是停靠条的实际高度没变但字体变大导致文字被截断。解决在程序清单里声明DPI感知方式。如果老库没有做Per-Monitor适配最稳妥的方法是声明System DPI感知让系统统一做位图缩放整体变大但布局不变。在app.manifest里加dpiAwaretrue/dpiAware或者用较新的配置dpiAwarenesssystem/dpiAwareness不要急着上PerMonitorV2。Per-Monitor V2要求程序自己响应每个显示器的DPI变化CJ60lib内部没有这套逻辑切过去反而会出现“拖动窗口到另一块屏幕时界面挤成一团”的新问题。先以System DPI方式跑在100%、150%、200%三个缩放档位下验证界面没有结构性错位再考虑是否值得为了清晰度做额外适配。6. 让CJ60窗口状态可回归SaveBarState与PostMessage自检的组合拳前面讲了创建、参数和避坑但接老工程真正耗时间的往往不是写功能而是界面布局改坏之后不知道怎么回退。我现在的习惯是任何CJ60工程至少先做好两件事状态回归和启动自检。6.1 用SaveBarState/LoadBarState让界面布局回归可控把状态保存接到窗口关闭和创建流程里这是CJ60工程最便宜的后悔药。前面已经给了基本用法关键点是顺序。LoadBarState必须在所有窗格创建并完成首次DockControlBar之后调用否则它恢复的坐标没有对应的窗格ID去匹配等于白读。SaveBarState则放在OnClose里、基类关闭流程之前。void CMainFrame::OnClose() { SaveBarState(_T(MainLayout)); CJMDIFrameWnd::OnClose(); }保存文件名按程序名区分避免多个工具共用同一个键名导致布局互相覆盖。CJ60内部会记录每个窗格的停靠方向、浮动位置和尺寸。布局一旦被拖乱重开程序就能恢复省去手动调整的时间。6.2 一段自检代码在启动时发现“窗口漂出工作区”状态保存也有失效的时候比如显示器分辨率变了、用户拔掉副屏再开机的场景窗格可能被恢复到一个已经不存在的坐标上。我在程序启动后加一段自检检查主窗口是否还和工作区有交集void CMainFrame::SelfTestLayout() { WINDOWPLACEMENT wp { sizeof(wp) }; GetWindowPlacement(wp); CRect rcWork; SystemParametersInfo(SPI_GETWORKAREA, 0, rcWork, 0); CRect rcNormal(wp.rcNormalPosition); CRect rcInter; if (!rcInter.IntersectRect(rcWork, rcNormal)) { // 主窗口完全漂出工作区恢复到默认位置 MoveWindow(rcWork.left 20, rcWork.top 20, rcWork.Width() - 40, rcWork.Height() - 40, TRUE); } }GetWindowPlacement返回窗口最小化、最大化和普通三种状态的位置rcNormalPosition是普通状态的矩形。SPI_GETWORKAREA拿到不含任务栏的工作区IntersectRect判断两者是否有交集。完全没有交集说明布局已经坏了直接强制移回工作区内部。这段自检代码放在OnCreate末尾或OnInitMenuPopup前都行每个CJ60窗格创建完之后再跑保证布局数据已经加载。这是我从一次血泪经验里沉淀下来的做法。曾经一个内部工具被拖到副屏上再也拉不回来用户只能重装配置后来才意识到CJ60的SaveBarState存的是绝对坐标显示器变了坐标就失效。于是把自检和强制归位固定成了所有老工程的标配。给老界面做回归比换皮肤更值得先做。希望帮到你。本文还有配套的精品资源点击获取
返回列表