[article paru dans LinuxMagazine n° 37 de février 2002, légèrement adapté]

Des signaux et des slots

Voici le deuxième article consacré à la librairie graphique Qt. Dans le précédent nous avons installé la librairie et testé un petit exemple, ce mois-ci nous allons découvrir de nouveau widgets ainsi que le mécanisme utilisé par Qt pour réagir aux évènements provoqués par l'utilisateur.

Widgets parents et enfants

D'abord un petit mot sur l'organisation d'une interface graphique. D'une manière générale, les widgets sont hiérarchiquement inclus les uns dans les autres, ce qui offre plusieurs avantages :

Ce genre d'organisation se rencontre assez fréquemment, on la trouve par exemple dans Gtk+. Naturellement, tous les widgets ne sont pas vraiment destinés à en contenir d'autres : le QLabel que nous avons rencontré le mois dernier n'est, par exemple, pas vraiment adapté. Certains sont par contre vraiment prévus pour jouer ce rôle de « contenant ». Les plus utilisés sont :

De nombreux autres existent, nous les rencontrerons par la suite.

Lorsque l'on créé un widget, on indique dans quel widget parent il est contenu en donnant l'adresse de celui-ci au constructeur. On donne 0 s'il n'est contenu dans aucun autre, ce qui est notamment le cas duwidget principal, appellé aussi top-level widget).

Considérons un exemple : un widget qui contient deux frames, avec un label dans la première et deux labels dans la deuxième (code) :

01: #include <qapplication.h>
02: #include <qwidget.h>
03: #include <qframe.h>
04: #include <qlabel.h>
05:
06: int main (int argc, char* argv[])
07: { QApplication app (argc, argv) ;
08:   QWidget w_princ ;
09:   w_princ.resize(220, 100) ;
10:   QFrame frame1(&w_princ) ;
11:   frame1.resize(80, 50) ;
12:   frame1.move(4, 4) ;
13:   frame1.setFrameStyle(QFrame::Box | QFrame::Sunken) ;
14:   frame1.setMidLineWidth(2) ;
15:   QFrame frame2(&w_princ) ;
16:   frame2.resize(120, 90) ;
17:   frame2.move(90, 4) ;
18:   frame2.setFrameStyle(QFrame::Panel | QFrame::Raised) ;
19:   frame2.setLineWidth(2) ;
20:   QLabel label1 ("Label 1", &frame1) ;
21:   label1.move(6, 6) ;
22:   label1.resize(60, label1.height()) ;
23:   QLabel label2a ("Label 2 - a", &frame2) ;
24:   label2a.move(6, 6) ;
25:   QLabel label2b ("Label 2 - b", &frame2) ;
26:   label2b.move(6, 40) ;
27:   label2b.setFrameStyle(QFrame::WinPanel | QFrame::Raised) ;
28:
29:   app.setMainWidget(&w_princ) ;
30:   w_princ.show() ;
31:   return app.exec() ;
32: }

Compilez, exécutez, vous obtenez ceci :

 frames

Explications :

Si vous changez les valeurs dans les méthodes move() appliquées aux frames, vous constaterez qu'elles se déplacent avec leur contenu. On a donc bien défini une hiérarchie des objets, certains étant placés dans d'autres. Expérimentez aussi en donnant des largeurs aux diverses lignes des frames, pour voir les possibilités offertes.

Encore une fois, la hiérarchie que nous avons définie ici est surtout visuelle.

Un bouton pour quitter

Jusqu'à présent, nous devions utiliser le moyen fourni par le gestionnaire de fenêtres pour quitter l'application - en général, un petit X à une extrémité de la barre d'outils. Mais ce serait tout de même mieux si nous pouvions fournir un moyen « élégant » de faire cela...

Voici un nouveau petit programme, qui contient simplement un bouton, qui termine le programme quand on clique dessus (code) :

01: #include <qapplication.h>
02: #include <qpushbutton.h>
03:
04: int main (int argc, char* argv[])
05: {
06:     QApplication app(argc, argv) ;
07:     QPushButton bouton ("Quitter", &w_princ) ;
08:     bouton.connect (&bouton, SIGNAL(clicked()),
09:                     &app, SLOT(quit())) ;
10:     app.setMainWidget(&bouton) ;
11:     bouton.show() ;
12:     return app.exec() ;
13: }

Le résultat de ce programme est très simple :

quitter

Si vous cliquez sur le bouton, le programme se termine. Comment cela se passe-t-il ?

D'abord, voyez la ligne 7, la déclaration d'un objet QPushButton (défini dans le #include en ligne 2). Il s'agit simplement d'une classe de boutons cliquables, qui dérive de la classe plus générale QButton, elle-même dérivant de QWidget. Celle-ci donne également les boutons radios, cases à cocher, ou les éléments d'une barre d'outils. Ici, comme le programme se limite à un bouton, nous allons l'utiliser comme widgetprincipal (ligne 10), c'est pourquoi le bouton est créé en indiquant 0 comme widget parent.

Le plus intéressant est la ligne 8, qui se continue ligne 9.

Pour Qt, les divers évènements qui peuvent survenir (cliquer sur un bouton, choisir dans une liste, dérouler un menu...) sont des signaux. La plupart des widgets sont capables d'émettre des signaux pour signaler la survenu d'évènements, par exemple ici QPushButton émet le signal clicked() lorsqu'on active le bouton, que cela soit par la souris ou par le clavier. Il s'agit d'un signal sans paramètres, un peu comme un signal système envoyé par la commande kill.

Pour répondre aux signaux, de nombreuses classes possèdent des méthodes spéciales, appelées slots dans le jargon Qt (le verbe anglais to slot signifit emboîter). Pour réagir à un évènement, il suffit donc de connecter le signal correspondant à un slot approprié. On peut mettre ce mécanisme en parallèle de celui des fonctions callback, que l'on trouve par exemple dans Gtk+.

Justement, la classe QApplication contient un slot (sans paramètres) nommé quit(), dont le rôle est précisément de terminer le programme - en fait, de sortir de la boucle interne lancée par l'appel à QApplication::exec() (ligne 12).

Les classes QApplication et QWidget (entre autres...) dérivent d'une même classe de base QObject. Celle-ci contient une méthode statique connect(), destinée à réaliser la connexion entre un signal et un slot. Son prototype ressemble à ceci :

bool QObject::connect (const QObject * émetteur, const char* signal, const QObject * récepteur, const char* slot)

Elle retourne true si la connexion a réussie, false sinon. La connexion peut échouer si le signal ou le slot indiqué n'existe pas, ou bien s'ils ne sont pas compatibles, c'est-à-dire n'ont pas la même signature. QObject étant une classe de base pour la plupart des classes Qt, émetteur et récepteur sont des pointeurs très génériques : dans l'exemple, on donne simplement les adresses de l'objet bouton et de l'objet application.

Le signal et le slot sont donnés par des chaînes de caractères. Mais les développeurs de TrollTech ont estimés qu'il était un peu pénible de manipuler ces chaînes, surtout lorsque l'on défini ses signaux et ses slots personnalisés. Aussi existe-il deux macros, SIGNAL et SLOT, qui prennent en argument un prototype de méthode et fournissent la chaîne de caractères correspondante.

Il est intéressant de noter que cette méthode étant statique, et venant de QObject qui fait partie des ancêtres de QPushButton, nous aurions obtenu exactement le même résultat en utilisant :

bouton.connect (&bouton, SIGNAL(clicked()), &app, SLOT(quit())) ;

Précisons dès maintenant qu'il est possible de connecter plusieurs signaux à un même slot, et plusieurs slots à un même signal. On peut donc avoir un schéma de connexions assez complexe, dans le genre de ceci :

sig_slots

Il faut simplement garder à l'esprit que l'ordre d'exécution des slots est parfaitement imprévisible. En particuliers, ce n'est pas forcément l'ordre dans lequel ils ont été connectés.

Pour être complet, il existe également une méthode disconnect() prenant les mêmes paramètres, qui permet comme son nom l'indique de déconnecter un signal d'un slot. Par ailleurs des versions surdéfinies de connect() et disconnect() existent, que nous rencontrerons plus tard.

Règle et afficheur LCD

Voici un autre exemple de mise en oeuvre des signaux et des slots, cette fois avec un paramètre. Nous allons utiliser pour cela deux widgets fréquemment utilisés ensembles. Voyez (code) :

01: #include <qapplication.h>
02: #include <qframe.h>
03: #include <qpushbutton.h>
04: #include <qslider.h>
05: #include <qlcdnumber.h>
06:
07: int main (int argc, char* argv[])
08: { QApplication app(argc, argv) ;
09:   QFrame w_princ(0) ;
10:   QSlider regle(10, 100, 10, 20, Qt::Horizontal, &w_princ) ;
11:   regle.setGeometry(4, 4, 152, 24) ;
12:   regle.setTickmarks(QSlider::Both) ;
13:   QLCDNumber lcd(3, &w_princ) ;
14:   lcd.setGeometry(160, 4, 36, 28) ;
15:   regle.connect (&regle, SIGNAL(valueChanged(int)),
16:                  &lcd,   SLOT(display(int))) ;
17:   lcd.display(20) ;
18:   QPushButton bouton ("Quitter", &w_princ) ;
19:   bouton.setGeometry(4, 36, 192, bouton.height()) ;
20:   bouton.connect (&bouton, SIGNAL(clicked()),
21:                   &app, SLOT(quit())) ;
22:   w_princ.resize(200, 36+bouton.height()+4) ;
23:   app.setMainWidget(&w_princ) ;
24:   w_princ.show() ;
25:   return app.exec() ;
26: }

Ce qui nous donne ceci à l'exécution :

slider_lcd

Voici les détails :

Le reste du code est du déjà vu.

Conclusion

Le mécanisme des signaux et des slots définis par Qt, s'il ne présente pas toute la souplesse de celui des callbacks de Gtk+, ni l'élégance de celui de la librairie libsigc++, est l'un des plus simples à mettre en oeuvre (nonobstant une petite difficulté que nous verrons plus tard).

Les deux reproches qui lui sont essentiellement faits sont :

  1. signaux et slots doivent être compatibles pour être connectés, c'est-à-dire présenter les mêmes prototypes (mêmes types de paramètres dans le même ordre) ; personnellement j'estime qu'il s'agit plutôt là d'un bienfait, l'idéal serait même d'être prévenu à la compilation si on tente de connecter des méthodes incompatibles ;
  2. pour l'instant, il est impossible pour une classe générique (template) d'émettre ou de recevoir des signaux, ce qui est parfois bien gênant pour écrire du code réutilisable ; aux dernières nouvelles, les développeurs de TrollTech n'ont pas encore trouvé de solution satisfaisante à ce problème, la cause principale étant de devoir supporter des compilateurs ne respectant la norme C++ que de parfois fort loin..

Dans la pratique, ces contraintes posent rarement des problèmes insolubles. D'un certain point de vue, on peut considérer que la première limitation n'est pas si grave, car elle pousse à une structure plus claire. La deuxième est plus problématique, et cela fait longtemps que nombreux sont ceux qui réclament des progrès de ce côté...

Pour nous résumer, nous avons jusqu'à présent découvert cette hiérarchie objet :

La prochaine fois, nous verrons comment définir des slots et des signaux à nous en créant nos propres widgets, d'autres widgets fournis par Qt, et surtout comment éviter les laborieuses séries de resize/move pour organiser notre interface graphique.