Il est né ?
Vous êtes ici : mk0phpgtk.xgarreau.org >> aide >> devel >> langtk : Initiations Langage/Toolkit graphique: C++/gtkmm
Version imprimable

Langage / Toolkit graphique : C++/Gtkmm

Nous avons bien défriché les bases du langage C++ au cours des premiers articles et comme promis je vais maintenant vous livrer des articles présentant des points particuliers. Je commence pour ce dixième article par la présentation du toolkit gtkmm.

Introduction

gtkmm est au c++ ce que le gtk+ est au c en terme de fonctionnalités offertes. gtkmm permet de créer des interfaces graphiques comme gtk+ peut le faire. gtkmm est un ensemble de ce qu'on appelle des "wrappers", des "englobeurs" en vilain français. Le but d'un wrapper est de créer une interface pour un langage dont l'implémentation est réalisée à partie d'un autre langage. Quand vous coderez à l'aide de gtkmm, vous manipulerez des instances de classes de façon habituelle. Pourtant ces classes utilisent en fait des appels à une librairie en C. Il s'agit bien sûr de gtk+.

Pourquoi ne pas utiliser gtk+ directement ? Concentrez vous sur l'aspect objet de votre projet, d'autres ont pris la peine de créer des classes. S'il vous démange d'utiliser gtk+ dans du code C++, vous pouvez toutefois le faire. Et si vous le faites, devenez donc contributeurs pour le projet gtkmm ;-).

gtkmm ou Qt ? Ce n'est pas mon genre d'alimenter les trolls. La meilleure raison est la préférence personnelle. On trouve sur Internet des intégristes des deux camps. Leurs arguments sont souvent intéressants et parfois honnêtes. Faites vous votre propre idée en recherchant "gtkmm vs Qt" ou "Qt vs gtkmm" dans google. Je vous ai mis 3 liens en fin d'article qui représnetent bien les divers points échangés entre passionnés. Encore une fois, essayez les 2 kits et choisissez celui qui vous convient pour l'application que vous en faites.

Installation

Je base cet article sur gtkmm-2, version stable à l'heure de l'écriture de cet article. Pour installer gtkmm, plusieurs solutions s'offrent à vous. Pour ma part ma debian est en testing, j'ai donc tout simplement utilisé apt-get (on ne va pas se priver). La version dont je dispose est donc gtkmm2.0-2.2.7. Vous pouvez également compiler les sources... Si vous avez déjà GTK2, vous ne serez pas ennuyé. Par ailleurs, la majorité des distributions fournissent des paquets pour gtkmm2 ( RedHat9, YellowDog3, Madrake Cooker), il y a même un installeur pour windows ;-) mais qui nécessite lui aussi la présence de gtk+ et qui requiert Cygwin ou MinGW (Minimalist GNU for Windows, qui fera l'objet d'un article dans un futur numéro de votre magazine préféré).

Quel que soit le moyen que vous utiliserez, vous devez parvenir au bout de l'installation. Sinon, programmez avec Qt ;-)

Codons

Maintenant qu'on a un système à jour, on va s'intéresser à la façon de coder une application simple. Pour faire un peu plus évolué qu'un simple 'Hello World !', elle sera composée d'un texte explicatif, d'une zone pour saisir du texte et d'un bouton. Dans la zone de texte, on saisira son nom et lors du clic sur le bouton, un dialogue vous dira Bonjour.

Commençons par les choses simples... Pour développer un programme utilisant gtkmm, vous devez inclure une ligne #include <gtkmm.h> dans votre code source. Ca c'est la méthode brutale. Vous incluez ainsi tout les entêtes de gtkmm... Vouq pouvez faire les choses plus finement en n'incluant que les éléments qui vous servent dans votre programme. A titre d'exemple, notre application nécessite les inclusions suivantes :

#include <gtkmm/main.h>
#include <gtkmm/window.h>
#include <gtkmm/box.h>
#include <gtkmm/button.h>
#include <gtkmm/label.h>
#include <gtkmm/entry.h>

La première ligne est obligatoire puisqu'elle inclut la définition la classe Gtk::Main permettant de lancer la boucle gtk principale. Les autres includes on des noms explicites, ils nous permettent l'utilisation de fenêtres, boîtes de rangement, boutons, texte statique et zone de texte éditable.

La librairie gtkmm s'appuie sur glibmm. Le contenu des librairies sont réparties dans plusieurs espaces de noms, Gtk, Gdk, Glib, Atk et Pango sont les cinq principaux. Il est déconseillé d'utiliser les directives using namespace mais plutôt de spécifier les espaces de nom lorsque c'est nécessaire.

Lorsqu'on développe avec un toolkit tel que Qt ou gtkmm, les différents widgets sont représentés par des classes. Comment savoir si on doit instancier un objet à partir d'une classe du toolkit ou créer sa propre classe qui hérite de ce widget ? Je vous recommande d'instancier le widget directement lorsque son contenu, son comportement, son apparence, etc ... par défaut vous conviennent et de créer votre widget si vous voulez le modifier. Typiquement on utilise directement la classe bouton mais on hérite de la classe fenêtre, comme on le verra en fin d'article.

Programme Minimal

Code source :

#include <gtkmm/main.h>

int main(int argc, char **argv)
{
    Gtk::Main app(argc, argv);
    app.run();
    return 0;
}

Compilation & Exécution. Heureusement gtkmm utilise pkgconfig pour nous aider à écrire la ligne de compilation. Vous n'avez donc pas à connaître toutes les librairies nécessaires et les chemins vers ces dernières et les fichiers d'en-têtes. Vous utiliserez toujours au sein de cet article deux lignes de cette forme (en modifiant au besoin les noms des fichiers source et exécutable) :

$ g++ `pkg-config gtkmm-2.0 --cflags --libs` -o gtkmm_1 gtkmm_1.cpp
$ ./gtkmm_1

Ce programme minimal n'a aucun intérêt et en plus doit être arrêté violemment. Il montre toutefois la structure générale d'une application construite à l'aide de gtkmm.

Le premier objet créé doit être un objet Gtk::Main. On lui passe argc et argv en paramètre

Pour lancer la boucle gtk on utilise la méthode run() de l'instance de Gtk::Main.

Une fenêtre

Pour créer un widget de la classe Window, on doit instancier un objet de cette dernière.
Pour afficher un widget, on utilise la méthode show();, comme en gtk.

#include <gtkmm/main.h>
#include <gtkmm/window.h>

int main(int argc, char **argv)
{
    Gtk::Main app(argc, argv);
    Gtk::Window w;

    w.show();
    app.run();
    return 0;
}

Ce programme crée une fenêtre et l'affiche. Seulement voilà, lorsque l'on ferme la fenêtre, le programme continue de tourner. Celà va surement rappeler aux développeurs c/gtk+ un mauvais souvenir : le callback nécessaire à toute application pour terminer la boucle gtk lors de la destruction du widget père (top-level).

En C++/gtkmm, la vie est bien faite, comme souvent en c++. Il suffit de passer le widget père en paramètre à app.run(); pour que la boucle gtk se termine lors de la destruction de ce dernier. En outre, celà rend l'appel à w.show() inutile puisque le widget passé en paramètre est affiché par l'objet Gtk::Main.

Notre programme afficheur de fenêtre avec fermeture propre devient donc :

#include <gtkmm/main.h>
#include <gtkmm/window.h>

int main(int argc, char **argv)
{
    Gtk::Main app(argc, argv);
    Gtk::Window w;
    app.run(w);
    return 0;
}
appli minimale

Des widgets

Le but de cette application est :

Nous allons, pour ce faire utiliser 4 widgets. Une fenêtre pour afficher l'application, une zone de saisie pour que l'utilisateur saisisse son prénom, un bouton pour informer l'application, qu'il a fini d'entrer son prénom et un champ non editable (label, ou étiquette) pour afficher le message de bienvenue.

Gtk est un tantinet capricieux, en celà, qu'on ne peux pas prendre une fenêtre et la remplir de widgets... Dans une fenêtre GTK, on met UN widget. Alors, quand on construit une application ayant besoin de plusieurs widgets... Le premier que l'on met dans la fenêtre est un conteneur.

Un conteneur, "container" en anglais, est un widget dont la raison d'être est d'en accueillir d'autres. Il en existe plusieurs types et notamment les conteneurs pouvant accueillir plusieurs widgets. Il existe des conteneurs verticaux, horizontaux, en tableaux et libres. Dans ces derniers, les widgets insérés sont respectivement rangés en colonnes, en lignes, en lignes et colonnes ou enfin librement (comme dans Visual Basic par défaut ;-) ).

On va utiliser une Gtk::VBox (je ne répèterai plus Gtk:: par la suite ..., pour alléger le texte) pour cette application, soit une boîte de rangement de widgets verticale.
On ajoutera cette VBox à notre Window grâce à la méthode Window::add.

Une fois le contenant créé, on peut créer le contenu, c'est à dire un Label, une Entry, et un Button. Le constructeur du bouton est le premier constructeur de widget que nous rencontrons prenant un paramètre, il s'agit du texte affiché dans le bouton.

Une fois les widgets créés, on signale à Gtk qu'ils sont prêts en invoquant la méthode show.

On ajoute les composants dans la VBox créée à cet effet, de haut en bas, à l'aide de la méthode VBox::pack_start.

En conséquence de ce que l'on vient de dire, on obtient le code suivant :

#include <gtkmm/main.h>
#include <gtkmm/window.h>
#include <gtkmm/box.h>
#include <gtkmm/label.h>
#include <gtkmm/entry.h>
#include <gtkmm/button.h>

int main(int argc, char **argv)
{
    Gtk::Main app(argc, argv);
    Gtk::Window w;
    Gtk::VBox vb;
    w.add(vb);

    Gtk::Label l;
    vb.pack_start(l);
    l.show();
    Gtk::Entry e;
    vb.pack_start(e);
    e.show();
    Gtk::Button b("Bonjour !");
    vb.pack_start(b);
    b.show();

    vb.show();
    app.run(w);
    return 0;
}

Rien de très compliqué dans ce code, ce n'est que l'application de ce qui a été dit plus haut. Amusez vous avec l'interface avant de passer à la suite ;-)

Les signaux

Que rien ne se passe lors d'un clic sur le bouton est relativement frustrant, non ? Nous allons remedier à celà et en profiter pour voir la gestion des signaux en Gtkmm, prise en charge par la librairie libsigc++.

Commençons par définir un comportement simple, affichons juste un messages sur la sortie standard. (N'oubliez pas d'ajouter une ligne #include <iostream> en tête de fichier.

void on_button_clicked() {
	std::cout << "Bonjour" << std::endl;
}

Pas de surprise concernant l'affichage du message ... Passons à la connexion avec l'action du clic sur le bouton.
On utilise pour ce faire la méthode connect des signaux. On connecte un signal non pas à une fonction mais à un slot. Pour transformer un pointeur de fonction en un slot, on utilise la méthode SigC::slot. Cette dernière méthode existe en deux formes. Soit elle prend en paramètres un objet et une de ses méthodes, soit une fonction globale. Commençons par voir le cas le plus simple. Transformez le bloc de construction du bouton ci-dessus par :

    Gtk::Button b("Bonjour !");
    b.signal_clicked().connect(SigC::slot(&on_button_clicked));
    vb.pack_start(b);
    b.show();

On connecte le signal signal_clicked du bouton b à la fonction globale on_button_clicked.

Si vous compilez ce code, vous verrez s'afficher "Bonjour" lorsque vous cliquerez sur le bouton.

Paramètres de slots

Le but de notre application est d'afficher ce message dans le Label, à partir du contenu de l'Entry. Le problème est que notre fonction n'a pas accès aux éléments de la fenêtre elle même puisque ces derniers ne sont déclarés que dans la fonction main du programme.

On pourrait résoudre ce problème en déclarant les widgets qui nous intéressent avec une portée globale. Ce ne serait toutefois pas très propre, et c'est là un bel euphémisme ;-)

On peut également les passer en paramètre au gestionnaire du signal, en utilisant la méthode SigC::bind. Cette fonction est en fait un patron prenant en paramètre de patron le type de paramètre que l'on veut passer au slot.

Nous pouvons utiliser cette astuce pour passer UN paramètre mais il nous faut en passer DEUX : l'Entry et le Label. On crée donc une structure regroupant ces deux entités :

struct param {
	Gtk::Entry *e;
	Gtk::Label *l;
};

Le bloc de construction du bouton intègre la déclaration du paramètre et la méthode SigC::bind pour devenir :

    Gtk::Button b("Bonjour !");
    param p = { &e, &l };
    b.signal_clicked().connect(SigC::bind<param>(SigC::slot(&on_button_clicked), p));
    vb.pack_start(b);
    b.show();

Le gestionnaire de signal peut, dès lors, faire usage des widgets passés en paramètre, au travers de la structure param p.

void on_button_clicked(param p) {
	p.l->set_text("Bonjour " + p.e->get_text() + " !");
}

On voit à présent apparaître dans le gestionnaire deux méthodes extrêmement simples mais très utiles : Label::set_text et Entry::get_text. Je ne précise pas le rôle de ces méthodes, tant leurs noms sont équivoques et vous invite à consulter la documentation pour décourvrir leurs amies.

Je profite de cette occasion pour préciser que la classe utilisée par Gtkmm pour représenter les chaînes de caractères, bien que l'on ne s'en rende pas compte dans les exemples de cet article est Glib::ustring, qui est presque équivalente à std::string, à cette énorme différence près qu'elle utilise des caractères Unicode et est donc prête pour l'internationalisation de vos programmes.

La ZazouFenetre

Voilà ! Notre application est d'ores et déjà fonctionnelle. Nous allons toutefois pousser un peu cet article pour aborder une programmation plus propre et logique dans un premier temps puis pour ajouter quelques fioritures à notre interface utilisateur.

La méthode de transmission des widgets au gestionnaire que nous avons utilisée ci dessus n'est pas la plus efficace, ni la plus "propre" à notre disposition. On est ici pour faire du C++, nous allons donc modéliser notre fenêtre avec ses widgets et ses gestionnaires de signaux au moyen d'une classe maFenetre.

Déclaration de la classe

class maFenetre : public Gtk::Window
{
protected:
    Gtk::VBox vb;
    Gtk::Label l;
    Gtk::Entry e;
    Gtk::Button b;

    void on_button_clicked();
public:
	maFenetre();
	virtual ~maFenetre();
};

On déclare les widgets et gestionnaires de signaux protected, de sorte qu'ils puissent être réutilisés par d'éventuelles classes filles de maFenetre. En revanche pour pouvoir constuire et détruire la fenêtre, on déclare le constructeur et le destructeur comme publics.

Définition de la classe

maFenetre::maFenetre() : b("Bonjour !")
{
    vb.pack_start(l);
    l.show();
    vb.pack_start(e);
    e.show();
    b.signal_clicked().connect(SigC::slot(*this, &maFenetre::on_button_clicked));
    vb.pack_start(b);
    b.show();

    add(vb);
    vb.show();
}

maFenetre::~maFenetre()
{
}

void maFenetre::on_button_clicked()
{
	l.set_text("Bonjour " + e.get_text() + " !");
}

On retrouve dan l'impémentation de la classe maFenetre le code précédemment utilisé, jsute déplacé, à une exception près. On utilise maintenant une autre forme de SigC::slot, prenant en paramètre une classe et un pointeur de fonction. On utilise ici le membre maFenetre::on_button_clicked de la classe maFenetre. Etant donné que nous sommes dans la portée de la classe maFenetre, l'objet passé en paramètre est logiquement *this.

N'oubliez surtout pas de spécifier entièrement le gestionnaire de signal, sous peine d'être gratifié d'injures par g++ semblables à :

zazou@ZazouMobile:~/devel_c++/linuxmag_cpp_10$ g++ `pkg-config gtkmm-2.0 --cflags --libs` -o gtkmm_6 gtkmm_6.cpp
gtkmm_6.cpp: Dans constructor « maFenetre::maFenetre() »:
gtkmm_6.cpp:28: error: le C++ ISO interdit de prendre l'adress d'un membre de
   fonction non statique non qualifié pour former un pointeur d'un membre de
   fonction. Disons «&maFenetre::on_button_clicked»

Pour me faire ainsi insulter, j'avais écrit :
b.signal_clicked().connect(SigC::slot(*this, &on_button_clicked));

Boucle principale

int main(int argc, char **argv)
{
    Gtk::Main app(argc, argv);
    maFenetre w;

    app.run(w);
    return 0;
}

Maintenant que nous avons fini par écrire le main, force est de constater qu'ainsi organisé, le code est plus clair et plus logique. L'objet graphique que vous avez devant vos yeux ébahis par tant de beauté lors de l'exécution correspond à un objet logique dans votre programme. Ce dernier possède les objets qui le constituent et les gestionnaires de signaux qu'il est susceptible d'intercepter (et pouvant nous intéresser, il intercepte par aileurs une foule de trucs inutiles ...).

Décoration

Pour finir, nous llons utiliser quelques méthodes pratiques de la classe Gtk::Window permettant d'affecter un icône à notre application ou de lui donner un titre plus sympa que le nom de l'exécutable lancé. Il existe bien entendu d'autres méthodes utilitaires que je vous laisse le soin de découvrir par vous mêmes.

set_title donne un titre à la fenêtre, son paramètre est une chaîne de caractères.

set_icon affecte un icône à la fenêtre. Selon les gestionnaires de fenêtres, ce dernier est affiché dans la barre d'applications (barre des tâches) et/ou dans la barre de titre de l'application. C'est également lui qui est utilisé lorsque vous minimiser votre fenêtre.
Cette méthode prend en paramètre un const Glib::RefPtr<Gdk::Pixbuf>&... Ce qui explique qu'ici, on péfère utilise plutôt une version modifiée, set_icon_from_file qui prend comme paramètre, de type const std::string&, le chemin vers un fichier image.

Le code de l'application terminée (en un seul fichier pour plus de lisibilité) :

#include <gtkmm/main.h>
#include <gtkmm/window.h>
#include <gtkmm/box.h>
#include <gtkmm/label.h>
#include <gtkmm/entry.h>
#include <gtkmm/button.h>

class maFenetre : public Gtk::Window
{
protected:
    Gtk::VBox vb;
    Gtk::Label l;
    Gtk::Entry e;
    Gtk::Button b;

    void on_button_clicked();
public:
	maFenetre();
	virtual ~maFenetre();
};

maFenetre::maFenetre() : b("Bonjour !")
{
    set_title("Bonjour toi !");
    set_icon_from_file("hi.png");
    vb.pack_start(l);
    l.show();
    vb.pack_start(e);
    e.show();
    b.signal_clicked().connect(SigC::slot(*this, &maFenetre::on_button_clicked));
    vb.pack_start(b);
    b.show();

    add(vb);
    vb.show();
}

maFenetre::~maFenetre()
{
}

void maFenetre::on_button_clicked()
{
	l.set_text("Bonjour " + e.get_text() + " !");
}

int main(int argc, char **argv)
{
    Gtk::Main app(argc, argv);
    maFenetre w;

    app.run(w);
    return 0;
}

Un screenshot de cette première application en c++/gtkmm :

gtkmm scshot

Conclusion

Je vous ai, dans cet article, sommairement présenté gtkmm. Je vous laisse creuser le sujet par vous mêmes en utilisant l'excellente (et généreuse) documentation présente sur Internet. Il est effectivement important de signaler que l'on n'a utilisé ici que les bases de ce que nous pouvons faire avec Gtk, pour donner une idée des fonctionnalités uniquement. Vous devrez fouiller pour trouver comment agencer plus joliment les widgets, ajouter des icônes, ...


Naturellement, vous remarquerez qu'il existe d'autres librairies finissant par mm pour avoir le support gnome, la possibilité de créer des composants bonobo et d'interagir avec ces derniers, etc ... Toutes ces librairies sont autant de sujets à explorer.

Vous pouvez également si le coeur vous en dit, utiliser le framework Bakery, permettant de faciliter le développement d'applications pour gnome en c++, incluant si besoin le support du modèle document/vue, utiliser glade pour dessiner vos interfaces utilisateur, éventuellement utiliser libglademm pour les charger lors de l'exécution plutôt qu'elles soient figées dans le code... Bref, j'espère vous avoir donné envie d'aller voir ce qui existe d'autre que Qt. Pas forcément pour quitter ce dernier mais pour être conscient qu'il existe des alternatives dans le domaine des interfaces en c++, comme toujours dans le monde GNU/Linux.

Je rebondis d'ailleurs sur cette remarque pour vous informer que cet article sera suivi d'autres, du même genre, présentant toujours la même application mais en utilisant des couples langage / kit graphique différents. Cette série s'attardera notamment sur les couples inhabituels, tel que c++/gtkmm aujourd'hui et à venir Ruby/Qt ou encore Perl/SDL. Vous retrouverez d'ailleurs déjà, dans ce même magazine, le couple tcl/tk.

@+

Xavier Garreau - <xavier@xgarreau.org>
http://www.xgarreau.org/

Ingénieur de recherche PRIM'TIME TECHNOLOGY
http://www.prim-time.fr/

Membre fondateur du ROCHELUG
http://www.rochelug.org/

Liens & Références :

a+

Auteur : Xavier GARREAU
Modifié le 10.09.2004

Rechercher :

Google
 
Web www.xgarreau.org