🧬

Dioxus v0.6 でデスクトップアプリを作る実装 Tips

に公開

概要

Rust のフレームワーク「Dioxus」と画像処理ライブラリ「OpenCV」を組み合わせて、テンプレートマッチングアプリを開発した際の実装 Tips をまとめました。

環境

  • Rust 1.86.0
  • Dioxus CLI 0.6.3
  • OpenCV 4.11.0
  • npm 10.8.2
  • Tailwind CSS 4.1.7
  • daisyUI 5.0.35

作ったもの

  • Windows の画面をキャプチャし、指定したテンプレート画像と一致する箇所を検出
  • GUI でテンプレート画像や検出感度などを設定可能
  • 検出時に通知やサウンド、Discord 通知も送信できる

リポジトリ

GitHub: rust-screen-suite

実装 Tips

VSCode

拡張機能 (Extensions)

  • Dioxus の開発に便利な拡張機能を .vscode/extensions.jsonrecommendations に追加します。

    .vscode\extensions.json
    {
      "recommendations": [
        "rust-lang.rust-analyzer",
        "DioxusLabs.dioxus",
      ]
    }
    

設定 (Settings)

  • rust-analyzer を使用し、Rust ファイル保存時に自動フォーマットをおこないます。

    .vscode\settings.json
    {
        "editor.defaultFormatter": "rust-lang.rust-analyzer",
        "[rust]": {
            "editor.defaultFormatter": "rust-lang.rust-analyzer",
            "editor.formatOnSave": true
        }
    }
    

プロジェクト構成

  • asset_dir - 静的アセット(HTML ファイルや i18n の ftl ファイルなど)を格納するディレクトリを指定します。

    Dioxus.toml
    [application]
    asset_dir = "public"
    

Tailwind CSS

  • Tailwind を使用するには Tailwind CSS, Tailwind Cli をインストールする必要があります。

    npm install tailwindcss @tailwindcss/cli
    
  • 次の内容の css ファイルを作成します。

    tailwind.css
    @import "tailwindcss";
    
  • CLI ツールを実行して、ソースファイル内のクラスをスキャンし、CSS を生成します。

    npx tailwindcss -i ./tailwind.css -o ./assets/styling/tailwind.css --watch
    

daisyUI

  • daisyUI を使用するには Tailwind plugin をインストールする必要があります。

    npm i -D daisyui@latest
    
  • 次の内容の css ファイルを作成します。

    tailwind.css
    @import "tailwindcss";
    @plugin "daisyui";
    

テーマ

  • daisyUI のテーマは tailwind.css の @plugin "daisyui" で設定できます。

    tailwind.css
    @import "tailwindcss";
    @plugin "daisyui" {
      themes: light --default, dark --prefersdark;
    }
    

    すべてのテーマを有効にしたい場合:

    tailwind.css
    @import "tailwindcss";
    @plugin "daisyui" {
      themes: all;
    }
    
  • テーマの切り替えは、HTML 要素に data-theme 属性を指定することで行えます。

    #[component]
    fn App() -> Element {
        rsx! {
            div {
                "data-theme": "cupcake",
                // ...
            }
        }
    }
    

Localization (多言語対応)

多言語対応には dioxus-i18n クレートを使用します。

  • dioxus-i18n を使うには unic-langid も必要です。

    Cargo.toml
    [dependencies]
    dioxus-i18n = "0.4.3"
    unic-langid =  "0.9.0"
    
  • ftl ファイルのメッセージ定義は「キー = 値」で記述し、変数は { $name } のように埋め込みます。

    public\i18n\ja-JP.ftl
    greeting = こんにちは、{ $name }さん!
    
  • App() で初期化と言語設定を行います。

    src\main.rs
    use dioxus_i18n::prelude::*;
    use unic_langid::langid;
    
    #[component]
    fn App() -> Element {
        use_init_i18n(|| {
            I18nConfig::new(langid!("en-US"))
                .with_locale((langid!("en-US"), include_str!("../public/i18n/en-US.ftl")))
                .with_locale((langid!("ja-JP"), include_str!("../public/i18n/ja-JP.ftl")))
        });
    
        let mut i18n = i18n();
        i18n.set_language(langid!("en-US"));
    }
    
  • 翻訳テキストの利用例(t!マクロでメッセージを取得)

    use dioxus_i18n::t;
    
    rsx! {
      span { class: "text-lg font-bold", {t!("greeting", name: "hoge")} }
    }
    

t!マクロ利用時の注意

t!マクロはDioxus のリアクティブスコープ(コンポーネント本体や use_effect など)内でのみ安全に利用できます
非同期タスクやuse_futureの中など、リアクティブスコープ外でt!マクロを使うとパニックが発生します

安全な使い方:

  • コンポーネントのrsx!内やuse_effect内で使う
  • バックグラウンドスレッドや非同期タスク内で使いたい場合は、必要なテキストを事前に取得して値として渡す

例:NG パターン

use_future(move || async move {
    // ここでt!()を使うとパニック
    let msg = t!("greeting", name: "hoge");
    // ...
});

例:OK パターン

use_effect(move || {
    let msg = t!("greeting", name: "hoge");
    // ...
});

OpenCV

テンプレートマッチングには opencv クレートを使用します。

  • エラー「error: failed to run custom build command for opencv v0.94.4」が発生する場合は、features に "clang-runtime" を追加してください。

    Cargo.toml
    [dependencies]
    opencv = {version = "0.94.4", features = ["clang-runtime"]}
    

Scrap

画面キャプチャには Scrap クレートを使用します。

  • キャプチャごとに Capturer::new を実行するとメモリリークの原因になるため、インスタンスを使い回すようにしましょう。

    scrap/examples/screenshot.rs
    // https://github.com/quadrupleslap/scrap/blob/master/examples/screenshot.rs
    let display = Display::primary().expect("Couldn't find primary display.");
    let mut capturer = Capturer::new(display).expect("Couldn't begin capture.");
    

OSS Licenses

使用した OSS のライセンスの一覧を作成します。

  • クレートのライセンスファイルの生成は cargo-about クレートを使用します。

    cargo install --locked cargo-about
    cargo about init
    cargo about generate about.hbs --output-file public/licenses/rust-licenses.html
    
  • Node モジュールのライセンスファイル生成には license-checker-rseidelsohn を使います。

    npx license-checker-rseidelsohn --markdown > public/licenses/node-licenses.md
    

ターミナルウィンドウを非表示にする

Windows で GUI アプリを実行すると、デフォルトではターミナル(コンソール)ウィンドウが同時に表示されます。
これを非表示にするには、windows_subsystem = "windows" 属性を追加します。

  • Cargo の features にbundleを追加します。

    Cargo.toml
    [features]
    bundle = []
    
  • main.rs の先頭に以下の属性を追加します。

    main.rs
    #![cfg_attr(feature = "bundle", windows_subsystem = "windows")]
    

パニックを検知してアプリを終了する

アプリでパニックが発生した場合、メッセージダイアログを表示してアプリを安全に終了するようにします。

  • メッセージダイアログの表示には windows-sys クレートを使用します。

    Cargo.toml
    [dependencies.windows-sys]
    version = "0.60"
    features = [
        "Win32_Security",
        "Win32_System_Threading",
        "Win32_UI_WindowsAndMessaging",
    ]
    
  • main() でパニックフックを設定し、パニック発生時にメッセージダイアログを表示してアプリを終了します。

    main.rs
    use std::{panic, process};
    
    fn main() {
        // Set a custom panic hook to handle panics gracefully
        panic::set_hook(Box::new(|info| {
            // Log the panic information
            #[cfg(windows)]
            {
                use std::ffi::OsStr;
                use std::os::windows::ffi::OsStrExt;
                use windows_sys::Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_ICONERROR, MB_OK};
    
                let msg = format!(
                    "An error occurred, and the application will now exit.\n\nDetails: {}",
                    info
                );
                let wide: Vec<u16> = OsStr::new(&msg).encode_wide().chain(Some(0)).collect();
                let title: Vec<u16> = OsStr::new("Alert Sentinel")
                    .encode_wide()
                    .chain(Some(0))
                    .collect();
                unsafe {
                    // Show a message box with the panic information
                    MessageBoxW(0 as _, wide.as_ptr(), title.as_ptr(), MB_OK | MB_ICONERROR);
                }
            }
            // Log the panic information to stderr
            eprintln!("Application panicked: {info}");
            // Exit the application with a non-zero status code
            process::exit(1);
        }));
        // ...
    }
    

その他

  • 通知音の再生は rodio クレートを使用しました。
  • Discord 連携は discord-webhook-rs クレートを使用しました。

メモリ使用量の比較(C#プロトタイプとの参考比較)

今回のアプリを開発する前に、C#(Windows フォームアプリ)でプロトタイプを実装していました。
実装内容や機能が完全に同じではなく、厳密な計測を行ったわけではありませんが、
C#版ではメモリ使用量が約 2GB だったのに対し、Rust 版では約 200MB と大幅に削減できました。

あくまで参考値ですが、Rust での実装はメモリ効率の面でも大きなメリットがあると感じました。

Discussion