属性

属性(Properties)为访问 GObject 的状态提供了一个公共 API.

让我们通过 Switch 控件来看看如何实现这一点。 它的一个属性叫做 active. 根据 GTK 文档,它可以被读取和写入。 因此,gtk-rs 提供了相应的 is_activeset_active 方法。

文件名:listings/g_object_properties/1/main.rs

use gtk::prelude::*;
use gtk::{glib, Align, Application, ApplicationWindow, Box, Orientation, Switch};

const APP_ID: &str = "org.gtk_rs.GObjectProperties1";

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) {
    // Create the switch
    let switch = Switch::new();

    // Set and then immediately obtain active property
    switch.set_active(true);
    let switch_active = switch.is_active();

    // This prints: "The active property of switch is true"
    println!("The active property of switch is {}", switch_active);

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&switch);

    // Create a window
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .child(&gtk_box)
        .build();

    // Present the window
    window.present();
}

属性不仅可以通过 getter 和 setter 访问,还可以相互绑定。 让我们看看两个 Switch 实例是如何绑定的。

文件名:listings/g_object_properties/2/main.rs

use gtk::prelude::*;
use gtk::{glib, Align, Application, ApplicationWindow, Box, Orientation, Switch};

const APP_ID: &str = "org.gtk_rs.GObjectProperties3";

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) {
    // Create the switches
    let switch_1 = Switch::new();
    let switch_2 = Switch::new();

    switch_1
        .bind_property("active", &switch_2, "active")
        .bidirectional()
        .build();

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&switch_1);
    gtk_box.append(&switch_2);

    // Create a window
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .child(&gtk_box)
        .build();

    // Present the window
    window.present();
}

在本例中,我们希望将 switch_1 的 "active" 属性与 switch_2 的 "active" 属性绑定。 我们还希望绑定是双向的,因此通过调用 bidirectional 方法来指定。

文件名:listings/g_object_properties/2/main.rs

use gtk::prelude::*;
use gtk::{glib, Align, Application, ApplicationWindow, Box, Orientation, Switch};

const APP_ID: &str = "org.gtk_rs.GObjectProperties3";

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) {
    // Create the switches
    let switch_1 = Switch::new();
    let switch_2 = Switch::new();

    switch_1
        .bind_property("active", &switch_2, "active")
        .bidirectional()
        .build();

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&switch_1);
    gtk_box.append(&switch_2);

    // Create a window
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .child(&gtk_box)
        .build();

    // Present the window
    window.present();
}

现在,当我们点击两个开关中的一个时,另一个也会被切换。

给自定义 GObject 添加属性

我们还可以为自定义 GObject 添加属性。 我们可以通过将 CustomButtonnumber 绑定到一个属性来演示这一点。 大部分工作由 glib::Properties 派生宏(derive macro)完成。 我们告诉它封装类型是 super::CustomButton。 我们还给 number 添加了注释,这样宏就知道它应该创建一个可读可写的属性 "number"。 宏还会生成本章稍后将使用的封装方法

文件名:listings/g_object_properties/3/custom_button/imp.rs

use std::cell::Cell;

use glib::Properties;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;

// Object holding the state
#[derive(Properties, Default)]
#[properties(wrapper_type = super::CustomButton)]
pub struct CustomButton {
    #[property(get, set)]
    number: Cell<i32>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
    const NAME: &'static str = "MyGtkAppCustomButton";
    type Type = super::CustomButton;
    type ParentType = gtk::Button;
}

// Trait shared by all GObjects
#[glib::derived_properties]
impl ObjectImpl for CustomButton {
    fn constructed(&self) {
        self.parent_constructed();

        // Bind label to number
        // `SYNC_CREATE` ensures that the label will be immediately set
        let obj = self.obj();
        obj.bind_property("number", obj.as_ref(), "label")
            .sync_create()
            .build();
    }
}

// Trait shared by all widgets
impl WidgetImpl for CustomButton {}

// Trait shared by all buttons
impl ButtonImpl for CustomButton {
    fn clicked(&self) {
        let incremented_number = self.obj().number() + 1;
        self.obj().set_number(incremented_number);
    }
}

glib::derived_properties 宏为每个使用 Property 宏生成属性的 GObject 生成了模板。 在 constructed 中,我们通过绑定 "label "属性来使用新属性 "number"。bind_property 会自行将 "number "的整数值转换为 "label "的字符串。 现在,我们不必再在 "clicked" 回调函数中调整标签了。

文件名:listings/g_object_properties/3/custom_button/imp.rs

use std::cell::Cell;

use glib::Properties;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;

// Object holding the state
#[derive(Properties, Default)]
#[properties(wrapper_type = super::CustomButton)]
pub struct CustomButton {
    #[property(get, set)]
    number: Cell<i32>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
    const NAME: &'static str = "MyGtkAppCustomButton";
    type Type = super::CustomButton;
    type ParentType = gtk::Button;
}

// Trait shared by all GObjects
#[glib::derived_properties]
impl ObjectImpl for CustomButton {
    fn constructed(&self) {
        self.parent_constructed();

        // Bind label to number
        // `SYNC_CREATE` ensures that the label will be immediately set
        let obj = self.obj();
        obj.bind_property("number", obj.as_ref(), "label")
            .sync_create()
            .build();
    }
}

// Trait shared by all widgets
impl WidgetImpl for CustomButton {}

// Trait shared by all buttons
impl ButtonImpl for CustomButton {
    fn clicked(&self) {
        let incremented_number = self.obj().number() + 1;
        self.obj().set_number(incremented_number);
    }
}

我们还必须调整 clicked 方法。 以前我们直接修改 number,现在我们可以使用生成的封装方法 numberset_number。 这样就会发出 "notify "信号,这对于绑定正常工作是必要的。

use std::cell::Cell;

use glib::Properties;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;

// Object holding the state
#[derive(Properties, Default)]
#[properties(wrapper_type = super::CustomButton)]
pub struct CustomButton {
    #[property(get, set)]
    number: Cell<i32>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
    const NAME: &'static str = "MyGtkAppCustomButton";
    type Type = super::CustomButton;
    type ParentType = gtk::Button;
}

// Trait shared by all GObjects
#[glib::derived_properties]
impl ObjectImpl for CustomButton {
    fn constructed(&self) {
        self.parent_constructed();

        // Bind label to number
        // `SYNC_CREATE` ensures that the label will be immediately set
        let obj = self.obj();
        obj.bind_property("number", obj.as_ref(), "label")
            .sync_create()
            .build();
    }
}

// Trait shared by all widgets
impl WidgetImpl for CustomButton {}

// Trait shared by all buttons
impl ButtonImpl for CustomButton {
    fn clicked(&self) {
        let incremented_number = self.obj().number() + 1;
        self.obj().set_number(incremented_number);
    }
}

让我们通过创建两个自定义按钮来看看能做些什么。

文件名:listings/g_object_properties/3/main.rs

mod custom_button;

use custom_button::CustomButton;
use gtk::prelude::*;
use gtk::{glib, Align, Application, ApplicationWindow, Box, Orientation};

const APP_ID: &str = "org.gtk_rs.GObjectProperties4";

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) {
    // Create the buttons
    let button_1 = CustomButton::new();
    let button_2 = CustomButton::new();

    // Assure that "number" of `button_2` is always 1 higher than "number" of `button_1`
    button_1
        .bind_property("number", &button_2, "number")
        // How to transform "number" from `button_1` to "number" of `button_2`
        .transform_to(|_, number: i32| {
            let incremented_number = number + 1;
            Some(incremented_number.to_value())
        })
        // How to transform "number" from `button_2` to "number" of `button_1`
        .transform_from(|_, number: i32| {
            let decremented_number = number - 1;
            Some(decremented_number.to_value())
        })
        .bidirectional()
        .sync_create()
        .build();

    // The closure will be called
    // whenever the property "number" of `button_1` gets changed
    button_1.connect_number_notify(|button| {
        println!("The current number of `button_1` is {}.", button.number());
    });

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&button_1);
    gtk_box.append(&button_2);

    // Create a window
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .child(&gtk_box)
        .build();

    // Present the window
    window.present();
}

我们已经看到,绑定的属性不一定必须是同一类型。 利用 transform_totransform_from, 我们可以确保 button_2 显示的数字总是比 button_1 显示的数字大 1.

文件名:listings/g_object_properties/3/main.rs

mod custom_button;

use custom_button::CustomButton;
use gtk::prelude::*;
use gtk::{glib, Align, Application, ApplicationWindow, Box, Orientation};

const APP_ID: &str = "org.gtk_rs.GObjectProperties4";

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) {
    // Create the buttons
    let button_1 = CustomButton::new();
    let button_2 = CustomButton::new();

    // Assure that "number" of `button_2` is always 1 higher than "number" of `button_1`
    button_1
        .bind_property("number", &button_2, "number")
        // How to transform "number" from `button_1` to "number" of `button_2`
        .transform_to(|_, number: i32| {
            let incremented_number = number + 1;
            Some(incremented_number.to_value())
        })
        // How to transform "number" from `button_2` to "number" of `button_1`
        .transform_from(|_, number: i32| {
            let decremented_number = number - 1;
            Some(decremented_number.to_value())
        })
        .bidirectional()
        .sync_create()
        .build();

    // The closure will be called
    // whenever the property "number" of `button_1` gets changed
    button_1.connect_number_notify(|button| {
        println!("The current number of `button_1` is {}.", button.number());
    });

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&button_1);
    gtk_box.append(&button_2);

    // Create a window
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .child(&gtk_box)
        .build();

    // Present the window
    window.present();
}

现在,如果我们点击其中一个按钮,另一个按钮的 "number" 和 "label" 属性也会随之改变。

属性的另一个优点是,当属性发生变化时,可以将回调连接到事件。 例如:

文件名:listings/g_object_properties/3/main.rs

mod custom_button;

use custom_button::CustomButton;
use gtk::prelude::*;
use gtk::{glib, Align, Application, ApplicationWindow, Box, Orientation};

const APP_ID: &str = "org.gtk_rs.GObjectProperties4";

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) {
    // Create the buttons
    let button_1 = CustomButton::new();
    let button_2 = CustomButton::new();

    // Assure that "number" of `button_2` is always 1 higher than "number" of `button_1`
    button_1
        .bind_property("number", &button_2, "number")
        // How to transform "number" from `button_1` to "number" of `button_2`
        .transform_to(|_, number: i32| {
            let incremented_number = number + 1;
            Some(incremented_number.to_value())
        })
        // How to transform "number" from `button_2` to "number" of `button_1`
        .transform_from(|_, number: i32| {
            let decremented_number = number - 1;
            Some(decremented_number.to_value())
        })
        .bidirectional()
        .sync_create()
        .build();

    // The closure will be called
    // whenever the property "number" of `button_1` gets changed
    button_1.connect_number_notify(|button| {
        println!("The current number of `button_1` is {}.", button.number());
    });

    // Set up box
    let gtk_box = Box::builder()
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .valign(Align::Center)
        .halign(Align::Center)
        .spacing(12)
        .orientation(Orientation::Vertical)
        .build();
    gtk_box.append(&button_1);
    gtk_box.append(&button_2);

    // Create a window
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .child(&gtk_box)
        .build();

    // Present the window
    window.present();
}

现在,每当 "number" 属性发生变化时,闭包就会被执行,并将 "number" 的当前值打印到标准输出中。

将属性引入您的自定义 GObject 将会非常有用,如果您想:

  • 绑定(不同)GObject 的状态
  • 在属性值发生变化时通知消费者

请注意,每次值发生变化时发送信号都会产生(计算)代价。 如果您只想暴露内部状态,添加 getter 和 setter 方法是更好的选择。