前回に続き、gtk-rsを使ったデスクトップアプリ開発を取り上げていきます。前回、サンプルアプリケーションで基本の形を見てきました。GTKは作り込まれたライブラリですので、関連する概念が多く、アプリケーションを作るまでの道程は遠めです。今回は、前回の記事でも時折説明なしに出てきたGTKのオブジェクト指向のシステムや関連する概念を押さえておきましょう。
- 前回と同様に、本記事で登場するコードは以下に置いておきます。
- https://
github. com/ KeenS/ gtk-examples
GTKのアーキテクチャ
GTKはいくつかのライブラリに分割されており、それらの上にGTKが作られています。プログラマは時にはGTKだけでなく、その下にあるライブラリも使うことになります。
https://
中でもGLibはGTKの基礎となるライブラリです。独自のオブジェクト指向システム
GObject
GObjectはクラスベースの単一継承のオブジェクト指向をサポートした型システムです。ベースクラスはGObject
で、GTKのウィジェットもGObject
のサブクラスになっています。少し変わっているのは、Cの型システムを拡張しているわけではなく、データとしてクラスやインスタンスを表現している点です。静的にクラスが定義されるC++やJavaよりは動的にいろいろ変わるRubyやPythonなどに似ています。
Rustから扱うにあたっては以下の点で注意が必要です。
- クラスや継承などの概念を用いている
- サブクラス判定やメソッドの探索を動的に(実行時に)行っている
- クラスを表わす構造体とインスタンスを表わす構造体に分かれる
- オブジェクトはGLib独自の参照カウント方式によるメモリ管理がされる
模範的なRustとは大きく異なる設計を要求するため、Rustから自然に扱うには大量の橋渡しをするコードが必要になります。幸いにもgtk-rsではこれらの扱いを大幅に簡略化してくれるマクロを提供しているので、ある程度のボイラープレート
これらを念頭の置きつつ、これからgtk-rsを使ううえで意識しておく必要のあるメモリ管理、サブクラス、プロパティ、シグナルの4つの概念について学びます。
メモリ管理
GObjectは独自のメモリ管理システムを持っています。中身は参照カウントで、ミュータブルにアクセスできます。RustでいうところのRc<RefCell<T>>
のような扱いが可能というわけです。実際、GObjectのオブジェクトはClone
などのRustのトレイトも実装しています。
例を見てみます。解放処理
$ cargo new gtk-memory-management $ cd gtk-memory-management $ cargo add gtk4@0.6.6 --rename gtk
2つのボタンで1つの数値を操作するGUIアプリケーションを作りたいのですが、以下のコードはコンパイルが通りません。
use std::{cell::Cell, rc::Rc};
use gtk::{glib, prelude::*, Application, ApplicationWindow};
const APP_ID: &str = "com.github.keens.gtk-examples.memory-management";
fn main() -> glib::ExitCode {
let app = Application::builder().application_id(APP_ID).build();
app.connect_activate(build_ui);
app.run()
}
fn build_ui(application: &Application) {
// ボタンを2つ作る
let button_increase = gtk::Button::builder()
.label("Increase")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
let button_decrease = gtk::Button::builder()
.label("Decrease")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
// 2つのボタンから変更する数字を用意する
let number = Rc::new(Cell::new(0));
// コールバックを接続する
// ボタンをクリックすると `number` ともう1つのボタンのラベルを変える
button_increase.connect_clicked(move |_| {
// 数字を増やす
number.set(number.get() + 1);
// 他方のボタンのラベルを更新する
button_decrease.set_label(&number.get().to_string());
});
button_decrease.connect_clicked(move |_| {
// 数字を減らす
number.set(number.get() - 1);
// 他方のボタンのラベルを更新する
button_increase.set_label(&number.get().to_string());
});
// ボタンをgtk_boxに追加する
let gtk_box = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
gtk_box.append(&button_increase);
gtk_box.append(&button_decrease);
// ウィンドウを作る
let window = ApplicationWindow::builder()
.application(application)
.title("My GTK App")
.child(>k_box)
.build();
// ウィンドウを表示する
window.present();
}
実際、このコードをコンパイルしようとすると以下のようなエラーメッセージが出ます。
error[E0382]: borrow of moved value: `button_decrease`
--> gtk-memory-management/src/main.rs:44:5
|
27 | let button_decrease = gtk::Button::builder()
| --------------- move occurs because `button_decrease` has type `gtk4::Button`, which does not implement the `Copy` trait
...
40 | button_increase.connect_clicked(move |_| {
| -------- value moved into closure here
41 | number.set(number.get() + 1);
42 | button_decrease.set_label(&number.get().to_string());
| --------------- variable moved due to use in closure
43 | });
44 | / button_decrease.connect_clicked(move |_| {
45 | | number.set(number.get() - 1);
46 | | button_increase.set_label(&number.get().to_string());
47 | | });
| |______^ value borrowed here after move
何やら所有権に関連したエラーが出ているようですね。問題があるのは以下の部分です。
let number = Rc::new(Cell::new(0));
button_increase.connect_clicked(move |_| {
number.set(number.get() + 1);
button_decrease.set_label(&number.get().to_string());
});
button_decrease.connect_clicked(move |_| {
number.set(number.get() - 1);
button_increase.set_label(&number.get().to_string());
});
number
やbutton_
、button_
などの値を複数箇所で所有しようとしているので、所有権検査で弾かれてしまいます。Rc
で包んでいるnumber
やGObjectであるButton
はClone
が使えるので安直には以下のようにすればよさそうです。
let number = Rc::new(Cell::new(0));
// クロージャにムーブするために事前にcloneしておく
let number_clone = number.clone();
let button_decrease_clone = button_decrease.clone();
let button_increase_clone = button_increase.clone();
button_increase.connect_clicked(move |_| {
number_clone.set(number_clone.get() + 1);
button_decrease_clone.set_label(&number_clone.get().to_string());
});
button_decrease.connect_clicked(move |_| {
number.set(number.get() - 1);
button_increase_clone.set_label(&number.get().to_string());
});
しかし、これではコードが汚なくなりますし、何よりbutton_
とbutton_
で相互参照
循環参照をうまく扱うにはRc::
を呼んで適切な箇所で弱参照にすればいいのでした。ところが、そうするとさらにコードが散らかってしまいます。
glibではこういうときのためにclone!
マクロを用意しています。clone!
マクロを使えば、参照カウントのクローンや弱参照/強参照の管理を宣言的に行えます。以下のように使います。
button_increase.connect_clicked(glib::clone!(@strong number, @weak button_decrease =>
move |_| {
number.set(number.get() + 1);
button_decrease.set_label(&number.get().to_string());
}));
button_decrease.connect_clicked(glib::clone!(@weak button_increase =>
move |_| {
number.set(number.get() - 1);
button_increase.set_label(&number.get().to_string());
}));
button_
で指定している@strong number,@weak button_
という指定はnumber
を強参照clone
)button_
を弱参照clone
したあとにdowngrade
)
ところで弱参照の値にアクセスするには、一度upgrade
できるか試してみて、失敗したら処理をやめるということが必要ですが、それもclone!
マクロが内部で相当するコードを生成してくれています。ですので参照の違いを意識することなくクロージャを書けるようになっています。
サブクラス
GObjectは継承ベースのオブジェクト指向フレームワークなので、アプリケーションを作るときは自然とサブクラスを作ることになります。サブクラスの作り方を見てみましょう。gtk-subclass
クレートを作ります
$ cargo new gtk-subclass $ cd gtk-subclass $ cargo add gtk4@0.6.6 --rename gtk
すでに出てきたButton
クラスのサブクラスを作ってみます。
https://
先ほど説明したとおり、GObjectではクラスやインスタンスを表わす構造体が登場します。GObjectの流儀に沿ってそれらをまとめて定義するコードと、それをRustらしくラップするコードに分けて記述します。
完成形は以下のようなディレクトリ構成になります。
. ├── Cargo.lock ├── Cargo.toml └── src ├── custom_button │ ├── imp.rs │ └── mod.rs └── main.rs
まずは実体となる方のコードです。custom_
に以下のようなコードを書きます。
use gtk::glib;
use gtk::subclass::prelude::*;
#[derive(Default)]
pub struct CustomButton;
// サブクラスを定義するときの主要なトレイト
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
const NAME: &'static str = "MyGtkAppCustomButton";
// 後で定義する
type Type = super::CustomButton;
type ParentType = gtk::Button;
}
// GObjectのサブクラスが実装するトレイト
impl ObjectImpl for CustomButton {}
// Widgetのサブクラスが実装するトレイト
impl WidgetImpl for CustomButton {}
// Buttonのサブクラスが実装するトレイト
impl ButtonImpl for CustomButton {}
ここでキーとなるのが#[glib::
です。
クラスを作るうえで構造体に求められるフィールドのようなものはありません。代わりにObjectSubclass
を実装することで継承関係を表現します。ObjectSubclass
はここでの実装よりもう少し大きな定義をもつトレイトですが、#[glib::
アトリビュートをつけることで定型文を埋めてくれるので記述は少なくなります。
#[glib::
アトリビュートはObjectSubclass
のメンバを補完してくれるだけでなく、他にもいくつかのトレイトの実装を生成します。
一方、#[glib::
でカバーできない部分もあり、3つのトレイトの実装は手動で与えています。これらはObject
、Widget
、Button
のサブクラスを定義するのに必要なトレイトの実装です。Rustには継承がないのでトレイトを使ってRustの型システムの上でのサブクラス関係やオーバーライドを実現します。今回は特にオーバーライドをしないので空の実装を与えます。
これらのトレイトはRustの型システム上でもトレイトの継承関係にあるのでRustのコードからも自然に扱えます。トレイトの継承関係を見るとオブジェクトの継承関係と一致していることがわかります。
// それぞれのトレイトの宣言
pub trait ObjectImpl: ObjectSubclass + ObjectImplExt {
// ...
}
pub trait WidgetImpl: WidgetImplExt + ObjectImpl {
// ...
}
pub trait ButtonImpl: ButtonImplExt + WidgetImpl {
// ...
}
次に、これをラップするコードです。ライブラリの外部から呼び出す際にはこちらのコードを使います。custom_
に以下を書きましょう。
// impの方にpubはつけずにプライベートモジュールとする
mod imp;
use glib::Object;
use gtk::glib;
glib::wrapper! {
pub struct CustomButton(ObjectSubclass<imp::CustomButton>)
@extends gtk::Button, gtk::Widget,
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
}
impl CustomButton {
pub fn new() -> Self {
Object::builder().build()
}
pub fn with_label(label: &str) -> Self {
Object::builder().property("label", label).build()
}
}
glib::
マクロで一気にいろいろなコードを生成してしまいます。マクロにはラップする型とすべての親クラスと実装するインターフェースを渡してあげます。これで親クラスやインターフェースのメソッドを継承したかのように呼び出せます。本来は祖先クラスやインターフェースは親クラスからわかるので、もう少し記述が少なくなると嬉しいのですが、これがRustの中からできる限界のようです。我慢してすべて手書きしましょう。
最後に、メソッドではない関連関数を実装しています。メソッドは継承できますが、関連関数はそうはいかないので手で書きます。今回はnew
とwith_
の関連関数を定義しましょう。Button
には他にも関連関数がありますが、さしあたってこれだけ実装すれば困らないでしょう。
今回作ったサブクラスはほとんど継承元であるButton
と同じように使えます。main.
に以下を書きましょう。
mod custom_button;
use gtk::prelude::*;
use custom_button::CustomButton;
fn main() {
let application =
gtk::Application::new(Some("com.github.gtk-rs.examples.basic"), Default::default());
application.connect_activate(build_ui);
application.run();
}
fn build_ui(application: >k::Application) {
let window = gtk::ApplicationWindow::new(application);
window.set_title(Some("First GTK Program"));
window.set_default_size(350, 70);
// CustomButtonを使う
let button = CustomButton::with_label("Click me!");
button.connect_clicked(|_| {
println!("Clicked!");
});
window.set_child(Some(&button));
window.show();
}
これでGTKの主要な機能の1つ、クラスと継承が扱えました。
property
次にプロパティについてです。プロパティはGTKが提供するオブジェクトの状態へアクセスする手段です。APIを通して値の読み書きができます。プログラム内から扱う他、UIの部品を作るときなどにも参照する重要な機能です。
例を見てみましょう。CustomButton
を定義するときに、特に構造体のフィールドは定義しませんでした。しかし、Button
を継承しているのでButton
が提供しているプロパティへアクセスできます。実は、すでにButton
のプロパティへアクセスしています。with_
関連関数の定義を振り返ってみましょう。
impl CustomButton {
// ...
pub fn with_label(label: &str) -> Self {
Object::builder().property("label", label).build()
}
}
Object
のビルダーを通してlabel
プロパティに対して&str
の値を設定していますね。
作ったオブジェクトのプロパティを扱うには継承したproperty
/set_
メソッドを使います。
let button = CustomButton::with_label("Click me!");
// `property` で値を取得
assert_eq!(button.property::<String>("label"), "Click me!");
// `set_property` で値を設定
button.set_property("label", "Changed");
assert_eq!(button.property::<String>("label"), "Changed");
property
で値を取得します。このとき、取得する型は正しく指定する必要があります。
今回はGTKのButton
のドキュメントからUTF-8文字列であることがわかるのでString
を指定しています。
GTKの文字列からRustの文字列への変換はgtk-rsがやってくれます。もっと一般に、リターン型を指定するときはFromValue
を実装している型を使います。
同様にset_
で値を設定します。property
同様に、渡す値は Value
へ変換できる型を使います。
素のプロパティは以上のようなしくみですが、API上はそのラッパを通して触ることが多いです。例えばButton
では、これらのプロパティにアクセスするlabel
/set_
メソッドを実装しているので、プロパティを直接触る必要はありません。
プロパティを自分で定義するにはderiveマクロのProperties
を使います。
CustomButton
にclicked_
というプロパティを生やしてみましょう。custom_
を編集します。空だったCustomButton
構造体にclicked_
フィールドを追加し、Properties
をderive
します。
use gtk::prelude::*;
use gtk::glib::Properties;
use std::cell::Cell;
#[derive(Default, Properties)]
#[properties(wrapper_type = super::CustomButton)]
pub struct CustomButton {
#[property(get, set)]
clicked_count: Cell<u32>,
}
Properties
導出マクロはさまざまな使い方があります。基本は#[properties]
属性マクロの引数にwrapper!
で作った型を指定しつつ、プロパティにしたいフィールドに#[property]
属性マクロをつけます。今回の例では素直にclicked_
フィールドから、それを操作するclicked_
/set_
メソッドを生成しています。このように、メソッドを生成するためには、フィールドの型はProperty
を実装していなければなりません。
このあと、Object
から継承したプロパティ関連のメソッドをオーバーライドする必要があります。
// GObjectのサブクラスが実装するトレイト
impl ObjectImpl for CustomButton {
fn properties() -> &'static [glib::ParamSpec] {
Self::derived_properties()
}
fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
self.derived_set_property(id, value, pspec)
}
fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
self.derived_property(id, pspec)
}
}
少し面倒な手順が挟まりますが、概ね素直にプロパティを定義できます。
Signal
シグナルは何かイベントが起きたときに、登録しておいたコールバックを呼び出すしくみです。
例を見てみましょう。これまたシグナルもHello Worldのときにすでに扱っています。
fn build_ui(application: >k::Application) {
// ...
button.connect_clicked(|_| {
println!("Clicked!");
});
// ...
}
connect_
メソッドを通してclicked
シグナルにコールバックを登録しています。これもプロパティと同様にラッパが用意されている形です。素のシグナルは以下のように使われます。
// ボタンの "clicked" シグナルに接続する
button.connect_closure(
"clicked",
false,
closure_local!(move |button: Button| {
// ボタンがクリックされたらラベルを "Hello World!" に設定する
button.set_label("Hello World!");
}),
);
シグナルも自分で定義して送出できますが、本記事では扱わないので詳しくはシグナルのドキュメントを参照してください。
まだまだあるGTKの機能
今回はアプリケーションを作るための最低限の機能だけを扱いましたが、他にもいろいろな機能があります。設定を扱ったり、ウィンドウの状態を保存したり、UIをXMLで定義したりとさまざまなことができますし、GTKの周辺ライブラリもまだまだあります。
まとめ
今回はGTKの機能とgtk-rsを通したそれらの使い方をみてきました。次回、今までの知識を使ってアプリケーションを作ってみましょう。
- 前回と同様に、本記事で登場するコードは以下に置いておきます。
- https://
github. com/ KeenS/ gtk-examples