GUI development with Rust and GTK4 阅读笔记
简记
这是我第二次从头开始阅读,有第一次的印象要容易不少。
如果只关心具体的做法,而不思考为什么这样做,以及整体的框架,阅读的过程将会举步维艰。
简略记录 gtk-rs 的书中提到的点。对同一个问题书中所演示了多种处理方法,而且跨度比较大,第一次阅读的时候经常出现忘记之前的内容。
fn signals() -> &'static [Signal] {
static SIGNALS: OnceLock<Vec<Signal>> = OnceLock::new();
SIGNALS.get_or_init(|| {
vec![Signal::builder("max-number-reached")
.param_types([i32::static_type()])
.build()]
})
}
signals:
max-number-reached(i32)
GTK GObject 的缺点
- Reference cycles
- Not thread safe
使用异步块/函数一般是通过 glib::spawn_future_local()
spawn.
异步函数之间的通信是使用 async_channel
。
有些异步库依赖 tokio
runtime 不能直接通过 GLib 的主线程 spawn。 通过这样一个函数返回的 runtime 来 spawn。
use std::sync::OnceLock;
use tokio::runtime::Runtime;
fn runtime() -> &'static Runtime {
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
RUNTIME.get_or_init(|| {
Runtime::new().expect("Setting up tokio runtime needs to succeed.")
})
}
设置(Settings)
GTK 应用的设置是通过一个 .gschema.xml
文件描述。
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<schema id="org.gtk_rs.Settings1" path="/org/gtk_rs/Settings1/">
<key name="is-switch-enabled" type="b">
<default>false</default>
<summary>Default switch state</summary>
</key>
</schema>
</schemalist>
设置是在应用关闭后再打开,仍然保持关闭前的状态。这个 GSchema
xml 需要编译安装。
mkdir -p $HOME/.local/share/glib-2.0/schemas
cp org.gtk_rs.Settings1.gschema.xml $HOME/.local/share/glib-2.0/schemas/
glib-compile-schemas $HOME/.local/share/glib-2.0/schemas/
settings 的状态绑定到 widget,是调用 settings 的 bind()
方法。把某个属性绑定到某一个属性是通过源对象 bind_property()
方法
第8章 Saving Window State 还是使用 GTK 的设置,只不过在 GTK 的对象系统中使用了自己定义的方法(在 Rust 中我分不出哪个是class struct 和 instance struct,不过我知道状态放在 imp.rs
,方法一般是 mod.rs
,如果实际写的时候放在一个文件里面就是把 imp.rs
中的内容放到对应的 mod.rs
并且用 mod imp {}
包裹)。
List Widgets
当 ListView 创建 ListItem 的时候会调 factory.connect_setup
注册的回调,
factory.connect_bind
是将 Model 中的数据绑定到单独的 List Item,
绑定数据到 Widget:
- 设置 Widget 的效果
label.set_label(&integer_object.number().to_string());
- 将数据的属性绑定到 Widget 的属性
integer_object
.bind_property("number", &label, "label")
.sync_create()
.build();
- 在
connect_setup
里面完成绑定,可以解决上一个解决方案的问题
list_item
.property_expression("item")
.chain_property::<IntegerObject>("number")
.bind(&label, "label", Widget::NONE);
// 拿到 list_item 中的 item (data) 中的 number 属性,绑定到实际的 Widget
Composite Templates
除了直接编写 Rust 代码创造界面,还可以使用 XML 文件描述窗口组件。在 XML 文件里面可以使用直接在 Rust 代码中继承的组件。
如果要手动设置组件中的child的信号处理,需要在 Rust 中声明。
You use it by adding a struct member with the same name as one
id
attribute in the template.
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/example/window.ui")]
pub struct Window {
#[template_child]
pub button: TemplateChild<CustomButton>,
}
在XML文件中, signal 标签声明组件的信号的处理函数。
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks(); // 使用了 callback 就必须要有这一句
}
<object class="MyGtkAppCustomButton" id="button">
<signal name="clicked" handler="handle_button_clicked"/>
<property name="label">Press me!</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
</object>
而具体的回调函数在哪里定义?在 imp (模)块的结构体的 impl 中定义。
#[gtk::template_callbacks]
impl Window {
#[template_callback]
fn handle_button_clicked(button: &CustomButton) {
// Set the label to "Hello World!" after the button has been clicked on
button.set_label("Hello World!");
}
}
In order to access the widget's state we have to add
swapped="true"
to thesignal
tag.
要访问 XML 中定义 signal
标签的组件的状态,说直白一点,就是回调函数访问 self (ui文件对应的表达状态的结构体,impl Window 的 Window 就是 self),需要在 XML 中的 signal 中添加 swapped="true"
。有了swapped="true"
handle_button_clicked() 的第一个参数就可以是 self。
#[gtk::template_callbacks]
impl Window {
#[template_callback]
fn handle_button_clicked(&self, button: &CustomButton) {
let number_increased = self.number.get() + 1;
self.number.set(number_increased);
button.set_label(&number_increased.to_string())
}
}
在 Rust 中移除继承的 GTK widget 中的 Child,通过模板中的回调函数来访问组件中的child,而不在结构体中注册 template_child,。 class_init()
里必须添加 CustomButton::ensure_type();
Actions
app.set_accels_for_action("win.close", &["<Ctrl>W"]);
设置快捷键
创建一个 Action 不只是可以接收参数还可以设置状态。而状态也可以在设置的回调函数里面获取到,并且可以改变。 ActionEntry
这里的activate
接收的回调函数有 3 个参数。
// Add action "count" to `window` taking an integer as parameter
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |_, action, parameter| {
// Get state
let mut state = action
.state()
.expect("Could not get state.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Get parameter
let parameter = parameter
.expect("Could not get parameter.")
.get::<i32>()
.expect("The variant needs to be of type `i32`.");
// Increase state by parameter and store state
state += parameter;
action.set_state(&state.to_variant());
// Update label with new state
label.set_label(&format!("Counter: {state}"));
})
.build();
用 Action 这里少了很多的绑定的操作,这里只声明按钮点击的信号处理和 Action。状态是直接将普通变量 to_variant() 转换一下,不需要声明一个属性,也不需要声明额外的信号处理不同的状态,可以直接在这个注册给 Action 的回调里面做。所以处理用户的输入不止是可以注册一个信号的处理,还可以将信号的处理再委托给 Action。
所有的 Button 都实现了 Actionable 接口,可以给 Button 指定 action_name()
,action_target()
是指定 Action 的参数。
在 UI (xml)文件 内部可以指定 "action-name" 和 "action-target"。只需在 Rust 中构造组件后注册对应的 Action 即可。
<object class="GtkButton" id="button">
<property name="label">Press me!</property>
<property name="action-name">win.count</property>
<property name="action-target">1</property>
</object>
也就是说,读代码的时候看到 XML 文件里面有指定 action, 那么在 Rust 里面肯定是注册了这个Action处理函数的。
// setup_actions() in impl Window block of mod.rs
self.add_action_entries([action_count]);
// Trait shared by all GObjects
impl ObjectImpl for Window {
fn constructed(&self) {
// Call "constructed" on parent
self.parent_constructed();
// Add actions
self.obj().setup_actions();
}
}
覆盖父类的方法,在imp.rs的对应的trait的实现里实现覆盖的方法。
自定义组件方法,在mod.rs的模块的impl块里定义,自定义的 setup 的方法都在 constructed() 里面调用。
Menu
如果要创建菜单,就必须用 Action。一个菜单项符合3种描述:
- no parameter and no state, or
- no parameter and boolean state, or
- string parameter and string state.
menu 标签声明菜单的 model,在 GtkMenuButton 中指定菜单的 model, 菜单的具体行为是 action 去做的。
<template class="MyGtkAppWindow" parent="GtkApplicationWindow">
<property name="title">My GTK App</property>
+ <property name="width-request">360</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <child type ="end">
+ <object class="GtkMenuButton">
+ <property name="icon-name">open-menu-symbolic</property>
+ <property name="menu-model">main-menu</property>
+ </object>
+ </child>
+ </object>
+ </child>
菜单可以很好的显示我们的有状态的action,菜单的action的状态需要通过设置来保存。
设置提供了一个 create_action 的方法可以很方便的创建 action.