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. 01HTML、JavaScript、MATLABバックエンドの連携
  2. 02プロパティとイベントによるデータ交換
  3. 03入力検証とイベントエラー処理
  4. 04UI設計と性能に関する指針
  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 コンポーネントを使用して、HTML/JavaScript インターフェースと MATLAB の計算バックエンドを組み合わせたインタラクティブな Web アプリケーションを構築する方法を説明します。HTML 側が UI を処理し、MATLAB が計算を行います。

このスキルを使用する場面

  • HTML/JavaScript インターフェースを備えたインタラクティブな MATLAB アプリを構築する場合
  • MATLAB アプリケーション向けの Web ベース UI を作成する場合
  • HTML/CSS/JS を使用してレスポンシブな MATLAB GUI を構築する場合
  • ユーザーが uihtml、HTML、JavaScript、Web アプリ、Web インターフェース、インタラクティブ GUI について言及した場合
  • Web UI の設計と MATLAB の計算能力を組み合わせる場合
  • 電卓アプリ、データ可視化ツール、フォームベースの MATLAB ツールを作成する場合

基本アーキテクチャ

4 つのコンポーネント
  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 オブジェクトは、すでに MATLAB の struct に変換された状態で event.HTMLEventData に届きます。jsondecode を呼び出さないでください。struct に対して実行すると失敗します。
  • sendEventToHTMLSource 経由で送信された MATLAB の struct は、すでに JavaScript オブジェクトとして event.Data に届きます。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
    
  • MATLAB で処理する前に、JavaScript からのすべての入力を必ず検証してください。

  • 厳格なサニタイズを行わずに、ユーザー入力に対して eval() を絶対に使用しないでください。

  • 式に使用できる文字を、ユーザー入力で必ず制限してください。

エラー処理

MATLAB のイベントハンドラーは必ず try-catch ブロックで囲んでください。

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 を使用して、さまざまなウィンドウサイズに適応するレスポンシブレイアウトを作成します。1 次元には Flexbox、2 次元には 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 でデータを処理し、プロットを更新します
  • MATLAB のプロットには uiaxes を使用するか、JavaScript でプロットするためのデータを送信します
  • リアルタイムの更新とアニメーションをサポートします
パターン 3:フォーム処理パターン
  • JavaScript でフォームデータを収集し、構造化されたオブジェクトにまとめます
  • フォームデータ全体を単一のイベントとして送信します
  • MATLAB で各フィールドを検証します
  • データを処理します(保存、計算、エクスポート)
  • 確認メッセージまたはエラーメッセージを返します
  • 結果に基づいて UI を更新します
パターン 4:リアルタイム監視パターン
  • MATLAB でデータを継続的に生成します(シミュレーション、センサーの読み取り)
  • 間隔を空けて sendEventToHTMLSource を使い、更新情報を送信します
  • JavaScript で表示をリアルタイムに更新します
  • 開始/停止/一時停止のコントロールを実装します
  • 効率的なデータ形式(配列、構造体)を使用します

長時間実行される操作

MATLAB がアニメーション、積分のスイープ、ストリーミングシミュレーションなど、数分の一秒を超える操作を実行する場合、上記のパターンにタイマー、リソースのクリーンアップ、キャンセル方法、実行中に操作を調整する方法を追加する必要があります。

タイマー駆動のアニメーション

MATLAB の timer を使って操作を駆動します。ハンドルを fig.UserData に保存しておけば、ほかのイベントハンドラー(Reset、Stop、図のクローズ)からタイマーを見つけて停止できます。

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 イベントを処理するため、ユーザーは引き続き操作(スクロール、スライダーのドラッグ、Stop のクリック)できます。
  • 自動クリーンアップ:最後のティックでタイマーを停止して削除し、保存したハンドルを空にします。
図を閉じるときのリソースのクリーンアップ

**図を閉じるときは、タイマー、リスナー、バックグラウンドリソースを必ず終了してください。**そうしないと、再実行をまたいでリソースが残り、削除済みのグラフィックスオブジェクトに対してコールバックが実行される可能性があります。

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 を Reset と Stop のハンドラーで再利用するため、保守するクリーンアップ経路は1つだけです。

キャンセル可能な操作(実行 ↔ 停止)

ユーザーがキャンセルする可能性のある操作では、JS の状態マシンを使ってメインのアクションボタンを切り替えます。Stop ボタンを別に追加しないでください。パネルが煩雑になり、状況に応じて変化する1つのボタンのほうが、再生/一時停止の UX に合っています。

// 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 に送信する完了イベントを次の3種類に分けます。

イベント発生時
OperationDone自然終了(完全な結果)
OperationStoppedユーザーが Stop をクリック(部分的な結果)
OperationError例外を捕捉(エラーメッセージ)

ボタンを「実行」に戻すため、3種類すべてで JS の setRunning("idle") を呼び出してください。

操作の実行中にリアルタイムで制御する

操作の実行中にユーザーがパラメーター(アニメーションの速度、シミュレーションのレート、プロットの範囲など)を調整したい場合、JS のスライダーの input イベントで、操作を再開せずに MATLAB へ更新を送信する必要があります。

次の2つのルールに従います。

  1. スライダーのドラッグによって MATLAB にイベントが殺到しないよう、JS 側の送信をスロットリングします。

    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

BackgroundColor として fig.Color を渡すと、エクスポートした画像の背景が画面上の図の背景と一致します。デフォルトの白い背景では不自然になるダークテーマで特に重要です。

実装チェックリスト

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 に設定されている:Figure が閉じられたときに、あらゆる timer、リスナー、バックグラウンドタスクが停止・削除される。これを行わないと、ユーザーがアプリを閉じた後に、削除済みのグラフィックスに対してコールバックが実行されます。
  • 時間のかかる処理をキャンセルできる:処理に約 1 秒以上かかる場合、Figure を強制終了せずにユーザーが処理を停止できます。

トラブルシューティング

問題:uihtml コンポーネントで HTML ファイルが読み込まれない

  • 解決策:ファイルパスが絶対パス、または現在のディレクトリからの相対パスであることを確認します
    h.HTMLSource = fullfile(pwd, 'app.html');  % Absolute path
    

問題:イベントが MATLAB のコールバックをトリガーしない

  • 解決策:HTML の読み込み前に HTMLEventReceivedFcn が設定されていることを確認します
  • 解決策:JavaScript から sendEventToMATLAB が正しく呼び出されていることを確認します

問題:JavaScript のデータが更新されない

  • 解決策:DataChanged イベントリスナーが setup() に登録されていることを確認します
  • 解決策:MATLAB がイベントを送信するのではなく、h.Data プロパティを設定していることを確認します

問題:ブラウザーのコンソールに JavaScript エラーが表示される

  • 解決策:ブラウザーの開発者ツール(F12)を開いて、詳細なエラーメッセージを確認します
  • 解決策:setup() 関数に htmlComponent が渡されていることを確認します
  • 解決策:要素 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
  • Figure の作成:doc uifigure
  • デバッグ:ブラウザーの開発者ツール(F12)を使用します

関連スキル

  • matlab-uihtml-design:すぐに使える HTML テンプレートとデザイン仕様を含むビジュアルデザインスタイル(Clean、Material、Cosmic Dark など)。uihtml コントロールのスタイリングに使用します。

コメント