Voir le flux RSS

tails

[Rust][Gtk-Rs][Relm][tutoriel] Une simple application saluant l'utilisateur par son prénom (qu'il renseigne)

Noter ce billet
par , 05/02/2020 à 19h38 (438 Affichages)
Le "crate" Relm est une librairie visant à simplifier la création d'applications GTK en Rust en permettant d'utiliser une syntaxe déclarative.
Afin de mettre le pied à l'étrier, j'ai décidé de réaliser une application minimaliste, où le but est simplement de saluer l'utilisateur par le prénom qu'il a renseigné.

Nom : apercu_saluer_utilisateur.png
Affichages : 416
Taille : 17,5 Ko

Tout d'abord, afin de pouvoir suivre ce billet, vous devez
  • connaître un minimum le fonctionnement de Rust, les modules, l'utilitaire Cargo, ainsi que les bases des Structs/Traits/Impl,
  • connaître les bases du développement en Gtk, même par le biais d'un autre langage (tel que Python). En effet, les connaissances peuvent être transposées.


Configuration des dépendances

Après avoir créé un nouveau projet binaire (remplacer $nom_projet par le nom voulu) nous pouvons ajouter la courte liste de dépendances dans le fichier Cargo.toml

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
gtk = "0.7.0"
relm = "0.18.0"
relm-derive = "0.18.0"
Ajout du code et exécution

Module d'interface

Nous pouvons ajouter le code graphique, que j'expliquerais plus bas, dans un fichier gui.rs, directement dans le sous-dossier src.

Code Rust : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
 
use relm::Widget;
use relm_derive::{Msg, widget};
 
use gtk::{LabelExt, WidgetExt, Inhibit, OrientableExt, EntryExt};
 
#[derive(Msg)]
pub enum AppMsg {
    Ignore,
    Quit,
    UpdateMessage(String),
}
 
pub struct AppModel {
    message: String,
}
 
#[widget]
impl Widget for AppWindow {
    fn model() -> AppModel {
        AppModel {
            message: String::from("Bonjour le monde !"),
        }
    }
 
    fn update(&mut self, event: AppMsg) {
        match event {
            AppMsg::Ignore => {},
            AppMsg::Quit => gtk::main_quit(),
            AppMsg::UpdateMessage(name) => self.update_hello_message(name),
        }
    }
 
    fn update_hello_message(&mut self, name: String) {
        self.model.message = format!("Bonjour {} !", name);
    }
 
    view! {
        gtk::Window {
            gtk::Box {
                orientation: gtk::Orientation::Vertical,
                gtk::Box {
                    orientation: gtk::Orientation::Horizontal,
                    gtk::Label {
                        text: "Votre prénom: ",
                    },
                    #[name="name_input"]
                    gtk::Entry {
                        text: "le monde",
                        activate(text_comp) => match text_comp.get_text() {
                            Some(name) => AppMsg::UpdateMessage(String::from(name)),
                            None => AppMsg::Ignore,
                        },
                    }
                },
                gtk::Label {
                    text: &self.model.message.to_owned(),
                },
            },
            delete_event(_self, _event) => (AppMsg::Quit, Inhibit(false)),
        }
    }
}
 
pub fn run_app() {
    AppWindow::run(()).expect("Echec de demarrage de l'application");
}

La fonction main

Nous pouvons modifier le fichier main.rs ainsi :

Code Rust : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
 
mod gui;
 
use gui::run_app;
 
fn main() {
    run_app();
}

L'application devrait donc pouvoir compiler et fonctionner Fonctionnement général de Relm

Avant d'expliquer le fonctionnement du code exemple, il nous faut d'abord comprendre comment fonctionne la librairie Relm de manière générale.

En Relm, on peut considérer que chaque composant est le reflet d'un modèle (des données qu'il est censé gérer), qui reçoit des messages en guise d’événements, et met à jour sa vue (son interface graphique) en fonction de ces messages. Une application Relm est constituée d'au moins un composant : le composant qui gère la fenêtre principale (et qui donc aura un gtk::Window comme composant racine).

Dans ce billet, nous voyons comment construire un composant en utilisant des attributs Rust (#[widget] entre autres) qui génère le code correspondant, mais sachez qu'il est également possible de s'en passer et de ne faire qu'avec du code. Pour cela, je vous invite à consulter les exemples officiels (ceux qui ne se terminent pas par "-attribute.rs").

Ainsi dans notre exemple :
  • le model (AppModel) est constitué d'un unique champ message (String),
  • l'énumération AppMessages définit l'ensemble des messages possibles pour le composant associé AppWindow. A noter l'utilisation de l'attribut #[derive(Msg)] et d'un Tuple Struct UpdateMessage acceptant une String en paramètre,
  • l'implémentation AppWindow permet de définir le composant. A noter l'utilisation de l'attribut #[widget] qui générera la classe composant.


La classe AppWindow définit également deux méthodes de base de tout composant :
  • fn model() -> AppModel, chargé de définir la valeur initiale du model associé au composant. Ici nous retournons une instance de la structure AppModel que nous avons défini plus haut. Mais nous aurions pu lui associer une autre structure,
  • fn update(&mut self, event: AppMsg) qui doit mettre à jour le modèle en fonction du message reçu. De même, nous aurions pu associer une autre énumération utilisant l'attribut #[derive(Msg)].


Et enfin, la macro view! permet de définir l'apparence graphique du composant. Et comme nous le verrons plus bas, nous pouvons simplement définir les propriétés des composants et Widgets GTK déclarés dans cette hiérarchie (notamment certaines en fonction du modèle associé au composant), ainsi que de lier des événements aux messages que nous avons définis. Pour rappel, on doit se contenter d'envoyer des messages, et c'est la méthode update qui met à jour le modèle, qui lui-même déclenchera une mise à jour de la vue s'il change.

Maintenant, il est tant d'essayer de mieux comprendre le fonctionnement de notre projet.

Décortiquons notre code

Le module graphique gui.rs

Lignes 2 et 3, nous importons quelques éléments utiles des modules relm et relm-derive : l'attribut widget, le trait Widget et le type Msg.

Ligne 5, nous importons divers traits de la librairie Gtk ainsi que la structure Inhibit. La structure Inhibit est utilisé pour gérer l'évènement Quit du composant, comme nous le verrons plus loin. Tandis que les différents traits suffixés par Ext sont des traits définissant différentes fonctionnalités des Widgets Gtk utilisés par notre composant. Par exemple, LabelExt apporte certaines fonctionnalités au Widget Label : dont notamment la méthode set_text(). Autre exemple, le trait OrientableExt permet de définir l'orientation de certains widgets, via la méthode set_orientation(), dont le Widget Box.
Alors comment savoir quel trait apporte quelle fonctionnalité ? Il suffit, dans la documentation de Gtk-Rs, de rechercher la documentation correspondant au Widget voulu, prenons par exemple le Widget gtk::Button. Nous avons donc accès à la section Implements, listant les différents traits implémentés par le Widget, et de naviguer dans les différentes documentations de ces traits. Ainsi, en naviguant dans la documentation de ButtonExt par exemple, nous retrouvons entre autres la méthode connect_clicked : permettant de gérer l’événement clic.

Lignes 7 à 12: les différents messages que nous souhaitons voir gérer par notre composant, et qui mettra à jour le modèle en conséquence. Quand un message a besoin de paramètres, on peut facilement définir un Tuple Struct, tout comme nous l'avons fait pour UpdateMessage. Le nom de l'énumération est évidemment libre.

Lignes 14 à 16: le modèle de notre composant : ici nous souhaitons juste gérer le message destiné à l'utilisateur.

Lines 18 et 19: pour définir notre composant, nous implémentons le trait Widget, sans oublier de préciser l'attribut #[widget], le nom de la structure étant encore une fois, libre.

Lignes 20 à 24: la définition du modèle initial pour notre composant.

Lignes 26 à 32: la mise à jour du composant, ou plutôt de son modèle, en fonction des messages reçus. Nous utilisons un simple pattern matching sur le type du message. Nous avons aussi bien fait directement appel à du code Gtk, gtk::main_quit() pour quitter l'application, que fait appel à une méthode personnelle pour du traitement plus complexe (update_hello_message(name)). Il est également possible de ne rien associer au message, comme pour le message Ignore.

Lignes 38 à 62: la définition de l'interface par l'intermédiaire de la macro view!. Les éléments y sont imbriquées, chaque propriété/événement/composant ou widget enfant étant séparé par une virgule. Il faut alors distinguer propriété et événement.

Les propriétés sont définis par toute méthode préfixée par set_ dans l'ensemble des Traits décorant les Widgets Gtk, comme expliqué plus haut (les différentes classes suffixées par Ext). Ainsi le trait LabelExt, que le Widget Label implémente, définit une méthode set_text(). Nous pouvons donc utiliser la propriété text sur le Widget Label, comme en ligne 57. Pour définir une propriété d'un composant, on supprime donc le préfixe set_ ainsi que les parenthèses, et nous définissons la valeur après le signe de ponctuation deux points ":". Un autre exemple de propriété: orientation pour le widget gtk::Box, ligne 43.

Les évènements sont définis par toute méthode préfixée par connect_ dans l'ensemble des Traits décorant les Widgets Gtk. Ainsi, le trait EntryExt, implémenté notamment par la structure Entry, définit la méthode connect_activate. Cet évènement permet de gérer la validation de la valeur du champ texte par la touche Enter. Nous pouvons voir cela à l'oeuvre en ligne 50. Ainsi, pour définir un évènement nous enlevons le préfixe connect_, puis ajoutons les paramètres en donnant des noms arbitraires, car cela fonctionne avec le pattern matching. Ensuite il faut ajouter le signe égal suivi du signe supérieur "=>". Et enfin, nous définissons le message à envoyer au composant. Attention ! Les paramètres sont définis, dans la documentation, dans le paramètre Fn. Par exemple "fn connect_activate<F: Fn(&Self) + 'static>(&self, f: F) -> SignalHandlerId", l'évènement est activate, le paramètre &self (Fn(&self)), donc on écrira activate(comp) => $handler, par exemple, ou même activate(_) => $handler. Où $handler est le code de votre gestionnaire pour cet évènement.

Ainsi lignes 50 à 53, l’événement Activate du widget Entry est lié au message UpdateMessage en cas de succès de récupération de la valeur du champ texte, ou à Ignore en cas d'échec divers.

Par contre, ligne 60, pour l'événement delete_event() de la fenêtre principale, nous retournons un Tuple dont le 1er paramètre est le type de message (ici Quit) et le deuxième paramètre est la valeur de retour attendue par l'évènement delete_event. En effet, si vous consultez la documentation de connect_delete_event, du trait WidgetExt (implémenté par gtk::Window), vous verrez qu'il faut retourner une valeur Inhibit. En effet la méthode est défini comme suit
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
fn connect_delete_event<F: Fn(&Self, &Event) -> Inhibit + 'static>(
    &self,
    f: F
) -> SignalHandlerId
(J'ai mis en évidence ce qui nous intéresse vraiment.)
Cette valeur sert à déterminer si les gestionnaires par défauts de l'événement restent actifs ou non. Même si la documentation recommande de ne pas les désactiver (donc de passer Inhibit(false)), il est possible de les désactiver (Inhibit(true)).

Lignes 65 à 67: cette méthode nous permet d'appeler simplement le code d’exécution de l'application dans la fonction main. En effet, le code qu'elle contient doit être définit dans la même "portée" que la définition de notre composant, macro de génération oblige.

Le code principal main.rs

Ici rien de bien compliqué, grâce notamment à la définition de la méthode run_app dans le module gui.rs .

Conclusion

Dans ce tutoriel, nous avons vu une utilisation basique du framework Relm, dont le but est simplifier la création d'applications GTK en Rust.
J'espère que ce billet n'a pas été trop indigeste.

Je voudrais remercier antoyo, l'auteur du framework, pour ses précieuses réponses à mes questions sur le forum Gitter dédié à Relm.

Envoyer le billet « [Rust][Gtk-Rs][Relm][tutoriel] Une simple application saluant l'utilisateur par son prénom (qu'il renseigne) » dans le blog Viadeo Envoyer le billet « [Rust][Gtk-Rs][Relm][tutoriel] Une simple application saluant l'utilisateur par son prénom (qu'il renseigne) » dans le blog Twitter Envoyer le billet « [Rust][Gtk-Rs][Relm][tutoriel] Une simple application saluant l'utilisateur par son prénom (qu'il renseigne) » dans le blog Google Envoyer le billet « [Rust][Gtk-Rs][Relm][tutoriel] Une simple application saluant l'utilisateur par son prénom (qu'il renseigne) » dans le blog Facebook Envoyer le billet « [Rust][Gtk-Rs][Relm][tutoriel] Une simple application saluant l'utilisateur par son prénom (qu'il renseigne) » dans le blog Digg Envoyer le billet « [Rust][Gtk-Rs][Relm][tutoriel] Une simple application saluant l'utilisateur par son prénom (qu'il renseigne) » dans le blog Delicious Envoyer le billet « [Rust][Gtk-Rs][Relm][tutoriel] Une simple application saluant l'utilisateur par son prénom (qu'il renseigne) » dans le blog MySpace Envoyer le billet « [Rust][Gtk-Rs][Relm][tutoriel] Une simple application saluant l'utilisateur par son prénom (qu'il renseigne) » dans le blog Yahoo

Mis à jour 05/02/2020 à 20h07 par tails

Catégories
Rust

Commentaires