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 structinstance 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 the signal 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() 里面调用。

如果要创建菜单,就必须用 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.

posted @ 2024-04-09 14:49  wngtk  阅读(56)  评论(0编辑  收藏  举报