2回にわたって取り上げてきた、gtk-rsを使ったデスクトップアプリ開発。このテーマの締めくくりとして、今まで学んだことのまとめとして簡単なアプリを作ってみましょう。目指すのは財布からの出入金と残高を確認できる家計簿アプリです。
複雑な機能はありませんが、これまでに学んだことを試すには手頃な例題でしょう。それでは作っていきます。
- 前回と同様に、本記事で登場するコードは以下に置いておきます。
- https://
github. com/ KeenS/ gtk-examples
レイアウト
最終的には以下のようなレイアウトで画面を作っていきましょう。
前回までの記事で登場していないウィジェットや、これから作るものもありますが、追い追い説明していきます。
プロジェクトの初期化
gtk4-rs
を使うプロジェクトを作りましょう。ここでのプロジェクト名は、和同開珎からとってワドーにしておきます。みなさんは好きに名前をつけてください。
$ cargo new wado $ cd wado $ cargo add gtk4 --rename gtk
前回までと同様にgtk4
をgtk
にリネームして使います。
main.
にはHello Worldと同様のコードを置いておきましょう。これを骨格にしてコードを組み立てていきます。
use gtk::prelude::*;
use gtk::{self, glib};
fn main() {
let application =
// プロジェクト名
gtk::Application::new(Some("com.github.keens.gtk-examples.wado"), 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("ワドー"));
window.set_default_size(600, 600);
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 10);
// ☆ 後でいろいろウィジェットを追加する
window.set_child(Some(&vbox));
window.show();
}
コード中の ☆ の箇所にこれからコードを書いていきます。
ラベルの追加
まずは1つ目のウィジェット、合計金額を表示しましょう。Frame
の中にLabel
を入れます。Label
は普通の文字列を表示する他に、多少のマークアップをすることもできます。.use_
をしておきましょう。
let label = gtk::Label::builder()
.use_markup(true)
.label(format!("<big>{}円</big>", 0))
.build();
let frame = gtk::Frame::builder()
.label("残高")
.child(&label)
.build();
vbox.append(&frame);
今回は金額を大きめに表示するためにbig
タグを使いました。他に使えるタグはPangoのドキュメントに記載されています。
ここまでを実行すると、以下のような見た目になります。
出入金リストの追加
次のウィジェットとなる出入金リストを追加しましょう。使うのはListBox
ですが、いくつか準備が必要です。まず、行データの準備が必要です。まずはひとつひとつの出入金を表わすデータ型を定義しましょう。そして行データをどう表示するかも決めないといけません。
データ型の準備
出入金を表わすデータ型Payment
を定義します。これはウィジェットにデータとして渡したいので、GLibのオブジェクトとして定義しておきます。
GLibのオブジェクトを作るときの定石どおりpayment
ディレクトリを作ってimp.
とmod.
を置きます。
. ├── Cargo.toml └── src ├── main.rs └── payment ├── imp.rs └── mod.rs
まずはimp.
にコードを書いていきます。出入金を表わすオブジェクトのプロパティは以下があれば十分でしょう。
- 名前
- 金額
- 日付
出金と入金は区別せず、金額の正負で表わすことにします。また、日付に時刻はあまり必要ありませんが、他のAPIとのやりとりの具合で時刻まで含んだDateTime
を使います。
オブジェクトとしては特に求められる要件がないので素直にObject
を継承します。
前回までの記事の内容を思い出して、以下のようなコードが書けるはずです。
use gtk::glib;
use gtk::glib::{prelude::*, DateTime, Properties};
use gtk::subclass::prelude::*;
use std::cell::{Cell, RefCell};
#[derive(Debug, Properties)]
#[properties(wrapper=super::Payment)]
pub struct Payment {
#[property(get, set)]
name: RefCell<String>,
#[property(get, set)]
amount: Cell<i64>,
#[property(get, set)]
date: RefCell<DateTime>,
}
// object_subclassに要求される
impl Default for Payment {
fn default() -> Self {
Self {
name: RefCell::new(String::new()),
amount: Cell::new(0),
date: RefCell::new(DateTime::now_local().unwrap()),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for Payment {
const NAME: &'static str = "Payment";
type Type = super::Payment;
type ParentType = glib::Object;
}
impl ObjectImpl for Payment {
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)
}
}
コードを見ながら復習しましょう。
まず、オブジェクトを定義するのは、構造体を定義したあとDefault
と#[glib::
を使ったObjectSubclass
を実装し、継承するクラスごとにXxxImpl
を実装するのでした。今回はObject
を継承するだけなのでObjectImpl
だけを実装しています。
次に、オブジェクトのフィールドをプロパティにするには、#[properties]
属性マクロをつけたあと、各フィールドにproperty
をつけるのでした。そのうえでObjectImpl
のいくつかのメソッドのデフォルト実装をオーバーライドします。各フィールドは変更可能なようにCell
かRefCell
を使います。また、GLibと互換があるデータ型を使いましょう。今回の日付データもGLibのDateTime
を使っています。
さて、これをラップするコードをmod.
に書きます。
mod imp;
use gtk::glib::{self, DateTime};
glib::wrapper! {
pub struct Payment(ObjectSubclass<imp::Payment>);
}
impl Payment {
pub fn new(name: String, amount: i64, date: DateTime) -> Self {
let obj = glib::Object::new::<Payment>();
obj.set_name(name);
obj.set_amount(amount);
obj.set_date(date);
obj
}
}
ラップするコードはglib::
でおおむね自動生成できるのでした。
これであとはこのモジュールを使うだけです。
main.
にインポートしておきましょう。
mod payment;
use payment::Payment;
これからPayment
を使います。
UIの構築
次に、行、つまりPayment
の表示方法を決めましょう。新しくWidget
を継承したクラスを定義する方法もありますが、今回は簡素に関数でUIを作ります。すなわち、以下のような型の関数を定義してPayment
からUIを構築します。
pub fn display_ui(payment: &Payment) -> impl IsA<gtk::Widget>;
ui.
というファイルを作り、そこに関数を書いていきます。
. ├── Cargo.toml └── src ├── ui.rs ← NEW ├── main.rs └── payment ├── imp.rs └── mod.rs
行は以下のように日付、名前、金額の順に同じ幅を割り当て、日付を左寄せ、金額を右寄せで表示することにしましょう。
+------------+------------+------------+
| datetime~~ | ~~~name~~~ | ~~~amount |
+------------+------------+------------+
それではコードを書いていきます。書き始めは素直にhbox
を作ります。
use gtk::glib::DateTime;
use gtk::prelude::*;
use gtk;
use crate::Payment;
pub fn display_ui(payment: &Payment) -> impl IsA<gtk::Widget> {
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(10)
.homogeneous(true)
.build();
// ...
}
ここで.homogeneous(true)
を指定することでそれぞれの列が同じ幅になります。
それぞれの表示にはLabel
を使います。Label
にhalign
を指定することで、始端寄せ
let date = gtk::Label::builder().halign(gtk::Align::Start).build();
let name = gtk::Label::new(None);
let amount = gtk::Label::builder().halign(gtk::Align::End).build();
さて、このラベルに表示する文字列データですが、少し懸念があります。素直にPayment
から文字列をコピーするとPayment
とは独立してデータを持つことになるのでPayment
が更新されてもラベルが更新されません。Payment
を更新するたびに手動でUIも更新するという手もありますが、もっと便利な方法があります。bind_
という機能を使えば、あるオブジェクトのプロパティを更新すると別のオブジェクトのプロパティも連動して更新することができます。今回はこの仕組みを使いましょう。
例えば以下のコードでpayment
の"name"
プロパティが更新されたらname
という変数に束縛してあるLabel
の"label"
プロパティも連動して更新させられます。
payment
.bind_property("name", &name, "label")
.sync_create()
.build();
同様にしてdate
やamount
もやりたいのですが、これらは文字列型ではないのでもうワンクッション必要です。そこでtransform_
を使うことで変換方法を指定できるので、文字列に変換しましょう。例えばamount
なら以下のとおりです。
payment
.bind_property("amount", &amount, "label")
.transform_to(|_, a: i64| Some(format!("{}円", a)))
.sync_create()
.build();
日付については、日時データ型を日付に流用していることもあって少し手間がかかります。今後も何度か使うので、今のうちに便利関数を定義しておきましょう。
src/
を作りましょう。
. ├── Cargo.toml └── src ├── ui.rs ├── util.rs ← NEW ├── main.rs └── payment ├── imp.rs └── mod.rs
そして以下の関数をutil.
に書いて保存します。
use gtk::glib;
pub fn format_date(d: glib::DateTime) -> String {
format!("{:04}-{:02}-{:02}", d.year(), d.month(), d.day_of_month())
}
main.
にmod util;
を追加したら、ui.
に以下のようなコードが書けるはずです。
use crate::util;
payment
.bind_property("date", &date, "label")
.transform_to(|_, d: DateTime| Some(util::format_date(d)))
.sync_create()
.build();
少し順序が前後しましたが、これでdate
、name
、amount
の3つのラベルが用意できました。これらをhbox
に追加してdisplay_
は完成です。わかりづらいと思うので関数全体を掲載しておきます。
use crate::{util, Payment};
use gtk;
use gtk::glib::{self, DateTime};
use gtk::prelude::*;
pub fn display_ui(payment: &Payment) -> impl IsA<gtk::Widget> {
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(10)
.homogeneous(true)
.build();
let date = gtk::Label::builder().halign(gtk::Align::Start).build();
payment
.bind_property("date", &date, "label")
.transform_to(|_, d: DateTime| Some(util::format_date(d)))
.sync_create()
.build();
let name = gtk::Label::new(None);
payment
.bind_property("name", &name, "label")
.sync_create()
.build();
let amount = gtk::Label::builder().halign(gtk::Align::End).build();
payment
.bind_property("amount", &amount, "label")
.transform_to(|_, a: i64| Some(format!("{}円", a)))
.sync_create()
.build();
hbox.append(&date);
hbox.append(&name);
hbox.append(&amount);
hbox
}
ListBox
を作る
これで準備が整いました。出入金のリストを画面に追加しましょう。main.
に戻ってbuild_
関数を変更します。build_
ではvbox
を用意して、その子ウィジェットとしていろいろな部品を追加しているのでした。出入金のリストをvbox
の子ウィジェットとして追加していきます。
まずはPayment
のListStore
を作って、それをモデルにしてListBox
を作ります。
use gtk::gio;
mod ui;
let model = gio::ListStore::new(Payment::static_type());
let list_box = gtk::ListBox::new();
list_box.bind_model(Some(&model), |item| {
let payment = item.downcast_ref::<Payment>().unwrap();
ui::display_ui(payment).upcast::<gtk::Widget>()
});
少しややこしいかもしれませんが、GUIアプリケーションではモデルとビューを分離するのが良いプラクティスとされています。ここではListStore
がモデル、ListBox
がビューになっています。行も我々はPayment
とdisplay_
に分けましたね。
さて、このままlist_
をvbox
に追加すると、長くなったときに溢れてしまうのでScrolledWindow
を噛ませてからvbox
に追加しましょう。
let scrolled_window = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never) // Disable horizontal scrolling
.min_content_height(400)
.child(&list_box)
.build();
vbox.append(&scrolled_window);
これで画面に出入金リストが追加されました。リスト内にデータが1つもないので手動でいくつかデータを追加しておきましょう。
model.append(&Payment::new(
"お小遣い".into(),
1000,
glib::DateTime::now_local().unwrap(),
));
model.append(&Payment::new(
"きゅうり".into(),
-150,
glib::DateTime::now_local().unwrap(),
));
こちらも全体像がつかみづらくなったと思うので概観を掲載しておきます。
fn build_ui(app: >k::Application) {
let window = /* ... */;
let vbox = /* ... */;
let model = /* ... */;
model.append(/* ... */);
model.append(/* ... */);
let list_box = /* ... */;
list_box.bind_model(Some(model), |item| {
/* ... */
});
let scrolled_window = /* ... */;
let label = /* ... */
let frame = /* ... */;
vbox.append(&frame);
vbox.append(&scrolled_window);
window.set_child(Some(&vbox));
window.show();
}
この状態で実行すると、以下のような見た目になるはずです。
まだウィジェットを構築しただけなので出入金リストと残高が釣り合っていませんね。続いてはそこに着手しましょう。
アプリケーションの再構成
ここまででウィジェットはできましたが、それぞれが連動していません。重要な部品をひとまとめにして機能を与えましょう。
重要なのは出入金リストと残高ラベルの連動です。出入金リストはListStore
がモデルなのでした。そこで、以下のような構造体を定義しましょう。main.
の先頭の方に以下を書きます。
#[derive(Debug)]
pub struct Wado {
model: gio::ListStore,
balance: gtk::Label,
}
この構造体をGTKのAPIに渡す予定はないので、GLibのオブジェクトではなくただのRustの構造体です。この構造体の初期化でそれぞれの値を初期化します。
impl Default for Wado {
fn default() -> Self {
let label = gtk::Label::builder()
.use_markup(true)
.label(format!("<big>{}円</big>", 0))
.build();
let model = gio::ListStore::new(Payment::static_type());
// ...
Self {
model,
balance: label,
}
}
}
そして、残高とラベルを連動させましょう。ListStore
のitems-changed
シグナルを使うとアイテムに変更があるたびにラベルを更新できます。default
関数の続きを書いていきます。
let label = /* ... */;
let model = /* ... */;
model.connect_items_changed(glib::clone!(@weak label => move |m, _, _, _| {
let balance = m
.into_iter()
.map(|item| item.unwrap().downcast::<Payment>().unwrap().amount())
.sum::<i64>();
label.set_markup(&format!("<big>{}</big>円", balance));
}));
Self { /* ...*/ }
connect_
の第一引数m
にはListStore
自身が入っています。&ListStore
はIntoIterator
を実装しているので普通のRustのデータ型のようにイテレータのAPIが使えます。
あとは便利関数をいくつか定義しておきます。
impl Wado {
pub fn record_payment(&mut self, payment: &Payment) {
self.model.append(payment);
}
pub fn model(&self) -> &gio::ListStore {
&self.model
}
pub fn balance(&self) -> >k::Label {
&self.balance
}
}
これを使ってbuild_
を書き換えましょう。
fn build_ui(app: >k::Application) {
let window = /* ... */;
let vbox = /* ... */;
// modelの代わりにwadoを初期化
let mut wado = Wado::default();
wado.record_payment(/* ... */);
wado.record_payment(/* ... */);
let list_box = /* ... */;
// &model ではなくwado.model()に
list_box.bind_model(Some(wado.model()), |item| {
/* ... */
});
let scrolled_window = /* ... */;
// labelではなくwadoのbalanceを使う
let frame = gtk::Frame::builder()
.label("残高")
.child(wado.balance())
.build();
vbox.append(&frame);
vbox.append(&scrolled_window);
window.set_child(Some(&vbox));
window.show();
}
これで再度アプリケーションを実行すると残高の部分が出入金リストと対応がとれるようになっているはずです。
入力を作る
出入金リストのデータをコード内で定義していましたが、UIから入力できるようにしましょう。
記述が長くなるのでbuild_
の中には直接書かずに独立した関数にします。main.
に以下の関数を追加しましょう。
use std::cell::RefCell;
use std::rc::Rc;
fn input_box(wado: Rc<RefCell<Wado>>) -> gtk::Box {
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
// ...
hbox
}
後でwado
をglib::
に渡したいのでRc<RefCell<>>
に包んでいます。これに入力フォームを追加していきます。
入力フォームはEntry
を使って作ります。名前のフィールドは以下のように書けるでしょう。
let name = gtk::Entry::builder().placeholder_text("name").build();
input_
を使って目的を設定することもできます。これを設定しておくと、スマホのキーボードのようなオンスクリーン入力の際に、自動で入力を切り替えてくれます。amount
ならDigits
を使って以下のように書きます。
let amount = gtk::Entry::builder()
.placeholder_text("amount")
.input_purpose(gtk::InputPurpose::Digits)
.build();
最終的に取り出せる値は文字列なのでこのあと処理します。
さて、date
ですが適当な入力ウィジェットがGTKに見当らなかったので作りましょう。
日付ピッカーを作る
日付ピッカーはutil
に作っておきましょう。ウィジェットがないと言っても、カレンダーウィジェットとポップオーバーウィジェットを組み合わせるだけです。
Button
、Popover
、Calendar
を組み合わせたウィジェットを作ります。ボタンを押すとカレンダーがポップアップしてきて、日付をクリックするとカレンダーがポップダウンしてその日付が選ばれた状態になるウィジェットです。
use gtk::prelude::*;
pub fn datepicker() -> (gtk::Box, gtk::Calendar) {
let hbox = gtk::Box::builder()
.homogeneous(false)
.orientation(gtk::Orientation::Horizontal)
.build();
let button = gtk::Button::new();
let cal = gtk::Calendar::new();
// デフォルトで今日の日付を選択しておく
cal.select_day(&glib::DateTime::now_local().unwrap());
let pop = gtk::Popover::builder().child(&cal).autohide(true).build();
button.connect_clicked(glib::clone!(@weak pop => move |_| {
pop.popup()
}));
button.set_label(&format_date(cal.date()));
cal.connect_day_selected(glib::clone!(@weak pop, @weak button => move |cal| {
pop.popdown();
// 日付が選ばれたらボタンのラベルに反映しておく。
button.set_label(&format_date(cal.date()));
}));
hbox.append(&button);
hbox.append(&pop);
// 親ウィジェットに追加するためのBoxと
// 選択された日付を取り出すためのカレンダーを返す
(hbox, cal)
}
馴れてきた頃だと思うので解説がなくても読めるでしょうか。特にトリッキーなところはありません。
日付ピッカーを使う
日付ピッカーを作ったので残りの入力フォームを作りましょう。3項目の入力と決定ボタンを用意し、決定ボタンが押されたらそれをアプリケーションに反映させます。
fn input_box(wado: Rc<RefCell<Wado>>) -> gtk::Box {
let hbox = /* ... */;
let name = /* ... */;
let amount = /* ... */;
let (picker, cal) = util::datepicker();
let new_button = gtk::Button::builder().label("new").build();
new_button.connect_clicked(
glib::clone!(@weak name, @weak amount, @weak cal, @strong wado => move |_| {
let n = name.buffer().text().to_string();
let amount = match amount.buffer().text().parse() {
Ok(a) => a,
Err(_) => {
// 後で `dialog` を作る
util::dialog("エラー", "金額は整数値で入力して下さい");
return;
},
};
let date = cal.date();
let payment = Payment::new(n, amount, date);
wado.borrow_mut().record_payment(&payment);
}),
);
// 並びは出入金リストと同じく日付、名前、金額の順
hbox.append(&picker);
hbox.append(&name);
hbox.append(&amount);
hbox.append(&new_button);
hbox
}
決定ボタンが押された場合、つまりclicked
が発火された場合はname
、amount
、cal
からそれぞれ値を読み出します。
ここで、amount
は整数値でなければならないので、一度整数としてパースして失敗すればreturn
でクロージャから抜けます。このときユーザにダイアログを出したいのですが、 Dialog
は非推奨となっており、 Window
を使うように誘導されているのでそのようにします。
この処理もやや煩雑になるのでutil
に切り出しましょう。
pub fn dialog(title: &str, message: &str) {
let label = gtk::Label::new(Some(message));
let button = gtk::Button::with_label("OK");
let vbox = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(10)
.build();
vbox.append(&label);
vbox.append(&button);
let dialog = gtk::Window::builder().title(title).child(&vbox).build();
button.connect_clicked(glib::clone!(@weak dialog => move |_| {
dialog.close()
}));
dialog.show();
}
新しいウィンドウにメッセージラベルとボタンを追加しているだけで特に難しいことはしていません。
仕上げにここで作った入力を画面に追加しましょう。
fn build_ui(application: >k::Application) {
// ...
// wadoをRc<RefCell<>>に包んだものを用意
let wado = Rc::new(RefCell::new(wado));
vbox.append(/* ... */);
vbox.append(/* ... */);
// input_boxを新たにvboxに追加
vbox.append(&input_box(wado));
// ...
}
さて、ここまできたら当初の画面までできたことになります。
画面から自由に出入金を追加できるようになりました。出入金を追加すると自動で残高も変わりますね。
かなり完成に近付きましたが、まだ追加した項目を編集/削除できません。最後にその機能を追加しましょう。
項目を編集/削除する
リストの項目をクリックすると、編集/削除できるようにしましょう。
ラベルを変更可能なエントリにし、編集を反映または項目を削除するボタンを追加します。
それではコードを書いていきましょう。項目がクリックされたことはListBox
のrow-activated
シグナルで検知できます。以下のようにシグナルに接続してウィジェットを差し替えればよさそうです。
list_box.connect_row_activated(glib::clone!(@weak model => move |_lbox, row| {
let payment = model.item(row.index() as u32).unwrap().downcast::<Payment>().unwrap();
// これから `edit_ui` 関数を定義する
row.set_child(Some(&ui::edit_ui(&payment, &model, row)));
}));
ということでedit_
関数をui
モジュールに実装しましょう。
書き出しはもう悩むことも少ないでしょう。hbox
を用意します。
use gtk::{gio, glib};
pub fn edit_ui(
payment: &Payment,
model: &gio::ListStore,
row: >k::ListBoxRow,
) -> impl IsA<gtk::Widget> {
let hbox = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
// ...
hbox
}
それぞれのラベルを編集可能なEntry
にします。このとき、デフォルトで入っている文字をbuffer
で与えておきます。中身は現在のPayment
の値です。
let name_entry = gtk::Entry::builder()
.placeholder_text("name")
.buffer(>k::EntryBuffer::builder().text(payment.name()).build())
.build();
amount
も同様に作れます。
let amount_entry = gtk::Entry::builder()
.placeholder_text("amount")
.buffer(
>k::EntryBuffer::builder()
.text(format!("{}", payment.amount()))
.build(),
)
.input_purpose(gtk::InputPurpose::Digits)
.build();
date
はdatepicker
を作ってselect_
しておきましょう。
let (picker, cal) = util::datepicker();
cal.select_day(&payment.date());
あとウィジェットを2つ、更新ボタンと削除ボタンを作ります。
let update_button = gtk::Button::builder().label("update").build();
let delete_button = gtk::Button::builder().label("delete").build();
row
からその行がリスト内の何行目かを取り出せるので、先に取り出しておきましょう。
let index = row.index() as u32;
削除ボタンは簡単に実装できます。delete_
が押されたら削除するだけです。
delete_button.connect_clicked(glib::clone!(@weak model => move|_|{
model.remove(index);
}));
編集ボタンは新規作成ボタンと似ていますが、既存のオブジェクトの値を変更する点、編集UIに切り替えたウィジェットを元に戻す点などが異なります。
update_button.connect_clicked(
glib::clone!(@weak name_entry, @weak amount_entry, @weak cal, @weak payment, @weak row, @weak model => move |_| {
let name = name_entry.buffer().text().to_string();
let amount = match amount_entry.buffer().text().parse::<i64>() {
Ok(a) => a,
Err(_) => {
util::dialog("エラー", "金額は整数値で入力してください");
return;
},
};
let date = cal.date();
// オブジェクトを更新
payment.set_name(name);
payment.set_amount(amount);
payment.set_date(date);
// ウィジェットを元に戻す
row.set_child(Some(&display_ui(&payment)));
// 残高を更新するために items-changed シグナルを発行
model.items_changed(index, 0, 0);
}),
);
編集が終わったら最後にitems-changed
シグナルを発行して、残高を再計算させます。
最後にこれらのウィジェットをhbox
に追加して完成です。
edit_
の概観を掲載すると以下のようになります。
pub fn edit_ui(
payment: &Payment,
model: &gio::ListStore,
row: >k::ListBoxRow,
) -> impl IsA<gtk::Widget> {
let hbox = /* ... */;
let name_entry = /* ... */;
let amount_entry = /* ... */;
let (picker, cal) = /* ... */;
cal.select_day(&payment.date());
let update_button = /* ... */;
let delete_button = /* ... */;
let index = row.index() as u32;
update_button.connect_clicked(
/* ... */
);
delete_button.connect_clicked
/* ... */
}));
hbox.append(&picker);
hbox.append(&name_entry);
hbox.append(&amount_entry);
hbox.append(&update_button);
hbox.append(&delete_button);
hbox
}
あとはこれを使うだけです。build_
を編集して、出入金をクリックしたらedit_
を呼び出すようにしましょう。
fn build_ui(app: >k::Application) {
// ...
let list_box = /* ... */;
list_box.bind_model(/* ... */);
// modelを一旦変数に束縛する
let model = wado.model();
// ListBoxのrow-activatedシグナルに接続して編集画面を呼び出す
list_box.connect_row_activated(glib::clone!(@weak model => move |_lbox, row| {
let payment = model.item(row.index() as u32).unwrap().downcast::<Payment>().unwrap();
row.set_child(Some(&ui::edit_ui(&payment, &model, row)));
}));
// ...
}
これでアプリケーションは完成です。build_
の中でいくつか項目を追加していましたが、そのコードは消してしまってよいでしょう。
項目を追加したり編集したり削除したりしてみましょう。ちゃんと動いていますか?
まとめ
今までに学んだことを活かしてgtk-rsを使ってGUIアプリケーションを作ってみました。いろいろな機能を使った気がしますが、これでもまだGTKの持つ機能は使いきれていません。例えば、XMLでUIを構築するような大規模なアプリケーションに必須となる機能にはふれていませんね。
また、今回の家計簿アプリ自体もまだまだ拡張可能性があります。扱う項目にカテゴリをつけたり、項目を日付順にソートしたり、データを永続化したりなど、簡単にできそうな機能でもたくさん思い付きます。ぜひ練習問題として取り組んでみてください。
gtk-rsにはすばらしい機能がありますが、直接さわるには煩雑だなと感じた方も多いかもしれません。Relm4というgtk-rsの上でElmのようなAPIでGUIアプリケーションを構築できるフレームワークなどもあります。興味があったら調べてみてくさい。
- 今回のコードはこちらに置いておきます。
- https://
github. com/ KeenS/ gtk-examples