mcpField
M
matlab-uihtml-app-builder

matlab-uihtml-app-builder

github.com/matlab开发192130 次安装2 周前更新
代码仓库

关于 matlab-uihtml-app-builder

matlab-uihtml-app-builder 是指导使用 MATLAB 的 uihtml 构建交互式应用的技能,面向将 HTML、JavaScript 界面与 MATLAB 计算结合的开发者。内容涵盖双向通信、数据验证、错误处理、测试及长时运行操作。

主要功能

  1. 01组合 HTML、JavaScript 与 MATLAB 后端
  2. 02通过属性和事件双向传递数据
  3. 03验证输入并处理事件错误
  4. 04提供界面设计与性能建议
  5. 05覆盖测试、调试和长时操作模式

适用场景

  1. 01构建交互式 MATLAB 计算器
  2. 02制作数据可视化应用界面
  3. 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 工具

核心架构

四个组成部分
  1. HTML 界面 - 包含按钮、表单和显示区域的用户界面
  2. JavaScript 逻辑 - 处理事件和 UI 交互
  3. MATLAB 后端 - 计算引擎和数据处理
  4. 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} 会变为 MATLAB struct('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
测试策略
  1. 单元测试——独立测试 MATLAB 函数

    % Test individual processing functions
    assert(validateExpression('2+2'), 'Validation should pass');
    
  2. 集成测试——测试 HTML 与 MATLAB 之间的通信

    % Test event handling with sample data
    testEvent = struct('HTMLEventName', 'Calculate', 'HTMLEventData', '2+2');
    handleEvent(h, testEvent);
    
  3. 用户测试——测试完整的用户工作流

    • 尝试所有按钮组合
    • 测试边界情况和无效输入
    • 确认视觉反馈清晰易懂
  4. 错误测试——测试错误情况

    • 无效输入字符
    • 空输入字段
    • 网络/超时情况
调试技巧
  • 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 发送更新,而不能重启操作。

两条规则:

  1. 限制 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
    });
    
  2. 原地更新实时计时器的 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 控件的样式。

评论

支持 Markdown