[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.
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 :
QWidget
, justement, le widget générique ancêtre
de tous les autres ;QFrame
, simple dérivation de QWidget
, offrant
quelques possibilités supplémetaires d'affichage comme le dessin d'un
cadre (QLabel
dérive d'ailleurs de QFrame
, et
frame signifie justement cadre en anglais) ;QScrollView
, dérivé de QFrame
, propose un
espace dont seule une partie est affichée, avec des barres de défilement
en cas de besoin.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 :
Explications :
#includes
pour utiliser les objets par la suite ;resize()
de la classe QWidget
permet de donner
les dimensions d'un widget, largeur et hauteur en pixels ;
cette méthode est donc disponible pour toutes les classes dérivant de
QWidget
, donc pour tous les widgets de
Qt ;frame1
est un widget enfant de
w_princ
;move()
(qui vient de QWidget
) permet de
positionner un widget relativement à son widget parent
(en pixels, l'origine étant le coin en haut à gauche) ;QFrame::setFrameStyle(
) est utilisée pour indiquer le style
de cadre que l'on veut (par défaut, aucun cadre n'est dessiné). Il existe
de nombreuses combinaisons, consultez
$QTDIR/doc/html/qframe.html
pour voir des exemples ;
remarquez qu'on utilise aussi cette méthode sur un label, ligne 27
(normal, QLabel
hérite de QFrame
) ;QFrame
est
un QWidget
dérivé...)QWidget
possède les méthodes width()
et
height()
, qui donnent respectivement la largeur et la
hauteur du widget ; ici, je donne simplement la largeur du
widget - pardon, du label, tout en conservant sa hauteur
par défaut.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.
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 :
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 :
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.
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 (®le, 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 :
Voici les détails :
QSlider
permet à l'utilisateur de choisir une valeur
entière entre deux bornes (les deux premiers paramètres), le curseur se
déplaçant de la valeur donnée en troisième paramètre si l'utilisateur
clique dans la zone grise, et la valeur initiale étant le quatrième
paramètre. Remarquez que nous avons utilisé la méthode
setGeometry()
, héritée de QWidget
, qui permet
de donner en une seule méthode la position et les dimensions du
widget ;QLCDNumber
est l'un des
plus anciens widgets de Qt !). Le premier paramètre est le
nombre de chiffres qu'il devra afficher. Comme nous voulons qu'il affiche
la valeur courante de la glissière, et que celle-ci peut aller jusqu'à
100, nous avons besoin de 3 chiffres. Ce widget est assez pratique ;
il est également capable d'afficher des entiers en base binaire, octale
ou hexadécimale, des nombres décimaux, et même du texte (mais limité à
certains caractères, comme toute montre à affichage
digital...) ;valueChanged(int)
, le paramètre étant la nouvelle valeur.
Justement, l'afficheur possède un slot
display(int)
, destiné à préciser le nombre entier à
afficher. S'il est possible d'utiliser cette méthode comme n'importe
quelle autre (ligne 17), nous l'utilisons comme cible du signal de la
glissière. De cette manière, tout mouvement du curseur apparaît
immédiatement dans l'afficheur.Le reste du code est du déjà vu.
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 :
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 :
QObject
QApplication
QWidget
QFrame
QLabel
QLCDNumber
QButton
QPushButton
QSlider
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.