Subclassing
GObjects rely heavily on inheritance. Therefore, it makes sense that if we want to create a custom GObject, this is done via subclassing. Let's see how this works by replacing the button in our "Hello World!" app with a custom one. First, we need to create an implementation struct that holds the state and overrides the virtual methods.
Filename: listings/g_object_subclassing/1/custom_button/imp.rs
use gtk::glib;
use gtk::subclass::prelude::*;
// Object holding the state
#[derive(Default)]
pub struct CustomButton;
// 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 {}
// Trait shared by all widgets
impl WidgetImpl for CustomButton {}
// Trait shared by all buttons
impl ButtonImpl for CustomButton {}
The description of the subclassing is in ObjectSubclass
.
NAME
should consist of crate-name and object-name in order to avoid name collisions. Use UpperCamelCase here.Type
refers to the actual GObject that will be created afterwards.ParentType
is the GObject we inherit of.
After that, we would have the option to override the virtual methods of our ancestors.
Since we only want to have a plain button for now, we override nothing.
We still have to add the empty impl
though.
Next, we describe the public interface of our custom GObject.
Filename: listings/g_object_subclassing/1/custom_button/mod.rs
mod imp;
use glib::Object;
use gtk::glib;
glib::wrapper! {
pub struct CustomButton(ObjectSubclass<imp::CustomButton>)
@extends gtk::Button, gtk::Widget,
@implements gtk::Accessible, gtk::Actionable, gtk::Buildable, gtk::ConstraintTarget;
}
impl CustomButton {
pub fn new() -> Self {
Object::builder().build()
}
pub fn with_label(label: &str) -> Self {
Object::builder().property("label", label).build()
}
}
impl Default for CustomButton {
fn default() -> Self {
Self::new()
}
}
glib::wrapper!
implements the same traits that our ParentType
implements.
Theoretically that would mean that the ParentType
is also the only thing we have to specify here.
Unfortunately, nobody has yet found a good way to do that.
Which is why, as of today, subclassing of GObjects in Rust requires to mention all ancestors and interfaces apart from GObject
and GInitiallyUnowned
.
For gtk::Button
, we can look up the ancestors and interfaces in the corresponding doc page of GTK4.
After these steps, nothing is stopping us from replacing gtk::Button
with our CustomButton
.
Filename: listings/g_object_subclassing/1/main.rs
mod custom_button;
use custom_button::CustomButton;
use gtk::prelude::*;
use gtk::{glib, Application, ApplicationWindow};
const APP_ID: &str = "org.gtk_rs.GObjectSubclassing1";
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 a button
let button = CustomButton::with_label("Press me!");
button.set_margin_top(12);
button.set_margin_bottom(12);
button.set_margin_start(12);
button.set_margin_end(12);
// Connect to "clicked" signal of `button`
button.connect_clicked(move |button| {
// Set the label to "Hello World!" after the button has been clicked on
button.set_label("Hello World!");
});
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.child(&button)
.build();
// Present window
window.present();
}
Describing objects with two structs is a peculiarity coming from how GObjects are defined in C.
imp::CustomButton
handles the state of the GObject and the overridden virtual methods.CustomButton
determines the exposed methods from the implemented traits and added methods.
Adding Functionality
We are able to use CustomButton
as a drop-in replacement for gtk::Button
.
This is cool, but also not very tempting to do in a real application.
For the gain of zero benefits, it did involve quite a bit of boilerplate after all.
So let's make it a bit more interesting!
gtk::Button
does not hold much state, but we can let CustomButton
hold a number.
Filename: listings/g_object_subclassing/2/custom_button/imp.rs
use std::cell::Cell;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
// 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 constructed(&self) {
self.parent_constructed();
self.obj().set_label(&self.number.get().to_string());
}
}
// Trait shared by all widgets
impl WidgetImpl for CustomButton {}
// Trait shared by all buttons
impl ButtonImpl for CustomButton {
fn clicked(&self) {
self.number.set(self.number.get() + 1);
self.obj().set_label(&self.number.get().to_string())
}
}
We override constructed
in ObjectImpl
so that the label of the button initializes with number
.
We also override clicked
in ButtonImpl
so that every click increases number
and updates the label.
Filename: listings/g_object_subclassing/2/main.rs
mod custom_button;
use custom_button::CustomButton;
use gtk::prelude::*;
use gtk::{glib, Application, ApplicationWindow};
const APP_ID: &str = "org.gtk_rs.GObjectSubclassing2";
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 a button
let button = CustomButton::new();
button.set_margin_top(12);
button.set_margin_bottom(12);
button.set_margin_start(12);
button.set_margin_end(12);
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.child(&button)
.build();
// Present window
window.present();
}
In build_ui
we stop calling connect_clicked
, and that was it.
After a rebuild, the app now features our CustomButton
with the label "0".
Every time we click on the button, the number displayed by the label increases by 1.
So, when do we want to inherit from GObject?
- We want to use a certain widget, but with added state and overridden virtual functions.
- We want to pass a Rust object to a function, but the function expects a GObject.
- We want to add properties or signals to an object.