动作(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(&parameter.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(&gtk_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(&gtk_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(&parameter.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(&parameter.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", &gtk_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", &gtk_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", &gtk_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 开发者文档是一个很好的开始。