Rust Monthly Topics

RustのGUIライブラリ「gtk-rs」 ~gtk-rsでデスクトップアプリ開発をはじめよう[前編]

RustのGUIライブラリとその中でのgtk-rsの位置付け

今現在、RustでGUIアプリケーションを開発するためのライブラリ/フレームワークは多種多様です。

突出して優れたものがあるわけでもなく、これを使えば間違いないと言えるものはありません。このような状況では、何を使えばいいか見当がつきませんが、それでもやりたいことから多少の取捨選択はできます。

百家争鳴のライブラリ群から類型をみると、大きく分けて自前スタックのタイプと既存のライブラリのバインディングになっているものがあります。

自前スタックのタイプはウィンドウ内のボタンといったウィジェットなども含めてすべて自前で実装しているものです。icedeguiなどがあります。これらを基礎づけているのがwinitというライブラリで、ウィンドウを作る部分を担当しています。winitはRustの対応プラットフォームの多さを活かして、デスクトップはもちろん、モバイルやWebでも動きます[1]。一方でウィンドウ以外の面倒は看ないのでシステムとの統合などの複雑なことをしようとすると機能が不足しはじめますし、ウィンドウ内での出来事、例えば日本語の描画といった処理は各GUIライブラリが実装することになり、品質がバラつきます。

既存のライブラリのバインディングはウィンドウや描画のしくみを既存のものに頼っているものです。以前記事になったtauriもこちらに分類されます。これらのライブラリは、多くはCで書かれた外部のライブラリに頼るので、ユーザにそのライブラリをインストールしてもらう必要がありますし、Web上で動かすのも難しいでしょう。しかしその分、歴史を刻んだライブラリの機能を十二分に使えます。

既存のGUIライブラリの二大巨頭といえばGTKとQtでしょう。QtはC++で書かれているからか、有力なRustバインディングがないのが現状です。もう一方のGTKには、よくメンテナンスされたライブラリがあります。それが本記事で扱うgtk-rsです。GTKにはこれまで広く使われてきたGTK 3と現在の最新版であるGTK 4がありますが、gtk-rsはそのどちらもサポートしています。最近はGTK 4が普及してきたことを受けて、GTK 4のバインディングに力が入っています[2]。さらにその上に作られたrelm4などエコシステムが広がっています。

少々前置きが長くなりましたが、成熟した既存ライブラリであるGTKを、よくメンテナンスされたラッパであるgtk-rsから使っていきましょう。Rustから使うことで安全かつ高速に動作するアプリケーションを構築できるようになります。本記事では最新版であるGTK 4を使っていきます。3とはAPIが異なる点があることに注意してください。

GTK 4のインストール

本記事ではGTK 4.10をベースに動作確認をしています。LinuxやmacOSでは、以下のコマンドでインストールできます。

Fedora系統
$ sudo dnf install gtk4-devel gcc
Debian系統
$ sudo apt install libgtk-4-dev build-essential
Arch系統
$ sudo pacman -S gtk4 base-devel
macOS
$ brew install gtk4

Windowsの場合はインストール手順が複雑です。以下の資料を参考に挑んでみてください。

Windows - GUI development with Rust and GTK 4

Hello, gtk-rs

何はともあれ、まずは使ってみましょう。

コンソールで以下のコマンドを叩き、gtk4の0.6.6を使うプロジェクトを作ります。

$ cargo new hello-gtk
$ cd hello-gtk
$ cargo add gtk4@0.6.6 --rename gtk

ライブラリを使うときにgtk4と書くのはくどいのでgtkにリネームしておきます。

これでsrc/main.rsに以下のコードを書きます。

// 1
use gtk::prelude::*;

fn main() {
    // 2
    let application =
        gtk::Application::new(Some("com.github.keens.gtk-examples.basic"), Default::default());
    // 3
    application.connect_activate(build_ui);
    // 4
    application.run();
}

fn build_ui(application: &gtk::Application) {
    // 5
    let window = gtk::ApplicationWindow::new(application);

    // 6
    window.set_title(Some("First GTK Program"));
    window.set_default_size(350, 70);

    // 7
    let button = gtk::Button::with_label("Click me!");
    // 8
    button.connect_clicked(|_| {
        println!("Clicked!");
    });

    // 9
    window.set_child(Some(&button));

    // 10
    window.show();
}

このプロジェクトを走らせてみましょう。

$ cargo run

すると、このようにボタンが1つだけ表示されたシンプルなアプリケーションが立ち上がります。

アプリケーションの画面
アプリケーションの画面

「Click me!」と書かれたボタンをクリックするとターミナルに文字が表示されます。

main.rsの解説

上記のコードを詳しくみていきましょう。コメントに書いた番号を参照しながら追ってみてください。

1でgtk::preludeをインポートします。GTKはさまざまなライブラリを組み合わせて成り立ってますが、今回のようなシンプルなアプリケーションではgtkのプレリュードのみで済んでいます。

mainの2でアプリケーションを初期化し、作るUIを3で接続し、4でアプリケーションを実行します。ここはほとんど定型文です。

build_uiに移ります。アプリケーションコードの大部分はここに書くことになるでしょう。

5でアプリケーションウィンドウを作り、6でウィンドウの設定を初期化します。ここもやや定型文に近いです。

7、8、9が主要な部分です。7でボタンを作り、8で押されたときのコールバックを登録します。9で5のときに作ったwindowbuttonを追加します。

10で最後に5のときに作ったウィンドウを表示します。

このくらいの内容であれば元々がCで書かれたライブラリであることを意識することなくコードを書けますね。

ウィジェットをいろいろ使ってみよう

このままいくつかGTKが用意しているGUIのパーツを使ってみましょう。GUIのパーツはウィジェットと呼ばれています。GTKで使えるウィジェットは公式サイトのVisual Indexで一覧できます。

Boxでウィジェットを並べる

これから、いくつかウィジェットを含んだアプリケーションを作ってみます。そのためにウィジェットを並べる入れ物を用意しましょう。Boxというウィジェットを使うと複数のウィジェットを並べられます。

gtk::Boxに限らず、GTKのオブジェクトを初期化する方法はいくつか用意されています。前述したgtk::Buttonはあまりカスタマイズしなかったのでwith_label関数で初期化しましたが、いろいろ設定をする場合はビルダーパターンで必要な設定を与えながら構築できます。gtk::Boxにもビルダーが用意されていますgtk::Boxはいくつか設定したい項目があるのでこれを使っていきましょう。以下のように設定していきます。

gtk::Box::builder()
    // アイテムを縦に並べる
    .orientation(gtk::Orientation::Vertical)
    // アイテムを左(始端)寄せする
    .halign(gtk::Align::Start)
    // 見た目を整える
    .spacing(6)
    .margin_bottom(6)
    .margin_top(6)
    .margin_start(6)
    .margin_end(6)
    //ビルドする
    .build()

内容はコメントに書いてある通りです。これらの設定は見た目を整えるためにやっているのでウィジェットの動作確認という意味では好きに変更しても差し障りありません。

さて、このボックスにウィジェットを追加していくための準備を整えましょう。build_ui関数でwindowの子に設定します。

fn build_ui(application: &gtk::Application) {
    let window = gtk::ApplicationWindow::new(application);

    window.set_title(Some("Gallery"));
    window.set_default_size(350, 70);

    let vbox = /* 先程のgtk::Boxのコード */;

    window.set_child(Some(&vbox));

    // これからここにコードを書く

    window.show();
}

// これからここに関数を追加する

ここからいろいろなウィジェットをボックスに追加していきます。例として先に作ったボタンを挙げます。コードの見通しのために一度関数を作り、以下のようにボタンを作ります。

fn build_button() -> gtk::Button {
    let button = gtk::Button::with_label("Click me!");
    button.connect_clicked(|_| {
        println!("Clicked!");
    });
    button
}

そしてbuild_ui内では以下のようにボタンをボックスに追加します。

vbox.append(&build_button());

コード全体は以下のようになります。

fn build_ui(application: &gtk::Application) {
    let window = gtk::ApplicationWindow::new(application);

    window.set_title(Some("Gallery"));
    window.set_default_size(350, 70);

    let vbox = /* 先程のgtk::Boxのコード */;

    window.set_child(Some(&vbox));

    vbox.append(&build_button());

    window.show();
}

fn build_button() -> gtk::Button {
    let button = gtk::Button::with_label("Click me!");
    button.connect_clicked(|_| {
        println!("Clicked!");
    });
    button
}

以後、build_button関数に相当するもののみを掲載するので、適宜build_ui内でvboxに追加していってください。

Scale

gtk::Scaleでスケール(スライダー)を作れます。

fn build_scale() -> gtk::Scale {
    let scale = gtk::Scale::builder()
        .draw_value(true)
        // 1
        .adjustment(
            &gtk::Adjustment::builder()
                .lower(0.0)
                .upper(100.0)
                .value(50.0)
                .step_increment(1.0)
                .page_increment(10.0)
                .build(),
        )
        // 2
        .digits(0)
        .round_digits(0)
        .build();
    // 3
    scale.connect_value_changed(|s| {
        println!("value changed: {}", s.value());
    });
    scale
}

1でスケールの動く範囲やキーボード操作などをadjustmentで指定します。lowerupperで動く範囲、valueで初期値を指定します。step_incrementpage_incrementでキーボードなどで操作したときに動く量を指定しており、矢印キーなどで操作するときはstep_incrementの値が、PageUp/PageDownやマウスホイールで操作するときはpage_incrementの値が使われます。

2で値の小数点以下何桁まで扱うかを制御します。digitsで表示される値の、round_digitsでプログラム内で扱う実際の値の桁数を変えます。

3でスケールが変更されたときに標準出力にその値を出力するようにします。後に説明しますが、スケールが変更されたときにvalue-changedのシグナルが発行されるのでそれに接続します。

Switch

gtk::Switchで(トグル)スイッチを作ります。

fn build_switch() -> gtk::Switch {
    let switch = gtk::Switch::builder().halign(gtk::Align::End).build();
    switch.connect_active_notify(|s| println!("state changed: {:?}", s.is_active()));
    switch
}

そろそろ馴れてきたかと思います。halignで右(終端)寄せ表示のトグルスイッチを作り、変更されたら標準出力にその値を出力します。

その他のウィジェット

ここまでボタン類を紹介しましたが、その他にも入力欄や他のウィジェットを囲うフレームなどいろいろなウィジェットがあります。

fn build_password_entry() -> gtk::PasswordEntry {
    gtk::PasswordEntry::new()
}

fn build_frame() -> gtk::Frame {
    let frame = gtk::Frame::builder()
        .label("Frame")
        .child(&build_switch())
        .build();
    frame
}

ここまでのウェジェットを追加したアプリケーションを起動すると以下のような画面になるはずです。

さまざまなウィジェットを追加したアプリケーションの画面
さまざまなウィジェットを追加したアプリケーションの画面

スケールやスイッチはシグナルを接続しているので、動作するとターミナルに出力されます。動かしてみましょう。

まとめ

今回はgtk-rsの紹介と、軽い動作確認でした。次回でGTKのライブラリ構造やRustからの使い方を試していきます。

本記事で登場したコードは以下に置いておきます⁠
https://github.com/KeenS/gtk-examples

おすすめ記事

記事・ニュース一覧