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 the state.
According to the GTK docs, it can be read and be written to.
That is why gtk-rs
provides corresponding state
and set_state
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 state
switch.set_state(true);
let current_state = switch.state();
// This prints: "The current state is true"
println!("The current state is {}", current_state);
// 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();
}
Alternatively, we can use the general property
and set_property
methods.
We use the turbofish syntax to specify the type if it cannot be inferred.
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.GObjectProperties2";
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 state
switch.set_property("state", &true);
let current_state = switch.property::<bool>("state");
// This prints: "The current state is true"
println!("The current state is {}", current_state);
// 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();
}
Both property
and set_property
panic if the property does not exist, has the wrong type or has the wrong permissions.
This is fine in most situations where these cases are hard-coded within the program.
However, if you want to decide for yourself how to react to failure, you can enforce a returned Option
by specifying property::<Option<T>>
or set_property::<Option<T>>
.
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/3/main.rs
use glib::BindingFlags;
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("state", &switch_2, "state")
.flags(BindingFlags::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 "state" property of switch_1
to the "state" property of switch_2
.
We also want the binding to be bidirectional, so we specify this with the BindingFlags
.
Filename: listings/g_object_properties/3/main.rs
use glib::BindingFlags;
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("state", &switch_2, "state")
.flags(BindingFlags::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.
For that we need to be able to lazily evaluate expressions.
The crate once_cell
provides the Lazy
type which allows us to do that.
once_cell
is already part of Rust nightly.
Until it hits stable, we will add it as external dependency.
cargo add once_cell
Now we define the "number" property within the ObjectImpl
implementation.
The properties
method describes our set of properties.
When naming our property, we make sure to do that in kebab-case.
set_property
describes how the underlying values can be changed.
property
takes care of returning the underlying value.
Filename: listings/g_object_properties/4/custom_button/imp.rs
use std::cell::Cell;
use glib::{BindingFlags, ParamSpec, ParamSpecInt, Value};
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use once_cell::sync::Lazy;
// Object holding the state
#[derive(Default)]
pub struct CustomButton {
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
impl ObjectImpl for CustomButton {
fn properties() -> &'static [ParamSpec] {
static PROPERTIES: Lazy<Vec<ParamSpec>> =
Lazy::new(|| vec![ParamSpecInt::builder("number").build()]);
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &Value, pspec: &ParamSpec) {
match pspec.name() {
"number" => {
let input_number =
value.get().expect("The value needs to be of type `i32`.");
self.number.replace(input_number);
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &ParamSpec) -> Value {
match pspec.name() {
"number" => self.number.get().to_value(),
_ => unimplemented!(),
}
}
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")
.flags(BindingFlags::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.number.get() + 1;
self.obj().set_property("number", &incremented_number);
}
}
We could immediately take advantage of this new property by binding the "label" property to it. It even converts the integer value of "number" to the string of "label". Now we don't have to adapt the label in the "clicked" callback anymore.
We also have to adapt the clicked
method.
Before we modified number
directly, now we do it through set_property
.
This way the "notify" signal will be emitted which bindings work as expected.
use std::cell::Cell;
use glib::{BindingFlags, ParamSpec, ParamSpecInt, Value};
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use once_cell::sync::Lazy;
// Object holding the state
#[derive(Default)]
pub struct CustomButton {
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
impl ObjectImpl for CustomButton {
fn properties() -> &'static [ParamSpec] {
static PROPERTIES: Lazy<Vec<ParamSpec>> =
Lazy::new(|| vec![ParamSpecInt::builder("number").build()]);
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &Value, pspec: &ParamSpec) {
match pspec.name() {
"number" => {
let input_number =
value.get().expect("The value needs to be of type `i32`.");
self.number.replace(input_number);
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &ParamSpec) -> Value {
match pspec.name() {
"number" => self.number.get().to_value(),
_ => unimplemented!(),
}
}
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")
.flags(BindingFlags::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.number.get() + 1;
self.obj().set_property("number", &incremented_number);
}
}
Let's see what we can do with this by creating two custom buttons.
Filename: listings/g_object_properties/4/main.rs
mod custom_button;
use custom_button::CustomButton;
use glib::BindingFlags;
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())
})
.flags(BindingFlags::BIDIRECTIONAL | BindingFlags::SYNC_CREATE)
.build();
// The closure will be called
// whenever the property "number" of `button_1` gets changed
button_1.connect_notify_local(Some("number"), move |button, _| {
let number = button.property::<i32>("number");
println!("The current number of `button_1` is {}.", 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/4/main.rs
mod custom_button;
use custom_button::CustomButton;
use glib::BindingFlags;
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())
})
.flags(BindingFlags::BIDIRECTIONAL | BindingFlags::SYNC_CREATE)
.build();
// The closure will be called
// whenever the property "number" of `button_1` gets changed
button_1.connect_notify_local(Some("number"), move |button, _| {
let number = button.property::<i32>("number");
println!("The current number of `button_1` is {}.", 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.
The final 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/4/main.rs
mod custom_button;
use custom_button::CustomButton;
use glib::BindingFlags;
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())
})
.flags(BindingFlags::BIDIRECTIONAL | BindingFlags::SYNC_CREATE)
.build();
// The closure will be called
// whenever the property "number" of `button_1` gets changed
button_1.connect_notify_local(Some("number"), move |button, _| {
let number = button.property::<i32>("number");
println!("The current number of `button_1` is {}.", 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.