CAE软件架构实战模式 / Practical Architecture Patterns for CAE Software
📅 创建时间:2026-07-13 🏷️ 标签:#Qt #软件架构 #插件系统 #CAE #设计模式 #QUndoStack 📚 前置知识:Model View Architecture、Plugin Architecture Patterns、Architecture
📋 本章目标
- 理解CAE软件的四层架构设计
- 掌握基于QPluginLoader的CAE插件系统设计
- 掌握QUndoStack实现撤销/重做的完整模式
- 掌握Action/Command注册模式构建菜单和工具栏
- 掌握Document-View多文档窗口架构
- 掌握CAE软件的配置管理与国际化方案
- 能够设计一个完整CAE软件的骨架架构
专题扩展
第1部分:CAE分层架构
1.1 四层架构模型
┌─────────────────────────────────────────────────────────────┐
│ CAE 软件四层架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 第1层:用户界面层 (UI Layer) │ │
│ │ QMainWindow / QMdiArea / QDockWidget │ │
│ │ QOpenGLWidget (3D视口) / QTreeView (零件树) │ │
│ │ QTableView (结果表格) / QPropertyBrowser (属性面板) │ │
│ │ ★ 只负责展示和交互,不包含业务逻辑 │
│ └─────────────────────────┬───────────────────────────┘ │
│ 信号槽 (松耦合,单向依赖) │
│ ┌─────────────────────────▼───────────────────────────┐ │
│ │ 第2层:业务逻辑层 (Business Logic) │ │
│ │ Document管理 (打开/保存/关闭项目) │ │
│ │ Command执行 (创建零件/施加载荷/划分网格) │ │
│ │ SolverManager (启动/监控/取消求解) │ │
│ │ PluginManager (发现/加载/卸载插件) │ │
│ │ ★ 核心协调层,不依赖具体算法 │
│ └─────────────────────────┬───────────────────────────┘ │
│ 直接函数调用 (同步,高性能) │
│ ┌─────────────────────────▼───────────────────────────┐ │
│ │ 第3层:核心算法层 (Core Algorithms) │ │
│ │ 几何导入 (STEP/IGES/Nastran BDF解析) │ │
│ │ 网格生成 (Delaunay/Advancing Front/wrapper TetGen) │ │
│ │ 求解器接口 (QProcess wrapper + 输入输出解析) │ │
│ │ 结果后处理 (等值面/切面/流线/动画) │ │
│ │ ★ 纯计算,无GUI依赖 │
│ └─────────────────────────┬───────────────────────────┘ │
│ │
│ ┌─────────────────────────▼───────────────────────────┐ │
│ │ 第4层:数据持久层 (Data Layer) │ │
│ │ QFile/QDataStream/QTextStream (二进制/文本I/O) │ │
│ │ QSettings (项目配置) / JSON/XML/HDF5 │ │
│ │ ★ 只负责存储和读取,不关心数据含义 │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 依赖方向: UI → Logic → Algorithm → Data (单向) │
│ 上层知道下层,下层不知道上层 │
│ │
└─────────────────────────────────────────────────────────────┘1.2 层间通信机制
// ===== UI <-> Logic:信号槽(松耦合)=====
// Logic层发信号,UI层连接监听
class MeshManager : public QObject {
Q_OBJECT
signals:
void meshLoaded(int nodeCount, int elementCount);
void meshLoadFailed(const QString& error);
};
// ===== Logic <-> Algorithm:直接函数调用(高性能)=====
// Algorithm层提供纯C++接口
class TetMesher {
public:
MeshResult tetrahedralize(const SurfaceMesh& input,
const MeshingParams& params);
};
// ===== 依赖注入:Logic持有Algorithm的接口指针 =====
class MeshManager : public QObject {
std::unique_ptr<IMesher> m_mesher; // 依赖接口,不依赖实现
public:
void setMesher(std::unique_ptr<IMesher> mesher) {
m_mesher = std::move(mesher);
}
};第2部分:插件系统设计
2.1 CAE插件接口定义
// ===== 所有CAE插件的基接口 =====
struct CaePluginInfo {
QString name;
QString version;
QString description;
QString author;
};
// ===== 导入器插件接口 =====
class IModelImporter {
public:
virtual ~IModelImporter() = default;
virtual CaePluginInfo pluginInfo() const = 0;
virtual QStringList supportedExtensions() const = 0;
virtual bool importFile(const QString& path, MeshData& out) = 0;
};
// 导出宏
#define CAE_IMPORTER_IID "com.fastcae.ImporterInterface/1.0"
Q_DECLARE_INTERFACE(IModelImporter, CAE_IMPORTER_IID)
// ===== 求解器插件接口 =====
class ISolver {
public:
virtual ~ISolver() = default;
virtual CaePluginInfo pluginInfo() const = 0;
virtual QStringList inputFileExtensions() const = 0;
virtual QProcess* createProcess(const SolverInput& input) = 0;
virtual SolverResult parseOutput(const QString& outputPath) = 0;
};
#define CAE_SOLVER_IID "com.fastcae.SolverInterface/1.0"
Q_DECLARE_INTERFACE(ISolver, CAE_SOLVER_IID)2.2 插件实现(以导入器为例)
// ===== 插件项目中的实现 =====
class NastranBdfImporter : public QObject, public IModelImporter {
Q_OBJECT
Q_PLUGIN_METADATA(IID CAE_IMPORTER_IID FILE "nastran_importer.json")
Q_INTERFACES(IModelImporter)
public:
CaePluginInfo pluginInfo() const override {
return {"Nastran BDF Importer", "1.0",
"Import Nastran bulk data files", "FastCAE Team"};
}
QStringList supportedExtensions() const override {
return {"bdf", "dat", "nas"};
}
bool importFile(const QString& path, MeshData& out) override {
// 解析Nastran BDF格式...
return true;
}
};2.3 PluginManager——插件发现与加载
class CaePluginManager : public QObject {
Q_OBJECT
public:
void loadAllPlugins(const QString& pluginDir) {
QDir dir(pluginDir);
for (const QString& file : dir.entryList(QDir::Files)) {
QPluginLoader loader(dir.absoluteFilePath(file));
QObject* instance = loader.instance();
if (auto* importer = qobject_cast<IModelImporter*>(instance)) {
m_importers.append(importer);
qDebug() << "Loaded importer:" << importer->pluginInfo().name;
}
else if (auto* solver = qobject_cast<ISolver*>(instance)) {
m_solvers.append(solver);
qDebug() << "Loaded solver:" << solver->pluginInfo().name;
}
}
}
QList<IModelImporter*> importers() const { return m_importers; }
QList<ISolver*> solvers() const { return m_solvers; }
private:
QList<IModelImporter*> m_importers;
QList<ISolver*> m_solvers;
};第3部分:命令模式与撤销/重做
3.1 QUndoCommand基础
// ===== 每个CAE操作封装为一个QUndoCommand =====
class CreatePartCommand : public QUndoCommand {
public:
CreatePartCommand(const QString& name, PartTreeModel* model,
const QModelIndex& parent)
: m_name(name), m_model(model), m_parent(parent)
{
setText(QString("Create Part '%1'").arg(name));
}
void undo() override {
// 删除最后添加的Part
int lastRow = m_model->rowCount(m_parent) - 1;
m_model->removePart(lastRow, m_parent);
}
void redo() override {
m_model->addPart(m_name, m_parent);
}
private:
QString m_name;
PartTreeModel* m_model;
QModelIndex m_parent;
};
// ===== 使用 =====
QUndoStack* undoStack = new QUndoStack(this);
// 创建操作并推入栈
undoStack->push(new CreatePartCommand("Wing", model, rootIndex));
// 撤销/重做通过快捷键
QAction* undoAction = undoStack->createUndoAction(this, "&Undo");
undoAction->setShortcut(QKeySequence::Undo); // Ctrl+Z
QAction* redoAction = undoStack->createRedoAction(this, "&Redo");
redoAction->setShortcut(QKeySequence::Redo); // Ctrl+Y3.2 宏命令——组合多个操作为一步
// ===== 划分网格是一个"宏命令" =====
class MeshPartCommand : public QUndoCommand {
public:
MeshPartCommand(Part* part, const MeshingParams& params)
: QUndoCommand("Mesh Part")
{
// 子命令构成一个不可分割的整体
new CreateMeshCommand(part, &m_meshData, this); // child 1
new SetMeshParamsCommand(&m_meshData, params, this); // child 2
new GenerateMeshCommand(&m_meshData, this); // child 3
}
// QUndoCommand的child自动管理:
// undo时逆序undo每个child
// redo时正序redo每个child
// 只需要实现顶层命令
void undo() override { /* QUndoCommand自动处理children */ }
void redo() override { /* QUndoCommand自动处理children */ }
private:
MeshData m_meshData;
};3.3 视图同步——undo/redo后通知所有视图
// 通过信号通知视图刷新
class Document : public QObject {
Q_OBJECT
public:
void executeCommand(QUndoCommand* cmd) {
m_undoStack.push(cmd);
// undo/redo自动触发 indexChanged 信号
}
signals:
void dataChanged(); // 数据变更了,视图们刷新吧
};
// 在MainWindow中连接
connect(document, &Document::dataChanged, viewport3D,
QOverload<>::of(&QOpenGLWidget::update));
connect(document, &Document::dataChanged, treeView,
[this]() { partTreeModel->refresh(); });第4部分:Action与菜单/工具栏
4.1 集中注册模式
class ActionManager : public QObject {
Q_OBJECT
public:
enum ActionId {
File_New, File_Open, File_Save, File_SaveAs,
Edit_Undo, Edit_Redo,
Mesh_Tetrahedral, Mesh_Hexahedral,
Solver_Run, Solver_Cancel,
View_ZoomFit, View_Wireframe, View_Shaded
};
// ★ 集中注册所有Action
void registerAllActions(QWidget* parent) {
registerAction(File_New, "&New", QKeySequence::New,
parent, []{ /* new project */ });
registerAction(File_Open, "&Open", QKeySequence::Open,
parent, []{ /* open project */ });
// ... 注册所有action
}
QAction* action(ActionId id) const { return m_actions.value(id); }
private:
QHash<ActionId, QAction*> m_actions;
};
// 插件也可以注册Action
void ImporterPlugin::registerActions(ActionManager* mgr) {
mgr->registerAction(ActionManager::File_Import_Nastran,
"Import &Nastran...", {}, this,
[this]{ showImportDialog(); });
}4.2 动态菜单构建
void MainWindow::buildMenuBar() {
QMenu* fileMenu = menuBar()->addMenu("&File");
fileMenu->addAction(m_actionMgr->action(ActionManager::File_New));
fileMenu->addAction(m_actionMgr->action(ActionManager::File_Open));
// 动态子菜单——所有已加载的导入器
QMenu* importMenu = fileMenu->addMenu("&Import");
for (IModelImporter* importer : m_pluginMgr->importers()) {
QAction* act = importMenu->addAction(
importer->pluginInfo().name);
connect(act, &QAction::triggered, this, [importer, this]() {
importWithPlugin(importer);
});
}
fileMenu->addSeparator();
fileMenu->addAction(m_actionMgr->action(ActionManager::File_Save));
}第5部分:Document-View多文档
5.1 QMdiArea多文档界面
class MainWindow : public QMainWindow {
public:
MainWindow() {
m_mdiArea = new QMdiArea(this);
setCentralWidget(m_mdiArea);
}
void openProject(const QString& path) {
auto* doc = new ModelDocument(path);
// 为这个文档创建一个子窗口
auto* subWindow = new QMdiSubWindow();
subWindow->setWidget(createViewport(doc)); // 3D视口
subWindow->setWindowTitle(QFileInfo(path).fileName());
m_mdiArea->addSubWindow(subWindow);
subWindow->show();
// Document拥有数据,SubWindow拥有视图
// 关闭SubWindow → 检查Document是否dirty → 提示保存
connect(subWindow, &QMdiSubWindow::aboutToActivate,
this, &MainWindow::onDocumentActivated);
}
private:
QMdiArea* m_mdiArea;
};第6部分:配置与国际化
6.1 分层配置系统
// 三层配置:默认值 → 用户设置 → 项目设置
class ConfigManager {
public:
QVariant value(const QString& key) const {
// 优先级:项目 > 用户 > 默认
if (m_projectSettings.contains(key))
return m_projectSettings[key];
if (m_userSettings.contains(key))
return m_userSettings[key];
return m_defaults.value(key);
}
private:
QSettings m_userSettings; // 用户级
QVariantMap m_projectSettings; // 项目级(存在工程文件中)
static const QHash<QString, QVariant> m_defaults; // 默认值
};6.2 QTranslator国际化
void MainWindow::switchLanguage(const QString& lang) {
static QTranslator translator;
qApp->removeTranslator(&translator);
if (translator.load(QString("fastcae_%1.qm").arg(lang),
":/translations")) {
qApp->installTranslator(&translator);
}
// UI自动切换语言(所有 tr() 调用重新翻译)
}核心总结
总结1:四层架构的依赖方向
UI → Logic → Algorithm → Data(单向!)
每一层只依赖下一层的接口,不知道具体实现
Logic是"总指挥":协调UI的请求,调用Algorithm,管理Data总结2:QUndoStack的标准工作流
用户操作 → new QUndoCommand → undoStack.push(cmd)
→ cmd->redo() 执行
→ undoStack indexChanged 信号
→ 所有View刷新
Ctrl+Z → undoStack.undo() → cmd->undo()
→ 反转操作 → View刷新总结3:插件系统的关键接口
CAE 插件可按 Importer/Exporter/Solver/Mesher/PostProcessor/Material 等角色分类。纯虚接口、Q_DECLARE_INTERFACE、QPluginLoader 和 qobject_cast 负责接口发现,但不自动构成长久稳定 ABI;Qt/编译器/CRT、IID/元数据版本、所有权和卸载协议必须另行约束,完整规则见插件工程知识区。
章节测试
测试1:分层架构
在四层架构中,SolverManager属于哪一层? A. UI层 B. 业务逻辑层 C. 核心算法层 D. 数据持久层
测试2:插件系统
QPluginLoader加载插件后,如何判断它实现了哪个接口? A. 检查文件名 B. 调用instance()获取QObject*,然后用qobject_cast尝试转换 C. 读取metadata.json D. 检查Q_PLUGIN_METADATA宏
测试3:QUndoStack
QUndoCommand的子命令(child commands)被undo时,顺序是什么? A. 正序(先加的先undo) B. 逆序(后加的先undo) C. 同时undo D. 随机顺序
参考答案
测试1答案
答案:B。SolverManager负责启动/监控/取消求解——这是协调逻辑,不涉及具体求解算法(那是第3层的事),也不直接显示UI(那是第1层的事)。
测试2答案
答案:B。loader.instance()返回QObject*,然后用qobject_cast<IModelImporter*>(instance)逐个尝试所有已知接口类型。非空即表示实现了该接口。
测试3答案
答案:B。逆序——后加的先undo,确保状态回滚的一致性。这和QObject对象树删除children的顺序一致。
相关笔记
- Model View Architecture - Model/View(Document-View的基础)
- Plugin Architecture Patterns - 通用插件设计模式
- Industrial Plugin Case Study - FreeCAD/ParaView插件分析
- Architecture - CAE软件架构详解
下一步学习
- [ ] 阅读 07 - 3D可视化与交互
- [ ] 阅读 08 - 实战案例:FEA前后处理器
- [ ] 在你项目中尝试实现QUndoStack的撤销/重做
学习状态:🟡 开始学习
CAE 依赖方向图
UI widgets/controllers
|
Qt Model/View adapters Render adapters
| |
Application commands/use cases --+
|
Domain Document/entities/results
|
Ports: storage / solver / importer / plugin
|
Infrastructure implementations依赖指向领域和用例,领域不包含 QWidget、线程池或 GPU handle。Qt signal 是外层通知机制,不应成为领域唯一调用协议。
修改事务
UI intent -> Command validate
-> prepare delta
-> commit domain invariant
-> revision/dirty/undo update
-> publish model/render notifications通知在提交后发出;失败前不留下半改状态。异步求解只读取 snapshot,结果按 revision 提交。
面试连续追问
问:MVC/MVVM 名字为何不够指导 CAE 架构? 答:还需明确 Document 所有权、长任务、版本化文件、渲染资源和插件 ABI 的边界与关闭顺序。
问:Service Locator 有何问题? 答:依赖隐藏、测试顺序和全局生命周期不清;组合根显式注入更可审查。
问:领域对象应否继承 QObject? 答:不是绝对禁止,但会耦合线程/所有权/元对象;长期可移植领域通常保持普通 C++ 值,再由 Qt adapter 通知。
自测与答案
- 谁拥有 Document? 答:应用/窗口级组合根明确拥有,views/controllers 观察或持受控引用。
- solver 如何避免改 GUI 状态? 答:读取不可变 snapshot,生成独立结果,GUI/应用线程验证 revision 后提交。
- 插件接口放在哪层? 答:基础设施边界,通过窄 port/capability 适配用例,不让插件任意摸内部对象图。