Créer une application GTK avec Rust et Glade

Quelques points à connaitre pour faire de jolies applications

Bonjour à tous. Ça fait longtemps que j'ai joué avec un framework pour créer des interfaces graphiques, et vous ? C'est vrai qu'avec la tendance de tout faire en web, ce savoir faire se perd. J'ai déjà beaucoup joué avec Qt auparavant, mais avec la sortie de GNOME 3 et GTK3 il y a quelques temps, ce framework remonte un peu dans mon estime. Les nouveaux composants et paradigmes apportés par cette mouture sont sympathiques.

Un peu de blabla

Tout d'abord un peu de contexte sur les motivations qui m'ont amenées à écrire cet article.

Ma (non) expérience avec GTK

Une des choses qui m'a fait choisir Qt à l'époque où je cherchais un framework était que GTK était avant tout destiné à être utilisé en C. Je lui ai préféré Qt car c'était du C++ un peu moins tiré par les cheveux que le C utilisé dans GTK qui est orienté objet malgré le langage.

À un moment Vala a pointé le bout de son nez et on avait un langage vraiment adapté à GTK mais qui sonnait creux en dehors de ce domaine précis d'application. Essai également de gtkmm, interface C++ à GTK qui avait selon moi plus d'avenir que Vala, mais sans suite.

To GUI or not to GUI?

De manière générale je ne suis pas spécialement friand des interfaces graphiques, étant assez fan des interfaces en ligne de commande, pour le meilleur et pour le pire. Il ne faut cependant pas être fermé, je pense que les GUIs sont nécessaires pour des programmes demandant de nombreuses interactions avec l'utilisateur, par exemple la saisie de données ou la visualisation. Un peu comme tout ce qui justifie de faire une interface web.

GTK3 a apporté de nombreuses améliorations et concepts, un coup de frais dans le monde des interfaces graphiques. De quoi me motiver à essayer de m'y remettre. Reste le point du langage à choisir. Heureusement Rust est né et a bien grandi, apportant la performance du C, les abstractions du C++ (sans sa complexité) modulo la gestion des lifetimes (ouf, quelle formule !).

Première étape : faire joujou avec Glade

Comme tout bon développeur le sait, il faut définir l'interface graphique avant le code… ou l'inverse, je ne sais plus ! Toujours est-il que ma principale motivation est de jouer avec les interfaces graphique plutôt que de faire quelque-chose d'utile. C'est pour cela que nous allons commencer par créer une interface bidon dans Glade plutôt que de coder.

Ce dernier est un éditeur d'interface graphique, comme QtCreator mais pour GTK.

Capture d'écran de Glade

Les widgets à gauche, les propriétés à droite, un classique.

Attention à Wayland !

Premier lancement, premier piège : Glade ne fonctionne pas correctement sous Wayland. En effet le glisser-déposer de widgets depuis le menu de gauche vers la fenêtre ne fonctionne pas. Autant vous dire que c'est pénible. Le plus simple est de démarrer GNOME sur Xorg. C'est lent — toutes les animations de GNOME 3 passent par le CPU — mais on peut désormais ajouter des widgets.

Je pense que les développeurs sont au courant de ce problème majeur et y travaillent, en attendant pas le choix.

La barre de titre interactive

Une des modifications que l'on voit se propager avec GNOME 3 est l'utilisation de la barre de titre pour y placer des boutons et composants divers. La dénomination technique de ce composant est la HeaderBar.

Capture d'écran de l'ajout d'événement de l'application Agenda

L'ajout d'événement dans Agenda utilise une HeaderBar pour la validation de l'événement

Pour retrouver la composition des différents éléments constituant une application GNOME 3 moderne, je vous invite à jeter un rapide coup d'œil à la page Design patterns de GNOME 3, partie Anatomy of a GNOME 3 application.

Je vous propose de commencer par créer une barre de titre de ce genre, parce-que ce gadget me parait complètement essentiel. Sachez que les ressources sur l'utilisation de cette barre de titre via GTK3 sont rares et qu'il m'a fallu pas mal creuser pour trouver les informations que je vais vous présenter ici.

Création de notre première HeaderBar

Alors, retournons sur Glade. La première étape est de créer une ApplicationWindow qui va nous servir comme base de travail (simple clic dessus). Dans les propriétés de la fenêtre, partie Général → Apparence, il faut cocher la case Décoration de la fenêtre côté client. Celle-ci va nous permettre de personnaliser la barre de titre de notre fenêtre.

Ensuite, prenez une barre d'en-tête dans la catégorie conteneurs et glissez la dans la partie haute de la fenêtre. Si le glisser-déposer n'a aucun effet vous êtes probablement sous Wayland.

Dans les attributs de cette barre d'en-tête, passez le nombre d'éléments à 2 pour avoir deux emplacements de bouton. Enfin placez un bouton dans chacune des cases affichées sur la gauche de la barre de titre. Vous pouvez également définir un titre et sous-titre dans la barre de titre. N'oubliez pas de cocher afficher les commandes de la fenêtre (vous comprendrez pourquoi en la décochant).

Cerise sur le gateau, au niveau des boutons vous pouvez attribuer la classe CSS destructive-action sur le premier et suggested-action sur le second, dans l'onglet Commun (merci le wiki sur les boutons).

Appuyez sur l'icône avec un engrenage pour lancer un aperçu de votre chef-d'œuvre. Si vous avez tout bien suivi vous devriez avoir quelque-chose comme ceci :

Capture d'écran premier essai Glade

Si c'est pas joli

Un bouton à tribord

Comme pour les boites de dialogue que l'on voit souvent sur GNOME 3, il serait intéressant de mettre le bouton rouge pour annuler à gauche et le bouton pour valider à droite, tout en enlevant la croix si on a déjà un bouton pour annuler.

Pour obtenir ce résultat il va falloir tricher et éditer à la main le fichier de Glade car celui-ci n'offre pas de menu dans son interface permettant d'utiliser cette fonctionnalité. Enregistrez votre fichier d'interface, fermez Glade puis ouvrez ce fichier avec votre éditeur de texte favori. Le deuxième bouton est décrit de la manière suivante dans l'arbre XML :

<child>
  <object class="GtkButton">
    <property name="label" translatable="yes">button</property>
    <property name="visible">True</property>
    <property name="can_focus">True</property>
    <property name="receives_default">True</property>
    <style>
      <class name="suggested-action"/>
    </style>
  </object>
  <packing>
    <property name="position">1</property>
  </packing>
</child>

Nous allons devoir ajouter une propriété pack_type avec la valeur end pour le nœud packing afin de signaler que l'on veut que l'élément soit ajouté à la fin du contenant :

<packing>
  <property name="pack_type">end</property>
  <property name="position">1</property>
</packing>

Ouvrez à nouveau votre fichier dans Glade et vous devriez observer le bouton de validation à sa nouvelle position.

Capture d'écran second essai Glade

On a bien avancé pour la partie titre, n'est-il pas ?

Plusieurs autres changements ont été effectués pour arriver à ce résultat :

  • Désactivation de l'affichage des commandes de la fenêtre afin de le pas afficher la croix de fermeture.
  • Renommage des boutons avec le nom des actions souhaitées.
  • Passage du bouton d'annulation dans une couleur classique et non plus rouge. En effet la couleur rouge est réservée aux opérations destructives d'après les guidelines de GNOME 3. Une annulation n'atteint pas ce degré de dangerosité, c'est pourquoi un bouton neutre est plus adéquat.

Et pour quelques widgets de plus

Il nous reste maintenant à créer un formulaire bidon pour essayer de rendre notre démo presque utile. Je vous conseille de mettre un conteneur grille comme base pour alterner ensuite entre les labels et les champs.

Après un peu de temps passé dans l'éditeur je suis parvenu à ce résultat :

Capture d'écran finale Glade

Je vais m'arrêter ici pour mon interface de démonstration

Pas grand chose à détailler ici si ce n'est une astuce : par défaut les widgets ne prennent pas toute la place disponible et se contentent du minimum. Ce comportement n'est pas souhaitable dans la capture d'écran ci-dessus :

  • Les champs de texte doivent prendre toute la largeur disponible.
  • Les étiquettes devant les interrupteurs doivent également prendre toute la largeur pour s'assurer que cela pousse ces interrupteurs sur la droite.

Pour activer cette option sur un composant, il faut aller dans Commun → Espacement des composants, cocher la case élargissement horizontal puis activer l'interrupteur à côté. On remarque ici d'ailleurs un choix étonnant de la part des concepteurs de Glade car cette double activation est tout sauf intuitive.

Option pour activer le remplissage dans Glade

En plus d'être répétitifs, les deux composants copulent… pas joli joli

Seconde étape : intégration avec Rust via gtk-rs

Nous allons maintenant voir comment utiliser cette interface avec Rust. Le code sera très simple et se contentera d'afficher dans le terminal les valeurs contenues dans notre formulaire lors de l'appui sur valider.

Création du projet et choix de version

Première étape : créer un nouveau projet Rust avec Cargo :

$ cargo new --bin hellogtk

Voici ci-dessous le contenu à spécifier dans le fichier Cargo.toml.

[package]
name = "hellogtk"
version = "0.1.0"
authors = ["Your Name <your.name@example.com>"]

[dependencies.gtk]
version = "0.3.0"
features = ["v3_20"]

On importe ici Gtk-rs pour pouvoir utiliser GTK en Rust. Il est nécessaire de spécifier à quelle version de GTK on veut se lier pour pouvoir utiliser cette bibliothèque, d'où la déclaration de dépendance dans ce format un peu spécial.

La version spécifiée dans features est la version minimale nécessaire de GTK pour pouvoir faire tourner votre programme. Sur la documentation de Gtk-rs on remarque que plusieurs fonctions ne sont disponible qu'à partir d'une certaine version de GTK. C'est ce genre d'information qui va vous guider vers le choix d'une version suffisamment récente pour utiliser certaines fonctionnalités, mais pas trop récente pour pouvoir être utilisé sur des systèmes utilisant une version un peu plus ancienne de GTK 3.

Une autre méthode pour choisir cette version est d'utiliser la version la plus basse et de compiler votre projet. Si une fonction ou structure n'est pas disponible alors le compilateur Rust vous le signalera. Il faudra alors aller dans la documentation pour trouver la version minimale nécessaire pour utiliser la fonctionnalité.

Cependant, le fait de choisir une version plus récente fera recompiler la crate gtk ainsi que toutes ses dépendances, ce qui prend du temps. Tournant pour ma part sur Archlinux, j'ai choisi de mettre une version très récente afin de de pas avoir à recompiler à chaque fois que j'ai besoin d'une nouvelle fonctionnalité.

Le code d'interface en Rust

Plutôt que de vous expliquer étape par étape comment réaliser le code, je vais vous le mettre ici et détailler quelques points importants. Veillez également à placer votre fichier .glade dans le dossier src/ du projet, à côté du fichier main.rs.

extern crate gtk;

use gtk::prelude::*;

#[derive(Debug)]
struct User {
    username: String,
    firstname: String,
    lastname: String,
    authorized: bool,
    superuser: bool,
}

fn main() {
    if gtk::init().is_err() {
        println!("Failed to initialize GTK.");
        return;
    }

    // First we get the file content.
    let glade_src = include_str!("test.glade");
    // Then we call the Builder call.
    let builder = gtk::Builder::new_from_string(glade_src);

    let window: gtk::Window = builder.get_object("applicationWindow").unwrap();
    let cancel_button: gtk::Button = builder.get_object("cancelButton").unwrap();
    let validate_button: gtk::Button = builder.get_object("validateButton").unwrap();

    let username: gtk::Entry = builder.get_object("username").unwrap();
    let firstname: gtk::Entry = builder.get_object("firstname").unwrap();
    let lastname: gtk::Entry = builder.get_object("lastname").unwrap();
    let authorized: gtk::Switch = builder.get_object("authorized").unwrap();
    let superuser: gtk::Switch = builder.get_object("superuser").unwrap();

    window.connect_delete_event(|_, _| {
        // Stop the main loop.
        gtk::main_quit();
        // Let the default handler destroy the window.
        Inhibit(false)
    });

    let window_clone = window.clone();
    cancel_button.connect_clicked(move |_| {
        window.close();
    });

    validate_button.connect_clicked(move |_| {
        let user = User {
            username: username.get_text().unwrap(),
            firstname: firstname.get_text().unwrap(),
            lastname: lastname.get_text().unwrap(),
            authorized: authorized.get_state(),
            superuser: superuser.get_state(),
        };

        println!("{:?}", user);
    });

    window_clone.show_all();

    gtk::main();
}

Tout d'abord on peut observer la structure Rust décrivant les informations que l'on veut récupérer depuis l'interface graphique. Rien de bien particuler, on dérive de Debug pour pouvoir faire un println! rapide de la structure et de son contenu.

Dans la fonction main, on peut voir l'appel à la macro include_str! qui va effectivement lire le contenu du fichier test.glade et le mettre dans une variable au format texte. De cette façon, le fichier d'interface est directement présent dans l'exécutable, au lieu d'avoir à le chercher dans le système de fichier de manière hasardeuse.

Ensuite on récupère tous les éléments intéressants depuis le fichier en utilisant leurs identifiants respectifs. Il faut à chaque fois appeler unwrap() car on est pas sûr que l'entrée soit présente dans le fichier d'interface. Si celle-ci n'existe pas (ou que vous avez mal tapé son nom, ça m'est arrivé) alors le programme compilera mais explosera en vol lors de l'exécution avec un panic! pas très joli.

Il serait intéressant de faire un programme qui parcours ce fichier XML et qui extrait les composants dans une structure Rust pour éviter tous ces appels dynamiques afin de les récupérer. Si vous n'avez pas d'idée de projet Rust, ça pourrait être sympa à développer pour garantir la présence des composants à la compilation plutôt qu'à l'exécution !

Enfin, on connecte les différents appuis sur les boutons à des actions. C'est la partie la moins « propre » de cette bibliothèque à mon avis. En effet, le fait d'utiliser des closures pour référencer la fenêtre window fait que l'on doit utiliser un move pour pouvoir effectuer des actions dessus lors du callback (et non pas un borrow qui ne peut avoir lieu qu'une seule fois).

L'astuce ici est donc de cloner la variable window en window_clone pour pouvoir y faire référence dans l'action du bouton et après la déclaration des actions pour lancer la fenêtre. La documentation nous indique que c'est actuellement la seule méthode convenable qu'ils ont trouvé pour l'instant. L'appel à clone copie juste le pointeur vers la fenêtre en interne, cela ne coûte donc rien du point de vue des performances. Par contre au niveau de la rédaction du programme c'est assez déroutant car on doit cloner dans une nouvelle variable à chaque fois que l'on souhaite utiliser la fenêtre.

Espérons que les développeurs de cette bibliothèque pourront nous proposer une méthode un peu plus Rust-ique pour palier à ce problème !

Le résultat

Affichage du contenu de la fenêtre dans le terminal

La liaison est opérationnelle, on peut même y mettre des émojis

Conclusion

Voilà pour cette article ! Ça fait longtemps que j'ai écrit du contenu aussi complet… Facilement trois heures de rédaction répartis en deux jours. La mise en place de ce petit hack a prit quant à lui une petite après-midi, je pense même moins de temps que la rédaction de cet article.

J'espère néanmoins que vous avez appris des choses et que vous allez nous pondre de jolis prototypes. Et si vous êtes vraiment allergique aux GUI, il reste toujours l'extracteur de fichier Glade vers structure Rust à réaliser…

Au boulot !

Merci à Seb pour sa relecture.

Une question ou remarque ? N'hésitez pas à me contacter en envoyant un mail à microjoe, suivi d'un arobase, puis encore microjoe et enfin un « point org ».