Les signaux et slots dans Qt5

La nouvelle version de Qt vient de sortir en version 5 alpha. Cette version améliore la prise en charge de la nouvelle norme du C++, le C++11, et modifie ainsi le fonctionnement des signaux et slots de Qt. Cet article fait un rappel sur l’utilisation des signaux et slots et présente les nouvelles fonctionnalités offertes par Qt5.

Couplage entre classes et intérêt des signaux et slots

Lorsque l’on souhaite faire communiquer des objets entre eux, il nécessaire en général que les objets se connaissent mutuellement. Par exemple

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Receiver {
public:
    void slot() { cout << "slot exécuté" <slot(); }
private:
    Receiver* receiver;
};
 
int main() {
    // on crée nos objets
    Receiver r;
    Sender s;
 
    // on connecte le sender et le receiver
    s.connect(&r);
 
    // on émet le signal
    s.signal();
}

Ce code présente cependant plusieurs problèmes :

  • il faut que la classe Sender connaisse la classe Receiver et cela implique d’ajouter une dépendance (include) entre ces classes ;
  • il est nécessaire de créer une fonction spécifique dans Sender pour chaque type de classe Receiver et pour chaque slot possible ;
  • il ne permet pas de gérer des connexions vers plusieurs objets Receiver et il faut modifier le code pour gérer une liste de Receiver.

Ces contraintes s’accumulent dans un framework complexe comme Qt et cela alourdit fortement le code en ajoutant un nombre important de dépendances inutiles. Le code devient très vite ingérable (*).

Le système des signaux et slots permet de faire de s’affranchir de ces contraintes. Il est ainsi possible de faire communiquer des objets entre eux sans qu’il soit nécessaire que ces objets se connaissent mutuellement. On peut également choisir lors de la connexion le slot que l’on souhaite appeler lorsque le signal est émis. On passe ainsi d’un couplage fort (nécessité d’avoir une dépendance) à un couplage faible (plus de dépendance nécessaire) et l’on parle de découplage des classes.

(*) Il est possible d’utiliser d’autres approches que celle présentée ici. En particulier, on peut utiliser les pointeurs de fonctions ou équivalents (callback). Le lecteur intéressé par la question pourra par exemple étudier l’approche utilisée dans Boost.Signals

Le système des signaux et slots dans Qt

Le système de signaux et slots de Qt est relativement simple. Lorsqu’un événement se produit, un signal est émis. Tous les slots qui sont connectés à ce signal sont alors exécutés. La fonction QObject::connect permet de créer une telle connexion. La forme la plus classique de cette fonction prend en paramètres un pointeur vers l’objet émetteur, le nom du signal (ainsi que la liste des types des arguments du signal), un pointeur vers l’objet recepteur et pour terminer le nom du slot (ainsi que la liste des types des arguments du slot). Il est possible de connecter plusieurs signaux à un même slot, un signal à plusieurs slots ou un signal avec un signal. Le compatibilité entre les classes et les signaux et slots est vérifiés lors de la compilation.

1
2
3
4
5
6
QAction a;
QWidget w;
QObject::connect(
    &a, SIGNAL(triggered()), // connecte le signal triggered() de QAction
      &w, SLOT(show()));       // au slot show() de QWidget
// ce code permet donc d'afficher le QWidget lorsque l'utilisateur active la QAction

Exemple de connexions dans Qt

Les classes de Qt fournissent de nombreux signaux et slots par défaut. Ces signaux et slots seront disponibles dans les classes créées par les utilisateurs et dérivant des classes de Qt. Il est également possible de créer ses propres signaux et slots et de les connecter aux signaux et slots par défaut de Qt :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include  
 
class Counter : public QObject // on hérite de QObject pour bénéficier des méta-informations de Qt
{
   Q_OBJECT // cette macro permet de générer les signaux et slots lors de la compilation
 
public slots:
   void setValue(int value)
   {
      if (value != m_value) // lorsque la valeur est changée
      {
         m_value = value;
         emit valueChanged(value); // on émet un signal valueChanged
      }
   }
 
signals:
    void valueChanged(int newValue); // signal émis lorsque la valeur est changée
 
private:
    int m_value;
};
 
Counter a, b;
 
// on connecte valueChanged de a à setValue de b
QObject::connect(&a, &Counter::valueChanged, &b, &Counter::setValue);
 
a.setValue(12);  
// a émet un signal valueChanged qui active le slot setValue de b
// a.value() == 12, b.value() == 12
 
b.setValue(48);      
// b émet un signal valueChanged mais ce signal n'est pas connecté à un slot
// a.value() == 12, b.value() == 48

Créer une connexion dans Qt 5

Dans Qt 4, il est possible de connecter uniquement les fonctions déclarées comme signaux et
slots dans la classes, comme indiqué dans le code d’exemple précédant. Dans Qt 5, il est
maintenant possible de connecter directement des pointeurs de fonctions ou d’utiliser des
fonctions lambdas.

La connexion de pointeurs de fonctions est similaire à une connexion classique, en donnant un pointeur sur les objets et sur les fonctions. Les classes émettrices et réceptrices doivent dériver de QObject mais il n’est pas nécessaire de déclarer les fonctions slots avec le mot clé « slots ».

1
2
3
4
5
6
7
8
9
10
11
class Sender : public QObject {
    Q_OBJECT
 
signals:
    void send(int i = 0);
};
 
class Receiver : public QObject {
 
public:
    void receive(int i = 0) { std::cout << "receive:" << i <send(123);

L’avantage de cette écriture est que la compatibilité des paramètres est effectuée lors de la compilation et non lors de l’exécution.

Pour les fonctions lambdas :

1
2
3
// connexion avec les lambdas
QObject::connect(s, &Sender::send, [r](int i = 0){ r->receive(i); });
emit s->send(456);

Dans ce code, on capture le pointeur vers l’objet recepteur et on récupère le paramètre passé par la fonction send() puis on appelle dans le corps de la lambda le fonction receive(). Le résultat obtenu est identique au code précédant, mais il est possible de faire beaucoup d’autres choses dans la lambda (par exemple déconnecter tous le signaux ou parcourir tous les enfants de l’objet récepteur).

Si le compilateur utilisé ne support pas les variadic template, les signaux et slots doivent avoir moins de 6 paramètres.

Remarques
Vous pouvez télécharger un projet d’exemple montrant ces nouvelles fonctionnalités en action : la page de téléchargement.

Les images et codes d’exemple sont issus de la documentation de Qt5 disponible à cette page : Qt 5.0: Signals & Slots.

6 réflexions au sujet de « Les signaux et slots dans Qt5 »

  1. Ping : Les modules de Qt 5 « C++, Qt et GPU

  2. Bonjour à tous

    Je me suis posé les mêmes questions, c’est pour cela que j’ai fait un projet de test pour voir ce qu’il en était en pratique.
    Comme vous pouvez le voir dans le code, il faut :

    • que l’émetteur dérive de QObject (pour les méta informations) ;
    • que l’émetteur possède la macro Q_OBJECT (cela permet au moc de reconnaître le signal) ;
    • pour la connexion avec des pointeurs de fonctions uniquement, il faut que le récepteur hérite de QObject.

    Donc on a besoin du moc dans tous les cas et de dériver de QObject pour les pointeurs de fonctions. Par contre, les fonctions lambdas sont plus permissives et peuvent s’utiliser avec n’importe quelle classe/fonction en réception.

  3. En parcourant en diagonale l’article, la première réaction que j’ai eue était : Enfin !

    Mais la question que je me pose est : Y a-t-il encore besoin de moc, ou serait-il possible d’avoir une chaîne de compilation plus classique.

  4. Klaim, pour le point 4, oui, car c’est QObject qui garde « en mémoire » la liste des connexions et qui « génère » les signaux (via les fichiers moc_xxxx.cpp).

  5. Donc,

    1. On peut utiliser des lambda comme slot (et par la meme, on peut embarquer n importe quoi avec la lambda pour son appel)
    2. On peut donc aussi fournir un std::function comme slot ou un boost::function?
    3. Cela implique qu’on a pas besoin d’avoir d’heritage au niveau du type dont la fonction membre est un slot?
    4. Par contre on a toujours besoin d’heritage pour l emetteur du signal.

    J’ai bien compris?

Les commentaires sont fermés.