Properties
Properties provide a public API for accessing state of GObjects.
Let's see how this is done by experimenting with the Switch
widget.
One of its properties is called active.
According to the GTK docs, it can be read and be written to.
That is why gtk-rs
provides corresponding is_active
and set_active
methods.
Filename: 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();
}
Properties can not only be accessed via getters & setters, they can also be bound to each other.
Let's see how that would look like for two Switch
instances.
Filename: 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();
}
In our case, we want to bind the "active" property of switch_1
to the "active" property of switch_2
.
We also want the binding to be bidirectional, so we specify by calling the bidirectional
method.
Filename: 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();
}
Now when we click on one of the two switches, the other one is toggled as well.
Adding Properties to Custom GObjects
We can also add properties to custom GObjects.
We can demonstrate that by binding the number
of our CustomButton
to a property.
Most of the work is done by the glib::Properties
derive macro.
We tell it that the wrapper type is super::CustomButton
.
We also annotate number
, so that macro knows that it should create a property "number" that is readable and writable.
It also generates wrapper methods which we are going to use later in this chapter.
Filename: 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);
}
}
The glib::derived_properties
macro generates boilerplate that is the same for every GObject that generates its properties with the Property
macro.
In constructed
we use our new property "number" by binding the "label" property to it.
bind_property
converts the integer value of "number" to the string of "label" on its own.
Now we don't have to adapt the label in the "clicked" callback anymore.
Filename: 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);
}
}
We also have to adapt the clicked
method.
Before we modified number
directly, now we can use the generated wrapper methods number
and set_number
.
This way the "notify" signal will be emitted, which is necessary for the bindings to work as expected.
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);
}
}
Let's see what we can do with this by creating two custom buttons.
Filename: 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();
}
We have already seen that bound properties don't necessarily have to be of the same type.
By leveraging transform_to
and transform_from
, we can assure that button_2
always displays a number which is 1 higher than the number of button_1
.
Filename: 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();
}
Now if we click on one button, the "number" and "label" properties of the other button change as well.
Another nice feature of properties is, that you can connect a callback to the event, when a property gets changed. For example like this:
Filename: 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();
}
Now, whenever the "number" property gets changed, the closure gets executed and prints the current value of "number" to standard output.
Introducing properties to your custom GObjects is useful if you want to
- bind state of (different) GObjects
- notify consumers whenever a property value changes
Note that it has a (computational) cost to send a signal each time the value changes. If you only want to expose internal state, adding getter and setter methods is the better option.