3、文档视图架构:文档类(CDocument)设计、视图类(CView)设计、文档模板与多文档支持、文档序列化
好,咱们今天聊聊 EPS 软件里最核心的骨架——文档视图架构。
说实话,我早年刚接触这个架构时,觉得它挺绕的。文档、视图、模板……一堆概念。但后来做项目多了,才明白这套设计有多巧妙。它把数据和显示彻底解耦了,你改数据不影响界面,换界面也不丢数据。
3.1 文档类(CDocument)设计
文档类,说白了就是数据的管家。它不关心数据怎么画出来,只关心数据怎么存、怎么改。
核心职责:
- 管理应用数据(比如 EPS 里的点、线、面)
- 提供数据增删改查的接口
- 负责数据的序列化(存盘和读盘)
- 通知视图数据发生了变化
我个人习惯,在设计文档类时,会先想清楚数据模型。比如一个简单的绘图文档:
class CEpsDocument : public CDocument
{
public:
// 数据操作
void AddShape(CEpsShape* pShape);
void RemoveShape(int nIndex);
CEpsShape* GetShape(int nIndex) const;
int GetShapeCount() const;
// 序列化
virtual void Serialize(CArchive& ar);
// 通知视图更新
void UpdateAllViews();
private:
CObArray m_shapes; // 存储所有图形对象
};
这里有个坑,我踩过。很多人喜欢把视图相关的代码塞进文档类,比如直接调用某个控件的刷新。千万别这么干!文档类应该对视图一无所知。它只负责说「我变了」,至于谁来看、怎么刷新,那是视图的事。
3.2 视图类(CView)设计
视图类,就是数据的展示窗口。它从文档拿数据,然后画出来。用户的所有交互,比如鼠标点击、拖拽,也都在视图里处理。
核心职责:
- 获取文档数据并渲染
- 处理用户输入(鼠标、键盘)
- 将用户操作转化为文档数据的修改
- 响应文档的更新通知
我举个例子,一个 EPS 视图的典型实现:
class CEpsView : public CView
{
public:
// 获取关联的文档
CEpsDocument* GetDocument() const;
// 绘制所有图形
virtual void OnDraw(CDC* pDC);
// 鼠标交互
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
// 文档更新通知
virtual void OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint);
private:
void DrawShape(CDC* pDC, CEpsShape* pShape);
};
你想想看,视图和文档之间怎么通信?其实就两条路:
- 文档→视图:文档调用 UpdateAllViews,视图的 OnUpdate 被触发
- 视图→文档:视图通过 GetDocument 拿到文档指针,直接调用文档的接口改数据
嗯,这里要注意。视图修改数据后,一定要记得调用文档的 SetModifiedFlag,告诉框架「文档被改过了」。不然退出时可能不会提示保存。
3.3 文档模板与多文档支持
文档模板,听起来高大上,其实就是一个工厂。它负责把文档、视图、框架窗口这三样东西绑定在一起。
MFC 里,文档模板分两种:
| 模板类型 | 适用场景 | 特点 |
|---|---|---|
| CSingleDocTemplate | 单文档应用(SDI) | 一次只能打开一个文档 |
| CMultiDocTemplate | 多文档应用(MDI) | 可以同时打开多个文档 |
我记得在做第一个 EPS 项目时,用的是多文档架构。用户希望同时打开多个图纸,互相参考。MDI 正好满足这个需求。
注册文档模板的代码通常放在 InitInstance 里:
BOOL CEpsApp::InitInstance()
{
// 注册文档模板
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_EpsTYPE,
RUNTIME_CLASS(CEpsDocument),
RUNTIME_CLASS(CChildFrame), // 子框架窗口
RUNTIME_CLASS(CEpsView));
AddDocTemplate(pDocTemplate);
// 其他初始化...
return TRUE;
}
这里有个设计要点:每个文档模板对应一种文档类型。如果你的 EPS 软件支持多种格式(比如 .eps 和 .dxf),可以注册多个模板,每个模板关联不同的文档类和视图类。
3.4 文档序列化
序列化,说白了就是「存盘」和「读盘」。这是文档类最重要的能力之一。
MFC 的序列化机制基于 CArchive 类。它把对象的状态写入文件,或者从文件恢复。我当年第一次写序列化时,觉得挺神奇的——一个对象怎么就能变成一堆字节呢?
一个典型的序列化实现:
void CEpsDocument::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// 保存数据
ar << m_shapes.GetSize(); // 先存数量
for (int i = 0; i < m_shapes.GetSize(); i++)
{
CEpsShape* pShape = m_shapes[i];
ar << pShape->GetType(); // 存类型
pShape->Serialize(ar); // 存具体数据
}
}
else
{
// 加载数据
int nCount;
ar >> nCount;
for (int i = 0; i < nCount; i++)
{
int nType;
ar >> nType;
CEpsShape* pShape = CreateShapeByType(nType);
pShape->Serialize(ar);
m_shapes.Add(pShape);
}
}
}
写序列化时,有几点要特别注意:
- 版本兼容:文件格式一定要带版本号。我吃过这个亏,旧版本软件打不开新版本的文件,用户直接炸了。
- 指针处理:如果文档里有对象指针,序列化时要小心。MFC 的 CArchive 支持指针序列化,但需要对象支持 CRuntimeClass。
- 性能优化:大数据量时,逐对象序列化可能很慢。可以考虑用内存映射文件或者分块存储。
最后,说个我自己的习惯。我会在文档类里加一个版本号宏:
#define EPS_FILE_VERSION 0x0100 // 1.0 版本
void CEpsDocument::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar << EPS_FILE_VERSION; // 先写版本号
// 然后写数据...
}
else
{
WORD wVersion;
ar >> wVersion;
// 根据版本号做兼容处理
// ...
}
}
这样,以后升级文件格式时,老版本也能优雅地提示「文件版本过高,请升级软件」。而不是直接崩溃。
好了,文档视图架构的核心就这些。说白了,就是一套「数据-显示-窗口」的三角关系。理解了这个,EPS 软件的骨架你就拿下了。