Les signaux et slots dans Qt

Les nouvelles syntaxes utilisables dans Qt5 et Qt6

Cet article a été publié initialement sur mon site.

Les signaux-slots dans Qt 4

L’idée des signaux-slots est de créer un "lien" particulier entre deux fonctions de deux classes indépendantes, de façon à ce que lorsque l’on appelle la fonction du premier objet, la fonction du second objet est automatiquement appelée. La première fonction s’appelle "signal", la seconde "slot", le lien entre les deux s’appelle une "connexion".

class A {
public:
    void fonction_de_A() const {}
};

class B {
public:
    void fonction_de_B() const {}
};

int main() {
    A un_objet_A;
    B un_objet_B;

    // création d'une connexion entre les deux fonction
    creer_connexion(un_objet_A, fonction_de_A, un_objet_B, fonction_de_B);

    // lorsque l'on appelle la fonction de A, la fonction de B est 
    // automatiquement appelée
    un_objet_A.fonction_de_A(); // appelle un_objet_B.fonction_de_B()
}

Avec ce type d’approche, vous pouvez utiliser autant de classes et de fonctions que vous souhaitez, celles-ci seront indépendantes. A n’a pas besoin de connaitre B et B n’a pas besoin de connaitre A. On parle dans cette situation de "couplage faible". Nous allons voir maintenant comme cela s’utilise dans Qt, mais sachez qu’il est également possible d’utiliser cette technique avec d’autres bibliothèques (par exemple, en utilisant Boost.Signals2) ou en utilisant des fonctions comme argument (quelques mots-clés : std::function, functor object, lambda function, etc).

Le système de signaux et slots de Qt est relativement simple : lorsqu’un signal est émis avec le mot clé emit, tous les slots qui sont connectés à ce signal sont exécutés. Une connexion est créée en utilisant la fonction QObject::connect, en donnant les arguments suivants :

  • un pointeur vers l’objet émetteur ;
  • le nom du signal et la liste des types d’argument de la fonction signal (en utilisant la macro SIGNAL) ;
  • un pointeur vers l’objet récepteur ;
  • le nom du slot et la liste des types d’arguments de la fonction slot (en utilisant la macro SLOT).

Il est possible de connecter plusieurs signaux à un même slot, un signal à plusieurs slots ou un signal avec un signal.

abstract-connections.png

La correspondance entre les arguments des signaux et slots est vérifiée comme une chaîne de caractères, lors de l’exécution. Ainsi, si vous connectez un signal(float) avec une slot(double), la connexion sera refusée puisque les arguments ne correspondent pas, malgré le fait qu’un float peut être converti en double sans problème.

QAction* a = new QAction(this);
QWidget* w = new QWidget(this);
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

Les classes de Qt fournissent de nombreux signaux et slots par défaut (la liste des signaux et slots des classes Qt est indiquée dans la documentation de Qt). Vous pouvez également créer vos propres signaux et slots dans vos classes, en respectant les règles suivantes :

  • la classe doit dériver de QObject (directement ou indirectement) ;
  • il faut appeler la macro Q_OBJECT au début de la classe ;
  • les slots sont des fonctions classiques déclarées avec le mot-clé slots ;
  • les signaux sont des fonctions non implémentées déclarées avec le mot-clé signals.

Concrètement, une classe doit ressembler à quelque chose comme cela :

#include <QObject>
 
class UneClasse : public QObject
{
    Q_OBJECT
 
public slots:
    void unSlot() { /* code du slot */ }
    
signals:
    void unSignal();
};

Comme le principe est de pouvoir connecter n’importe quelle classe et et n’importe quels signaux et slots (à partir du moment où les paramètres de fonctions corresponds), vous pouvez sans problème connecter les classes Qt entre elles, avec vos propres classes ou vos propres classes entre elles.

Remarque importante : il faut obligatoirement déclarer les classes dérivant de QObject dans un fichier .h et non dans un fichier .cpp, sinon le pré-compilateur moc de Qt ne pourra pas fonctionner.

Les signaux et slots dans Qt 5

Dans Qt 4, il est possible de connecter uniquement les fonctions déclarées comme signaux et slots dans la classe, comme indiqué dans les codes d’exemple précédent. De plus, il faut que les déclarations des fonctions dans les macros SIGNAL et SLOT correspondent exactement, ce qui interdit l’utilisation de typedef/using, les espaces de noms ou les conversions implicites des types. Pour terminer, la connexion étant créée lors de l’exécution, il n’est pas possible de savoir dès la compilation s’il y a un problème, ce qui retarde le diagnostic des problèmes.

Dans Qt 5, il est maintenant possible de connecter directement des pointeurs de fonctions ou d’utiliser des fonctions lambdas.

Connecter des pointeurs de fonctions membres

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 utilisées comme slots avec le mot clé slots.

class Sender : public QObject {
    Q_OBJECT
signals:
    void send(int i = 0);
};
class Receiver : public QObject {
    Q_OBJECT
public:
    void receive(int i = 0) { std::cout << "received: " << i << std::endl; }
};
QObject::connect(s, &Sender::send, r, &Receiver::receive);

Notez bien le passage des signaux / slots :

  • un & devant ;
  • (obligatoirement) le NomDeLaClasse:: devant ;
  • aucune parenthèse.

Il s’agit de passer l’adresse de la fonction membre, pas d’effectuer un appel.

L’avantage de cette écriture est que la compatibilité des paramètres est effectuée lors de la compilation et une conversion implicite est réalisée si nécessaire. Ainsi, il est possible de connecter un signal mon_signal(float) vers un slot mon_slot(float), mais également vers un slot mon_slot(double).

Lorsqu’un signal ou slot possède plusieurs surcharges, le compilateur ne sait pas à quelle fonction vous faites référence dans la connexion. Il faut donc donner explicitement la signature de la fonction, en effectuant une conversion. Par exemple, pour connecter les signaux QComboBox::currentIndexChanged(int) et QComboBox::currentIndexChanged(const QString &)1 de la classe QComboBox, il est possible d’écrire :

QComboBox * comboBox = new QComboBox;
 
// Utilisation de : void currentIndexChanged(int index)
connect(
    comboBox,
    static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
    /* … */);
 
// Utilisation de : void currentIndexChanged(const QString & text)
connect(
    comboBox,
    static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentIndexChanged),
    /* … */);

Cette écriture de base étant plutôt lourde, la version Qt 5.7 a introduit la variable template qOverload<>() (ainsi que ses dérivés qConstOverload<>() et qNonConstOverload<>() pour distinguer les surcharges constantes et non constantes).

Basons-nous sur la classe suivante et sa fonction membre qui possède quatre surcharges :

class VotreClasse : public QObject {
    Q_OBJECT
public:
    void fonctionSurcharge();                   // (1)
    void fonctionSurcharge(QString);            // (2)
    void fonctionSurcharge(int, QString);       // (3)
    void fonctionSurcharge(int, QString) const; // (4)
};

Les types des paramètres de la surcharge ciblée sont spécifiés dans les paramètres template :

qOverload<>(&VotreClasse::fonctionSurcharge)                     // (1)
qOverload<QString>(&VotreClasse::fonctionSurcharge)              // (2)
qNonConstOverload<int, QString>(&VotreClasse::fonctionSurcharge) // (3)
qConstOverload<int, QString>(&VotreClasse::fonctionSurcharge)    // (4)

L’exemple précédent devient alors :

// Utilisation de : void currentIndexChanged(int index)
connect(
    comboBox,
    qOverload<int>(&QComboBox::currentIndexChanged),
    /* … */);

// Utilisation de : void currentIndexChanged(const QString & text)
connect(
    comboBox,
    qOverload<const QString &>(&QComboBox::currentIndexChanged),
    /* … */);

Note : ces variables template nécessitant C++14, vous avez en C++11 les classes équivalentes et leur fonction membre statique ::of() :

QOverload<>::of(&VotreClasse::fonctionSurcharge)                     // (1)
QOverload<QString>::of(&VotreClasse::fonctionSurcharge)              // (2)
QNonConstOverload<int, QString>::of(&VotreClasse::fonctionSurcharge) // (3)
QConstOverload<int, QString>::of(&VotreClasse::fonctionSurcharge)    // (4)

1 Surcharge devenue obsolète au profit de currentTextChanged(const QString &), et supprimée depuis Qt 6.

Connecter des lambdas, fonctions libres et foncteurs

Il est également possible d’utiliser une fonction lambda, une fonction libre ou un foncteur comme slot dans une connexion. Par exemple, lorsque l’on souhaite connecter un signal QPushButton::clicked avec un slot QLabel::setText(QString) dans Qt 4, il n’est pas possible de créer directement la connexion.

QPushButton* button = new QPushButton;
QLabel* label = new QLabel;
QObject::connect(
    button, SIGNAL(clicked()), 
    label, SLOT(setText(QString)); 
    // erreur, les paramètres de fonctions ne correspondent pas

Il faut écrire un intermédiaire, par exemple :

class MyWidget : public QWidget {
    Q_OBJECT
private:
    QPushButton* button = nullptr;
    QLabel* label = nullptr;
public:
    MyWidget(QWidget* parent = nullptr) :
        QWidget(parent),
        button { new QPushButton(this) },
        label { new QLabel(this) }
    {
        connect(button, SIGNAL(clicked()), label, SLOT(setText("Mon texte")); // Erreur
        connect(button, SIGNAL(clicked()), this, SLOT(new_slot()); // Ok
    }
private slots:
    void new_slot() {
        label->setText("Mon texte");
    }
};

En utilisant les lambdas, il est maintenant possible d’appeler directement :

connect(button, &QPushButton::clicked, this, [this](){ label->setText("Mon texte"); });

Dans ce code, on capture this, pointeur vers l’objet courant, afin d’utiliser son membre label (implicitement this->label). Le résultat obtenu est identique au code précédent, mais il est possible de faire beaucoup d’autres choses dans la fonction lambda (par exemple déconnecter tous les signaux ou parcourir tous les enfants de l’objet récepteur).

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

De la même façon, pour les fonctions libres et les foncteurs :

// Un foncteur
class MyObject {
public:
    void operator()() { /* … */ }
};

MyObject object { /* create object */ };
QObject::connect(sender, &Sender::unSignal, object);
// Une fonction libre
void foo() { /* … */ }

QObject::connect(sender, &Sender::unSignal, foo);

C++14

Le C++14 apporte quelques nouvelles syntaxes pour écrire des fonctions lambda : les fonctions lambda génériques et la capture étendue.

Ces syntaxes sont utilisables avec Qt 5.1 :

// lambda générique
connect(sender, &Sender::valueChanged, [=](const auto &newValue) {
    receiver->updateValue("senderValue", newValue);
});

// capture étendue
connect(sender, &Sender::valueChanged, [receiver=getReceiver()](const auto &newValue) {
    receiver->updateValue("senderValue", newValue);
});

Résumé des syntaxes utilisables

// Qt 4
QMetaObject::Connection QObject::connect(
    const QObject * sender, const char * signal, 
    const QObject * receiver, const char * method, 
    Qt::ConnectionType type = Qt::AutoConnection) [static]

QMetaObject::Connection QObject::connect(
    const QObject * sender, const char * signal, 
    const char * method, 
    Qt::ConnectionType type = Qt::AutoConnection) const
    
// Qt 4.8
QMetaObject::Connection QObject::connect(
    const QObject * sender, const QMetaMethod & signal, 
    const QObject * receiver, const QMetaMethod & method, 
    Qt::ConnectionType type = Qt::AutoConnection) [static]

// Qt 5.0
QMetaObject::Connection QObject::connect(
    const QObject * sender, PointerToMemberFunction signal, 
    const QObject * receiver, PointerToMemberFunction method, 
    Qt::ConnectionType type = Qt::AutoConnection) [static]

QMetaObject::Connection QObject::connect(
    const QObject * sender, PointerToMemberFunction signal, 
    Functor functor) [static]

// Qt 5.2
QMetaObject::Connection QObject::connect(
    const QObject * sender, PointerToMemberFunction signal, 
    const QObject * context, Functor functor, 
    Qt::ConnectionType type = Qt::AutoConnection) [static]

Remarque : toutes ces fonctions sont static, sauf la seconde.

Conclusion

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

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

Mises à jour :

  • 20/10/2022 : ajout de la partie sur qOverload<>.
  • 27/10/2014 : mise à jour pour Qt 5.4 et suppression de la partie sur le couplage (cela fera partie d’un article plus généraliste sur la communication inter-classes).
  • 27/11/2014 : ajout de la partie sur le C++14.

Sources :

Pour aller plus loin :


8 commentaires

Très bon article! J'aime bien la nouvelle syntaxe et la possibilité d'utiliser des lambdas ou des fonctions-objets de manière générale.

Je tiens à signaler une coquille: "&QPushButton::clicked()" qui normalement doit être "&QPushButton::clicked" (vu que ça utilise la nouvelle syntaxe et non pas celle de "SIGNAL(clicked())" ).

En ce qui concerne Boost.Signals, elle n'est plus maintenu. Il faut passer à Boost.Signals2 (ou au moins la citer à la place de Boost.Signals).

C'est bon a savoir. J'utilise assez peu qt, uniquement quand je fais des interfaces graphiques ce qui n'est pas souvent, et même si je trouve que c'est clairement le meilleur framework pour ça, je n'ai jamais aimé tous les ajouts de syntaxe et les passages dans les Mocs. Je comprend que c'était indispensable autre fois, maintenant ça l'est beaucoup moins. Je suis donc assez agréablement surpris que qt bouge enfin vers un c++ plus standard.

@d@rk-marouane: bien vu pour l'erreur et pour Boost.signals2. Merci

@kje: si tu es interesse, les articles de woboq que j'ai cite entre plus en detail sur le pourquoi du moc et des ajouts de syntaxe (pour info, woboq est un des developeur de QtCore et du moc)

+0 -0

Merci pour cet article très clair :) Mais avec quelques erreurs :p

Dans ce code :

1
2
3
4
5
MyObject* object = /* create object */;

QObject::connect(
    sender, unSignal(),
    &object);

Ce serait *object à passer à la méthode connect() et non l'adresse (d'un pointeur en plus).

J'ai aussi les erreurs

1
2
'label' is not captured
no matching function for call to 'QObject::connect(...

lorsque j'essaie ce code

1
2
3
QPushButton button;
QLabel label;
QObject::connect(&button, &QPushButton::clicked, label, [](){ label->setText("Mon texte"); });

outre le & manquant devant label vu qu'ici ce n'est pas un pointeur, êtes-vous sûr de ceci

Dans Qt 5.2, une nouvelle syntaxe a été ajoutée pour éviter d'avoir à récupérer un objet dans la liste de capture du lambda ou récupérer un contexte dans un environnement multithreads

?

Dans la documentation, il n'est mention que de placer l'exécution de la fonction dans le contexte de ce 3e paramètre, mais aucune substitution à la capture de la lambda… ou ai-je loupé quelque chose ?

Je note aussi que les deux premiers liens de fin concernant woboq sont vides ainsi qu'une faute d'orthographe concernant receiver dans

1
2
3
connect(sender, &Sender::valueChanged, [reciever=getReciever()](const auto &newValue) {
    receiver->updateValue("senderValue", newValue);
});

Voilà, encore merci pour cet article, mais aussi tous les autres que vous (avez/êtes en train de) rédigé/er ! :)

+0 -0

Impressionnant comment il peut rester autant d'erreurs aussi grossières, après autant de relecture :) Pour la syntaxe avec lambda, je ne sais pas d'ou cela vient et surtout je ne vois pas comment cela est possible. Supprimé.

C'est corrige (les corrections seront visibles après validation par ZdS)

Merci pour les retours

+0 -0

Je vous en prie c'est la moindre des choses :) Vous m'en voyez navré, j'en remarque d'autres aujourd'hui :(

1
2
3
4
5
    label, SIGNAL(setText(QString)); 
        connect(button, SIGNAL(clicked()), SIGNAL(new_slot()); // Ok
// devraient être
    label, SLOT(setText(QString)); 
        connect(button, SIGNAL(clicked()), SLOT(new_slot()); // Ok

aussi pourquoi avoir mis emit sur cette ligne

1
        emit label->setText("Mon texte");

? setText() est un slot et non un signal.

Enfin, les deux liens de fin sur QSignalMapper et QSignalBlocker sont aussi vides.

En espérant que la liste soit maintenant exhaustive… :)

+0 -0

Bon, il faut se rendre a l’évidence : tu es le premier a faire une relecture complète et sérieuse :)

En fait, non. Une partie des corrections m'ont déjà été signale par WinJerome… et j'avais oublie de les reporter. Désolé

Corrigé et merci.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte