2024年6月15日土曜日

Tauri で各プラットフォーム向けのアイコンを生成する

掲題の通り。

前提

  • 開発環境構築は済んでいるものとする
  • Tauri:
    • tauri-cli: 1.5.14
    • rust: 1.78.0
    • node: v22.2.0
  • この回 で作成した delete-default-event をベースに作業をしたので、そのディレクトリとの差分を取ってください

ベースのアイコンを生成

TIL/svg/icon/myicon.svg at master · mikoto2000/TIL に SVG 形式の自分のアイコンがあるので、これを 1024x1024 の背景透過 png へ変換する。

※この作業は Windows の ImageMagick を使って実行した

magick.exe convert -background transparent .\myicon.svg -resize 1024x1024 .\myicon.png

これで生成した png を、プロジェクトルートの一つ上のディレクトリへ配置した。

png を基に各プラットフォーム向けのアイコンを生成

プロジェクトルートの一つ上に配置したアイコンを、 npm run tauri icon コマンドに渡すと、各プラットフォーム向けのアイコンを生成してくれる。

$ npm run tauri icon ../myicon.png

> tauri-delete-default-event@0.0.0 tauri
> tauri icon ../myicon.png

        Appx Creating StoreLogo.png
        Appx Creating Square30x30Logo.png
        Appx Creating Square44x44Logo.png
        Appx Creating Square71x71Logo.png
        Appx Creating Square89x89Logo.png
        Appx Creating Square107x107Logo.png
        Appx Creating Square142x142Logo.png
        Appx Creating Square150x150Logo.png
        Appx Creating Square284x284Logo.png
        Appx Creating Square310x310Logo.png
        ICNS Creating icon.icns
         ICO Creating icon.ico
         PNG Creating 128x128@2x.png
         PNG Creating icon.png
         PNG Creating 32x32.png
         PNG Creating 128x128.png

src-tauri/icons に生成されたファイルが格納される。

生成後のアイコンたち

アプリを起動すると、タスクバーのアイコンが今回せいせいした物に代わっているのがわかる。

以上。

参考資料

Tauri でフロントエンド↔バックエンドでイベントを送信しあう

今回は、フロントエンドとバックエンドでイベントの送受信を行う。 Events | Tauri Apps の内容。

実行画面

前提

  • 開発環境構築は済んでいるものとする
  • Tauri:
    • tauri-cli: 1.5.14
    • rust: 1.78.0
    • node: v22.2.0
  • 前々々回 作成した delete-default-event をベースに作業をしたので、そのディレクトリとの差分を取ってください
  • ウィンドウについてはまだ知らないので、グローバルイベントのみ作ってみる

実装

以下の実装を行う。

  1. フロントエンドのボタン押下時に greet イベントを送信
  2. バックエンドは、 greet イベントをリッスン
  3. バックエンドは、 greet イベントが発火した 2 秒後に、 greet_ended イベントを発火する
  4. フロントエンドは、 greet_ended イベントをリッスン
  5. フロントエンドは、 greet_ended イベントが発火したら、受信したメッセージを画面に表示する

フロントエンド

listen でイベントを待ち受け、 emit でイベントを送信する。

...(snip)
import { emit, listen } from '@tauri-apps/api/event'
...(snip)
  const [greetEndedMsg, setGreetEndedMsg] = useState("");

  {/* `greet_ended` イベントをリッスン */}
  listen('greet_ended', (event) => {
    setGreetEndedMsg(JSON.stringify(event));
  })
...(snip)
      <form
        className="row"
        onSubmit={(e) => {
          e.preventDefault();
          setGreetMsg(`Hello, ${name}! You've been greeted from Rust!`);

          {/*ボタン押下で `greet` イベントを emit */}
          emit('greet', { greet_message: name })
        }}
      >
        <input
          id="greet-input"
          onChange={(e) => setName(e.currentTarget.value)}
          placeholder="Enter a name..."
        />
        <button type="submit">Greet</button>
      </form>

      <p>{greetMsg}</p>

      {/* 受信した `greet_ended` イベントのメッセージを表示 */}
      <p>{greetEndedMsg}</p>
...(snip)

バックエンド

フロントエンドから受け取るイベントのペイロードは、「JSON 文字列」。

フロントエンドへ送るイベントのペイロードは、「serde::Serialize を付与した構造体」。

こちらもフロントエンドと似たような形で、 App#listen_global でグローバルイベントを待ち受け、 AppHandle#emit_all でイベントをグローバルに送信する。

...(snip)
use std::{thread, time::Duration};

use tauri::Manager;

// フロントエンドから受信したメッセージをデシリアライズするための構造体
#[derive(Clone, Debug, serde::Deserialize)]
struct GreetPayload {
  greet_message: String,
}

// フロントエンドへ送信するメッセージをシリアライズするための構造体
#[derive(Clone, Debug, serde::Serialize)]
struct GreetEndedPayload {
  greet_ended_message: String,
}

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let app_handle = app.handle();

            // イベント `greet` の listen 開始
            // listen を止めたいことがある場合、
            // 戻り値の id を使って `app.unlisten(id)` とする
            let _id = app.listen_global("greet", move |event| {
                let emit_payload_json = event.payload().unwrap();
                println!("EmitPayloadJson: {:?}", emit_payload_json);

                // JSON 文字列で受け取るので、構造体へデシリアライズ
                let emit_payload = serde_json::from_str::<GreetPayload>(&emit_payload_json);
                println!("GreetPayload: {:?}", emit_payload);

                // 構造体からメッセージを抜き出す
                let greet_message = emit_payload.unwrap().greet_message;
                println!("GreetPayload.greet_message: {:?}", greet_message);

                // 2 秒後にイベント `greet_ended` を発火
                thread::sleep(Duration::from_millis(2000));
                let greet_message = format!("greet end by message: {}", greet_message);
                let app_handle = app_handle.clone();
                app_handle.emit_all("greet_ended", GreetEndedPayload {
                    greet_ended_message: greet_message
                }).unwrap();
            });
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

以上。

参考資料

Tauri のフロントエンド(TypeScript)からバックエンド(Rust)を呼び出す

今回は、フロントエンド(TypeScript)からバックエンド(Rust)を呼び出すための仕組みである、 Command の使い方を勉強する。

実行画面

前提

  • 開発環境構築は済んでいるものとする
  • Tauri:
    • tauri-cli: 1.5.14
    • rust: 1.78.0
    • node: v22.2.0
  • 前々回 作成した delete-default-event をベースに作業をしたので、そのディレクトリとの差分を取ってください

実装 その1 - 引数・戻り値の無いコマンド

コマンドを実装(バックエンド)

コマンドの実体である関数を定義(バックエンド)

src-tauri/src/main.rs:

...(snip)
#[tauri::command]
fn implemented_command_function() {
    println!("Called implementedCommandFunction!!!!!");
}
...(snip)

戻り値のないコマンドを定義した。やるべきことは以下 2 つ。

  1. コマンド処理関数の実装
  2. #[tauri::command] アノテーションの付与

これでコマンド定義の完了。

コマンドをアプリに登録(バックエンド)

定義したコマンドを、アプリが使えるようにするため、登録処理を行う。

src-tauri/src/main.rs:

...(snip)
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![implemented_command_function])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

invoke_handler の行を追加。

これで、アプリがコマンドを呼び出す準備ができた。

コマンドを呼び出す(フロントエンド)

src/App.tsx:

...(snip)
import { invoke } from '@tauri-apps/api/tauri'
...(snip)
  function callImplementedCommandFunction() {
    invoke('implemented_command_function');
  }
...(snip)
      <div>
        <label>
          Call implemented_command_function command:
          <button
            onClick={() => {callImplementedCommandFunction()}}>
            call
          </button>
        </label>
      </div>
...(snip)

invoke でコマンドを呼び出せる。

ボタンを追加し、イベントハンドラを登録し、そこから invoke で implemented_command_function を呼び出すようにした。

動作確認 その1 - 引数・戻り値の無いコマンド

今回は、コマンドライン引数が絡まないので、dev コマンドで開発用サーバーを立ち上げ、動作確認を行う。

$ npm run tauri dev

> tauri-delete-default-event@0.0.0 tauri
> tauri dev

     Running BeforeDevCommand (`npm run dev`)

> tauri-delete-default-event@0.0.0 dev
> vite


  VITE v5.2.13  ready in 103 ms

  ➜  Local:   http://localhost:1420/
  ➜  Network: use --host to expose
        Info Watching /workspaces/TIL/tauri/1.0.0/CallingRustFromTheFrontend/tauri-delete-default-event/src-tauri for changes...
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
実行画面

call ボタンを押下すると、以下メッセージがターミナルに表示される。

Called implemented_command_function!!!!!

OK っぽい。

実装 その2 - 引数・戻り値のあるコマンド

コマンドを実装(バックエンド)

引数・戻り値がある以外は「その1」と同様。

src-tauri/src/main.rs:

...(snip)
#[tauri::command]
fn sum(x:i64, y: i64) -> i64 {
    return x + y;
}
...(snip)
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            implemented_command_function,
            sum])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

これで、アプリがコマンドを呼び出す準備ができた。

コマンドを呼び出す(フロントエンド)

以下 3 点を理解しつつ追加していく。

  1. 引数は、バックエンドで定義した仮引数名をパラメーターとして持つ JSON オブジェクトとして定義し、 invoke へ渡す
  2. フロントエンドで使う仮引数名は、「ローワーキャメルケース」で定義する(Tauri が変換処理を行う)
  3. invoke の戻り値は、 then の引数として受け取る。

src/App.tsx:

  const [sumResult, setSumResult] = useState("");
...(snip)
  function callSum() {
    invoke('sum', {xForSum: 1, yForSum: 2})
      .then((result) => setSumResult(result));
  }
...(snip)
      <div>
        <label>
          Execute 1 + 2: {sumResult}
          <button
            onClick={() => {callSum()}}>
            call
          </button>
          <button
            onClick={() => setSumResult("")}>
            Clear
          </button>
        </label>
      </div>
...(snip)

動作確認 その2 - 引数・戻り値のあるコマンド

今回追加した call ボタンを押下すると、Rust で計算された結果が表示される。

実装 その3 - 非同期コマンド

コマンドを実装(バックエンド)

関数に async 修飾子が付く以外は「その1」と同様。

今回は簡便にカウンタを用意するために static mut なグローバル変数と unsafe を使って実装。

src-tauri/src/main.rs:

...(snip)
static mut CALL_NUMBER : u64 = 0;
#[tauri::command]
async fn async_command() -> String {
    println!("Called asyncCommand!!!!!");
    let call_number = unsafe { CALL_NUMBER }
    ;
    unsafe { CALL_NUMBER += 1 }
    thread::sleep(Duration::from_millis(2000));
    return format!("Called asyncCommand {}.", call_number)
}
...(snip)
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            implemented_command_function,
            sum,
            async_command])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

これで、アプリがコマンドを呼び出す準備ができた。

コマンドを呼び出す(フロントエンド)

普通の戻り値ありのものと同じように呼び出せば OK.

src/App.tsx:

  const [asyncCommandResult, setAsyncCommandResult] = useState([]);
...(snip)
  function callAsyncCommand() {
    invoke('async_command')
      .then((result) => {
        setAsyncCommandResult((x) => [...x, result])
      })
  }
...(snip)
      <div>
        <label>
          Execute asyncCommand:
          <button
            onClick={() => {callAsyncCommand()}}>
            call
          </button>
          <button
            onClick={() => setAsyncCommandResult([])}>
            Clear
          </button>
        </label>
        <ol>
          {
            asyncCommandResult.map((e) => <li>{e}</li>)
          }
        </ol>
      </div>
...(snip)

動作確認 その3 - 非同期コマンド

今回追加した call ボタンを押下すると、2 秒後にリストに追加される。 async によりメインとは別スレッドで処理が行われるため、 UI がブロックされない事も確認できる。

実装 その4 - エラーの可能性のあるコマンド

コマンドを実装(バックエンド)

引数が Result になる以外は「その1」と同様。

src-tauri/src/main.rs:

...(snip)
#[tauri::command]
fn success_or_failed(success: bool) -> Result<String, String> {
    println!("Called successOrFailed!!!!!");
    if success {
        Ok("Success!".into())
    } else {
        Err("Failed!".into())
    }
}
...(snip)
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            implemented_command_function,
            sum,
            async_command,
            success_or_failed])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

これで、アプリがコマンドを呼び出す準備ができた。

コマンドを呼び出す(フロントエンド)

よくある API と同じように、 then と catch で処理を分けて記述する。

src/App.tsx:

  const [successOrFailedMessage, setSuccessOrFailedMessage] = useState("");
...(snip)
  function callSuccessOrFailed(success) {
    invoke('success_or_failed', {success: success})
      .then((result) => {
        setSuccessOrFailedMessage(result)
      })
      .catch((error) => {
        setSuccessOrFailedMessage(error)
      })
  }
...(snip)
      <div>
        <label>
          Execute successOrFailed:
          <button
            onClick={() => {callSuccessOrFailed(true)}}>
            call success
          </button>
          <button
            onClick={() => {callSuccessOrFailed(false)}}>
            call failed
          </button>
          <button
            onClick={() => setSuccessOrFailedMessage("")}>
            Clear
          </button>
        </label>
        <p>{successOrFailedMessage}</p>
      </div>
...(snip)

動作確認 その4 - エラーの可能性のあるコマンド

バックエンド側で Ok が返却されると then の処理が、 Err が返却されると catch の処理が実行されることが確認できる。

他にも Window や AppHandle, State などがあるようだが、 そもそもそれらが何なのか今は分からないためここまでとする。

以上。

参考資料