技术演进中的开发沉思-39 MFC系列:多重文件和多重视图

当年做MFC开发,记得有个开发场景既要看列表,又要看图片,还得能同时打开三个模块的文件。用到的便是 MFC 里 "多重文件与多重视图" 的应用场景。

那时候的 CRT 显示器还带着厚重的玻璃外壳,屏幕刷新率低得能看见滚动的波纹。我看着MFC宝典对这个场景的描述,看着桌上的我的学习笔记,联想到自己的开发习惯:"我开发的时候 ------ 总不能看MFC宝典时,就把我的学习笔记收起来吧?" 瞬间,我就明白了 MFC 设计的核心逻辑。

这些技术说起来就像我们办公桌上的章法。有人习惯一张桌子只摊开一个笔记本(SDI),有人喜欢把合同、报表、草稿分三个抽屉放(MDI);有人会把一页笔记折成两半,左边写数据、右边画图表(窗口拆分),这正是 MFC 在二十年前就想明白的事:技术的本质,是把现实里的 "方便" 搬进屏幕里

一、SDI 与 MDI

这里涉及到SDI和MDI两个概念。

SDI(单文档界面)像个极简主义的书桌 ------ 每次只能打开一个文件,窗口标题栏跟着文件名变,就像你捧着一本笔记本,翻页时封面总印着当前章节名。早年的记事本、画图软件都是这路数,MFC 里用 CView 和 CSingleDocTemplate 搭建,框架代码干净得像刚擦过的桌面。我记得为了练手,用 SDI 做个人通讯录,整个程序就像个随身笔记本,打开时只能看一个人的联系方式,虽然简单,但运行时内存占用只有 800KB,在当年 256MB 内存的机器上,轻快得像羽毛。

cpp 复制代码
// SDI框架注册(简化版)

CSingleDocTemplate* pDocTemplate;

pDocTemplate = new CSingleDocTemplate(

IDR_MAINFRAME,

RUNTIME_CLASS(CMyDoc), // 数据核心

RUNTIME_CLASS(CMainFrame),// 主窗口

RUNTIME_CLASS(CMyView) // 视图

);

AddDocTemplate(pDocTemplate);

MDI(多文档界面)则是带抽屉的书桌。主窗口像书桌的桌面,每个打开的文件都是抽屉里的文件夹 ------ 例如:你可以把 "设备台账" 和 "维修记录" 两个窗口叠着放,也能平铺对比。当年做监控系统时,我们用 MDI 同时显示三个车间的实时数据,就像同时拉开三个抽屉取资料,效率瞬间提上来。MDI适合多个模块比对的场景,那时候的界面风格单一,一个界面实现一个报表。现在流行的驾驶舱大屏,在某种意义上实现了多报表在同一个屏幕,在我们那个年代,只能用 MDI 窗口实现多个报表,如:调出当前参数、历史曲线和标准值对照表,同时比对满足对比数据,校准的场景,想想要是用 SDI,光切换窗口就得耽误半分钟。

它的注册代码多了个 "多文档标记",就像给书桌加了抽屉轨道:

cpp 复制代码
// MDI框架注册(简化版)

CMultiDocTemplate* pDocTemplate;

pDocTemplate = new CMultiDocTemplate(

IDR_MYTYPE,

RUNTIME_CLASS(CMyDoc),

RUNTIME_CLASS(CChildFrame), // 子窗口(抽屉)

RUNTIME_CLASS(CMyView)

);

AddDocTemplate(pDocTemplate);

这两种模式的核心区别,就在于 "能不能同时伺候多个文件"。SDI 适合专注一件事,MDI 适合多任务并行 ------ 就像写文章时,有人喜欢写完一篇再写下一篇,有人习惯同时开着提纲、正文、资料三个窗口。后来到了移动互联网时代,手机 APP 大多用 SDI 思路设计,因为小屏幕容不下太多窗口;但电脑端的 IDE 至今保留 MDI 影子,比如 VS 里能同时打开多个代码文件,本质上还是当年的抽屉逻辑。

二、多重视图

刚刚提到驾驶舱大屏,在2000年时,跟该场景很类似的就是。用户提需求:"同一份销售数据,我想有时看表格,有时看柱状图,最好能并排看。" 这正是多重视图(Multiple Views)要解决的问题。

那时候还没有现成的图表控件,柱状图得自己用 GDI 画。但凡我先做了个表格视图,用 CListCtrl 显示地区、销售额、同比增长这些数据;又做了个图表视图,在 OnDraw 函数里计算坐标,用 Rectangle 画柱子。切换视图时,用户总说像 "翻菜谱"------ 同样的食材,换种做法就有新味道。

它就像你手里的一份食材 ------ 同样的猪肉,能做成红烧肉(表格视图)、肉丸子(图表视图)、肉燥(明细视图)。在 MFC 里,这些 "视图" 都盯着同一个 "文档"(数据核心),就像三个厨师共用一盆肉馅,有人做丸子,有人炒肉片,但原料始终是同一批。

我当时用了 CFormView 做表格,用 CChartView 画图表,切换视图时就像转动餐桌上的转盘:"您刚看的是红烧肉(表格),现在转过来的是丸子(图表)。" 实现时只需给文档绑定多个视图模板,就像给食材准备不同的菜谱:

cpp 复制代码
// 给同一个文档绑定两个视图(简化思路)

// 视图1:表格视图

pDocTemplate1 = new CMultiDocTemplate(

IDR_TABLETYPE,

RUNTIME_CLASS(CSalesDoc),

RUNTIME_CLASS(CChildFrame),

RUNTIME_CLASS(CTableView)

);

AddDocTemplate(pDocTemplate1);

// 视图2:图表视图

pDocTemplate2 = new CMultiDocTemplate(

IDR_CHARTTYPE,

RUNTIME_CLASS(CSalesDoc),

RUNTIME_CLASS(CChildFrame),

RUNTIME_CLASS(CChartView)

);

AddDocTemplate(pDocTemplate2);

最妙的是数据同步 ------ 当表格里改了某个销售额,图表会自动刷新。这就像你往肉馅里加了盐,不管是丸子还是肉片,都会变咸。有次销售经理在表格里改了华东区的数值,抬头就看见图表里的柱子长高了,惊得直说 "这比 Excel 还方便"。这种 "数据与视图分离" 的思路,后来在 Web 开发里演变成了 MVVM,二十年前的 MFC 早就埋下了伏笔。

三、窗口拆分

窗口拆分是个特别 "实用主义" 的设计。就像你把笔记本对折,左边记笔记,右边画草图;或者折成三折,分别写待办、进度、总结。MFC 里分静态拆分和动态拆分,前者像装订死的笔记本(拆分方式固定),后者像活页本(随时调整拆分比例)。

第一次用拆分是做一个参数配置界面:左边列参数列表,右边显示参数说明。静态拆分用 CSplitterWnd,在 OnCreateClient 里 "划条线" 就行,就像用尺子在笔记本上画分隔线。那个系统上线后,由于那时候信息化才开始,很多客户都说 ------ 以前共工作看参数得来回翻手册,现在左边点一下,右边说明就出来了,真是很方便。

cpp 复制代码
// 静态拆分窗口(左右两栏)

CSplitterWnd m_wndSplitter;

BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext)

{

// 拆成1行2列

return m_wndSplitter.CreateStatic(this, 1, 2) &&

m_wndSplitter.CreateView(0, 0, RUNTIME_CLASS(CListView), CSize(200, 0), pContext) &&

m_wndSplitter.CreateView(0, 1, RUNTIME_CLASS(CEditView), CSize(0, 0), pContext);

}

动态拆分更灵活,就像用可移动的隔板分隔抽屉,用户可以自己拖到合适的位置。当年做日志分析工具时,用户经常把窗口拆成上下两部分,上面看实时日志,下面查历史记录,拖来拖去调整高度 ------ 这场景和现在手机上 "分屏看视频 + 聊微信" 简直一模一样。有个运维大哥特别有意思,他总把拆分线拖到最上面,只留一条缝看实时日志,说这样 "有新错误一眼就能看见"。

动态拆分需要在创建时指定可拆分的行数和列数,就像告诉系统 "这个笔记本能折几折":

cpp 复制代码
// 动态拆分窗口(可上下左右拆分)

m_wndSplitter.Create(this, 2, 2, CSize(10, 10), pContext);

后来接触到 Eclipse 的透视图,发现它的面板拆分逻辑和 MFC 如出一辙。技术迭代再快,那些让人用着顺手的设计,总会以不同形式留下来。

四、同源子窗口与多重文件

还有一个场景,有时候用户会说:"我想同时开两个窗口看同一个台账,一个看全部,一个看筛选后的。" 这就是同源子窗口 ------ 就像复印了一份文件,原件和复印件内容相同,但可以在复印件上圈画,原件不变。它们共用一个文档对象,却有各自的视图状态。

记得一个ERP客户提出这样的需求,一个窗口看所有库存,另一个窗口筛选出 "低于安全库存" 的商品。我在文档类里加了个筛选标记,视图根据这个标记决定显示内容。用户每次盘点时,就把两个窗口并排放在大屏幕上,一边对照一边补货,比以前拿着两张打印纸核对高效多了。

而多重文件则是另一回事,以ERP为例:同时打开 "设备 A 台账""设备 B 台账""设备 C 台账",就像桌上摊着三个不同的笔记本,各自独立,却能随时切换。MFC 用 CDocument 管理每个文件的数据,用 CMDIChildWnd 做每个窗口的 "容器",就像给每个笔记本配了个文件夹。有次给汽车厂做设备管理系统,维修工要同时查看三台机床的保养记录,多重文件模式让他们不用反复打开关闭文件,光这个细节就节省了不少时间。

这两种模式的区别,就在于 "数据是不是同一个源头"。同源子窗口是 "一个数据,多个视角",多重文件是 "多个数据,各自视角"------ 就像你既可以把一张照片放大看细节、缩小看整体(同源),也可以同时看今天和昨天拍的照片(多重文件)。

实现同源子窗口时,需要在文档类里维护视图列表,数据变化时通知所有视图更新:

cpp 复制代码
// 文档类中通知所有视图更新

void CMyDoc::UpdateAllViews(CView* pSender, LPARAM lHint, CObject* pHint)

{

POSITION pos = GetFirstViewPosition();

while (pos != NULL)

{

CView* pView = GetNextView(pos);

if (pView != pSender)

{

pView->OnUpdate(pSender, lHint, pHint);

}

}

}

而多重文件则依赖 MFC 的文档模板机制,每个文件对应一个文档实例,就像每个笔记本有自己的内容。

最后小结

现在回头看,MFC 的这些设计从不是炫技。当年在科研所,用户不会关心 "什么是 MDI",他们只在乎 "能不能同时打开三个文件";也不会问 "怎么拆分窗口",只希望 "左边看列表,右边看详情"。

所有的技术都源于要解决的场景,我想这些代码和框架背后,是一群开发者在想:"怎么让屏幕里的操作,像在桌上办公一样自然?" 就像 SDI 是怕人分心,MDI 是怕人来回翻找,多重视图是怕人重复录入 ------ 技术的温度,往往藏在这些 "怕麻烦" 的细节里。

后来到了 Web 时代,移动互联网时代,我发现 用 iframe 做多窗口,用 Vue 组件做视图切换,但骨子里的逻辑和当年的 MFC 没什么不同。毕竟,不管工具怎么变,人对 "方便" 的需求,从来没变过。就像现在我看到 React 组件时,觉得就是当年在MFC 里写视图类的方式,归根结底都是为了让数据以最合适的样子,出现在用户眼前。

相关推荐
丁劲犇17 分钟前
Qt Graphs 模块拟取代 charts 和 data visualization还有很长的路要走
c++·qt·qml·visualization·charts·graphs
mit6.8241 小时前
异步解决一切问题 |消息队列 |减少嵌套 |hadoop |rabbitmq |postsql
c++
老马啸西风1 小时前
windows wsl ubuntu 如何安装 maven
linux·运维·windows·ubuntu·docker·k8s·maven
我要成为c嘎嘎大王2 小时前
【C++】初识C++(2)
开发语言·c++
C语言小火车2 小时前
【面试题】大厂高压面经实录丨第二期
c语言·开发语言·数据结构·c++·算法
落羽的落羽2 小时前
【C++】红黑树,“红“与“黑”的较量
开发语言·c++
万能小锦鲤3 小时前
《计算机网络》实验报告一 常用网络命令
linux·windows·计算机网络·实验报告·常用网络命令·文档资源
街霸星星3 小时前
Windows11安装scoop及优化配置
windows
RickyWasYoung4 小时前
Matlab打开慢、加载慢的解决办法
开发语言·windows·matlab
十五年专注C++开发4 小时前
pugiXML:一个轻量级、高性能的 C++ XML 解析库
xml·c++·跨平台·cmake