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.