普段使うツールをRustで作る場合、コマンドラインベースのものが多いと思います。それでも、Rustを使ってデスクトップアプリを作ることもできます。今回は、実績のあるGUIツールキットの「Tcl/Tk」をラップしたクレートを使って簡単なGUIを作成してみましょう。

  • Rustで単位変換ツールを作ったところ

    Rustで単位変換ツールを作ったところ

GUIツールキット「Tcl/Tk」のインストール

Rustのライブラリを集約したcrates.ioを見ると、多くのGUIライブラリが存在します。Rustはさまざまな環境で動作することを想定していることもあり、「これこそGUIの定番」というものはありません。多くの選択肢があり、用途に応じて使い分けるのが正しい作法です。

WebブラウザベースのTauri、ゲーム開発と相性の良いegui、GTK+を利用するGTK-rs、Tcl/Tkをラップしたライブラリのtcl/tkなど、いろいろなクレートがあります。今回は、この中からTcl/TkをラップしたGUIライブラリの「tcl」と「tk」クレートを利用してみましょう。

このクレートは、伝統的なGUIツールキット「Tcl/Tk」のラッパーです。そのため、別途Tcl/Tkのインストールが必要となります。OSごとにTcl/Tkをインストールしましょう。

なお、Tcl/Tkについては、姉妹連載のこちらで詳しく紹介しています。

【WindowsでTcl/Tkをインストールする場合】

Windowsであれば、Rustに加えて、ActiveTclのインストールが必要になります。なお、ActiveTclをダウンロードする際には、ActiveStateのアカウントを作成する必要がありますが、無料でダウンロードできます。インストーラーをダウンロードしたら、ダブルクリックでActiveTclをインストールします。

【macOSでTcl/Tkをインストールする場合】

macOSの場合は、Homebrewを使ってTcl/Tkをインストールします。Homebrewをインストールした上で、ターミナルにて下記のコマンドを実行します。

brew install tcl-tk

簡単なテストプロジェクトを作ろう

最初に、簡単なテストプログラムを作って動作を確認してみましょう。ターミナル(WindowsならPowerShell、macOSならターミナル.app)を起動して、次のコマンドを実行しましょう。

# ディレクトリを作成してプロジェクトを初期化
mkdir hello_tk
cd hello_tk
cargo init
# クレートを追加
cargo add tcl tk

次にメインプログラム「src/main.rs」を記述しましょう。プログラムを以下のように書き換えましょう。こちらからもコピーすることができます。

use tk::*;
use tk::cmd::*;

fn main() -> TkResult<()> {
    // Tkの初期化 --- (*1)
    let tk = make_tk!()?;
    let root = tk.root();
    // ウィンドウ上にフレームを作成 --- (*2)
    let content = root
        .add_ttk_frame(())?
        .pack(())?;
    content.configure( -padding(12) )?; // 余白を設定
    // ラベルを配置 --- (*3)
    content
        .add_label("lbl1" -text("Tkのテスト"))?
        .pack(())?;
    // ボタンを配置 --- (*4)
    content
        .add_button("btn1" -text("閉じる") -command("destroy ."))?
        .pack( -side("bottom") )?;
    // メインループ --- (*5)
    Ok( main_loop() )
}

プログラムを実行するために、ターミナルで下記のコマンドを実行しましょう。

cargo run

なお、Windowsでエラーが出て失敗する場合、下記の点を確認してください。
(1)ActiveTclをデフォルト設定でインストールし、PATHにwish.exeのパスを追加する
(2)libclangが見つからないというエラー『 thread 'main' panicked at 'Unable to find libclang: "couldn't find any valid shared libraries matching: ['clang.dll', 'libclang.dll']』が出る場合には、Chocolateyをインストールした上で管理者権限でPowerShellを起動して以下のコマンドを実行し、最新版のLLVMをインストールしてください。

choco install llvm

(3)stdio.hがないというエラーが出る場合、Visual Studioをインストールして、「C++によるデスクトップ開発」にチェックを付けてインストールする必要があります。
それでも、エラーが解決しない場合、こちらにフォロー記事を投稿しましたのでご確認ください。

プログラムが正しくコンパイルされると、次のようなウィンドウが表示されます。ウィンドウに、ラベルとボタンが配置され、「閉じる」ボタンを押すとプログラムを終了します。

  • 一番簡単なプログラム

    一番簡単なプログラム

プログラムを確認してみましょう。(*1)ではTkを初期化します。(*2)ではウィンドウ上にフレームを作成して配置します。configureメソッドを使ってフレームに余白を設定します。

(*3)では、add_labelメソッドでラベルを作成して、packメソッドで配置します。(*4)では、add_buttonメソッドでボタンを作成してpackメソッドで配置します。

tkクレートのadd_labelやadd_buttonのメソッドには、ウィジェットのIDに加えて、初期パラメータを指定できます。パラメータには、-padding(…)や-text(…)などのように値を指定します。

(*5)ではメインループを実行します。これによって、ウィンドウが動作します。

単位変換プログラムを作ろう

次に、単位変換を行うプログラムを作ってみましょう。分かりやすく、単位変換プログラムの定番である、インチをセンチに変換するプログラムを作ってみましょう。

プロジェクトを初期化するために、ターミナルで下記のコマンドを実行しましょう。

mkdir unit_converter
cd unit_converter
cargo init
cargo add tcl tk

そして、メインプログラム「src/main.rs」に下記のプログラムを記述しましょう。こちらからコピーすることもできます。

use tk::*;
use tk::cmd::*;
use tcl::*;
fn main() -> TkResult<()> {
    // Tkの初期化
    let tk = make_tk!()?;
    let root = tk.root();
    root.set_wm_title("インチからセンチへの変換")?;
    // ウィンドウ上にフレームを作成
    let c = root
        .add_ttk_frame(())?
        .pack(())?;
    c.configure( -padding(12) )?; // 余白を設定
    // Gridレイアウトを利用してウィジェットを配置 --- (*1)
    c.add_ttk_label( "inch_lbl" -text("インチ") )?
        .grid( -column(1) -row(1) -sticky("ne") )?;
    c.add_entry( "inch_entry" -textvariable("inch_var") )?
        .grid( -column(2) -row(1) -sticky("nw") )?;
    c.add_button( "calc" -text("計算") -command("calculate") )?
        .grid( -column(3) -row(1) -sticky("w") )?;
    c.add_ttk_label( "cm_lbl" -text("センチ") )?
        .grid( -column(1) -row(2) -sticky("ne") )?;
    c.add_entry( "cm_entry" -textvariable("cm_var") )?
        .grid( -column(2) -row(2) -sticky("nw") )?;
    // Entryに初期値を設定 --- (*2)
    tk.run(("set", "inch_var", "30.0"))?;
    // 計算関数を定義 --- (*3)
    #[proc] fn calculate() -> TkResult<()> {
        let interp = tcl_interp!();
        let inch: f64 = interp.get_double("inch_var")?;
        let cm = inch * 2.54;
        interp.set_double("cm_var", cm);
        Ok(())
    }
    // 定義した関数を登録 --- (*4)
    unsafe{ tk.def_proc( "calculate", calculate ); }
    // メインループ
    Ok( main_loop() )
}

プログラムを実行するには、ターミナルで下記のコマンドを実行します。

cargo run

すると、次のようなウィンドウが表示されます。インチの右横にあるテキストボックスに数値を入力して「計算」ボタンを押すと、インチをセンチに変換してテキストボックスに表示します。

  • インチからセンチへの変換プログラムを実行したところ

    インチからセンチへの変換プログラムを実行したところ

プログラムを確認してみましょう。(*1)ではGridレイアウトを利用して、各種ウィジェットを配置します。(*2)では、Entryに初期値を配置します。(*3)では、計算関数を定義します。インチの値を取得して、センチに変換して値を設定します。(*4)では、定義した関数を登録します。

このプログラムを理解するには、いくつかのポイントを抑える必要があります。と言うのも、このクレートは、Tcl/Tkを薄くラップしているだけなのです。Tcl/Tkに合わせて、Rustのプログラムを記述する必要があります。

まず、Tcl/TkにRustの関数を登録するために、特別な記述が必要になります。Tcl/TkにRustの関数を登録するために、(*3)で関数を定義する際「#[proc]」を追加して、(*4)で「tk.def_func」で関数を登録します。これによって、add_buttonでボタンを追加するときに、ボタンを押した時に実行される関数を「-command("関数名")」のように指定できるようになります。これは、「計算」ボタンをクリックした時に、計算処理を行って結果を、テキストボックスに設定します。

それから、テキストボックスにデータを設定したり、取得したりする場合にも、ちょっと特別な指定が必要です。add_entryメソッドでテキストボックスを作成できるのですが、引数に「-textvariable("変数名")」のように指定します。これによって、Tcl/Tkに読み書き用の変数を作成して、テキストボックスのテキストと結びつけます。

それで、「tk.run(("set", "inch_var", "30.0"))」のように記述することで、Tk内の変数inch_varに"30.0"を設定できます。また、プログラムの(*3)の部分のように、tcl_interp!()で、Tclのインタプリタのオブジェクトを取得し、get_doubleやset_doubleのメソッドを通して、f64型の値を取得したり設定したりします。

●まとめ

以上、今回はRustからTcl/Tkを利用してGUIアプリを作る方法を紹介しました。今回紹介した「tcl」「tk」クレートは、RustからTcl/Tkの機能を呼び出して使うというスタイルとなっているため、少し癖があるのは否めません。(逆に、Tcl/Tkを使ったことがあるのであれば、それほど苦労することなく、GUIアプリを作成できることでしょう。)

それでも、Rustの柔軟なマクロを利用することで、比較的手軽にTcl/Tkの機能を呼び出して、GUIを構築できるようになっています。また、Tcl/Tkは実績のあるGUIツールキットであるため、マルチプラットフォームで軽快に動作する安定したGUIを構築できます。配付する際のバイナリサイズが小さいというのもメリットの一つです。Tcl/Tkは、RustでGUIライブラリを作る際の決定版とは言えないものの候補の一つになるでしょう。試してみてください。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。技術書も多く執筆している。直近では、「実践力をアップする Pythonによるアルゴリズムの教科書(マイナビ出版)」「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」「すぐに使える!業務で実践できる! PythonによるAI・機械学習・深層学習アプリのつくり方 TensorFlow2対応(ソシム)」「マンガでざっくり学ぶPython(マイナビ出版)」など。