介绍

本文主要实现了Simulink下模块化GUI与外部Dll数据交互的功能。首先将需要用到的交互接口封装,导出为C风格的函数,然后在模块中创建可视化界面并导入外部Dll,用于显示和操作数据,并在修改数据后通过接口保存到文件,如果成功,则与后续接口通信,调用求解器并显示结果

导出交互接口

1
2
3
4
5
6
7
8
9
// 参数交互
EXPORT_API void LoadParametersFromIni(void* p, const char* filename);
EXPORT_API int GetGlobalParameterCount(void* p);
EXPORT_API const char* GetParameterName(void* p, int index);
EXPORT_API double GetParameterValue(void* p, int index);
EXPORT_API double GetParameterMin(void* p, int index);
EXPORT_API double GetParameterMax(void* p, int index);
EXPORT_API const char* GetParameterDescription(void* p, int index);
EXPORT_API bool RewriteBWIIni_Param(void* p, const char* paramName, double newValue, const char* projectPath);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//-------------参数交互用-------------------
EXPORT_API void LoadParametersFromIni(void* p, const char* filename) {
if (!p || !filename) return;
auto proc = static_cast<CXXXProcessor*>(p);
proc->LoadIniParametersToGlobalParameter(filename); // 调用现有方法
}
EXPORT_API int GetGlobalParameterCount(void* p) {
if (!p) return 0;
auto proc = static_cast<CXXXProcessor*>(p);
return static_cast<int>(proc->m_vglobalParams.size());
}
EXPORT_API const char* GetParameterName(void* p, int index) {
if (!p || index < 0) return nullptr;
auto proc = static_cast<CXXXProcessor*>(p);
if (index < proc->m_vglobalParams.size()) {
return proc->m_vglobalParams[index]->GetName().c_str();
}
return nullptr;
}
EXPORT_API double GetParameterValue(void* p, int index) {
if (!p || index < 0) return 0.0;
auto proc = static_cast<CXXXProcessor*>(p);
if (index < proc->m_vglobalParams.size()) {
return proc->m_vglobalParams[index]->GetValue();
}
return 0.0;
}
EXPORT_API double GetParameterMin(void* p, int index) {
if (!p || index < 0) return 0.0;
auto proc = static_cast<CXXXProcessor*>(p);
if (index < proc->m_vglobalParams.size()) {
return proc->m_vglobalParams[index]->GetMin();
}
return 0.0;
}
EXPORT_API double GetParameterMax(void* p, int index) {
if (!p || index < 0) return 0.0;
auto proc = static_cast<CXXXProcessor*>(p);
if (index < proc->m_vglobalParams.size()) {
return proc->m_vglobalParams[index]->GetMax();
}
return 0.0;
}
EXPORT_API const char* GetParameterDescription(void* p, int index) {
if (!p || index < 0) return nullptr;
auto proc = static_cast<CXXXProcessor*>(p);
if (index < proc->m_vglobalParams.size()) {
return proc->m_vglobalParams[index]->GetDescription().c_str();
}
return nullptr;
}
EXPORT_API bool RewriteBWIIni_Param(void* p, const char* paramName, double newValue, const char* projectPath) {
if (!p || !paramName || !projectPath) {
return false;
}
try {
auto proc = static_cast<CXXXProcessor*>(p);

// 调用 C++ 类成员函数进行参数修改
return proc->RewriteBWIIni_Param(paramName, newValue, projectPath);

}
catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << std::endl;
return false;
}
}

这段代码主要是通过 C++ 的 EXPORT_API 宏将 CXXXProcessor 类的相关函数暴露为 API 接口,允许外部调用进行参数的读取、修改以及加载配置。

1. EXPORT_API

每个函数前面都使用了 EXPORT_API 宏,这通常是为了将这些函数导出为 DLL 接口,让外部应用可以调用这些函数。这种做法常见于需要将 C++ 类和方法暴露给其他编程语言(如 Python 或 C#)的情况。EXPORT_API 一般在头文件中定义,并用来标记需要导出的函数。

2. 参数校验

在所有函数内部,第一步都是检查指针或参数是否为空或无效。例如:

1
if (!p || !filename) return;

这种做法可以确保在程序运行时避免因为传入无效参数而导致的崩溃,增强了代码的鲁棒性。

3. static_cast 类型转换

很多地方使用了 static_cast 来进行类型转换。比如:

1
auto proc = static_cast<CXXXProcessor*>(p);

这里 p 是一个 void* 类型的指针,指向 CXXXProcessor 类型的对象。static_castvoid* 转换成 CXXXProcessor* 类型,以便调用该类的成员函数。通过这种方式,可以在不暴露类内部实现的情况下,提供面向外部的 API 接口。

4. 函数功能设计

加载参数 (LoadParametersFromIni)

1
proc->LoadIniParametersToGlobalParameter(filename);

该函数的设计目的是加载指定的 .ini 配置文件,并将参数值导入到类内部的全局参数列表中。它的设计思想是将文件读取与内存中数据的存储解耦,便于后续的参数操作。

参数获取函数

包括:

  • GetGlobalParameterCount:返回全局参数的数量。
  • GetParameterNameGetParameterValueGetParameterMinGetParameterMax:返回指定参数的不同属性(名称、值、最小值、最大值)。
  • GetParameterDescription:返回参数的描述信息。

这些函数的设计方式非常直观,主要是通过访问类内部 m_vglobalParams 成员(一个存储参数的容器,如 std::vector)来返回相应的信息。每个函数都首先检查传入的索引是否有效,确保在访问数组或容器时不会越界。

修改参数 (RewriteBWIIni_Param)

1
proc->RewriteBWIIni_Param(paramName, newValue, projectPath);

此函数负责修改指定参数的值并将其更新回 .ini 配置文件中。它利用 CXXXProcessor 类中的现有方法 RewriteBWIIni_Param 来执行实际的修改操作。在设计上,这个函数有一个异常处理机制(try-catch 语句),用于捕获运行时可能出现的异常,防止程序崩溃。错误信息会通过 std::cerr 输出,便于调试。

5. 数据封装与分离

函数内部使用了 CXXXProcessor 类封装了所有参数相关的操作,外部调用者仅通过接口来操作这些数据。这样的封装设计符合面向对象编程(OOP)的原则,数据与操作方法分离,外部只关心接口而无需了解内部实现细节。

  • m_vglobalParams 是存储所有全局参数的容器,封装了参数的读取、修改等操作。
  • CXXXProcessor 类中提供了对参数的处理函数,如 LoadIniParametersToGlobalParameterRewriteBWIIni_Param,而 API 函数仅暴露必要的接口给外部调用。

6. 错误处理与返回值

大多数函数都进行了参数校验和错误处理。比如 GetParameterName 和其他类似的获取参数信息的函数都返回 nullptr 或默认值(如 0.0),当输入无效时避免出现错误:

1
if (!p || index < 0) return nullptr;

RewriteBWIIni_Param 函数则使用 try-catch 机制来捕获异常,确保在发生异常时能返回 false,并输出错误信息。

7. 代码设计的一致性与可扩展性

  • 所有获取参数的函数(如 GetParameterNameGetParameterValue 等)具有一致的接口设计,参数索引作为输入,返回相关数据。这使得 API 易于扩展和维护,后期可以轻松增加更多的参数获取功能。
  • RewriteBWIIni_Param 与加载参数的功能解耦,遵循了单一职责原则,使得每个函数都专注于自己的一部分工作,易于理解和测试。

Matlab GUI实现

将上个步骤的代码编译为Dll,并把其头文件、dll文件以及所有用到的其他dll拷贝到Matlab的运行环境下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
function sfun_callInterface(block)
% 初始化 S-Function
setup(block);
end

% ─────────────────── SETUP ────────────────────
function setup(block)
% I/O 配置(没有输入端口)
block.NumInputPorts = 0;
block.NumOutputPorts = 1;
block.OutputPort(1).Dimensions = 1;
block.OutputPort(1).DatatypeID = 0; % 输出一个常数,作为标志位

% 明确设置输出端口的采样模式(设置为样本数据)
block.OutputPort(1).SamplingMode = 'Sample'; % FRAME_NO: 样本数据

% SampleTime 配置
block.SampleTimes = [0 0]; % 立即执行,停止仿真时使用
block.SimStateCompliance = 'DefaultSimState';

% 注册方法
block.RegBlockMethod('PostPropagationSetup', @PostProp);
block.RegBlockMethod('InitializeConditions', @InitCond);
block.RegBlockMethod('Outputs', @Outputs);
block.RegBlockMethod('Terminate', @Terminate);

% 设置 OpenFcn 触发 GUI
set_param(block.BlockHandle, 'OpenFcn', 'start_import_gui');
end

% ─── PostPropagationSetup:声明 DWork ───
function PostProp(block)
block.NumDworks = 3;

% Dwork 1: 用于存储 GUI 是否已启动的标志
block.Dwork(1).Name = 'guiLaunched';
block.Dwork(1).Dimensions = 1;
block.Dwork(1).DatatypeID = 0;
block.Dwork(1).Complexity = 'Real';
block.Dwork(1).UsedAsDiscState = true;

% Dwork 2: 用于存储确认标志
block.Dwork(2).Name = 'confirmationFlag';
block.Dwork(2).Dimensions = 1;
block.Dwork(2).DatatypeID = 0;
block.Dwork(2).Complexity = 'Real';
block.Dwork(2).UsedAsDiscState = true;

% Dwork 3: 用于存储是否已尝试启动仿真的标志(防止多次调用)
block.Dwork(3).Name = 'simStarted';
block.Dwork(3).Dimensions = 1;
block.Dwork(3).DatatypeID = 0;
block.Dwork(3).Complexity = 'Real';
block.Dwork(3).UsedAsDiscState = true;
end

% ─── InitializeConditions:初始化 DWork ───
function InitCond(block)
% 每次仿真开始时重置标志
block.Dwork(1).Data = 0; % GUI 未启动
block.Dwork(2).Data = 0; % 确认标志为 0
block.Dwork(3).Data = 0; % 未启动仿真
end

% ─── Outputs ───
function Outputs(block)
% 获取当前状态
guiLaunched = block.Dwork(1).Data;
confirmationFlag = block.Dwork(2).Data;
simStarted = block.Dwork(3).Data;

% 如果 GUI 尚未启动,则启动 GUI
if guiLaunched == 0
start_import_gui(block.BlockHandle); % 传递块句柄给 GUI
block.Dwork(1).Data = 1; % 设置 GUI 已启动标志
end

% 尝试从基础工作空间获取确认状态
try
confirmationFlag = evalin('base', 'confirmationFlag');
block.Dwork(2).Data = confirmationFlag;
catch
% 如果变量不存在,保持当前值
end

% 输出确认标志
block.OutputPort(1).Data = confirmationFlag;

% 如果确认标志是 1,且尚未启动仿真,则自动运行仿真
if confirmationFlag == 1 && simStarted == 0
% 自动开始仿真,无需手动输入
set_param(bdroot(block.BlockHandle), 'SimulationCommand', 'start');
block.Dwork(3).Data = 1; % 设置已启动标志
end

% 如果确认标志是 -1,表示取消,停止仿真
if confirmationFlag == -1
try
% 检查仿真是否正在运行,若正在运行则停止
simStatus = get_param(bdroot(block.BlockHandle), 'SimulationStatus');
if strcmp(simStatus, 'running')
% 如果仿真正在运行,取消仿真
set_param(bdroot(block.BlockHandle), 'SimulationCommand', 'stop');
disp('仿真已取消');
end
catch
% 如果没有仿真,或其他错误,则跳过
disp('未能检查到正在运行的仿真,或仿真未启动');
end
end
end

% ─── Terminate:处理进程退出 ───
function Terminate(~)
% 清理基础工作空间中的标志变量
evalin('base', 'clear confirmationFlag');
end

function start_import_gui(blockHandle)
% 获取屏幕大小
screenSize = get(0, 'ScreenSize');
screenWidth = screenSize(3);
screenHeight = screenSize(4);

% 设置图形界面大小
figWidth = 800;
figHeight = 400;

% 计算居中显示的坐标
figPosX = (screenWidth - figWidth) / 2;
figPosY = (screenHeight - figHeight) / 2;

% 创建图形界面(窗口),居中显示
fig = uifigure('Name', '数据导入与显示', 'Position', [figPosX, figPosY, figWidth, figHeight], ...
'CloseRequestFcn', @(src, event) close_gui(fig));

% 存储块句柄在图形对象的 UserData 中
fig.UserData.blockHandle = blockHandle;

% 加载 DLL(在 GUI 启动时加载一次)
dll = fullfile(pwd, 'bin', 'BWIBridgeDLL.dll');
hdr = fullfile(pwd, 'bin', 'BWIBridgeDLL.h');
if ~libisloaded('BWIBridgeDLL')
loadlibrary(dll, hdr);
end
fig.UserData.dllLoaded = true; % 标记 DLL 已加载

% 创建按钮来导入数据
btnImport = uibutton(fig, 'push', 'Text', '导入数据', 'Position', [680, 360, 100, 30], ...
'ButtonPushedFcn', @(src, event) import_data(fig));

% 创建一个uitable用于显示数据
uit = uitable(fig, 'Position', [20, 60, 760, 280], ...
'ColumnName', {'变量名', '数值', '最小值', '最大值', '说明'}, ...
'Data', {}, 'ColumnEditable', [false true false false false]);
uit.Tag = 'dataTable'; % 设置标签,方便后续操作

% 用于保存数据的临时变量(移到 fig.UserData)
fig.UserData.data = [];
fig.UserData.originalData = []; % 用于存储原始数据

% 创建一个TextArea来显示文件夹路径
filePathLabel = uilabel(fig, 'Text', '选择的文件夹路径:', 'Position', [20, 360, 100, 30]);
filePathTextArea = uitextarea(fig, 'Position', [130, 360, 530, 30], 'Tag', 'filePathTextArea');
filePathTextArea.Editable = 'off'; % 设置为只读

% 创建"确认"按钮
uibutton(fig, 'push', 'Text', '确认', 'Position', [520, 20, 100, 30], ...
'ButtonPushedFcn', @(src, event) confirm_action(fig));

% 创建"取消"按钮
uibutton(fig, 'push', 'Text', '取消', 'Position', [640, 20, 100, 30], ...
'ButtonPushedFcn', @(src, event) cancel_action(fig));
end

这段代码是一个用于在 Simulink 模型中调用自定义 S-Function 的实现,目的是在仿真过程中与图形用户界面 (GUI) 交互,导入和修改参数。

S-Function 主体

首先,sfun_callInterface 函数是 S-Function 的入口,调用了 setup 函数来进行初始化配置。setup 函数为 S-Function 设置了输入输出端口、采样时间、以及仿真相关的回调方法。

setup 函数

setup 函数中,设置了 S-Function 的输入和输出端口配置。它没有输入端口,只有一个输出端口,并且输出的维度和数据类型进行了设定。该端口的采样模式为 Sample,表示输出数据为常规的样本数据。

此外,SampleTimes 被设置为 [0 0],这意味着该 S-Function 会立即执行并且不会进行周期性的采样。通过注册 PostPropagationSetup, InitializeConditions, Outputs, 和 Terminate 四个方法,定义了仿真生命周期内不同阶段的行为。

最后,set_param 设置了 OpenFcn 触发,指向了 start_import_gui 函数,用于启动 GUI 界面。

PostPropagationSetup 函数

该函数用于声明 S-Function 中的 DWork(数据工作区)。DWork 是用来存储状态信息的地方。这里定义了三个 DWork 变量:

  • guiLaunched 用于标识 GUI 是否已启动。
  • confirmationFlag 用于存储从 GUI 获取的确认状态。
  • simStarted 用于标记仿真是否已经启动。

这些 DWork 变量的作用是确保 GUI 在需要时只启动一次,并且管理仿真状态,防止重复操作。

InitCond 函数

InitCond 函数在每次仿真开始时被调用,用来初始化 DWork 变量。在这里,所有的标志变量(guiLaunched, confirmationFlag, simStarted)都被初始化为 0,表示 GUI 尚未启动,确认标志为未设置,仿真尚未开始。

Outputs 函数

Outputs 函数是在仿真过程中持续被调用的,用来控制和更新仿真输出。首先,它获取了 GUI 启动状态、确认标志和仿真启动状态。如果 GUI 尚未启动,则调用 start_import_gui 函数启动 GUI 界面,并将 guiLaunched 标志设为 1。

如果 GUI 已经启动,尝试从基础工作空间获取 confirmationFlag 变量的值。如果获取到有效值,则更新 DWork 中的 confirmationFlag

然后,函数检查确认标志的值:

  • 如果 confirmationFlag 为 1 且仿真尚未启动,则自动启动仿真,并将 simStarted 设为 1。
  • 如果 confirmationFlag 为 -1,表示取消,尝试停止正在运行的仿真。

Terminate 函数

Terminate 函数在仿真结束时调用,主要作用是清理基础工作空间中的标志变量 confirmationFlag,以防止在下次仿真中出现残留的状态信息。

start_import_gui 函数

start_import_gui 函数用于创建并显示 GUI 窗口。首先,它计算并设置窗口居中显示的坐标。然后,创建了一个图形界面,并在其中添加了以下元素:

  • 一个按钮用来触发数据导入。
  • 一个表格用来显示导入的数据。
  • 一个文本框用来显示当前选择的文件夹路径。
  • “确认”和“取消”按钮,用来控制数据导入和取消操作。

此外,在 GUI 初始化时加载了一个动态链接库(DLL),用于后续的参数导入和修改操作。

调用Dll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
% 导入数据的回调函数
function import_data(fig)
% 弹出文件夹选择对话框
folderPath = uigetdir('', '选择数据文件夹');

if folderPath == 0 % 用户取消选择文件夹
return;
end

% 拼接完整路径
iniFilePath = fullfile(folderPath, 'bwiparamtemp.ini');

if ~isfile(iniFilePath)
uialert(fig, 'bwiparamtemp.ini 文件不存在', '导入错误');
return;
end

% 调用 DLL 读取数据
try
% 创建处理器
proc = calllib('BWIBridgeDLL', 'CreateProcessor');

% 调用函数加载 INI 参数
calllib('BWIBridgeDLL', 'LoadParametersFromIni', proc, iniFilePath);

% 获取参数数量
paramCount = calllib('BWIBridgeDLL', 'GetGlobalParameterCount', proc);

% 获取参数的名称、数值、最小值、最大值、描述等
data = cell(paramCount, 5); % 5 列:名称、数值、最小值、最大值、描述
for i = 0:paramCount-1
name = calllib('BWIBridgeDLL', 'GetParameterName', proc, i);
value = calllib('BWIBridgeDLL', 'GetParameterValue', proc, i);
minVal = calllib('BWIBridgeDLL', 'GetParameterMin', proc, i);
maxVal = calllib('BWIBridgeDLL', 'GetParameterMax', proc, i);
desc = calllib('BWIBridgeDLL', 'GetParameterDescription', proc, i);

% 假设 value, minVal, maxVal 是 double,确保一致
data{i+1, 1} = char(name); % 变量名 (确保字符串)
data{i+1, 2} = value; % 数值 (double)
data{i+1, 3} = minVal; % 最小值 (double)
data{i+1, 4} = maxVal; % 最大值 (double)
data{i+1, 5} = char(desc); % 说明 (确保字符串)
end

% 更新表格数据
uit = findobj(fig, 'Tag', 'dataTable');
uit.Data = data; % 显示数据

% 保存数据到 fig.UserData(原始数据和当前数据)
fig.UserData.data = data;
fig.UserData.originalData = data; % 存储原始数据副本

% 更新文件夹路径显示框
filePathTextArea = findobj(fig, 'Tag', 'filePathTextArea');
if ~isempty(filePathTextArea)
filePathTextArea.Value = {folderPath}; % 显示路径 (uitextarea Value 是 cell)
else
uialert(fig, '未找到文件夹路径文本框,无法更新路径', '更新错误');
end

% 释放处理器
calllib('BWIBridgeDLL', 'DeleteProcessor', proc);

disp('数据导入成功');
catch ME
% 读取文件失败时的处理
uialert(fig, ['文件导入失败: ', ME.message], '导入错误');
end
end

% 确认按钮的回调
function confirm_action(fig)
% 获取表格数据
uit = findobj(fig, 'Tag', 'dataTable');
data = uit.Data; % 表格中的所有数据

% 获取文件夹路径作为项目路径
filePathTextArea = findobj(fig, 'Tag', 'filePathTextArea');
folderPath = filePathTextArea.Value{1}; % 获取显示的文件夹路径 (从 cell 取)

if isempty(folderPath)
uialert(fig, '文件夹路径无效', '导入错误');
return;
end

% 确保将 folderPath 转换为字符向量
folderPath = char(folderPath);

% 创建处理器
proc = calllib('BWIBridgeDLL', 'CreateProcessor');

% 获取原始数据从 fig.UserData
originalData = fig.UserData.originalData;

% 如果没有原始数据,则使用当前数据作为原始数据
if isempty(originalData)
originalData = data;
fig.UserData.originalData = data;
end

% 遍历表格中的每一行,获取参数名和新值
updated = false; % 标记是否有更新
for i = 1:size(data, 1)
paramName = data{i, 1}; % 参数名
newValue = data{i, 2}; % 修改后的新值

% 确保 paramName 转换为字符向量
paramName = char(paramName);

% 确保 newValue 是数值类型 (表格编辑可能返回 char)
if ischar(newValue) || iscell(newValue)
newValue = str2double(newValue);
end
if isnan(newValue)
uialert(fig, ['参数 ', paramName, ' 的值无效(非数值)'], '导入错误');
calllib('BWIBridgeDLL', 'DeleteProcessor', proc);
return;
end

% 获取原始值
originalValue = originalData{i, 2};

% 检查值是否发生变化
if isequal(newValue, originalValue)
disp(['参数 ', paramName, ' 未发生变化,跳过更新']);
continue; % 跳过这个参数
end

% 更新参数
result = calllib('BWIBridgeDLL', 'RewriteBWIIni_Param', proc, paramName, newValue, folderPath);

% 检查更新是否成功
if result
disp(['参数 ', paramName, ' 修改成功']);
% 更新原始数据
originalData{i, 2} = newValue;
updated = true;
else
uialert(fig, ['参数 ', paramName, ' 修改失败'], '导入错误');
calllib('BWIBridgeDLL', 'DeleteProcessor', proc);
return;
end
end

% 更新原始数据
fig.UserData.originalData = originalData;

% 释放处理器
calllib('BWIBridgeDLL', 'DeleteProcessor', proc);

% 在基础工作空间中设置确认标志为 1
assignin('base', 'confirmationFlag', 1);

% 关闭 GUI 窗口
delete(fig);
end

% 取消按钮的回调
function cancel_action(fig)
% 在基础工作空间中设置确认标志为 -1
assignin('base', 'confirmationFlag', -1);

% 关闭 GUI 窗口
delete(fig);
end

% GUI 关闭回调函数
function close_gui(fig)
% 卸载 DLL 如果已加载
if isfield(fig.UserData, 'dllLoaded') && fig.UserData.dllLoaded && libisloaded('BWIBridgeDLL')
unloadlibrary('BWIBridgeDLL');
end
% 清理其他资源如果需要
delete(fig);
end

import_data 函数

import_data 函数是在用户点击导入按钮时调用的。它弹出一个文件夹选择对话框,允许用户选择包含数据文件的文件夹。如果用户选择了文件夹并且其中包含 bwiparamtemp.ini 文件,程序会调用 DLL 函数读取该文件中的参数并将其显示在表格中。

confirm_action 函数

confirm_action 函数在用户点击“确认”按钮时被调用。它从表格中读取每个参数的名称和值,并将更新的参数通过 DLL 保存到指定路径的 .ini 文件中。如果参数值有变化,则执行更新操作。

更新成功后,将 confirmationFlag 设置为 1,表示操作已完成,仿真可以开始。

cancel_action 函数

cancel_action 函数在用户点击“取消”按钮时调用。它将 confirmationFlag 设置为 -1,表示取消操作,然后关闭 GUI 窗口。

close_gui 函数

close_gui 函数用于关闭 GUI 窗口时调用。它会卸载已经加载的 DLL,并释放其他资源。

总结

这段代码实现了一个与 Simulink 模型交互的 GUI 系统,允许用户在仿真过程中导入和修改参数,启动或停止仿真。代码通过 S-Function 接口与 Simulink 仿真框架进行集成,并使用 MATLAB 图形界面元素提供用户交互界面。设计上使用了 DWork 数据工作区来存储和管理仿真状态,并通过回调函数实现不同阶段的功能。

关于文件在不同运行环境下编码不同的问题

image-20250826162904880

其主要原因在于,Matlab的运行环境和Visual Studio不同,而封装的读写代码中用到了windows的API,该API会根据运行编译环境来选择不同的写入方式,而Matlab默认使用UTF-8,从而导致写入数据时中文出现乱码的情况,如下图

image-20250826163227939

如果不需要在多个环境下运行(如Unicode),我们可以固定写入方式,如读写均采用ANSI方式

1
2
3
4
5
6
7
8
9
10
char *CIniFile::ReadString(string &m_Sec, string &m_Ident, char *m_Def)
{
GetPrivateProfileStringA(m_Sec.c_str(), m_Ident.c_str(), m_Def, Buffer, sizeof(Buffer), m_Name);
return Buffer;
}

bool CIniFile::WriteString(const char *m_Sec, const char *m_Ident, const char *m_Val)
{
return (bool)WritePrivateProfileStringA(m_Sec, m_Ident, m_Val, m_Name);
}

也可以使用开源替代库来替代原有的ini操作类,如SimpleIni,或者自己手动检测编码,手动写入,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void CIniFile::detectEncoding() {
m_isUtf8 = false;
m_hasUtf8BOM = false;

unsigned char bom[3] = { 0,0,0 };
if (FILE* f = std::fopen(m_Name, "rb")) {
std::fread(bom, 1, 3, f);
std::fclose(f);
if (bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF) {
m_isUtf8 = true;
m_hasUtf8BOM = true;
}
}
// 如果无 BOM:保守视为 MBCS/ANSI(如 GBK);这样不会把老文件强转 UTF-8。
}

bool CIniFile::saveInternal() {
if (!m_loaded) return false;
// UTF-8 文件:是否写 BOM:保持与原文件一致
const bool addSig = (m_isUtf8 && m_hasUtf8BOM);

// 如果是 GB2312/GBK 编码,保存时不写 BOM
if (!m_isUtf8) {
SI_Error rc = m_ini.SaveFile(m_Name, false); // No BOM for MBCS/ANSI
if (rc < 0) return false;
}
else {
// 保存为 UTF-8 编码,并且根据原文件是否有 BOM 来决定是否加 BOM
SI_Error rc = m_ini.SaveFile(m_Name, addSig);
if (rc < 0) return false;
}

m_dirty = false;
return true;
}

注意:如果非同一个项目,需要把所有用到的项目进行编译,并把运行需要的dll文件拷贝到Matlab环境下

运行示例

show