「Ace」を使って「TAURI」で「テキストエディタ」アプリを作ろう
ファイルを開いてテキストを読み込む
これまでの連載のように、TAURIコマンド関数を実装します。ここではファイルからテキストを読み込むだけのTAURIコマンドを実装します。図5のように、まずバックエンドのRust側で「File」→「Open」メニューが実行されたら、フロントエンドのJavaScript側でファイルダイアログを開いてファイル名をまたバックエンドのRust側に渡します。TAURIコマンドの「open_text」関数でテキストファイルが読み込まれたら、またフロントエンドのJavaScript側でAceエディタにテキストを表示します。
フロントエンドでファイルを開くダイアログ
「ファイルを開くダイアログ」に関しては、第6回でJavaScriptで実装した通りです。第5回のファイルを開くダイアログはRustクレートなので、今回とは違います。
TAURIイベントの「listen」メソッドで"open-file"メッセージの受け取り状態をセットします。"open-file"メッセージを受け取ったらダイアログから「.txt」「.html」「.js」の拡張子のファイルを開き、バックエンドのRust側のTAURIコマンド「open_text」関数にファイル名を送ります。開いたファイルをさらに「invoke」関数の"open_text"で受け取り、Aceエディタの「setValue」メソッドにテキストデータを渡します。
ここでは1つずつ拡張子を選ぶようになっていますが、複数の拡張子のファイルを同時にファイルダイアログに表示できるようにすると便利でしょう。例えば「extensions: ['txt','html','js']」のように拡張子を複数指定します。
・「src」→「main.js」ファイルのサンプルコードconst { invoke } = window.__TAURI__.tauri; //ダイアログを扱えるように const { open,save } = window.__TAURI__.dialog; let editor; window.addEventListener("DOMContentLoaded", () => { editor = ace.edit('text_edit'); editor.focus(); editor.setFontSize(16); setFontSize(); setNew(); //Openメニューが呼ばれる設定 setOpen(); setMode(); setTheme(); editor.session.setMode('ace/mode/html'); }); //Openメニューが呼ばれるセットをする関数 async function setOpen() { await window.__TAURI__.event.listen('open-file', () => { open({ filters: [ { name: 'Text', extensions: ['txt'] }, { name: 'HTML', extensions: ['html'] }, { name: 'JavaScript', extensions: ['js'] }, ], }).then(async file => { let text = await invoke("open_text", { path: file }); let ext = file.split("/").reverse()[0].split('.')[1]; editor.session.getDocument().setValue(text); switch (ext.toLowerCase()) { case "html": editor.session.setMode('ace/mode/html'); break; case "js": editor.session.setMode('ace/mode/javascript'); break; default: editor.session.setMode('ace/mode/text'); } }); }); } (後略)
【サンプルコードの解説】
TAURIのダイアログの「open」関数と「save」関数を読み込みます。
DOMが全て読み込み完了したら「setOpen」関数を呼び出します。
setOpen関数でファイルを開く「Open」メニューが実行されたときの処理をします。「listen」メソッドで「Open」メニューの実行を受け取り、ファイルを開くダイアログを開いてファイル名が選択されたら、バックエンドからテキストデータを受け取って表示します。
ファイルの拡張子次第で「HTML」モードか「JavaScript」モードか、それ以外の拡張子の場合は「Text」モードにセットします。
バックエンドでテキストファイルを読み込む
「Open」カスタムメニューを「File」サブメニューに追加します。既にFileメニューはメニューバーに追加されています。
「Open」メニューが実行されたら「open-file」メッセージをフロントエンドのJavaScriptに送り、また返ってきたファイル名を指定してテキストファイルをTAURIコマンド「open_text」関数で開き、またフロントエンドへ読み込んだテキストデータを送ります。
・「src-tauri」→「src」→「main.rs」ファイルのサンプルコード#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use tauri::{CustomMenuItem, Menu, MenuItem, Submenu,Manager}; //ファイルの読み書きを扱うクレート use std::fs; //ファイルを開くTAURIコマンド関数 #[tauri::command] fn open_text(path: &str) -> String { match fs::read_to_string(path) { Ok(text) => text, Err(_) => String::from("UTF-8エンコードのファイルを開いてください。") } } fn main() { let new_file = CustomMenuItem::new( "new".to_string(), "New"); //Openメニュー let open_file = CustomMenuItem::new( "open".to_string(), "Open"); (中略) let menu_file = Submenu::new( "File", Menu::new() .add_item(new_file) //Openメニューの追加 .add_item(open_file) .add_native_item(MenuItem::Separator) .add_item(quit)); (中略) //main.jsからopen_text関数が呼ばれた時のセット tauri::Builder::default() .invoke_handler(tauri::generate_handler![ open_text]) .menu(menu) .on_menu_event(|event| { match event.menu_item_id() { "new" => { event .window() .emit_all("new-file", "") .unwrap(); } //Openメニューを実行する時 "open" => { event .window() .emit_all("open-file", "") .unwrap(); } (中略) _ => {} } }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
【サンプルコードの解説】
「use」文でファイルを扱う「std::fs」クレートを使えるようにします。
TAURIコマンド「open_text」関数で「path」引数の「UTF-8」形式のテキストファイルを読み込んで文字列データを返します。
「File」→「Open」メニューを追加します。
TAURIビルダーの「invoke_handler」メソッドでTAURIコマンド「open_text」関数の呼び出しを登録します。
TAURIビルダーで「Open」メニューが実行されたら"open-file"メッセージをフロントエンドのJavaScriptへ送ります。
テキストをファイル名を指定して保存する
ファイルを開くのとは逆に、ファイルに名前を付けて保存するTAURIコマンド関数を実装します。テキストデータを保存するだけなので、保存した後にテキストデータを返して表示するなどの処理はファイルを開いたときより1手順少なくて済みます。
今回のTAURIコマンド関数の中身はテキストファイルを読み書きするだけなので、とてもシンプルに実装できます(図6)。もちろんファイルを読み書きする関数がRustに備わっているからですが、ピッタリの関数や構造体やメソッドがあると便利なので、クレートはとても重要です。
フロントエンドで名前を付けて保存するダイアログ
「名前を付けて保存ダイアログ」に関しては、第6回でJavaScriptで実装した通りです。第5回の名前を付けて保存ダイアログはRustクレートなので今回とは違います。
あまりお勧めできませんが、ファイルに名前を付けて保存ダイアログも複数の拡張子を同時に指定できます。
・「src」→「main.js」ファイルのサンプルコードconst { invoke } = window.__TAURI__.tauri; const { open,save } = window.__TAURI__.dialog; let editor; window.addEventListener("DOMContentLoaded", () => { editor = ace.edit('text_edit'); editor.focus(); editor.setFontSize(16); setFontSize(); setNew(); setOpen(); //Saveメニューが呼ばれる設定 setSave(); setMode(); setTheme(); editor.session.setMode('ace/mode/html'); }); //Saveメニューが呼ばれるセットをする関数 async function setSave() { await window.__TAURI__.event.listen('save-file', () => { save({ filters: [ { name: 'Text', extensions: ['txt'] }, { name: 'HTML', extensions: ['html'] }, { name: 'JavaScript', extensions: ['js'] }, ], }).then(async file => { let txt = editor.session.getDocument().getValue(); await invoke("save_text", { path:file, text:txt }); }); }); } (後略)
【サンプルコードの解説】
DOMが全て読み込み完了したら「setSave」関数を呼び出します。
「setSave」関数の「listen」メソッドでバックエンドのRust側から送られるSaveメニューの受け取りを登録します。「invoke」関数では第1引数でTAURIコマンド「save_text」関数を呼び出し、第2引数に「連想配列」でTAURIコマンド関数の引数名を任意の数だけまとめて渡せます。
バックエンドでテキストファイルを保存する
「Save」カスタムメニューを「File」サブメニューに追加します。Fileメニューは既にメニューバーに追加されています。
TAURIコマンド「save_text」関数内の「write」関数も、「read_to_string」関数のときのように「Result」型で場合分けした方が良いでしょう。「Result」型と「Option」型の使い方の解説が不十分だったので、次回で解説します。
・「src-tauri」→「src」→「main.rs」ファイルのサンプルコード(前略) //ファイルを保存するTAURIコマンド関数 #[tauri::command] fn save_text(path: &str,text: &str) { fs::write(path, text).unwrap(); } fn main() { let new_file = CustomMenuItem::new( "new".to_string(), "New"); let open_file = CustomMenuItem::new( "open".to_string(), "Open"); //Saveメニュー let save_file = CustomMenuItem::new( "save".to_string(), "Save"); (中略) let menu_file = Submenu::new( "File", Menu::new() .add_item(new_file) .add_item(open_file) //Saveメニューの追加 .add_item(save_file) .add_native_item(MenuItem::Separator) .add_item(quit)); (中略) //main.jsからsave_text関数が呼ばれた時のセット tauri::Builder::default() .invoke_handler(tauri::generate_handler![ open_text, save_text]) .menu(menu) .on_menu_event(|event| { match event.menu_item_id() { "new" => { event .window() .emit_all("new-file", "") .unwrap(); } "open" => { event .window() .emit_all("open-file", "") .unwrap(); } //Saveメニューを実行する時 "save" => { event .window() .emit_all("save-file", "") .unwrap(); } (中略) _ => {} } }) .run(tauri::generate_context!()) .expect("error while running tauri application"); }
【サンプルコードの解説】
TAURIコマンド「save_text」関数でテキストファイルを「path」引数名で、「text」引数データを「UTF-8」形式で保存します。
「File」→「Save」メニューを作成し、メニューバーに追加します。
TAURIビルダーの「invoke_handler」メソッドでTAURIコマンド「save_text」関数の呼び出しを登録します。
TAURIビルダーで「Save」メニューが実行されたら"save-file"メッセージをフロントエンドのJavaScriptへ送ります。
おわりに
今回は、新たにメニューを表示したり実行したりするメニューバーや、JavaScriptでRust側からメッセージを受け取るlistenの機能が出てきました。これでRustとJavaScriptとのやり取りがほとんど実装できるようになりました。
Aceライブラリには他にも様々な設定があるので、いろいろ試してみると良いでしょう。工夫次第では「Visual Studio Code」のTAURI版まで作れてしまうかもしれません。