Developpez.com - Qt
X

Choisissez d'abord la catégorieensuite la rubrique :

Viadeo Twitter Facebook Share on Google+   
Logo Documentation Qt ·  Page d'accueil  ·  Toutes les classes  ·  Toutes les fonctions  ·  Vues d'ensemble  · 

Signaux et slots

Les signaux et les slots sont utilisés pour la communication entre objets. Ce mécanisme signaux/slots est une fonctionnalité centrale de Qt, et probablement celle qui diffère le plus des fonctionnalités fournies par les autres frameworks.

Introduction

Dans la programmation des interfaces graphiques, lorsqu'on change un widget, nous voulons souvent qu'un autre widget en soit informé. De manière générale, nous souhaitons que les objets de tout type soient capables de communiquer entre eux. Par exemple, si l'utilisateur clique sur le bouton de fermeture, nous voulons probablement appeler la fonction de fermeture de la fenêtre (close()).

Les frameworks plus anciens réalisent ce type de communication en utilisant des fonctions de rappel (callbacks). Ce sont en fait des pointeurs sur une fonction, donc si vous voulez qu'une fonction de traitement vous avertisse lors de certains événements, vous lui passez un pointeur sur une autre fonction (fonction de rappel). Notre fonction de traitement va alors appeler notre fonction de rappel lorsque ce sera nécessaire. Les fonctions de rappel ont deux défauts fondamentaux : premièrement, elles n'ont pas de mécanisme de sûreté du typage ; nous ne pouvons jamais être certains que la fonction de traitement va appeler la fonction de rappel avec des arguments corrects. Deuxièmement, la fonction de rappel est fortement couplée à la fonction de traitement étant donné que cette dernière doit savoir quelle fonction de rappel appeler.

Signaux et slots

Dans Qt, nous avons une technique alternative aux fonctions de rappels : nous utilisons des signaux et des slots. Un signal est émis lorsqu'un événement particulier se produit. Les widgets de Qt possèdent de nombreux signaux prédéfinis mais vous pouvez aussi hériter de ces classes et leur ajouter vos propres signaux. Un slot est une fonction qui va être appelée en réponse à un signal particulier. De même, les widgets de Qt possèdent de nombreux signaux slots prédéfinis, mais il est très courant d'hériter de ces widgets et de créer ses propres slots afin de gérer les signaux qui vous intéressent.

image

Le mécanisme des signaux et slots fournit un contrôle des types : la signature d'un signal doit correspondre à la signature du slot récepteur (en réalité, un slot peut avoir une signature plus courte que celle du signal qu'il reçoit car il peut ignorer les arguments en trop). Étant donné que les signatures doivent être compatibles, le compilateur peut nous aider à détecter les erreurs de correspondance de types. Les signaux et les slots sont faiblement couplés : une classe qui émet un signal ne sait pas (et ne se soucie pas de) quels slots vont recevoir ce signal. C'est le mécanisme signaux/slots qui va garantir que, si vous connectez un signal à un slot, ce slot sera appelé avec les paramètres du signal en temps voulu. Les signaux et les slots peuvent prendre n'importe quel nombre d'arguments de n'importe quel type. Ils assurent un contrôle complet des types.

Toutes les classes qui héritent de QObject ou d'une de ses sous-classes (par exemple, QWidget) peuvent contenir des signaux et des slots. Les signaux sont émis par les objets lorsqu'ils changent d'état, et que ce changement peut être intéressant pour d'autres objets. C'est tout ce que font ces objets pour communiquer. Ils ne savent pas qui reçoit les signaux qu'ils envoient, et ne s'y s'intéressent pas. Ceci représente une véritable encapsulation de l'information et permet d'utiliser les objets comme des composants logiciels.

Les slots peuvent être utilisés pour recevoir des signaux, mais ce sont aussi des méthodes normales. De la même manière qu'un objet ne sait pas qui reçoit ses signaux, un slot ne sait pas si des signaux lui sont connectés. Cela permet la création de composants véritablement indépendants avec Qt.

Vous pouvez connecter autant de signaux que vous voulez au même slot, et un signal peut être connecté à autant de slots que nécessaire. Il est même possible de connecter un signal directement à un autre signal (le second signal sera émis immédiatement après le premier).

Ensemble, les signaux et les slots forment un puissant mécanisme de programmation de composants.

Un court exemple

La déclaration d'une classe C++ minimale pourrait être :

 class Counter
 {
 public:
     Counter() { m_value = 0; }
 
     int value() const { return m_value; }
     void setValue(int value);
 
 private:
     int m_value;
 };

Une petite classe basée sur QObject pourrait s'écrire :

 #include <QObject>
 
 class Counter : public QObject
 {
     Q_OBJECT
 
 public:
     Counter() { m_value = 0; }
 
     int value() const { return m_value; }
 
 public slots:
     void setValue(int value);
 
 signals:
     void valueChanged(int newValeur);
 
 private:
     int m_value;
 };

La version basée sur QObject possède le même état interne et fournit des méthodes publiques pour accéder à cet état, mais elle gère en plus la programmation de composants en utilisant le mécanisme des signaux et des slots. Cette classe peut avertir le monde extérieur que son état a changé en émettant un signal valueChanged() et elle possède un slot auquel d'autres objets vont pouvoir envoyer des signaux.

Toutes les classes qui contiennent des signaux ou des slots doivent mentionner Q_OBJECT au début de leur déclaration. Elles doivent aussi hériter (directement ou indirectement) de QObject.

Les slots sont implémentés par le programmeur de l'application. Voici ci-dessous une implémentation possible du slot Counter::setValue() :

 void Counter::setValue(int value)
 {
     if (value != m_value) {
         m_value = value;
         emit valueChanged(value);
     }
 }

La ligne contenant emit émet le signal valueChanged() depuis l'objet, avec la nouvelle valeur comme argument.

Dans le fragment de code suivant, nous créons deux objets Counter et nous connectons le signal valueChanged() du premier objet au slot setValue() du second en utilisant QObject::connect() :

     Counter a, b;
     QObject::connect(&a, SIGNAL(valueChanged(int)),
                      &b, SLOT(setValue(int)));
 
     a.setValue(12);     // a.value() == 12, b.value() == 12
     b.setValue(48);     // a.value() == 12, b.value() == 48

Appeler a.setValue(12) a pour conséquence l'émission du signal valueChanged() depuis l'objet a, que l'objet b va recevoir via son slot setValue(), c'est-à-dire que b.setValue(12) est appelée. Ensuite, b émet à son tour le signal valueChanged(). Cependant, comme aucun slot n'a été connecté à ce signal, il sera ignoré.

Notez que la fonction setValue() ne modifie l'attribut et n'émet le signal que si value != m_value. Cela permet de prévenir une boucle infinie dans le cas de connexions cycliques (par exemple, si b.valueChanged() était connecté à a.setValue()).

Par défaut, pour chaque connexion que vous faites, un signal est émis ; deux signaux sont émis dans le cas de connexions dupliquées. Vous pouvez couper toutes ces connexions avec un unique appel à disconnect(). Si vous passez le drapeau Qt::UniqueConnection au type de connexion, celle-ci ne sera établie que si elle ne fait pas doublon. S'il existe déjà une telle connexion (exactement le même signal connecté au même slot dans le même objet), la connexion échouera et connect() va renvoyer false.

Cet exemple illustre le fait que les objets peuvent travailler ensemble sans nécessairement avoir d'informations les uns sur les autres. Pour cela, ils ont seulement besoin d'être connectés ensemble et cela peut être effectué avec un simple appel à la fonction QObject::connect() ou avec la fonctionnalité de connexion automatique de l› uic.

Compiler l'exemple

Le préprocesseur C++ change ou supprime les mots-clés signals, slots, et emit pour que le code présenté au compilateur corresponde au C++ standard.

En exécutant le moc sur les définitions des classes qui contiennent des signaux ou des slots, on produit un fichier source C++ qui doit être compilé puis lié au reste des fichiers de l'application. Si vous utilisez qmake, des règles qui vont automatiquement appeler moc vont être ajoutées dans le fichier makefile de votre projet.

Les signaux

Les signaux sont émis par un objet lorsque son état interne a changé d'une manière pouvant être intéressante pour les clients ou le propriétaire de l'objet. Seules les classes définissant un signal ainsi que leurs sous-classes peuvent émettre le signal.

Quand un signal est émis, les slots qui y sont connectés sont habituellement exécutés immédiatement, tout comme un appel normal à une fonction. Dans ce cas, le mécanisme des signaux et slots est totalement indépendant d'une quelconque boucle d'événement de l'interface graphique. L'exécution du code qui suit la déclaration emit se produira une fois que tous les slots auront été exécutés. La situation est légèrement différente lors de l'utilisation des connexions avec file d'attente ; dans un tel cas, le code après le mot-clé emit sera exécuté immédiatement et les slots seront exécutés plus tard.

Lorsqu'un signal est émis, si plusieurs slots lui sont connectés, ils seront exécutés l'un après l'autre, dans l'ordre dans lequel ils ont été connectés.

Les signaux sont automatiquement générés par le moc et ne doivent pas être implémentés dans le fichier .cpp. Ils ne peuvent jamais retourner de valeurs (c'est-à-dire qu'ils renvoient void).

Une note concernant les arguments : l'expérience a montré que les signaux et les slots sont plus facilement réutilisables s'ils n'utilisent pas de types spéciaux. Si le signal QSCrollBar::valueChanged() utilisait un type spécial, comme une hypothétique QSCrollBar::Range, il ne pourrait être connecté qu'aux slots conçus spécialement pour QSCrollBar. Connecter différents widgets de saisie ensemble serait impossible.

Les slots

Un slot est appelé quand un signal qui lui est connecté est émis. Les slots sont des fonctions C++ normales et peuvent être appelés normalement ; leur seule particularité est que des signaux peuvent leur être connectés.

Étant donné que les slots sont des fonctions normales, ils suivent les règles normales du C++ lorsqu'ils sont appelés directement. Toutefois, en tant que slots, ils peuvent être appelés par n'importe quel composant, quel que soit son niveau d'accès, via une connexion signal-slot. Cela signifie qu'un signal émis depuis une instance d'une classe quelconque peut causer l'exécution d'un slot privé dans une instance d'une classe sans relation avec cette classe.

Vous pouvez aussi définir des slots virtuels, une méthode que nous avons trouvée très utile dans la pratique.

Comparés aux fonctions de rappel, les signaux et les slots sont légèrement plus lents à cause de la plus grande flexibilité qu'ils fournissent, bien que la différence pour des applications réelles soit négligeable. De manière générale, émettre un signal qui est connecté à des slots est approximativement dix fois plus lent que d'appeler directement le récepteur avec des appels à des fonctions non virtuelles. Ceci est le surcoût requis pour localiser l'objet de connexion, pour itérer de manière sécurisée sur toutes les connexions (c'est-à-dire à vérifier que les récepteurs suivants n'ont pas été détruits durant l'émission du signal) et pour gérer les différents paramètres de manière générique. Bien que le surcoût de dix appels à des fonctions non virtuelles peut paraître énorme, il reste beaucoup moins important qu'une quelconque opération new ou delete par exemple. Dès que vous effectuez des opérations sur une chaîne de texte, un vecteur ou une liste, qui entraînent des new ou des delete, le surcoût des signaux et des slots ne représente qu'une très petite proportion du coût complet d'un appel à une fonction.

La même chose est vraie à chaque fois que vous faites un appel au système dans un slot, ou un appel indirect à plus de dix fonctions. Sur un i586-500, vous pouvez émettre autour de 2 000 000 de signaux par seconde, pour des signaux connectés à un seul récepteur, ou encore aux alentours de 1 200 000 par seconde, pour des signaux connectés à deux slots. La simplicité et la flexibilité du mécanisme de signaux et de slots valent largement le surcoût, que vos utilisateurs ne remarqueront probablement pas.

Notez que d'autres bibliothèques qui définissent des variables appelées signals ou slots peuvent causer des avertissements ou même des erreurs lorsqu'elles sont compilées avec une application basée sur Qt. Pour résoudre ce problème, faites un #undef sur le symbole de préprocesseur concerné.

Les informations du métaobjet

Le compilateur de métaobjets (moc) analyse la déclaration de la classe dans un fichier C++ et génère du code C++ qui va initialiser le métaobjet. Ce dernier contient le nom de tous les signaux et les slots membres de la classe, ainsi que des pointeurs vers ces fonctions.

Le métaobjet contient des informations additionnelles telles que le nom de la classe de l'objet. Vous pouvez aussi vérifier qu'un objet hérite d'une classe spécifique, par exemple :

     if (widget->inherits("QAbstractButton")) {
         QAbstractButton *button = static_cast<QAbstractButton *>(widget);
         button->toggle();
     }

Les informations du métaobjet sont aussi utilisées par qobject_cast<T>(), qui est similaire à QObject::inherits(), mais qui est moins sujet aux erreurs.

     if (QAbstractButton *button = qobject_cast<QAbstractButton *>(widget))
         button->toggle();

Voir le système de métaobjets pour plus d'informations.

Un exemple réel

Voici un exemple simple et commenté d'un widget.

 #ifndef LCDNUMBER_H
 #define LCDNUMBER_H
 
 #include <QFrame>
 
 class LcdNumber : public QFrame
 {
     Q_OBJECT

LcdNumber hérite de QObject, qui possède la plus grande partie des connaissances relatives au mécanisme signaux-slots, via QFrame et QWidget. Il est assez similaire au widget intégré QLCDNumber.

La macro Q_OBJECT est remplacée par le préprocesseur par des déclarations de plusieurs fonctions membres qui sont implémentées par le moc ; si vous obtenez des erreurs de compilation telles que « undefined reference to vtable for LcdNumber », vous avez probablement oublié d'exécuter le moc ou d'inclure la sortie du moc dans la commande d'édition de liens.

 public:
     LcdNumber(QWidget *parent = 0);

Cela n'a pas de rapport évident avec le moc, mais si vous héritez de QWidget, vous voudrez très probablement avoir l'argument parent dans votre constructeur et le passer au constructeur de la classe de base.

Certains destructeurs et fonctions membres sont omis ici ; le moc ignore les fonctions membres.

 signals:
     void overflow();

LcdNumber émet un signal lorsqu'il lui est demandé d'afficher une valeur impossible.

Si vous ne vous souciez pas d'un débordement ou que vous savez qu'un tel débordement ne peut avoir lieu, vous pouvez ignorer le signal overflow(), c'est-à-dire ne le connecter à aucun slot.

Si par contre vous souhaitez appeler deux fonctions d'erreurs différentes lorsque le nombre déborde, connectez simplement le signal à deux slots différents. Qt va les appeler tous les deux (dans l'ordre dans lequel ils ont été connectés).

 public slots:
     void display(int num);
     void display(double num);
     void display(const QString &str);
     void setHexMode();
     void setDecMode();
     void setOctMode();
     void setBinMode();
     void setSmallDecimalPoint(bool point);
 };
 
 #endif

Un slot est une fonction réceptrice utilisée pour obtenir des informations sur les changements d'état dans d'autres widgets. LcdNumber utilise ce procédé, comme l'indique le code ci-dessus, pour définir le nombre affiché. Étant donné que display() fait partie de l'interface de la classe avec le reste du programme, le slot est public.

Plusieurs des programmes d'exemple connectent le signal valueChanged() d'une QScrollBar à un slot display(), pour que le nombre LCD affiche continuellement la valeur de la barre de défilement.

Notez que display() est surchargée ; Qt va sélectionner la version appropriée lorsque vous connectez le signal au slot. Avec des fonctions de rappel, vous auriez dû trouver cinq noms différents et gérer les différents types d'argument vous-mêmes.

Certaines fonctions membres sans rapport avec le sujet ont été omises dans cet exemple.

Les signaux et slots avec des arguments par défaut

Les signatures des signaux et slots peuvent contenir des arguments et les arguments peuvent avoir des valeurs par défaut. Voyez QObject::destroyed() :

 void destroyed(QObject* = 0);

Lorsqu'un QObject est détruit, il émet ce signal QObject::destroyed(). Nous voulons recevoir ce signal dans tous les cas où nous avons une référence au QObject détruit, pour pouvoir la nettoyer. Une signature de slot appropriée pourrait être :

 void objectDestroyed(QObject* obj = 0);

Pour connecter le signal au slot, nous utilisons QObject::connect() et les macros SIGNAL() et SLOT(). La règle pour l'inclusion ou non d'arguments dans les macros SIGNAL() et SLOT(), si les arguments ont des valeurs par défaut, est que la signature passée à la macro SIGNAL() ne doit pas avoir moins d'arguments que la signature passée à la macro SLOT().

Tous ces exemples fonctionnent :

 connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
 connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
 connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

Mais celui-ci ne fonctionne pas :

 connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));

...parce que le slot attend un QObject que le signal n'enverra pas. Cette connexion déclenchera une erreur à l'exécution.

Utilisation avancée des signaux et slots

Pour les cas où vous avez besoin d'informations sur l'émetteur du signal, Qt fournit la fonction QObject::sender(), qui retourne un pointeur sur l'objet ayant envoyé le signal.

La classe QSignalMapper est fournie pour les situations où plusieurs signaux sont connectés au même slot et que le slot doit gérer chaque signal différemment.

Supposez que vous avez trois boutons qui déterminent quel fichier vous allez ouvrir : « Tax File », « Accounts File » ou « Report File ».

Pour ouvrir le fichier correct, vous utilisez QSignalMapper::setMapping() pour faire correspondre les signaux clicked() à un objet QSignalMapper. Ensuite vous connectez le signal QPushButton::clicked() au slot QSignalMapper::map().

     signalMapper = new QSignalMapper(this);
     signalMapper->setMapping(taxFileButton, QString("taxfile.txt"));
     signalMapper->setMapping(accountFileButton, QString("accountsfile.txt"));
     signalMapper->setMapping(reportFileButton, QString("reportfile.txt"));
 
     connect(taxFileButton, SIGNAL(clicked()),
         signalMapper, SLOT (map()));
     connect(accountFileButton, SIGNAL(clicked()),
         signalMapper, SLOT (map()));
     connect(reportFileButton, SIGNAL(clicked()),
         signalMapper, SLOT (map()));

Enfin, vous connectez le signal mapped() à readFile() où un des fichiers sera ouvert, en fonction du bouton qui aura été choisi.

     connect(signalMapper, SIGNAL(mapped(QString)),
         this, SLOT(readFile(QString)));

Note : le code suivant se compilera et s'exécutera, mais la normalisation de signature le rendra plus lent à l'exécution.

     // plus lent à cause de la normalisation de signature
 
     connect(signalMapper, SIGNAL(mapped(const QString &)),
         this, SLOT(readFile(const QString &)));

Utiliser Qt avec d'autres mécanismes signaux et slots

Il est possible d'utiliser Qt avec un mécanisme signaux/slots fourni par une bibliothèque tierce. Vous pouvez même utiliser les deux mécanismes dans le même projet. Pour cela, ajoutez la ligne suivante dans votre fichier projet qmake (.pro) :

 CONFIG += no_keywords

Ceci demande à Qt de ne pas définir les mots-clés signals, slots et emit du moc, parce que ces noms seront utilisés par une bibliothèque tierce, par exemple Boost. Pour continuer à utiliser les signaux et slots Qt avec l'option no_keywords, remplacez simplement toutes les utilisations des mots-clés du moc de Qt dans vos sources avec les macros Qt correspondantes Q_SIGNALS (ou Q_SIGNAL), Q_SLOTS (ou Q_SLOT) et Q_EMIT.

Voir aussi Système de métaobjets et Système de propriétés de Qt.

Remerciements

Merci à K?vin P?rais pour la traduction, ainsi qu'à Ilya Diallo, Dimitry Ernot et Claude Leloup pour la relecture !

Cette page est une traduction d'une page de la documentation de Qt, écrite par Nokia Corporation and/or its subsidiary(-ies). Les éventuels problèmes résultant d'une mauvaise traduction ne sont pas imputables à Nokia. Qt 4.7
Copyright © 2018 Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon, vous encourez selon la loi jusqu'à 3 ans de prison et jusqu'à 300 000 E de dommages et intérêts. Cette page est déposée à la SACD.
Vous avez déniché une erreur, une redirection cassée ou tout autre problème, quel qu'il soit ? Ou bien vous désirez participer à ce projet de traduction ? N'hésitez pas à nous contacter ou par MP !
Responsable bénévole de la rubrique Qt : Thibaut Cuvelier -