属性
属性(Properties)为访问 GObject 的状态提供了一个公共 API.
让我们通过 Switch
控件来看看如何实现这一点。 它的一个属性叫做 active. 根据 GTK 文档,它可以被读取和写入。 因此,gtk-rs
提供了相应的 is_active
和 set_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(>k_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(>k_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(>k_box)
.build();
// Present the window
window.present();
}
现在,当我们点击两个开关中的一个时,另一个也会被切换。
给自定义 GObject 添加属性
我们还可以为自定义 GObject 添加属性。 我们可以通过将 CustomButton
的 number
绑定到一个属性来演示这一点。 大部分工作由 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
,现在我们可以使用生成的封装方法 number
和 set_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(>k_box)
.build();
// Present the window
window.present();
}
我们已经看到,绑定的属性不一定必须是同一类型。 利用 transform_to
和 transform_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(>k_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(>k_box)
.build();
// Present the window
window.present();
}
现在,每当 "number" 属性发生变化时,闭包就会被执行,并将 "number" 的当前值打印到标准输出中。
将属性引入您的自定义 GObject 将会非常有用,如果您想:
- 绑定(不同)GObject 的状态
- 在属性值发生变化时通知消费者
请注意,每次值发生变化时发送信号都会产生(计算)代价。 如果您只想暴露内部状态,添加 getter 和 setter 方法是更好的选择。