动作(Actions)
到目前为止,我们已经学会了许多将控件粘在一起的方法。 我们可以通过通道发送消息、发射信号、共享引用计数状态和绑定属性。 现在,我们将通过学习动作(Actions)来完成我们的设置。
动作是绑定到某个 GObject 的功能。 让我们来看看最简单的情况,即在没有参数的情况下激活一个动作。
文件名:listings/actions/1/main.rs
use gio::ActionEntry;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ApplicationWindow};
const APP_ID: &str = "org.gtk_rs.Actions1";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Set keyboard accelerator to trigger "win.close".
app.set_accels_for_action("win.close", &["<Ctrl>W"]);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a window and set the title
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.width_request(360)
.build();
// Add action "close" to `window` taking no parameter
let action_close = ActionEntry::builder("close")
.activate(|window: &ApplicationWindow, _, _| {
window.close();
})
.build();
window.add_action_entries([action_close]);
// Present window
window.present();
}
首先,我们创建了一个名为 "close" 的新 gio::ActionEntry
,它不需要任何参数。 我们还连接了一个回调,用于在激活动作时关闭窗口。 最后,我们通过 add_action_entries
将操作条目添加到窗口中。
文件名:listings/actions/1/main.rs
use gio::ActionEntry;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ApplicationWindow};
const APP_ID: &str = "org.gtk_rs.Actions1";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Set keyboard accelerator to trigger "win.close".
app.set_accels_for_action("win.close", &["<Ctrl>W"]);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a window and set the title
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.width_request(360)
.build();
// Add action "close" to `window` taking no parameter
let action_close = ActionEntry::builder("close")
.activate(|window: &ApplicationWindow, _, _| {
window.close();
})
.build();
window.add_action_entries([action_close]);
// Present window
window.present();
}
使用动作的最常见原因之一是快捷键,因此我们在此添加了一个。 通过 set_accels_for_action
,可以为某个动作分配一个或多个快捷键。 有关 accelerator_parse
的语法,请查阅文档。
在我们继续讨论动作的其他方面之前,让我们先来了解一下这里的一些奇特之处。 "win.close" 中的 "win" 是动作组。 但 GTK 如何知道 "win" 是我们窗口的动作组呢? 答案是,在窗口和应用程序中添加操作非常普遍,因此已经有两个预定义的组可用:
- "app" 用于应用程序的全局动作,
- "win" 用于与应用程序窗口相关的动作。
我们可以通过 insert_action_group
方法为任何控件添加动作组。 让我们将动作添加到动作组 "custom-group",然后将该组添加到我们的窗口。该动作项(action entry)不再是针对我们的窗口,"activate(激活)" 回调的第一个参数类型是 SimpleActionGroup
,而不是 ApplicationWindow
。 这意味着我们必须将窗口克隆到闭包中。
文件名:listings/actions/2/main.rs
use gio::ActionEntry;
use glib::clone;
use gtk::gio::SimpleActionGroup;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ApplicationWindow};
const APP_ID: &str = "org.gtk_rs.Actions2";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Set keyboard accelerator to trigger "custom-group.close".
app.set_accels_for_action("custom-group.close", &["<Ctrl>W"]);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a window and set the title
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.width_request(360)
.build();
// Add action "close" to `window` taking no parameter
let action_close = ActionEntry::builder("close")
.activate(clone!(
#[weak]
window,
move |_, _, _| {
window.close();
}
))
.build();
// Create a new action group and add actions to it
let actions = SimpleActionGroup::new();
actions.add_action_entries([action_close]);
window.insert_action_group("custom-group", Some(&actions));
// Present window
window.present();
}
如果我们将快捷键绑定到 "custom-group.close",它就会像以前一样工作。
文件名:listings/actions/2/main.rs
use gio::ActionEntry;
use glib::clone;
use gtk::gio::SimpleActionGroup;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ApplicationWindow};
const APP_ID: &str = "org.gtk_rs.Actions2";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Set keyboard accelerator to trigger "custom-group.close".
app.set_accels_for_action("custom-group.close", &["<Ctrl>W"]);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
// Create a window and set the title
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.width_request(360)
.build();
// Add action "close" to `window` taking no parameter
let action_close = ActionEntry::builder("close")
.activate(clone!(
#[weak]
window,
move |_, _, _| {
window.close();
}
))
.build();
// Create a new action group and add actions to it
let actions = SimpleActionGroup::new();
actions.add_action_entries([action_close]);
window.insert_action_group("custom-group", Some(&actions));
// Present window
window.present();
}
此外,如果我们有多个相同窗口的实例,我们会希望在激活 "win.close" 时只关闭当前聚焦的窗口。 事实上,"win.close" 将被派发到当前聚焦的窗口。 不过,这也意味着我们实际上为每个窗口实例定义了一个动作。 如果我们想使用一个全局动作,可以在应用程序上调用 add_action_entries
.
添加 "win.close" 作为一个简单的示例非常有用。 不过,今后我们将使用预定义的 "window.close" 操作,其作用完全相同。
参数和状态
与大多数函数一样,动作可以接受一个参数。 不过,与大多数函数不同的是,它也可以是有状态的。 让我们看看它是如何工作的。
文件名:listings/actions/3/main.rs
use gio::ActionEntry;
use gtk::prelude::*;
use gtk::{
gio, glib, Align, Application, ApplicationWindow, Button, Label, Orientation,
};
const APP_ID: &str = "org.gtk_rs.Actions3";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
let original_state = 0;
let label = Label::builder()
.label(format!("Counter: {original_state}"))
.build();
// Create a button with label
let button = Button::builder().label("Press me!").build();
// Connect to "clicked" signal of `button`
button.connect_clicked(move |button| {
// Activate "win.count" and pass "1" as parameter
let parameter = 1;
button
.activate_action("win.count", Some(¶meter.to_variant()))
.expect("The action does not exist.");
});
// Create a `gtk::Box` and add `button` and `label` to it
let gtk_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.spacing(12)
.halign(Align::Center)
.build();
gtk_box.append(&button);
gtk_box.append(&label);
// Create a window, set the title and add `gtk_box` to it
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.width_request(360)
.child(>k_box)
.build();
// 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();
window.add_action_entries([action_count]);
// Present window
window.present();
}
在这里,我们创建了一个 "win.count" 动作,每次激活时都会按给定参数增加状态。 它还负责用当前状态更新标签。 每次点击按钮都会激活动作,同时将 "1" 作为参数传递。 我们的应用程序就是这样运行的:
可执行动作的(Actionable)
将动作连接到按钮的 "clicked"(点击)信号是一个典型的使用案例,这就是为什么所有按钮都实现了 Actionable
接口。 这样,就可以通过设置 "action-name" 属性来指定动作。 如果动作接受参数,则可通过 "action-target" 属性进行设置。 有了 ButtonBuilder
, 我们可以通过调用其方法来设置一切。
文件名:listings/actions/4/main.rs
use gio::ActionEntry;
use gtk::prelude::*;
use gtk::{
gio, glib, Align, Application, ApplicationWindow, Button, Label, Orientation,
};
const APP_ID: &str = "org.gtk_rs.Actions4";
fn main() -> glib::ExitCode {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn build_ui(app: &Application) {
let original_state = 0;
let label = Label::builder()
.label(format!("Counter: {original_state}"))
.build();
// Create a button with label and action
let button = Button::builder()
.label("Press me!")
.action_name("win.count")
.action_target(&1.to_variant())
.build();
// Create `gtk_box` and add `button` and `label` to it
let gtk_box = gtk::Box::builder()
.orientation(Orientation::Vertical)
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.spacing(12)
.halign(Align::Center)
.build();
gtk_box.append(&button);
gtk_box.append(&label);
// Create a window and set the title
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.width_request(360)
.child(>k_box)
.build();
// 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();
window.add_action_entries([action_count]);
// Present window
window.present();
}
还可以通过界面生成器轻松访问可执行动作的部件。 像往常一样,我们通过一个复合模板来创建窗口。 然后,我们可以在模板中设置 "动作名称(action-name)"和 "动作目标(action-target)"属性。
文件名:listings/actions/5/resources/window.ui
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="MyGtkAppWindow" parent="GtkApplicationWindow">
<property name="title">My GTK App</property>
<child>
<object class="GtkBox" id="gtk_box">
<property name="orientation">vertical</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>
<property name="spacing">12</property>
<property name="halign">center</property>
<child>
<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>
</child>
<child>
<object class="GtkLabel" id="label">
<property name="label">Counter: 0</property>
</object>
</child>
</object>
</child>
</template>
</interface>
我们将在 Window::setup_actions
方法中连接操作并将其添加到窗口。
文件名:listings/actions/5/window/mod.rs
mod imp;
use gio::ActionEntry;
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application};
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn setup_actions(&self) {
// Add stateful action "count" to `window` taking an integer as parameter
let original_state = 0;
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |window: &Self, 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
window.imp().label.set_label(&format!("Counter: {state}"));
})
.build();
self.add_action_entries([action_count]);
}
}
最后,setup_actions
将在 constructed
函数内调用。
文件名:listings/actions/5/window/imp.rs
use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{glib, CompositeTemplate, Label};
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/example/window.ui")]
pub struct Window {
#[template_child]
pub label: TemplateChild<Label>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "MyGtkAppWindow";
type Type = super::Window;
type ParentType = gtk::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// 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();
}
}
// Trait shared by all widgets
impl WidgetImpl for Window {}
// Trait shared by all windows
impl WindowImpl for Window {}
// Trait shared by all application windows
impl ApplicationWindowImpl for Window {}
该应用程序的行为与我们之前的示例相同,但它将使我们在下一部分添加菜单时更加简单。
菜单
如果要创建菜单,就必须使用动作,而且要使用界面生成器。 通常情况下,菜单条目中的操作符合以下三种描述之一:
- 无参数和无状态,
- 或无参数和布尔状态,
- 或字符串参数和字符串状态。
让我们修改我们的小程序来演示这些情况。 首先,我们扩展 setup_actions
。 对于不带参数或状态的动作,我们可以使用预定义的 "window.close" 动作。 因此,我们无需在此处添加任何内容。
通过动作 "button-frame",我们可以操作按钮的 "has-frame" 属性。 这里的惯例是,不带参数并且为布尔状态的操作应该像切换操作一样。 这意味着调用者可以期待布尔状态在激活动作后切换。 幸运的是,这正是带有布尔属性的 gio::PropertyAction
的默认行为。
文件名:listings/actions/6/window/mod.rs
mod imp;
use gio::{ActionEntry, PropertyAction};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn setup_actions(&self) {
// Add stateful action "count" to `window` taking an integer as parameter
let original_state = 0;
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |window: &Self, 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
window.imp().label.set_label(&format!("Counter: {state}"));
})
.build();
// Add property action "button-frame" to `window`
let button = self.imp().button.get();
let action_button_frame =
PropertyAction::new("button-frame", &button, "has-frame");
self.add_action(&action_button_frame);
// Add stateful action "orientation" to `window` taking a string as parameter
let action_orientation = ActionEntry::builder("orientation")
.parameter_type(Some(&String::static_variant_type()))
.state("Vertical".to_variant())
.activate(move |window: &Self, action, parameter| {
// Get parameter
let parameter = parameter
.expect("Could not get parameter.")
.get::<String>()
.expect("The value needs to be of type `String`.");
let orientation = match parameter.as_str() {
"Horizontal" => Orientation::Horizontal,
"Vertical" => Orientation::Vertical,
_ => unreachable!(),
};
// Set orientation and save state
window.imp().gtk_box.set_orientation(orientation);
action.set_state(¶meter.to_variant());
})
.build();
self.add_action_entries([action_count, action_orientation]);
}
}
当您需要一个操作 GObject 属性的动作时,
PropertyAction
就会派上用场。 属性将作为动作的状态。 如上所述,如果属性是布尔型,则动作没有参数,并在激活时切换属性。 在所有其他情况下,操作都有一个与属性类型相同的参数。 激活动作时,属性会被设置为与动作参数相同的值。
最后,我们添加了 "win.orientation",一个带有字符串参数和字符串状态的动作。 该操作可用于改变 gtk_box
的朝向。 这里的惯例是状态(state)应设置为给定的参数。 我们并不需要动作状态来实现方向切换,但它在使菜单显示当前朝向时非常有用。
文件名:listings/actions/6/window/mod.rs
mod imp;
use gio::{ActionEntry, PropertyAction};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn setup_actions(&self) {
// Add stateful action "count" to `window` taking an integer as parameter
let original_state = 0;
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |window: &Self, 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
window.imp().label.set_label(&format!("Counter: {state}"));
})
.build();
// Add property action "button-frame" to `window`
let button = self.imp().button.get();
let action_button_frame =
PropertyAction::new("button-frame", &button, "has-frame");
self.add_action(&action_button_frame);
// Add stateful action "orientation" to `window` taking a string as parameter
let action_orientation = ActionEntry::builder("orientation")
.parameter_type(Some(&String::static_variant_type()))
.state("Vertical".to_variant())
.activate(move |window: &Self, action, parameter| {
// Get parameter
let parameter = parameter
.expect("Could not get parameter.")
.get::<String>()
.expect("The value needs to be of type `String`.");
let orientation = match parameter.as_str() {
"Horizontal" => Orientation::Horizontal,
"Vertical" => Orientation::Vertical,
_ => unreachable!(),
};
// Set orientation and save state
window.imp().gtk_box.set_orientation(orientation);
action.set_state(¶meter.to_variant());
})
.build();
self.add_action_entries([action_count, action_orientation]);
}
}
尽管 gio::Menu
也可以通过绑定创建,但最方便的方法还是使用界面生成器。 我们可以在模板前添加菜单。
文件名:listings/actions/6/resources/window.ui
<?xml version="1.0" encoding="UTF-8"?>
<interface>
+ <menu id="main-menu">
+ <item>
+ <attribute name="label" translatable="yes">_Close window</attribute>
+ <attribute name="action">window.close</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Toggle button frame</attribute>
+ <attribute name="action">win.button-frame</attribute>
+ </item>
+ <section>
+ <attribute name="label" translatable="yes">Orientation</attribute>
+ <item>
+ <attribute name="label" translatable="yes">_Horizontal</attribute>
+ <attribute name="action">win.orientation</attribute>
+ <attribute name="target">Horizontal</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Vertical</attribute>
+ <attribute name="action">win.orientation</attribute>
+ <attribute name="target">Vertical</attribute>
+ </item>
+ </section>
+ </menu>
<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>
<child>
<object class="GtkBox" id="gtk_box">
<property name="orientation">vertical</property>
由于我们通过 menu-model 属性将菜单连接到了 gtk::MenuButton
,因此菜单(Menu
)应为 gtk::PopoverMenu
. PopoverMenu
的文档还为界面生成器解释了其 xml 语法。
还要注意我们是如何指定目标的:
<attribute name="target">Horizontal</attribute>
字符串是目标的默认类型,因此我们无需指定类型。 对于其他类型的目标,则需要手动指定正确的 GVariant 格式字符串。 例如,一个值为 "5 "的 i32
变量对应的格式如下:
<attribute name="target" type="i">5</attribute>
这就是该应用程序的实际效果:
我们将菜单按钮(
MenuButton
)的属性 "icon-name" 设置为 "open-menu-symbolic",从而更改了菜单按钮的图标。 您可以在图标库中找到更多图标。 这些图标可以嵌入gio::Resource
,然后在合成模板(或其他地方)中引用。
设置(Settings)
菜单项很好地显示了有状态动作的状态,但在应用程序关闭后,对该状态的所有更改都会丢失。 像往常一样,我们使用 gio::Settings
解决这个问题。 首先,我们创建一个 schema,其中包含与之前创建的有状态操作相对应的设置。
文件名:listings/actions/7/org.gtk_rs.Actions7.gschema.xml
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<schema id="org.gtk_rs.Actions7" path="/org/gtk_rs/Actions7/">
<key name="button-frame" type="b">
<default>true</default>
<summary>Whether the button has a frame</summary>
</key>
<key name="orientation" type="s">
<choices>
<choice value='Horizontal'/>
<choice value='Vertical'/>
</choices>
<default>'Vertical'</default>
<summary>Orientation of GtkBox</summary>
</key>
</schema>
</schemalist>
同样,我们按照设置一章中描述来安装 schema. 然后将设置添加到 imp::Window
. 由于 gio::Settings
没有实现 Default
,我们将其封装在 std::cell::OnceCell
中。
文件名:listings/actions/7/window/imp.rs
use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Button, CompositeTemplate, Label};
use std::cell::OnceCell;
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/example/window.ui")]
pub struct Window {
#[template_child]
pub gtk_box: TemplateChild<gtk::Box>,
#[template_child]
pub button: TemplateChild<Button>,
#[template_child]
pub label: TemplateChild<Label>,
pub settings: OnceCell<Settings>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "MyGtkAppWindow";
type Type = super::Window;
type ParentType = gtk::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjects
impl ObjectImpl for Window {
fn constructed(&self) {
// Call "constructed" on parent
self.parent_constructed();
// Setup
let obj = self.obj();
obj.setup_settings();
obj.setup_actions();
obj.bind_settings();
}
}
// Trait shared by all widgets
impl WidgetImpl for Window {}
// Trait shared by all windows
impl WindowImpl for Window {}
// Trait shared by all application windows
impl ApplicationWindowImpl for Window {}
现在,我们创建一些函数,使设置更容易访问。
文件名:listings/actions/7/window/mod.rs
mod imp;
use gio::{ActionEntry, Settings};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn setup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fn settings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fn setup_actions(&self) {
// Add stateful action "count" to `window` taking an integer as parameter
let original_state = 0;
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |window: &Self, 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
window.imp().label.set_label(&format!("Counter: {state}"));
})
.build();
self.add_action_entries([action_count]);
// Create action from key "button-frame" and add to action group "win"
let action_button_frame = self.settings().create_action("button-frame");
self.add_action(&action_button_frame);
// Create action from key "orientation" and add to action group "win"
let action_orientation = self.settings().create_action("orientation");
self.add_action(&action_orientation);
}
fn bind_settings(&self) {
// Bind setting "button-frame" to "has-frame" property of `button`
let button = self.imp().button.get();
self.settings()
.bind("button-frame", &button, "has-frame")
.build();
// Bind setting "orientation" to "orientation" property of `button`
let gtk_box = self.imp().gtk_box.get();
self.settings()
.bind("orientation", >k_box, "orientation")
.mapping(|variant, _| {
let orientation = variant
.get::<String>()
.expect("The variant needs to be of type `String`.");
let orientation = match orientation.as_str() {
"Horizontal" => Orientation::Horizontal,
"Vertical" => Orientation::Vertical,
_ => unreachable!(),
};
Some(orientation.to_value())
})
.build();
}
}
通过设置条目创建有状态的动作非常常见,因此设置(Settings)
提供了一种方法来实现这一目的。 我们使用 create_action
方法创建动作,然后将其添加到窗口的动作组中。
文件名:listings/actions/7/window/mod.rs
mod imp;
use gio::{ActionEntry, Settings};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn setup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fn settings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fn setup_actions(&self) {
// Add stateful action "count" to `window` taking an integer as parameter
let original_state = 0;
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |window: &Self, 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
window.imp().label.set_label(&format!("Counter: {state}"));
})
.build();
self.add_action_entries([action_count]);
// Create action from key "button-frame" and add to action group "win"
let action_button_frame = self.settings().create_action("button-frame");
self.add_action(&action_button_frame);
// Create action from key "orientation" and add to action group "win"
let action_orientation = self.settings().create_action("orientation");
self.add_action(&action_orientation);
}
fn bind_settings(&self) {
// Bind setting "button-frame" to "has-frame" property of `button`
let button = self.imp().button.get();
self.settings()
.bind("button-frame", &button, "has-frame")
.build();
// Bind setting "orientation" to "orientation" property of `button`
let gtk_box = self.imp().gtk_box.get();
self.settings()
.bind("orientation", >k_box, "orientation")
.mapping(|variant, _| {
let orientation = variant
.get::<String>()
.expect("The variant needs to be of type `String`.");
let orientation = match orientation.as_str() {
"Horizontal" => Orientation::Horizontal,
"Vertical" => Orientation::Vertical,
_ => unreachable!(),
};
Some(orientation.to_value())
})
.build();
}
}
由于来自 create_action
的动作遵循上述约定,我们可以尽量减少进一步的改动。 每次激活时,"win.button-frame" 动作都会切换其状态,而 "win.orientation" 动作的状态则遵循给定的参数。
不过,我们仍需指定动作激活时应发生的情况。 对于有状态的操作,我们不用为它的"激活"信号添加回调,而是将设置绑定到我们要操作的属性上。
文件名:listings/actions/7/window/mod.rs
mod imp;
use gio::{ActionEntry, Settings};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn setup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fn settings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fn setup_actions(&self) {
// Add stateful action "count" to `window` taking an integer as parameter
let original_state = 0;
let action_count = ActionEntry::builder("count")
.parameter_type(Some(&i32::static_variant_type()))
.state(original_state.to_variant())
.activate(move |window: &Self, 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
window.imp().label.set_label(&format!("Counter: {state}"));
})
.build();
self.add_action_entries([action_count]);
// Create action from key "button-frame" and add to action group "win"
let action_button_frame = self.settings().create_action("button-frame");
self.add_action(&action_button_frame);
// Create action from key "orientation" and add to action group "win"
let action_orientation = self.settings().create_action("orientation");
self.add_action(&action_orientation);
}
fn bind_settings(&self) {
// Bind setting "button-frame" to "has-frame" property of `button`
let button = self.imp().button.get();
self.settings()
.bind("button-frame", &button, "has-frame")
.build();
// Bind setting "orientation" to "orientation" property of `button`
let gtk_box = self.imp().gtk_box.get();
self.settings()
.bind("orientation", >k_box, "orientation")
.mapping(|variant, _| {
let orientation = variant
.get::<String>()
.expect("The variant needs to be of type `String`.");
let orientation = match orientation.as_str() {
"Horizontal" => Orientation::Horizontal,
"Vertical" => Orientation::Vertical,
_ => unreachable!(),
};
Some(orientation.to_value())
})
.build();
}
}
最后,我们要确保 bind_settings
在构造(constructed
)内部被调用。
文件名:listings/actions/7/window/imp.rs
use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Button, CompositeTemplate, Label};
use std::cell::OnceCell;
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/example/window.ui")]
pub struct Window {
#[template_child]
pub gtk_box: TemplateChild<gtk::Box>,
#[template_child]
pub button: TemplateChild<Button>,
#[template_child]
pub label: TemplateChild<Label>,
pub settings: OnceCell<Settings>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "MyGtkAppWindow";
type Type = super::Window;
type ParentType = gtk::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjects
impl ObjectImpl for Window {
fn constructed(&self) {
// Call "constructed" on parent
self.parent_constructed();
// Setup
let obj = self.obj();
obj.setup_settings();
obj.setup_actions();
obj.bind_settings();
}
}
// Trait shared by all widgets
impl WidgetImpl for Window {}
// Trait shared by all windows
impl WindowImpl for Window {}
// Trait shared by all application windows
impl ApplicationWindowImpl for Window {}
动作的功能非常强大,我们在此只是浅尝辄止。 如果您想了解更多,GNOME 开发者文档是一个很好的开始。