# 专题说明
本文介绍利用 Qt 进行开发。
# Qt 界面开发教程
Sysplorer 依赖于 Qt 对界面进行开发,因此推荐用户对 APP 的界面开发也使用 Qt 程序,以下简单介绍 Qt 相关教程,更详细教程可在网上进行学习。
Qt 是一个跨平台的 C++ 开发库,主要用来开发图形用户界面(Graphical User Interface,GUI)程序,Qt 还存在 Python、Ruby、Perl 等脚本语言的绑定, 也就是说可以使用脚本语言开发基于 Qt 的程序。下面主要介绍 C++ 平台利用 Qt 进行界面开发。
# VS2017 中配置 Qt5.14教程 (opens new window)
# Qt 界面设计教程
基于 VS2017 新建一个 Qt Application 工程:

双击打开对应的
\*.ui文件,进入designer.exe程序如下:
软件主要界面介绍,详细介绍可查看相关官方教程。

创建一个用于设计的窗口:初次打开软件,应该已经直接创建了,若不存在可在 File>New 中打开,或者工具栏第一个,简略介绍如下:
直接选第一个,单击 Create,创建完成之后:

可将旁边的其他控件拖拽进主窗口中,进行界面的开发和设计,常用控件及介绍如下:
- 标签(Label)—— QLabel(PyQt5)
- 按钮(Button)—— QPushButton
- 行编辑(lineEdit)—— QLineEdit
- 组合框(ComboBox)—— QComboBox
- 复选框(CheckBox)—— QCheckBox

利用 Qt 开发完成的弹簧阻尼 APP 设计如下:

# Qt 信号和槽
什么是信号和槽?
信号和槽是用于对象之间的通信,它是 Qt 的核心机制,在 Qt 编程中有着广泛的应用。
举个例子,在一个十字路口,信号灯变成了绿色,对面的汽车看到后就启动了。信号灯就是发送信号的对象,绿灯亮是它发送的信号(signal),汽车是接收对象,汽车行驶是汽车对信号的响应,也叫槽(slot)。
再举一个例子,比如在一个主窗口内有一个关闭按钮,如果单击这个按钮,窗口就会关闭,那么关闭按钮是发送信号的对象,它发送的信号是单击,接收信号的对象是窗口,响应信号的槽是关闭窗口。

信号和槽代码实例
- 主要通过 connect + 宏的方式进行通信连接
connect(发送对象,信号,接收对象,槽函数),其中发送信号和槽函数需要用 SIGNAL() 和 SLOT() 来进行声明。
connect 函数声明如下:
[static] QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)比如单击按钮关闭窗口的例子,代码可以这样写:
connect(ui->pushButton, SIGNAL(clicked()), this, SLOT(close()));如果想自定义槽函数,需要先将槽函数的声明添加到类的 slots 中。比如我们对一个 QLineEdit 控件添加一个接收 textEdited 信号的槽函数 onTextEdited。
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: void onTextEdited(QString); private: Ui::MainWindow *ui; };然后实现函数,并用 connect 与信号连接。
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); connect(ui->pushButton, SIGNAL(clicked()), this, SLOT(close())); connect(ui->lineEdit, SIGNAL(textEdited(QString)), this, SLOT(onTextEdited(QString))); } void MainWindow::onTextEdited(QString s) { qDebug() << s; }
使用 Qt Creator 界面添加信号的槽函数
另外一种方式不需要使用 connect 函数,可以通过 Qt Creator 界面来完成发送信号和槽函数的连接,比如我们右击一个按钮,然后选择转到槽:
选择信号,我们单击 QAbstractButton 的 clicked() 信号,表示按钮被右击:
接下来,Qt Creator 会自动为我们生成如下代码,首先是槽函数的声明:
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: void on_pushButton_clicked(); private: Ui::MainWindow *ui; };然后是槽函数的实现:
void MainWindow::on_pushButton_clicked() { }使用这种方法我们不需要使用 connect 函数将信号与槽函数做连接。 这里槽函数的命名有一定的规则,一般是 on_objectname_signal 这样来命名的。这种方法优点是减少了自己手动敲代码的工作量,缺点是究竟有哪些信号与槽函数做了连接不易被发现,没有 connect 函数看起来直观。
总结:Qt 使用 QDesigner 进行界面设计开发,并利用 Qt 语言结合 C++ 特性以及相关的信号和槽函数机制对界面相关事件进行处理,详细开发教程可通过网上资源 QT 学习教程 (opens new window)进行学习。
# 开发工程构建
基于 VS2017 版本进行开发,请确保已安装 VS2017 Qt 插件,并安装 Qt5.14.2 等相关开发环境。
# 新建工程
基于 VS2017 新建一个 Qt Application 工程,命名为 MassSpringDamperApp:
# 输出目录配置
将输出目录配置到安装的 SDK 路径的 bin 目录:
$(MWBin)$(Configuration)将 MWBin 换为实际 SDK 对应路径:
SDK安装路径\bin\win_msvc2017x64
$(MWBin)、$(MoKeyerface)、$(MWInclude)需在电脑中的系统环境变量中将 SDK 安装路径配置上:

# 附加包含目录配置
包含目录添加 SDK 的 include 与 interface 目录:
.\GeneratedFiles . $(QTDIR)\include .\GeneratedFiles\$(ConfigurationName) $(QTDIR)\include\QtCore $(QTDIR)\include\QtGui $(QTDIR)\include\QtWidgets $(MWInclude) $(MWInclude)\boost161 $(MoKeyerface) $(MoKeyerface)\modelica_services $(MoKeyerface)\common_kitsMWInclude ——
SDK安装路径/includeMoKeyerface ——
SDK安装路径/interface

# 链接库依赖项配置
依赖库目录添加 SDK 的 bin\lib 与文件输出目录
$(OutDir) $(QTDIR)\lib $(MWBin)\lib

# 附加依赖项配置
Debug 下的附加依赖项设置为:
qtmaind.lib Qt5Cored.lib Qt5Guid.lib Qt5Widgetsd.lib mw_develop_d.lib modelica_compiler_d.lib mw_graphics_view_d.lib mw_class_manager_d.lib mw_global_d.lib mw_help_d.lib mw_sim_inst_d.lib mw_sim_plot_d.lib model_var_tree_d.libRelease 下的附件依赖项设置为:
qtmain.lib Qt5Core.lib Qt5Gui.lib Qt5Widgets.lib mw_develop.lib modelica_compiler.lib mw_graphics_view.lib mw_class_manager.lib mw_global.lib mw_help.lib mw_sim_inst.lib mw_sim_plot.lib model_var_tree.lib

# 内置案例库
SDK 安装完成后,SDK 安装目录下的 examples 存在以下案例,使用 Visual Stiudio 2017 打开Examples_vs2017.sln可查看并运行对应案例的源码,文件目录如下所示:
案例中的代码运行环境需满足以下配置:
| 类型 | 环境 |
|---|---|
| 操作系统 | Win7 SP1 及以上版本 |
| Qt | Qt5.14.2 x86 或 x64 版本 |
| IDE | Microsoft Visual Studio 2017 |
| IDE插件 | VS2017 的 Qt 开发插件 |
| 编译环境 | Release x64 或 Debug x64 |
# MassSpringDamperApp
质量-弹簧-阻尼仿真模型软件,软件界面如下所示,代码详解请参见质量弹簧阻尼 APP 介绍。
# MwBatchSimPlugin
批量仿真插件开发,软件界面如下所示,代码详解请参见批量仿真 APP。
# MwVehicleApp
车辆综合模型软件,软件界面如下所示,代码详解请参见车辆仿真 APP。
# Step Forward Demo
本示例主要展示单步仿真,包括:
- 单步模式启动仿真、控制仿真
- 仿真结束显示变量曲线
MWORKS.SDK 曲线窗口类定义为 MwSimPlotWindow,本示例 CustomPlotWindow 继承自该类。
编译 PID_Controller 模型成功后,创建 MwSimData 并设置仿真模式为 Sim_InteractiveStepMode,然后启动仿真。
void CustomPlotWindow::SlotCompileFinish()
{
compileThread->quit();
this->setEnabled(true);
QString sim_dir = QFileInfo(QApplication::applicationDirPath() + "\\..\\..\\..\\examples\\StepForward\\Sim").absoluteFilePath();
QString sim_result_path = QFileInfo(sim_dir + "\\Result.msr").absoluteFilePath();
simData = new MwSimData(L"PID_Controller", sim_result_path.toStdWString(), sim_dir.toStdWString());
if (!simData->InitializeSimInst())
{
simData = nullptr;
return;
}
simCtrl->RebindSimData(simData);
MwExperimentData exp_data;
exp_data.stopTime = 10.0;
exp_data.intervalLength = 1;
exp_data.numberOfIntervals = 10;
simData->ApplyExperimentData(&exp_data);
simCtrl->StartSimulate(MwSimControl::Sim_InteractiveStepMode);
progressBar->setValue(0);
labelTime->setText("0.0s");
}
单步模式下,每一步都会触发 SigSimStep 信号,并暂停仿真。 需调用 SimulateNextStep 恢复仿真。
void CustomPlotWindow::SlotSimStep(double cur_time)
{
progressBar->setValue(cur_time * 10);
labelTime->setText(QString::number(cur_time) + "s");
this->setEnabled(true);
}
仿真结束后,调用 MwSimPlotWindow 的接口 AddCurveToCurrentView 将变量曲线添加到曲线视图中。
void CustomPlotWindow::SlotSimStopped(int exit_code)
{
this->setEnabled(true);
progressBar->reset();
labelTime->setText("0.0s");
this->AddCurveToCurrentView("PI.y", simData);
actionSimStep->setEnabled(false);
}
# Component Parameter Panel Demo
本示例主要展示自定义修改组件参数面板,包括:
- 调整参数面板的列顺序
- 组件参数的显示与隐藏
MWORKS.SDK 组件参数面板的类定义为 MwModelParameterTabWidget,本示例 CustomParameterTabWidget 继承自该类。
MWORKS 组件参数面板的默认列顺序为:参数名称、参数值、参数单位、参数说明, 本示例将其调整为:参数说明、参数值、参数单位、参数名称。
此外,函数 IsNeedToDisplay() 让用户决定每个参数的显示或隐藏。系统默认是每个参数都显示。
CustomParameterTabWidget::CustomParameterTabWidget(MwMoGraphicsViewController* mo_controller,
MwParamEditMode pem, QWidget* parent)
: MwModelParameterTabWidget(mo_controller, pem, parent)
{
//调整列顺序
this->SetColumnOrder(3, 1, 2, 0);
}
CustomParameterTabWidget::~CustomParameterTabWidget()
{
}
bool CustomParameterTabWidget::IsNeedToDisplay(const MwParamInfo& param) const
{
if (iModelKey == 0)
{
return true;
}
MwClassManager *class_mgr = MainWindow::GetInstance()->GetClassManager();
//模型全名
std::string type_name = class_mgr->GetMoHandler()->GetFullnameProp(iModelKey);
//隐藏参数集
QList<std::string> hidden_params;
hidden_params << "pi";
if (hidden_params.count(param.ident))
{
return false;
}
return true;
}
# GraphicsViewDropEvent
本示例主要展示,将模型树上的组件拖拽到模型视图中:
主要通过在 TreeView 中重写startDrag函数,将拖拽的组件全名字符串与image/modelica-type进行绑定,设置到 QmimeData 数据中,主要代码如下:
void ClassBrowserTreeWidget::startDrag(Qt::DropActions supported_actions)
{
MoKey selected_key = GetSelectedKey();
if (selected_key == 0)
{
return;
}
QStandardItem* item = treeModel->itemFromIndex(currentIndex());
QModelIndexList index_list;
index_list << currentIndex();
QMimeData* mime_data = treeModel->mimeData(index_list);
QByteArray item_data;
QDataStream dataStream(&item_data, QIODevice::WriteOnly);
QString selected_name = QString::fromStdString(ClassMgrPtr->GetMoHandler()->GetFullnameProp(selected_key));
dataStream << selected_name;
mime_data->setData("image/modelica-type", item_data);
QDrag *drag = new QDrag(this);
drag->setMimeData(mime_data);
//Drag时图标
{
QIcon icon = item->icon();
QPixmap pixmap;
if (icon.isNull())
{
//获得默认图标
pixmap = QPixmap();
}
else
{
pixmap = icon.pixmap(QSize(50, 50));
}
drag->setPixmap(pixmap);
qreal adjust = 25;
drag->setHotSpot(QPoint(adjust, adjust));
}
drag->exec(supported_actions);
}
# License 配置
为保证大型模型能够正常仿真,及相关功能能够正常使用,在调用 SDK API 前需提前设置 License 文件,SDK 提供 MwLicenseService 类用于设置相关 License,该类中提供了两种设置方法,通过有 UI 方式弹出 License 设置对话框或无 UI 方式接口配置 License。
MwLicenseService 类初始化:
//license
MwLicenseService* pLicenseService = new MwLicenseService(nullptr);
pLicenseService->Initialize();
弹出 License 设置对话框:
pLicenseService->StartupLicenseSetDialog();
接口方式配置 License,可以设置单机版 License 文件路径,也可以设置网络版 License 的 IP 地址和端口号:
bool load_license_success = pLicenseService->SetLicenseFilePath(L"E:\\File\\MWorks.Sysplorer\\License\\mworks_sdk.lic"); bool load_license_success = pLicenseService->SetLicenseIPAddress("172.16.1.18", "27000");
# MwGraphicsView 响应机制
# 模块结构
MwGraphicsView 模块是 Sysplorer.SDK 的图形模块,同时也是核心模块,为系统提供图形化建模和文本建模能力,其结构如下图所示:
整个 MwGraphicsView 模块使用 MVC(Model、View、Controller)设计模式。
图形模块中的基本图元充当 MVC 中的“M”(Model),基本实体主要包括:
- 线段
- 矩形
- 椭圆
- 多边形
- 文字
- 图片,支持多种格式(bmp、jpg、png、gif 等)
具体结构如下:
文本视图、图标视图和图形视图三种视图充当 MVC 中的“V”(View),相同的 Model 在不同 View 中有不同的表现形式,同样的组件在图标视图中表示图标,在组件视图表示组件,在文本视图中表示 Modelica 文本;图形视图(MwModelicaGraphicsView)是系统可视化建模最主要的场所,主要负责显示和编辑模型的组件图形数据:
- 显示图形元素;
- 交互添加图形元素;
- 编辑图形元素:拣选,拖动,旋转,翻转,对齐,调整显示顺序等;
- 进入组件模式,显示组件类型对应的模型视图。
图形视图控制器(MwMoGraphicsViewController)充当 MVC 中的“C”(Controller),主要负责响应底层模型变化,刷新图形视图状态并同步转发模型变化信号,以同步更新界面的其他组件状态。
# 运行机制
图形视图控制器(MwMoGraphicsViewController)定义了一组视图事件(MoGvEvent),这组事件包含:
- 非法事件:None
- 图元选择变化:SelectChanged
- 视图中组件层次变化:ComponentLevelChanged
- 视图类型变化:ViewChanged
- 模型窗口变化:MoWindowChanged
- 活动窗口变化:ActivateWindowChanged
- 模型数据变化:ModelDataChanged
- 模型添加信号:ModelAppended
- 模型移除信号:ModelRemoved
界面组件一方面通过视图控制器向全局发送视图事件信号,另一方面每个组件自身定义了 SlotUpdate 槽函数并与视图控制器的 SigUpdate 信号连接,各自响应全局视图事件信号。
除了视图事件信号外,图形视图控制器还响应内核模型添加(AppendClassSignals)、模型移除(RemoveClassSignals)、模型替换(ReplaceClassSignals)和模型更新(UpdateClassSignals)信号。响应完毕后同步转发到全局供界面控件各自刷新。