reseau social

Découverte QT 3, 2ème partie

Un article de ToutProgrammer.com.

Dans le première article, Hervé Lefebvre nous proposait une présentation des nouvelles fonctionnalités de QT 3 et ses apports par rapport à la version précédente du framework. Dans ce nouvel article, nous allons découvrir le principe de fonctionnement de QT avec les slots et les signaux.

Après avoir rapidement survolé dans l'article précédent (Découverte QT 3, 1ère partie) les nouveautés majeures de la librairie graphique QT-3, vous vous attendiez probablement à ce que j'en détaille désormais exhaustivement le contenu. Perdu, ce sera pour une prochaine fois. Je vais vous proposer dans cet article de découvrir le concept général de Qt, en quoi ce formidable outil est différent des autres, comment développer avec, etc.

Tout d'abord, il faut bien comprendre que Qt n'est pas une simple librairie. Bien sûr, une large collection d'objets est fournie par ce toolkit, mais ce n'est là qu'un aspect des choses. Qt c'est aussi un modèle qui permet au programmeur de manipuler des méta-objets.

Voici tout d'abord à quoi ressemble une application minimale en Qt:

Hello world

pour l'instant aucune surprise pour ceux qui sont habitués à programmer sous un environnement graphique. On a un objet application (nommé "a"), un widget (nommé "hello"), le widget est affecté à l'application, puis l'application exécutée.

  1. #include <qapplication.h>
  2. #include <qpushbutton.h>
  3. int main( int argc, char **argv )
  4. {
  5. QApplication a( argc, argv );
  6.  
  7. QPushButton hello( "Hello LinuxFrench.net !", 0 );
  8. hello.resize( 300, 30 );
  9.  
  10. a.setMainWidget( &hello );
  11. hello.show();
  12. return a.exec();
  13. }

[modifier] Des appels par signaux et slots

Si je désire provoquer la fin de l'application lorsque l'utilisateur clique sur ce bouton, rien n'est plus simple avec un mécanisme de signal et de slot. En effet, Qt propose un système d'appels entre objets original. Chaque objet Qt peut émettre des signaux, et peut être doté de slots. Le lien entre un signal et un slot se fait à l'aide de la simple instruction connect.

Par exemple, l'objet standard QButton est doté d'un signal nommé clicked(), et qui est déclenché lorsque l'utilisateur clique sur le bouton (soit à l'aide de la souris, soit en le validant au clavier, peu importe). De même, la fonction quit() de l'objet standard QApplication est en fait un slot. Il nous suffit donc d'ajouter juste avant return a.exec() la ligne de code suivante:

QObject::connect( &hello, SIGNAL(clicked()), &a, SLOT(quit()) );

Ainsi, lorsque l'utilisateur cliquera sur le bouton, l'application prendra fin.

Il est bien évidemment possible de définir ses propres signaux, et ses propres slots. Ainsi, si nous désirons ajouter un second bouton, et faire en sorte que lorsque l'on clique sur le premier bouton, le texte sur le second bouton soit modifié ; nous allons pour cela créer un nouvel objet, dérivé du QPushButton. Voici à quoi ressemblera le header de déclaration de cette nouvelle classe:

  1. #include <qpushbutton.h>
  2.  
  3. class mybutton : public QPushButton
  4. {
  5. Q_OBJECT
  6. public:
  7. mybutton(const QString & text, QWidget * parent);
  8. ~mybutton();
  9.  
  10. public slots:
  11. void slotChangerLeTexte();
  12.  
  13. };

Ainsi que vous le remarquez, les clauses Q_OBJECT et slots: ne sont pas habituelles. Ce sont en fait d'une part des macros qui seront donc insérées dans le .h par le préprocesseur, mais également des clauses qui seront interprétées par le compilateur de méta-objets.

Regardons de plus près, nous déclarons donc une nouvelle classe héritée de QPushButton, rien de nouveau de ce côté là. La clause Q_OBJECT indique que je suis en train de définir un méta-objet Qt, ensuite, je déclare un slot d'accès public, nommé "slotChangerLeTexte" et qui n'admet pas de paramètre.

L'implémentation de la classe va, très classiquement, ressembler à la chose suivante:

  1. #include "mybutton.h"
  2.  
  3. mybutton::mybutton(const QString & text, QWidget * parent)&nbsp;: QPushButton(text,parent)
  4. {
  5. }
  6. mybutton::~mybutton()
  7. {
  8. }
  9.  
  10. void mybutton::slotChangerLeTexte()
  11. {
  12. if (text() == QString("Hello LinuxFrench.net&nbsp;!"))
  13. setText(QString("Je n'échange pas mon baril de Qt&nbsp;!"));
  14. else setText( QString("Hello LinuxFrench.net&nbsp;!") );
  15. }

Voyons désormais notre programme principal qui a un peu évolué:

  1. #include <qapplication.h>
  2. #include <qpushbutton.h>
  3. #include "mybutton.h"
  4. #include <qvbox.h>
  5.  
  6. int main( int argc, char **argv )
  7. {
  8. QApplication a( argc, argv );
  9. QVBox box; // box est maintenant le widget principal
  10. box.resize(300,200); // et les 2 boutons sont placés sur box
  11. QPushButton hello( "Hello LinuxFrench.net !",&box );
  12. mybutton monbouton( "Mon bouton à moi",&box );
  13.  
  14. a.setMainWidget( &box );
  15. // Quand on clique sur le bouton hello, on change le texte de monbouton
  16. QObject::connect( &hello, SIGNAL(clicked()), &monbouton, SLOT(slotChangerLeTexte()) );
  17.  
  18. // Quand on clique sur monbouton on ferme l'application
  19. QObject::connect( &monbouton, SIGNAL(clicked()), &a, SLOT(quit()) );
  20. box.show();
  21. return a.exec();
  22. }

Pour compiler notre programme, il faut bien évidemment générer des .o à partir de "hello.cpp" et "mybutton.cpp", mais il faut également utiliser le compilateur de méta-objets qui sera appliqué sur mybutton.h , et qui va générer un nouveau fichier .cpp qui devra à son tour être compilé. Voici les étapes:

# d'abord générer le source du méta-objet

moc -o mybutton.moc.cpp mybutton.h

#compiler le source du méta-objet généré

g++ -I /home/qt/qt-2.3.1/include/ -c -o mybutton.moc.o mybutton.moc.cpp

#compiler les autres .cpp

g++ -I /home/qt/qt-2.3.1/include/ -c -o mybutton.o mybutton.cpp

g++ -I /home/qt/qt-2.3.1/include/ -c -o hello.o hello.cpp

#linker

g++ -I /home/qt/qt-2.3.1/include/ -L /usr/lib/qt2/lib -lqt -o hello hello.o mybutton.o mybutton.moc.o

Voici maintenant à quoi ressemble notre nouvelle application lorsqu'on l'exécute. On remarque tout d'abord que notre fenêtre contient une box de 300x200 pixels, et que nos deux boutons ont été placés dessus lorsque "box" a été fourni en paramètre du constructeur de ces boutons.

2 boutons

Après avoir cliqué une première fois sur le bouton du haut, le slot changerLeTexte() a été appelé, et donc le bouton du bas prend pour libellé "Hello LinuxFrench.net". Après un second clic sur le bouton du haut, le libellé change à nouveau.

2 boutons

De nouveaux clics sur le bouton du haut recommenceraient le cycle "Hello.../ Je n'échange pas...".

2 boutons

Ce mécanisme de signaux et slot est particulièrement habile. En effet intuitivement on voit immédiatement le parti que l'on peut en tirer pour gérer une interface graphique (les actions de l'utilisateur génèrent des signaux, les traitements appropriés sont effectués par des slots). Mais le champ d'application est bien plus vaste. En effet, dans une application, on a toujours besoin d'effectuer des appels à tel ou tel objet. Habituellement pour effectuer ces appels, on fournit à l'objet appelant un pointeur sur l(es)'objet(s) qu'il est susceptible d'appeler, l'objet garde ces pointeurs en mémoire, et lorsqu'il en a besoin invoque une méthode de l'objet désigné par ce pointeur... en espérant que ledit objet soit toujours instancié en mémoire !

Avec les signaux, pas de problème. Un simple "connect" crée un lien entre 2 objets, plus de problème de résolution d'appel ! Lorsqu'un objet émet un signal, il ne se soucie pas de savoir si un (ou plusieurs) objets sont connectés à ce signal. S'il y en a plusieurs, chaque objet effectuera le traitement qui lui est affecté (à moins qu'un mécanisme de blocage de signal ne soit volontairement implémenté), s'il n'y en a aucun, le signal ne sera pas traité, ce n'est pas un problème. Ainsi, l'objet qui émet le signal n'a pas besoin de connaître quoique ce soit sur les objets qui traiteront éventuellement ces signaux. Et il suffit d'une seule ligne de code pour résoudre l'appel : connect (), et une autre pour lancer un signal : emit() !

Il est particulièrement appréciable d'avoir un tel mécanisme d'appel aussi frugal en lignes de codes. En effet, plus une application devient complexe, plus le nombre d'objets qu'elle contient augmente, et le nombre d'appels inter-objets a tendance à croître exponentiellement par rapport au nombre d'objets. Cela veut dire qu'avec un mécanisme aussi léger que les signaux/slots, plus l'application devient complexe, plus le pourcentage de lignes de codes économisées deviendra important ! Ceci est peut-être un début d'explication quant au rythme de plus en plus effréné de sorties de nouvelles versions de KDE.

[modifier] Encore ?

Il y aurait beaucoup d'autres choses à vous faire découvrir, tel que par exemple la gestion de mémoire qui parfois ressemble un peu à celle de Java (les désallocations sont effectuées automatiquement par Qt). J'espère que cet article vous en aura fait découvrir suffisamment pour avoir envie d'aller plus loin :-)

Dans le prochain article, nous reviendrons plus spécifiquement sur les nouveautés de QT 3.

[modifier] Historique de l'article

Cet article, réalisé par Hervé LEFEBVRE, a été publié pour la première fois le 31 juillet 2001 sur le site LinuxFrench.net sous licence Creative Commons License Non Commerciale.