tauriアプリを作るときのstateの管理と、終了の制御
tauri
バージョンは以下:
- rustc 1.69.0
- tauri 1.4.1
tauriはrustでGUIアプリを作れるフレームワーク。マルチプラットフォーム向けに作ることも可能。
この記事は、tauriを使うときに、mainエントリポイントに書くことが多いであろうtauri::Builder
を使ったtauriアプリの初期化について、いくつかメモを残しておきたくて書いたものだ。
具体的には、
- state(ステート)を追加するmanageメソッド
- 2通りある終了制御の使用に関する一例
について実装例を記述している。
tauriにstateを追加する
stateとして持たせると、フロント側から呼び出した関数のなかでその構造体T
を取り出すことができる(詳しくは下部リンクを参考)。
tauriで、ある構造体T
をstateとして持たせたい場合、
Builder.manage<T>(self, state: T)
を使う方法Builder.setup<F>(self, setup: F)
の中でsetup
の引数のApp
インスタンスのmanage<T>(&self, state: T)
を呼び出す方法
の2通りの方法がある:
// ...
fn main() {
tauri::Builder::default()
// way1. tauriのstateに追加。初期化にtauriのAPIが不要な時はこちらが楽。
.manage(MyAppManager::new())
.setup(|app| {
// tauriのAPIを使ってコンフィグファイルをロードする関数をコール
let appconfig = match AppConfig::load_config(&app.app_handle()) {
Ok(ac) => ac,
Err(e) => {
// ファイルが存在しなければデフォルト値を使う
eprintln!("{}", e);
AppConfig::default()
}
};
// way2. tauriのstateに追加
app.manage(AppConfigWrapper::from(appconfig));
Ok(())
})
.invoke_handler(tauri::generate_handler![
/* some handlers here */
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
AppConfig
はアプリの設定を保持している構造体で、jsonファイルとリンクしている。ファイルが存在しなければ、デフォルトの値で構築しておく。ここでは関係性が薄いが&app.app_handle()
を利用してtauri推奨のファイルのパスを取得している。
AppConfigWrapper
はAppConfig
を保持するラッパーでMutex<AppConfig>
として持つ。これは内部可変性を持たせるため。AppConfig
のフィールドをMutexにしてもいいのだが、ファイルのロード・セーブのデシリアライズ/シリアライズの都合で、全体をラップする方が簡単なので、このようにしている。
上記のように、stateで管理したい構造体でtauriのAPIを利用して初期化を行いたい場合は、setup
の中で行うのがいいだろう。
tauriのstateについての公式記述は少ない気もするが、この辺りに一部分がある:
終了時の制御
tauriアプリは特に何もしなければ、ウィンドウを閉じれば自動的にアプリも終了するようになっている。制御コードを追加すれば、この振る舞いを変更できる。
tauriのバックグラウンド処理化の制御については、GUIを隠すか、破棄するかの2通りを選択できるようだ。
ここではウィンドウを閉じる選択をしたとき、ウィンドウのGUIを隠すだけだが、システムトレイから終了するときはバックグラウンド処理を停止・終了させてからアプリを完全に終了させるようにする:
// ...
fn main() {
tauri::Builder::default()
// trayモジュールでシステムトレイの初期化・ハンドリングを行っている
.system_tray(tray::init_system_tray())
.on_system_tray_event(tray::handle_system_tray_event)
.on_window_event(|event| match event.event() {
tauri::WindowEvent::CloseRequested { api, .. } => {
let manager = event.window().state::<MyAppManager>();
match manager.get_state() {
ManagerState::Running(..) => {
// この状態のときは、アプリの終了はせず、タスクトレイにアイコンを残す
event.window().hide().unwrap();
api.prevent_close();
// システムトレイアイコンは既存なのでここでは何もしていない
}
_ => {}
}
}
_ => {}
})
.invoke_handler(tauri::generate_handler![
/* some handlers here */
])
// runではなく、build
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app_handle, event| match event {
tauri::RunEvent::ExitRequested { .. } => {
// app終了時のクリーンアップ。終了時、必ずここは呼ばれる。
// バックグラウンド処理が動いていれば、on_window_eventの方でGUIが隠され、保持され、ここは呼ばれない。
let manager = app_handle.state::<MyAppManager>();
// 停止する
manager.stop();
// システムトレイを取得し、破棄する
match tray::get_system_tray(&app_handle) {
Some(tray) => tray.destroy().expect("failed to destroy system tray"),
None => {}
};
}
_ => {}
});
}
MyAppManager
はバックグラウンド処理の管理などを行っていて、その状態はManagerState
で確認できるようにしている。
tauri::WindowEvent::CloseRequested
がウィンドウを閉じたときに呼ばれる。条件によってアプリの終了を抑制している。
tauri::RunEvent::ExitRequested
がアプリの終了開始の直前で呼ばれる。これが呼ばれた時点ですべてのGUIは破棄されているはず。GUIを破棄した状態でバックグラウンド処理をしたいなら、api.prevent_exit();
をここで使うようにする(詳しくは下の参考にて)。
公式のガイドは片方ずつ紹介しているが、どちらも併用できるようだ:
おわり
tauriは最終的なアプリサイズが小さくなるから結構好きかもしれない。しかし、開発中のサイズは普通に大きくなるので油断してはいけない。
rustを書くのも結構楽しいし、これは良い。
以上です。