Interface Builder

GTK Builder

Until now, whenever we constructed pre-defined widgets we relied on the builder pattern. As a reminder, that is how we used it in our trusty "Hello World!" app.

Filename: listings/hello_world/3/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::builder()
        .application_id("org.gtk.example")
        .build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(app: &Application) {
    // Create a window and set the title
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .build();

    // Create a button with label and margins
    let button = Button::builder()
        .label("Press me!")
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .build();

    // 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!");
    });

    // Add button
    window.set_child(Some(&button));

    // Present window to the user
    window.present();
}

Creating widgets directly from code is perfectly fine. However, with most toolkits you can describe your user interface with a markup language and GTK is no exception here. For example the following xml snippet describes the window widget of the "Hello World!" app.

Filename: listings/interface_builder/1/window.ui

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <object class="GtkApplicationWindow" id="window">
    <property name="title">My GTK App</property>
    <child>
      <object class="GtkButton" id="button">
        <property name="label">Press me!</property>
        <property name="margin-top">12</property>
        <property name="margin-bottom">12</property>
        <property name="margin-start">12</property>
        <property name="margin-end">12</property>  
      </object>
    </child>
  </object>
</interface>

The most outer tag always has to be the <interface>. Then you start listing the elements you want to describe. In our case, we want to have a gtk::ApplicationWindow. These xml files are independent of the programming language, which is why the classes have the original names. Luckily, they all convert like this: gtk::ApplicationWindowGtkApplicationWindow. We want to access the window later on, so we also give it an id. Then we can specify properties which are specified here for ApplicationWindow. Since ApplicationWindow can contain other widgets we use the <child> tag to add a Button.

To instantiate the widgets described by the xml files we use gtk::Builder1. All widgets that can be described that way can be found here

Filename: listings/interface_builder/1/main.rs

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Button};

fn main() {
    // Create a new application
    let app = Application::builder()
        .application_id("org.gtk.example")
        .build();

    // Connect to "activate" signal
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(app: &Application) {
    // Init `gtk::Builder` from file
    let builder = gtk::Builder::from_string(include_str!("window.ui"));

    // Get window and button from `gtk::Builder`
    let window: ApplicationWindow = builder
        .object("window")
        .expect("Could not get object `window` from builder.");
    let button: Button = builder
        .object("button")
        .expect("Could not get object `button` from builder.");

    // Set application
    window.set_application(Some(app));

    // Connect to "clicked" signal
    button.connect_clicked(move |button| {
        // Set the label to "Hello World!" after the button has been clicked on
        button.set_label("Hello World!");
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

This is a bit disappointing. Even though we have already described the UI in the markup file, the amount of code is still pretty much the same. There are still cases where it is valuable to know of the existence of gtk::Builder. We will see for example that ShortcutsWindow is quite a bit easier to instantiate that way.

At least we did not lose any flexibility by using gtk::Builder. It is for example still possible to refer to custom widgets such as this bare-bones CustomButton.

Filename: listings/interface_builder/2/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 {}
// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

Within the xml file we reference the widget with the NAME we gave it in imp.rs.

Filename: listings/interface_builder/3/window/window.ui

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <object class="GtkApplicationWindow" id="window">
    <property name="title">My GTK App</property>
    <child>
      <object class="MyGtkAppCustomButton" id="button">
        <property name="label">Press me!</property>
        <property name="margin-top">12</property>
        <property name="margin-bottom">12</property>
        <property name="margin-start">12</property>
        <property name="margin-end">12</property>  
      </object>
    </child>
  </object>
</interface>

We also have to make sure to register the custom widget before it is used by the interface builder.

Filename: listings/interface_builder/2/main.rs

mod custom_button;

use gtk::prelude::*;
use gtk::{Application, ApplicationWindow};

use custom_button::CustomButton;

fn main() {
    // Create a new application
    let app = Application::builder()
        .application_id("org.gtk.example")
        .build();

    // Connect to "activate" signal
    app.connect_activate(build_ui);

    // Register custom button
    CustomButton::static_type();

    // Run the application
    app.run();
}

fn build_ui(app: &Application) {
    // Init `gtk::Builder` from file
    let builder = gtk::Builder::from_string(include_str!("window.ui"));

    // Get window and button from `gtk::Builder`
    let window: ApplicationWindow = builder
        .object("window")
        .expect("Could not get object `window` from builder.");
    let button: CustomButton = builder
        .object("button")
        .expect("Could not get object `button` from builder.");

    // Set application
    window.set_application(Some(app));

    // Connect to "clicked" signal
    button.connect_clicked(move |button| {
        // Set the label to "Hello World!" after the button has been clicked on
        button.set_label("Hello World!");
    });

    // Add button
    window.set_child(Some(&button));
    window.present();
}

Composite Templates

The actual reason why we devote a whole chapter to the interface builder is the existence of composite templates. Again, composite templates are described by xml files.

Filename: listings/interface_builder/3/window/window.ui

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="MyGtkAppWindow" parent="GtkApplicationWindow">
    <property name="title">My GTK App</property>
    <child>
      <object class="MyGtkAppCustomButton" id="button">
        <property name="label">Press me!</property>
        <property name="margin-top">12</property>
        <property name="margin-bottom">12</property>
        <property name="margin-start">12</property>
        <property name="margin-end">12</property>  
      </object>
    </child>
  </template>
</interface>

At first glance, the content seems to be nearly the same. Before, we described a pre-existing widget.

<object class="GtkApplicationWindow" id="window">

Now, we create a custom widget and let it inherit from a pre-existing one.

<template class="MyGtkAppWindow" parent="GtkApplicationWindow">

Within our code we create a custom widget inheriting from gtk::ApplicationWindow to make use of our template.

Filename: listings/interface_builder/3/window/mod.rs

mod imp;

use glib::Object;
use gtk::Application;
use gtk::{gio, glib};

glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
                    gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}

impl Window {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::new(&[("application", app)]).expect("Failed to create Window")
    }
}
// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

In the private struct, we then add the derive macro gtk::CompositeTemplate. We also specify that the template information comes from a file window.ui in the same folder.

Filename: listings/interface_builder/3/window/imp.rs

use glib::subclass::InitializingObject;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;

use crate::custom_button::CustomButton;

// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(file = "window.ui")]
pub struct Window {
    #[template_child]
    pub button: TemplateChild<CustomButton>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
    // `NAME` needs to match `class` attribute of template
    const NAME: &'static str = "MyGtkAppWindow";
    type Type = super::Window;
    type ParentType = gtk::ApplicationWindow;

    fn class_init(klass: &mut Self::Class) {
        Self::bind_template(klass);
    }

    fn instance_init(obj: &InitializingObject<Self>) {
        obj.init_template();
    }
}

// Trait shared by all GObjects
impl ObjectImpl for Window {
    fn constructed(&self, obj: &Self::Type) {
        // Call "constructed" on parent
        self.parent_constructed(obj);

        // Connect to "clicked" signal of `button`
        self.button.connect_clicked(move |button| {
            // Set the label to "Hello World!" after the button has been clicked on
            button.set_label("Hello World!");
        });
    }
}

// Trait shared by all widgets
impl WidgetImpl for Window {}

// Trait shared by all windows
impl WindowImpl for Window {}

// Trait shared by all application
impl ApplicationWindowImpl for Window {}
// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

One very convenient feature of templates is the template child. You use it by adding a struct member with the same name as one id attribute in the template. Template child then:

  • assures that the widget gets registered without doing it manually in main.rs, and
  • stores a reference to the widget for later use.

We need both for our custom button, so we add it to the struct.

Filename: listings/interface_builder/3/window/imp.rs

use glib::subclass::InitializingObject;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;

use crate::custom_button::CustomButton;

// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(file = "window.ui")]
pub struct Window {
    #[template_child]
    pub button: TemplateChild<CustomButton>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
    // `NAME` needs to match `class` attribute of template
    const NAME: &'static str = "MyGtkAppWindow";
    type Type = super::Window;
    type ParentType = gtk::ApplicationWindow;

    fn class_init(klass: &mut Self::Class) {
        Self::bind_template(klass);
    }

    fn instance_init(obj: &InitializingObject<Self>) {
        obj.init_template();
    }
}

// Trait shared by all GObjects
impl ObjectImpl for Window {
    fn constructed(&self, obj: &Self::Type) {
        // Call "constructed" on parent
        self.parent_constructed(obj);

        // Connect to "clicked" signal of `button`
        self.button.connect_clicked(move |button| {
            // Set the label to "Hello World!" after the button has been clicked on
            button.set_label("Hello World!");
        });
    }
}

// Trait shared by all widgets
impl WidgetImpl for Window {}

// Trait shared by all windows
impl WindowImpl for Window {}

// Trait shared by all application
impl ApplicationWindowImpl for Window {}
// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

Within the ObjectSubclass trait, we make sure that NAME corresponds to class in the template and ParentType corresponds to parent in the template. We also bind and initialize the template in class_init and instance_init.

Filename: listings/interface_builder/3/window/imp.rs

use glib::subclass::InitializingObject;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;

use crate::custom_button::CustomButton;

// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(file = "window.ui")]
pub struct Window {
    #[template_child]
    pub button: TemplateChild<CustomButton>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
    // `NAME` needs to match `class` attribute of template
    const NAME: &'static str = "MyGtkAppWindow";
    type Type = super::Window;
    type ParentType = gtk::ApplicationWindow;

    fn class_init(klass: &mut Self::Class) {
        Self::bind_template(klass);
    }

    fn instance_init(obj: &InitializingObject<Self>) {
        obj.init_template();
    }
}

// Trait shared by all GObjects
impl ObjectImpl for Window {
    fn constructed(&self, obj: &Self::Type) {
        // Call "constructed" on parent
        self.parent_constructed(obj);

        // Connect to "clicked" signal of `button`
        self.button.connect_clicked(move |button| {
            // Set the label to "Hello World!" after the button has been clicked on
            button.set_label("Hello World!");
        });
    }
}

// Trait shared by all widgets
impl WidgetImpl for Window {}

// Trait shared by all windows
impl WindowImpl for Window {}

// Trait shared by all application
impl ApplicationWindowImpl for Window {}
// Please ignore this line
// It is only there to make mdbook happy
fn main() {}

Finally, we connect the callback to the "clicked" signal of button within constructed. The button is easily available thanks to the stored reference in self.

Filename: listings/interface_builder/3/main.rs

pub mod custom_button;
mod window;

use gtk::prelude::*;
use gtk::Application;

use window::Window;

fn main() {
    // Create a new application
    let app = Application::builder()
        .application_id("org.gtk.example")
        .build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);

    // Run the application
    app.run();
}

fn build_ui(app: &Application) {
    // Create new window and present it
    let window = Window::new(app);
    window.present();
}

With composite templates, main.rs actually became more concise. With regard to capabilities, we also get the best of both worlds.

Thanks to custom widgets we can

  • keep state and part of it as properties,
  • add signals as well as
  • override behavior.

Thanks to composite templates we can

  • describe complex user interfaces concisely, and
  • easily access widgets within the template.

In the following chapter, we will see how composite templates help us to create slightly bigger apps such as a To-Do app.


1

Puh, yet another builder? Let us summarize what we have so far:

  • GNOME Builder, an IDE used to create GNOME apps,
  • builder pattern, a design pattern used to create objects with many optional parameters and
  • gtk::Builder, the interface builder which creates widgets from xml files.

That was it with the builders. Promised!