介绍
本文主要实现了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);
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_cast
将 void*
转换成 CXXXProcessor*
类型,以便调用该类的成员函数。通过这种方式,可以在不暴露类内部实现的情况下,提供面向外部的 API 接口。
4. 函数功能设计
加载参数 (LoadParametersFromIni
)
1
| proc->LoadIniParametersToGlobalParameter(filename);
|
该函数的设计目的是加载指定的 .ini
配置文件,并将参数值导入到类内部的全局参数列表中。它的设计思想是将文件读取与内存中数据的存储解耦,便于后续的参数操作。
参数获取函数
包括:
GetGlobalParameterCount
:返回全局参数的数量。
GetParameterName
、GetParameterValue
、GetParameterMin
、GetParameterMax
:返回指定参数的不同属性(名称、值、最小值、最大值)。
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
类中提供了对参数的处理函数,如 LoadIniParametersToGlobalParameter
和 RewriteBWIIni_Param
,而 API 函数仅暴露必要的接口给外部调用。
6. 错误处理与返回值
大多数函数都进行了参数校验和错误处理。比如 GetParameterName
和其他类似的获取参数信息的函数都返回 nullptr
或默认值(如 0.0
),当输入无效时避免出现错误:
1
| if (!p || index < 0) return nullptr;
|
RewriteBWIIni_Param
函数则使用 try-catch
机制来捕获异常,确保在发生异常时能返回 false
,并输出错误信息。
7. 代码设计的一致性与可扩展性
- 所有获取参数的函数(如
GetParameterName
、GetParameterValue
等)具有一致的接口设计,参数索引作为输入,返回相关数据。这使得 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) setup(block); end
function setup(block) block.NumInputPorts = 0; block.NumOutputPorts = 1; block.OutputPort(1).Dimensions = 1; block.OutputPort(1).DatatypeID = 0;
block.OutputPort(1).SamplingMode = 'Sample';
block.SampleTimes = [0 0]; block.SimStateCompliance = 'DefaultSimState';
block.RegBlockMethod('PostPropagationSetup', @PostProp); block.RegBlockMethod('InitializeConditions', @InitCond); block.RegBlockMethod('Outputs', @Outputs); block.RegBlockMethod('Terminate', @Terminate);
set_param(block.BlockHandle, 'OpenFcn', 'start_import_gui'); end
function PostProp(block) block.NumDworks = 3; 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; 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;
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
function InitCond(block) block.Dwork(1).Data = 0; block.Dwork(2).Data = 0; block.Dwork(3).Data = 0; end
function Outputs(block) guiLaunched = block.Dwork(1).Data; confirmationFlag = block.Dwork(2).Data; simStarted = block.Dwork(3).Data; if guiLaunched == 0 start_import_gui(block.BlockHandle); block.Dwork(1).Data = 1; end try confirmationFlag = evalin('base', 'confirmationFlag'); block.Dwork(2).Data = confirmationFlag; catch end block.OutputPort(1).Data = confirmationFlag; if confirmationFlag == 1 && simStarted == 0 set_param(bdroot(block.BlockHandle), 'SimulationCommand', 'start'); block.Dwork(3).Data = 1; end
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
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));
fig.UserData.blockHandle = blockHandle;
dll = fullfile(pwd, 'bin', 'BWIBridgeDLL.dll'); hdr = fullfile(pwd, 'bin', 'BWIBridgeDLL.h'); if ~libisloaded('BWIBridgeDLL') loadlibrary(dll, hdr); end fig.UserData.dllLoaded = true;
btnImport = uibutton(fig, 'push', 'Text', '导入数据', 'Position', [680, 360, 100, 30], ... 'ButtonPushedFcn', @(src, event) import_data(fig));
uit = uitable(fig, 'Position', [20, 60, 760, 280], ... 'ColumnName', {'变量名', '数值', '最小值', '最大值', '说明'}, ... 'Data', {}, 'ColumnEditable', [false true false false false]); uit.Tag = 'dataTable';
fig.UserData.data = []; fig.UserData.originalData = [];
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 try proc = calllib('BWIBridgeDLL', 'CreateProcessor'); calllib('BWIBridgeDLL', 'LoadParametersFromIni', proc, iniFilePath); paramCount = calllib('BWIBridgeDLL', 'GetGlobalParameterCount', proc); data = cell(paramCount, 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); data{i+1, 1} = char(name); data{i+1, 2} = value; data{i+1, 3} = minVal; data{i+1, 4} = maxVal; data{i+1, 5} = char(desc); end uit = findobj(fig, 'Tag', 'dataTable'); uit.Data = data; fig.UserData.data = data; fig.UserData.originalData = data;
filePathTextArea = findobj(fig, 'Tag', 'filePathTextArea'); if ~isempty(filePathTextArea) filePathTextArea.Value = {folderPath}; 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}; if isempty(folderPath) uialert(fig, '文件夹路径无效', '导入错误'); return; end
folderPath = char(folderPath);
proc = calllib('BWIBridgeDLL', 'CreateProcessor');
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 = char(paramName); 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); assignin('base', 'confirmationFlag', 1); delete(fig); end
function cancel_action(fig) assignin('base', 'confirmationFlag', -1); delete(fig); end
function close_gui(fig) 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 数据工作区来存储和管理仿真状态,并通过回调函数实现不同阶段的功能。
关于文件在不同运行环境下编码不同的问题
其主要原因在于,Matlab的运行环境和Visual Studio不同,而封装的读写代码中用到了windows的API,该API会根据运行编译环境来选择不同的写入方式,而Matlab默认使用UTF-8,从而导致写入数据时中文出现乱码的情况,如下图

如果不需要在多个环境下运行(如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; } } }
bool CIniFile::saveInternal() { if (!m_loaded) return false; const bool addSig = (m_isUtf8 && m_hasUtf8BOM);
if (!m_isUtf8) { SI_Error rc = m_ini.SaveFile(m_Name, false); if (rc < 0) return false; } else { SI_Error rc = m_ini.SaveFile(m_Name, addSig); if (rc < 0) return false; }
m_dirty = false; return true; }
|
注意:如果非同一个项目,需要把所有用到的项目进行编译,并把运行需要的dll文件拷贝到Matlab环境下
运行示例