关于 matlab-uihtml-app-builder
matlab-uihtml-app-builder 是指导使用 MATLAB 的 uihtml 构建交互式应用的技能,面向将 HTML、JavaScript 界面与 MATLAB 计算结合的开发者。内容涵盖双向通信、数据验证、错误处理、测试及长时运行操作。
主要功能
- 01组合 HTML、JavaScript 与 MATLAB 后端
- 02通过属性和事件双向传递数据
- 03验证输入并处理事件错误
- 04提供界面设计与性能建议
- 05覆盖测试、调试和长时操作模式
适用场景
- 01构建交互式 MATLAB 计算器
- 02制作数据可视化应用界面
- 03创建表单处理或实时监控工具
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ matlab/ agent-skills-playground --skill matlab-uihtml-app-builder安装后在 Claude Code 中输入/matlab-uihtml-app-builder确认已加载。
详细介绍
MATLAB uihtml 应用构建器
本技能介绍如何使用 uihtml 组件构建交互式 Web 应用,将 HTML/JavaScript 界面与 MATLAB 计算后端相结合。HTML 端负责界面,MATLAB 负责计算。
何时使用此技能
- 构建带有 HTML/JavaScript 界面的交互式 MATLAB 应用
- 为 MATLAB 应用创建基于 Web 的 UI
- 使用 HTML/CSS/JS 构建响应式 MATLAB GUI
- 用户提及 uihtml、HTML、JavaScript、Web 应用、Web 界面或交互式 GUI 时
- 将 Web UI 设计与 MATLAB 的计算能力相结合
- 创建计算器应用、数据可视化工具或基于表单的 MATLAB 工具
核心架构
四个组成部分
- HTML 界面 - 包含按钮、表单和显示区域的用户界面
- JavaScript 逻辑 - 处理事件和 UI 交互
- MATLAB 后端 - 计算引擎和数据处理
- uihtml 组件 - HTML 与 MATLAB 之间的桥梁
通信模式
uihtml 组件支持 JavaScript 与 MATLAB 通过多种机制进行双向通信:
模式 1:MATLAB → JavaScript(数据属性)
用例:从 MATLAB 发送数据以更新 HTML 界面
% MATLAB side
h.Data = "Hello World!";
// JavaScript side
htmlComponent.addEventListener("DataChanged", function(event) {
document.getElementById("display").innerHTML = htmlComponent.Data;
});
模式 2:JavaScript → MATLAB(事件)
用例:通过用户交互触发 MATLAB 函数
// JavaScript side - send event to MATLAB
htmlComponent.sendEventToMATLAB("Calculate", expression);
% MATLAB side - receive and handle event
h.HTMLEventReceivedFcn = @handleEvent;
function handleEvent(src, event)
eventName = event.HTMLEventName;
eventData = event.HTMLEventData;
% Process event...
end
模式 3:MATLAB → JavaScript(自定义事件)
用例:向 JavaScript 发送计算结果或状态更新
% MATLAB side - send custom event to JavaScript
sendEventToHTMLSource(h, "ResultChanged", result);
// JavaScript side - listen for custom event
htmlComponent.addEventListener("ResultChanged", function(event) {
document.getElementById("display").textContent = event.Data;
});
模式 4:复杂数据传输
用例:在 MATLAB 与 JavaScript 之间传递结构化数据
% MATLAB side - struct data gets JSON encoded automatically
itemData = struct("ItemName","Apple","Price",2,"Quantity",10);
h.Data = itemData;
// JavaScript side - access as object properties
htmlComponent.Data.ItemName // "Apple"
htmlComponent.Data.Price // 2
htmlComponent.Data.Quantity // 10
重要提示:两个方向的数据都会自动解码。
- 通过
sendEventToMATLAB发送的 JS 对象到达event.HTMLEventData时,已经转换为 MATLAB 结构体。不要调用jsondecode;它无法处理结构体。 - 通过
sendEventToHTMLSource发送的 MATLAB 结构体到达event.Data时,已经是 JavaScript 对象。不要调用JSON.parse;它无法处理对象。 - 字段名在往返传递过程中保持不变:JS
{x0: 1}会变为 MATLABstruct('x0', 1),而不是struct('x_0', ...)或类似形式。 - 数值标量到达 MATLAB 时为
double。如果想谨慎处理类型,可使用double(data.x0)包装字段读取。
关键规则
安全要求
-
使用本地 HTML 文件时,始终设置
HTMLSource = 'trusted':h.HTMLSource = fullfile(pwd, 'myapp.html'); % This is treated as trusted automatically for local files -
必须先验证来自 JavaScript 的所有输入,再在 MATLAB 中处理
-
绝不要在未严格清理的情况下对用户输入使用
eval() -
始终限制用户输入表达式中允许的字符
错误处理
始终使用 try-catch 块包装 MATLAB 事件处理程序:
function handleEvent(src, event)
eventName = event.HTMLEventName;
eventData = event.HTMLEventData;
try
% Process the event
result = processData(eventData);
% Send result back to JavaScript
sendEventToHTMLSource(src, 'ResultEvent', result);
catch ME
% Handle errors gracefully
fprintf('Error: %s\n', ME.message);
sendEventToHTMLSource(src, 'ErrorEvent', ME.message);
end
end
数据验证
始终先验证用户输入,再进行处理:
function result = validateExpression(expression)
allowedChars = '0123456789+-*/.() ';
if ~all(ismember(expression, allowedChars))
error('Invalid characters in expression');
end
% Additional validation...
result = true;
end
文件组织
遵循以下目录结构:
project/
├── app.m # Main MATLAB function
├── app.html # HTML interface
├── README.md # Usage instructions
└── examples/ # Additional examples (optional)
完整示例
示例 1:简单计算器应用
MATLAB 端(calculator.m):
function calculator()
% Create main figure
fig = uifigure('Name', 'Calculator', 'Position', [100 100 400 500]);
% Create HTML component
h = uihtml(fig, 'Position', [25 25 350 450]);
h.HTMLSource = fullfile(pwd, 'calculator.html');
h.HTMLEventReceivedFcn = @(src, event) handleEvent(src, event);
end
function handleEvent(src, event)
eventName = event.HTMLEventName;
eventData = event.HTMLEventData;
try
switch eventName
case 'Calculate'
% Validate input
expression = char(eventData);
allowedChars = '0123456789+-*/.() ';
if ~all(ismember(expression, allowedChars))
error('Invalid characters in expression');
end
% Evaluate safely
result = eval(expression);
% Send result back
sendEventToHTMLSource(src, 'Result', num2str(result));
case 'Clear'
sendEventToHTMLSource(src, 'Result', '0');
end
catch ME
fprintf('Error: %s\n', ME.message);
sendEventToHTMLSource(src, 'Error', 'Invalid expression');
end
end
HTML 端(calculator.html):
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 20px;
}
.calculator {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.display {
width: 100%;
height: 60px;
font-size: 24px;
text-align: right;
padding: 10px;
border: 2px solid #ccc;
border-radius: 5px;
margin-bottom: 10px;
background: #f9f9f9;
}
.buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
button {
padding: 20px;
font-size: 18px;
border: none;
border-radius: 5px;
cursor: pointer;
background: #667eea;
color: white;
transition: background 0.3s;
}
button:hover {
background: #764ba2;
}
.operator {
background: #ff6b6b;
}
.operator:hover {
background: #ee5a52;
}
</style>
<script type="text/javascript">
let currentExpression = '';
function setup(htmlComponent) {
window.htmlComponent = htmlComponent;
// Listen for results from MATLAB
htmlComponent.addEventListener("Result", function(event) {
document.getElementById("display").value = event.Data;
currentExpression = event.Data;
});
htmlComponent.addEventListener("Error", function(event) {
document.getElementById("display").value = "Error";
currentExpression = '';
});
}
function appendToDisplay(value) {
currentExpression += value;
document.getElementById("display").value = currentExpression;
}
function clearDisplay() {
currentExpression = '';
document.getElementById("display").value = '0';
window.htmlComponent.sendEventToMATLAB("Clear", "");
}
function calculate() {
if (currentExpression) {
window.htmlComponent.sendEventToMATLAB("Calculate", currentExpression);
}
}
</script>
</head>
<body>
<div class="calculator">
<input type="text" id="display" class="display" value="0" readonly>
<div class="buttons">
<button onclick="appendToDisplay('7')">7</button>
<button onclick="appendToDisplay('8')">8</button>
<button onclick="appendToDisplay('9')">9</button>
<button class="operator" onclick="appendToDisplay('/')">/</button>
<button onclick="appendToDisplay('4')">4</button>
<button onclick="appendToDisplay('5')">5</button>
<button onclick="appendToDisplay('6')">6</button>
<button class="operator" onclick="appendToDisplay('*')">*</button>
<button onclick="appendToDisplay('1')">1</button>
<button onclick="appendToDisplay('2')">2</button>
<button onclick="appendToDisplay('3')">3</button>
<button class="operator" onclick="appendToDisplay('-')">-</button>
<button onclick="appendToDisplay('0')">0</button>
<button onclick="appendToDisplay('.')">.</button>
<button onclick="calculate()">=</button>
<button class="operator" onclick="appendToDisplay('+')">+</button>
<button style="grid-column: span 4; background: #ff6b6b;" onclick="clearDisplay()">Clear</button>
</div>
</div>
</body>
</html>
示例 2:数据可视化应用
MATLAB 端(visualizer.m):
function visualizer()
fig = uifigure('Name', 'Data Visualizer', 'Position', [100 100 800 600]);
% Create HTML component for controls
h = uihtml(fig, 'Position', [25 400 750 175]);
h.HTMLSource = fullfile(pwd, 'controls.html');
h.HTMLEventReceivedFcn = @(src, event) handleEvent(src, event, fig);
% Create axes for plotting
ax = uiaxes(fig, 'Position', [25 25 750 350]);
xlabel(ax, 'X');
ylabel(ax, 'Y');
title(ax, 'Interactive Plot');
end
function handleEvent(src, event, fig)
eventName = event.HTMLEventName;
eventData = event.HTMLEventData;
try
switch eventName
case 'UpdatePlot'
% Parse parameters from JavaScript
params = eventData;
frequency = params.frequency;
amplitude = params.amplitude;
plotType = params.plotType;
% Generate data
x = linspace(0, 4*pi, 200);
switch plotType
case 'sine'
y = amplitude * sin(frequency * x);
case 'cosine'
y = amplitude * cos(frequency * x);
case 'both'
y = amplitude * sin(frequency * x);
y2 = amplitude * cos(frequency * x);
end
% Find axes and plot
ax = findobj(fig, 'Type', 'axes');
cla(ax);
if strcmp(plotType, 'both')
plot(ax, x, y, 'LineWidth', 2);
hold(ax, 'on');
plot(ax, x, y2, 'LineWidth', 2);
hold(ax, 'off');
legend(ax, 'Sine', 'Cosine');
else
plot(ax, x, y, 'LineWidth', 2);
end
grid(ax, 'on');
% Send confirmation
sendEventToHTMLSource(src, 'PlotUpdated', 'Success');
end
catch ME
fprintf('Error: %s\n', ME.message);
sendEventToHTMLSource(src, 'Error', ME.message);
end
end
HTML 端(controls.html):
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
color: white;
margin: 0;
padding: 20px;
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
}
.control-group {
background: rgba(255,255,255,0.1);
padding: 15px;
border-radius: 8px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="range"] {
width: 100%;
}
select, button {
width: 100%;
padding: 8px;
border-radius: 5px;
border: none;
font-size: 14px;
}
button {
background: #3498db;
color: white;
cursor: pointer;
margin-top: 10px;
transition: background 0.3s;
}
button:hover {
background: #2980b9;
}
</style>
<script type="text/javascript">
function setup(htmlComponent) {
window.htmlComponent = htmlComponent;
htmlComponent.addEventListener("PlotUpdated", function(event) {
console.log("Plot updated successfully");
});
}
function updatePlot() {
const frequency = parseFloat(document.getElementById("frequency").value);
const amplitude = parseFloat(document.getElementById("amplitude").value);
const plotType = document.getElementById("plotType").value;
const params = {
frequency: frequency,
amplitude: amplitude,
plotType: plotType
};
window.htmlComponent.sendEventToMATLAB("UpdatePlot", params);
}
function updateFreqLabel(value) {
document.getElementById("freqValue").textContent = value;
}
function updateAmpLabel(value) {
document.getElementById("ampValue").textContent = value;
}
</script>
</head>
<body>
<div class="controls">
<div class="control-group">
<label>Frequency: <span id="freqValue">1</span></label>
<input type="range" id="frequency" min="0.1" max="5" step="0.1" value="1"
oninput="updateFreqLabel(this.value)">
</div>
<div class="control-group">
<label>Amplitude: <span id="ampValue">1</span></label>
<input type="range" id="amplitude" min="0.1" max="5" step="0.1" value="1"
oninput="updateAmpLabel(this.value)">
</div>
<div class="control-group">
<label>Plot Type:</label>
<select id="plotType">
<option value="sine">Sine</option>
<option value="cosine">Cosine</option>
<option value="both">Both</option>
</select>
</div>
</div>
<button onclick="updatePlot()">Update Plot</button>
</body>
</html>
示例 3:表单处理应用
MATLAB 端(formProcessor.m):
function formProcessor()
fig = uifigure('Name', 'Form Processor', 'Position', [100 100 600 400]);
h = uihtml(fig, 'Position', [25 25 550 350]);
h.HTMLSource = fullfile(pwd, 'form.html');
h.HTMLEventReceivedFcn = @(src, event) handleEvent(src, event);
end
function handleEvent(src, event)
eventName = event.HTMLEventName;
eventData = event.HTMLEventData;
try
switch eventName
case 'SubmitForm'
% Extract form data
name = eventData.name;
email = eventData.email;
age = eventData.age;
% Validate data
if isempty(name) || isempty(email)
error('Name and email are required');
end
if ~contains(email, '@')
error('Invalid email address');
end
if age < 0 || age > 120
error('Invalid age');
end
% Process data (example: save to file or database)
fprintf('Processing form:\n');
fprintf(' Name: %s\n', name);
fprintf(' Email: %s\n', email);
fprintf(' Age: %d\n', age);
% Send success message
result = struct('status', 'success', ...
'message', 'Form submitted');
sendEventToHTMLSource(src, 'FormResult', result);
case 'ClearForm'
sendEventToHTMLSource(src, 'FormCleared', '');
end
catch ME
fprintf('Error: %s\n', ME.message);
result = struct('status', 'error', 'message', ME.message);
sendEventToHTMLSource(src, 'FormResult', result);
end
end
最佳实践
UI 设计原则
以下示例中的 HTML 特意保持精简,以便聚焦于 MATLAB↔JS 的连接。内联样式(Arial、
#667eea → #764ba2紫蓝渐变、通用卡片)不是设计参考。这些正是需要避免的“AI 味”特征。对于用户实际会看到的任何面板,请采用matlab-uihtml-design中的视觉体系,并遵循其中的设计规范。
- 使用 CSS Grid 或 Flexbox 创建可适应不同窗口尺寸的响应式布局。一维布局用 Flexbox,二维布局用 Grid;如果
flex-wrap更简单,就不要默认使用 Grid - 实现悬停效果,提升用户体验并提供视觉反馈
- 为用户操作提供清晰的视觉反馈(按钮点击、表单提交、错误等),并为键盘用户提供可见的
:focus-visible状态 - 使用语义化 HTML 元素(button、input、form),提升无障碍性
- 有意识地选择配色方案(参见
matlab-uihtml-design中的现成样式)。避免默认的惯性选择:Inter/Roboto/Arial 字体、紫到蓝的渐变、侧边条纹border-left强调效果、渐变文字,以及圆角过大的(≥32px)卡片 - 验证对比度:正文和标签文字 ≥4.5:1,大号文字和 UI 边界 ≥3:1。为数字读数设置
font-variant-numeric: tabular-nums,避免数值宽度抖动 - 兼顾减少动态效果的偏好:将非必要动画放在
@media (prefers-reduced-motion: reduce)中,并提供交叉淡入淡出或即时显示的回退方案
性能优化
- 尽量减少 HTML 与 MATLAB 之间的数据传输——仅发送必要数据
- 使用合适的数据类型——数字、字符串、结构体(转换为 JSON)
- 为耗时的 MATLAB 操作实现加载指示器
- 适当使用 MATLAB 中的持久变量缓存结果
- 批量处理多次更新,而不是发送许多小事件
错误处理策略
JavaScript 端:
htmlComponent.addEventListener("Error", function(event) {
// Display user-friendly error messages
alert("Error: " + event.Data);
});
MATLAB 端:
try
result = processInput(input);
sendEventToHTMLSource(src, 'Success', result);
catch ME
fprintf('Error: %s\n', ME.message);
sendEventToHTMLSource(src, 'Error', 'Processing failed');
end
测试策略
-
单元测试——独立测试 MATLAB 函数
% Test individual processing functions assert(validateExpression('2+2'), 'Validation should pass'); -
集成测试——测试 HTML 与 MATLAB 之间的通信
% Test event handling with sample data testEvent = struct('HTMLEventName', 'Calculate', 'HTMLEventData', '2+2'); handleEvent(h, testEvent); -
用户测试——测试完整的用户工作流
- 尝试所有按钮组合
- 测试边界情况和无效输入
- 确认视觉反馈清晰易懂
-
错误测试——测试错误情况
- 无效输入字符
- 空输入字段
- 网络/超时情况
调试技巧
-
MATLAB 端:使用
fprintf()记录事件和数据fprintf('Received event: %s with data: %s\n', eventName, eventData); -
JavaScript 端:使用浏览器开发者工具(F12)进行调试
console.log("Sending to MATLAB:", data); -
分别测试每个通信方向
- 首先测试 MATLAB → JavaScript(Data 属性)
- 然后测试 JavaScript → MATLAB(事件)
- 最后测试双向通信
-
验证数据类型和格式
fprintf('Data type: %s\n', class(eventData)); fprintf('Data value: %s\n', string(eventData));
常见模式
模式 1:计算器模式
- JavaScript 根据按钮点击构建表达式字符串
- 通过
sendEventToMATLAB将表达式发送给 MATLAB - MATLAB 在输入验证后安全地求值
- 通过
sendEventToHTMLSource将结果发回 - 实时显示结果
模式 2:数据可视化模式
- JavaScript 处理用户交互(滑块、下拉菜单)
- 将参数发送给 MATLAB 进行计算
- MATLAB 处理数据并更新图表
- 可以使用 uiaxes 绘制 MATLAB 图表,也可以发送数据供 JavaScript 绘图
- 支持实时更新和动画
模式 3:表单处理模式
- JavaScript 将表单数据收集到结构化对象中
- 将整个表单数据作为单个事件发送
- MATLAB 验证每个字段
- 处理数据(保存、计算、导出)
- 将确认或错误消息发回
- 根据结果更新 UI
模式 4:实时监控模式
- MATLAB 持续生成数据(仿真、传感器读数)
- 定时通过
sendEventToHTMLSource发送更新 - JavaScript 实时更新显示
- 实现开始/停止/暂停控件
- 使用高效的数据格式(数组、结构体)
长时间运行的操作
当 MATLAB 驱动的操作耗时超过零点几秒(例如动画、积分扫描或流式仿真)时,以上模式需要补充一些内容:计时器、资源清理、取消操作的方式,以及在运行过程中调整操作参数的方式。
由计时器驱动的动画
使用 MATLAB timer 驱动操作。将句柄存储在 fig.UserData 中,这样其他事件处理程序(重置、停止、关闭图窗)就能找到并停止它。
function startAnimation(fig, ax, data)
stopAnimTimer(fig); % kill any prior run
state.data = data;
state.idx = 1;
state.line = animatedline(ax, 'Color', [0.65 0.55 0.94], 'LineWidth', 1.4);
state.step = 5; % points per tick (mutable from outside; see below)
tmr = timer( ...
'ExecutionMode', 'fixedSpacing', ...
'Period', 0.03, ...
'BusyMode', 'drop', ... % skip ticks if callback overruns
'TimerFcn', @(s, ~) animStep(s, fig));
tmr.UserData = state;
fig.UserData.AnimTimer = tmr;
start(tmr);
end
function animStep(tmr, fig)
if ~isvalid(fig) || ~isvalid(tmr), return; end
s = tmr.UserData;
iEnd = min(numel(s.data), s.idx + s.step - 1);
addpoints(s.line, s.data(s.idx:iEnd, 1), s.data(s.idx:iEnd, 2));
s.idx = iEnd + 1;
tmr.UserData = s;
drawnow limitrate;
if s.idx > numel(s.data)
stop(tmr); delete(tmr);
fig.UserData.AnimTimer = [];
% Notify JS that the operation completed
sendEventToHTMLSource(fig.UserData.UIHtml, 'OperationDone', struct('n', iEnd));
end
end
关键选择:
BusyMode='drop':如果回调耗时超过Period,则跳过下一个计时周期,而不是将其加入队列。这可以防止回调在运行缓慢的系统上堆积。drawnow limitrate:将渲染帧率限制在约 20 fps,同时仍处理 UI 事件,因此用户仍可进行交互(滚动、拖动滑块、点击“停止”)。- 自动清理:最后一个计时周期会停止并删除计时器,然后将存储的句柄置空。
关闭图窗时清理资源
关闭图窗时,必须拆除任何计时器、监听器或后台资源。 否则,这些资源会在重新运行时泄漏,还可能针对已删除的图形对象触发回调。
fig.UserData = struct('AnimTimer', [], 'UIHtml', h, 'Axes', ax);
fig.CloseRequestFcn = @(s, ~) closeFig(s);
function closeFig(fig)
stopAnimTimer(fig);
delete(fig);
end
function stopAnimTimer(fig)
if ~isvalid(fig) || ~isfield(fig.UserData, 'AnimTimer'), return; end
tmr = fig.UserData.AnimTimer;
if ~isempty(tmr) && isvalid(tmr)
try, stop(tmr); catch, end
delete(tmr);
end
fig.UserData.AnimTimer = [];
end
stopAnimTimer 也会供“重置”和“停止”处理程序复用,因此只需维护一条清理路径。
可取消的操作(运行 ↔ 停止)
对于用户可能想要取消的操作,可以通过 JS 状态机复用主操作按钮。不要添加单独的“停止”按钮;面板会变得拥挤,而根据上下文变化的单个按钮符合播放/暂停的用户体验。
// JS state machine
let runState = "idle"; // 'idle' | 'running' | 'cancellable'
function setRunning(state) {
runState = state;
const b = document.getElementById("runBtn");
b.classList.remove("busy");
if (state === "idle") {
b.textContent = "Run";
b.disabled = false;
} else if (state === "running") { // brief, non-cancellable
b.textContent = "Run";
b.classList.add("busy");
b.disabled = true;
} else if (state === "cancellable") { // long, user can stop
b.textContent = "Stop";
b.classList.add("busy");
b.disabled = false;
}
}
function onRunClick() {
if (runState === "cancellable") {
window.htmlComponent.sendEventToMATLAB("StopOperation", "");
return;
}
setRunning("cancellable");
window.htmlComponent.sendEventToMATLAB("RunOperation", collectParams());
}
在 MATLAB 端,StopOperation 会停止计时器,保留已有的部分输出(以便用户保留已计算的结果),并触发一个事件,让 JS 将按钮切回“运行”:
case 'StopOperation'
stopAnimTimer(fig);
s = lastState(fig); % whatever partial result you tracked
sendEventToHTMLSource(src, 'OperationStopped', s);
向 JS 区分以下三种完成事件:
| 事件 | 时机 |
|---|---|
OperationDone | 自然完成(完整结果) |
OperationStopped | 用户点击“停止”(部分结果) |
OperationError | 捕获到异常(错误消息) |
这三种事件都应在 JS 中调用 setRunning("idle"),使按钮恢复为“运行”。
操作运行期间的实时控制
如果用户想在操作运行期间调整参数(动画速度、仿真速率、绘图范围),JS 滑块的 input 事件需要向 MATLAB 发送更新,而不能重启操作。
两条规则:
-
限制 JS 端的发送频率,避免拖动滑块时向 MATLAB 发送过多事件:
let sendTimer = null; document.getElementById("speed").addEventListener("input", function() { if (sendTimer) return; sendTimer = setTimeout(function() { const v = parseInt(document.getElementById("speed").value, 10); window.htmlComponent.sendEventToMATLAB("SetSpeed", v); sendTimer = null; }, 80); // ~12 events/sec max }); -
原地更新实时计时器的
UserData。不要停止并重新启动计时器:case 'SetSpeed' tmr = fig.UserData.AnimTimer; if ~isempty(tmr) && isvalid(tmr) s = tmr.UserData; s.step = max(1, min(50, round(double(data)))); % clamp + sanitize tmr.UserData = s; end % no-op if no animation running; the slider still updates its label
MATLAB 会在主线程上串行执行所有回调,因此计时器周期和 SetSpeed 处理程序绝不会并发执行。从计时器周期的角度看,UserData 更新是原子的。
保存输出(从 uiaxes 导出)
常见需求:在 HTML 中添加“保存图像”按钮,以导出当前绘图。使用 uiputfile + exportgraphics:
case 'ExportImage'
ax = fig.UserData.Axes;
if isempty(ax.Children)
sendEventToHTMLSource(src, 'ExportError', 'Nothing to export');
return;
end
defaultName = sprintf('plot_%s.png', string(datetime('now', 'Format', 'yyyyMMdd_HHmmss')));
[file, path] = uiputfile({'*.png';'*.jpg';'*.pdf'}, 'Export plot', defaultName);
if isequal(file, 0)
sendEventToHTMLSource(src, 'ExportComplete', 'cancelled');
return;
end
try
exportgraphics(ax, fullfile(path, file), 'Resolution', 300, ...
'BackgroundColor', fig.Color);
sendEventToHTMLSource(src, 'ExportComplete', file);
catch ME
sendEventToHTMLSource(src, 'ExportError', ME.message);
end
将 fig.Color 作为 BackgroundColor 传入,使导出结果与屏幕上图窗的背景一致。这对深色主题尤为重要,因为默认的白色背景看起来不协调。
实现检查清单
部署 uihtml 应用前,请确认:
- HTML 文件位于正确的位置
-
HTMLSource属性已设置为正确的文件路径 - 已定义
HTMLEventReceivedFcn回调 - 已实现 JavaScript
setup(htmlComponent)函数 - 已添加用于 MATLAB→JS 通信的事件监听器
- 所有 MATLAB 事件处理均由 try-catch 代码块包裹
- 已对所有用户数据实施输入验证
- 已将错误事件发送回 JavaScript,以便向用户提供反馈
- 已一致地应用 CSS 样式(参见
matlab-uihtml-design),避免 AI 生成内容的常见特征(紫蓝渐变、侧边条纹边框、渐变文本、圆角过大的卡片、通用 Arial/Inter 字体) - 已验证对比度(正文/标签 ≥4.5:1,大号文本和 UI 边界 ≥3:1)
- 已提供减少动态效果的回退方案(
@media (prefers-reduced-motion: reduce)) - 键盘焦点清晰可见(
:focus-visible),没有仅用outline: none将其移除 - 已在不同窗口尺寸下测试响应式设计
- 所有用户交互都会提供视觉反馈
- 长时间操作会显示加载指示器
- 文件组织符合项目结构
- 已创建包含使用说明的文档(README)
- 已将资源清理逻辑连接到
CloseRequestFcn:图窗关闭时,任何timer、监听器或后台任务都会停止并删除。否则,用户关闭应用后,回调可能会在已删除的图形对象上触发。 - 长时间操作可取消:如果操作可能运行超过约 1 秒,用户可以停止操作,而无需强行关闭图窗。
故障排除
问题:uihtml 组件未加载 HTML 文件
- 解决方案:检查文件路径是绝对路径,还是相对于当前目录的路径
h.HTMLSource = fullfile(pwd, 'app.html'); % Absolute path
问题:事件未触发 MATLAB 回调
- 解决方案:确认在 HTML 加载前已设置
HTMLEventReceivedFcn - 解决方案:检查 JavaScript 是否正确调用
sendEventToMATLAB
问题:JavaScript 中的数据未更新
- 解决方案:确保已在
setup()中注册DataChanged事件监听器 - 解决方案:确认 MATLAB 设置的是
h.Data属性,而不是发送事件
问题:浏览器控制台中出现 JavaScript 错误
- 解决方案:打开浏览器开发者工具(F12)查看详细错误消息
- 解决方案:确保已将
htmlComponent传递给setup()函数 - 解决方案:检查元素 ID 和函数名称是否有拼写错误
问题:MATLAB 错误未显示给用户
- 解决方案:在 MATLAB 和 JavaScript 中都实现错误事件处理
- 解决方案:在 MATLAB 中使用 try-catch,并通过
sendEventToHTMLSource发送错误消息
问题:发送数据时性能较慢
- 解决方案:降低更新频率(对事件进行节流)
- 解决方案:仅发送已更改的数据,而非整个数据集
- 解决方案:使用合适的数据类型(数字而非字符串,或反之)
问题:复杂数据结构传输不正确
- 解决方案:使用 MATLAB 结构体(会自动转换为 JSON)
- 解决方案:避免使用嵌套元胞数组;改用结构体数组
- 解决方案:先用简单示例测试数据传输
问题:样式未正确显示
- 解决方案:确认 CSS 位于
<head>内的<style>块中 - 解决方案:检查 CSS 语法是否有错误
- 解决方案:使用浏览器开发者工具检查计算样式
更多资源
- MATLAB 文档:
doc uihtml - HTML/CSS/JavaScript:MDN Web Docs
- 事件处理:
doc sendEventToHTMLSource - 图窗创建:
doc uifigure - 调试:使用浏览器开发者工具(F12)
相关技能
- matlab-uihtml-design:提供可直接使用的 HTML 模板和设计规范,包含多种视觉设计风格(Clean、Material、Cosmic Dark 等)。可用于设置 uihtml 控件的样式。