Mis à jour le 04/12/2018
  • 50 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Utilisez les signaux et les slots

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Nous commençons à maîtriser petit à petit la création d'une fenêtre. Au chapitre précédent, nous avons posé de solides bases pour le développement ultérieur de notre application. Nous avons réalisé une classe personnalisée, héritant de QWidget.

Nous allons maintenant découvrir le mécanisme des signaux et des slots, un principe propre à Qt qui est clairement un de ses points forts. Il s'agit d'une technique séduisante pour gérer les évènements au sein d'une fenêtre.
Par exemple, si on clique sur un bouton, on voudrait qu'une fonction soit appelée pour réagir au clic. C'est précisément ce que nous apprendrons à faire dans ce chapitre, qui va enfin rendre notre application dynamique.

Le principe des signaux et slots

Le principe est plutôt simple à comprendre : une application de type GUI réagit à partir d'évènements. C'est ce qui rend votre fenêtre dynamique.

Dans Qt, on parle de signaux et de slots, mais qu'est-ce que c'est concrètement ? C'est un concept inventé par Qt. Voici, en guise d'introduction, une petite définition :

    • Un signal : c'est un message envoyé par un widget lorsqu'un évènement se produit.

Exemple : on a cliqué sur un bouton.

    • Un slot : c'est la fonction qui est appelée lorsqu'un évènement s'est produit. On dit que le signal appelle le slot. Concrètement, un slot est une méthode d'une classe.

Exemple : le slot quit()de la classe QApplicationprovoque l'arrêt du programme.

Les signaux et les slots sont considérés par Qt comme des éléments d'une classe à part entière, en plus des attributs et des méthodes.

Voici un schéma qui montre ce qu'un objet pouvait contenir avant Qt, ainsi que ce qu'il peut contenir maintenant qu'on utilise Qt :

Un objet avec des signaux et des slots
Un objet avec des signaux et des slots

Avant Qt, un objet était constitué d'attributs et de méthodes. C'est tout.
Qt rajoute en plus la possibilité d'utiliser ce qu'il appelle des signaux et des slots afin de gérer les évènements.

Un signal est un message envoyé par l'objet (par exemple « on a cliqué sur le bouton »).
Un slot est une… méthode. En fait, c'est une méthode classique comme toutes les autres, à la différence près qu'elle a le droit d'être connectée à un signal.

Avec Qt, on dit que l'on connecte des signaux et des slots entre eux. Supposons que vous ayez deux objets, chacun ayant ses propres attributs, méthodes, signaux et slots (figure suivante). Pour simplifier, je n'ai pas représenté les attributs et les méthodes sur mon schéma.

Des signaux et des slots
Des signaux et des slots

Sur le schéma suivante, on a connecté le signal 1 de l'objet 1 avec le slot 2 de l'objet 2.

Connexion d'un signal à un slot simple

Voyons un cas très concret. Je vais prendre deux objets, l'un de type QPushButton et l'autre de type QApplication. Dans le schéma suivant, les indications que vous voyez sont de vrais signaux et slots que vous allez pouvoir utiliser.

Signaux et slots en pratique

Regardez attentivement ce schéma. Nous avons d'un côté notre bouton appelém_bouton(de typeQPushButton) et de l'autre, notre application (de typeQApplication, utilisée dans lemain).

Nous voudrions par exemple connecter le signal « bouton cliqué » au slot « quitter l'application ». Ainsi, un clic sur le bouton provoquerait l'arrêt de l'application.

Pour cela, nous devons utiliser une méthode statique deQObject:connect().

Le principe de la méthodeconnect()

connect()est une méthode statique. Vous vous souvenez ce que cela veut dire ?
Une méthode statique est une méthode d'une classe que l'on peut appeler sans créer d'objet. C'est en fait exactement comme une fonction classique.

Pour appeler une méthode statique, il faut faire précéder son intitulé du nom de la classe dans laquelle elle est déclarée. Commeconnect()appartient à la classeQObject, il faut donc écrire :

QObject::connect();

La méthodeconnectprend 4 arguments :

  • un pointeur vers l'objet qui émet le signal ;

  • le nom du signal que l'on souhaite « intercepter » ;

  • un pointeur vers l'objet qui contient le slot récepteur ;

  • le nom du slot qui doit s'exécuter lorsque le signal se produit.

Il existe aussi une méthodedisconnect()permettant de casser la connexion entre deux objets mais on n'en parlera pas ici car on en a rarement besoin.

Utilisation de la méthodeconnect()pour quitter

Revenons au code, et plus précisément au constructeur deMaFenetre(fichierMaFenetre.cpp). Ajoutez cette ligne :

#include "MaFenetre.h"
 
MaFenetre::MaFenetre() : QWidget()
{
    setFixedSize(300, 150);
 
    m_bouton = new QPushButton("Quitter", this);
    m_bouton->setFont(QFont("Comic Sans MS", 14));
    m_bouton->move(110, 50);
 
    // Connexion du clic du bouton à la fermeture de l'application
    QObject::connect(m_bouton, SIGNAL(clicked()), qApp, SLOT(quit()));
}

Étudions attentivement cette ligne et plus particulièrement les paramètres envoyés àconnect():

  • m_bouton: c'est un pointeur vers le bouton qui va émettre le signal. Facile.

  • SIGNAL(clicked()): là, c'est assez perturbant comme façon d'envoyer un paramètre. En fait, SIGNAL() est une macro du préprocesseur. Qt transformera cela en un code « acceptable » pour la compilation. Le but de cette technique est de vous faire écrire un code court et compréhensible. Ne cherchez pas à comprendre comment Qt fait pour transformer le code, ce n'est pas notre problème.

  • qApp: c'est un pointeur vers l'objet de type QApplicationque nous avons créé dans lemain. D'où sort ce pointeur ? En fait, Qt crée automatiquement un pointeur appelé qAppvers l'objet de typeQApplicationque nous avons créé. Ce pointeur est défini dans le header  <QApplication>que nous avons inclus dans MaFenetre.h.

  • SLOT(quit()): c'est le slot qui doit être appelé lorsqu'on a cliqué sur le bouton. Là encore, il faut utiliser la macroSLOT()pour que Qt traduise ce code « bizarre » en quelque chose de compilable.

Le slot quit()de notre objet de type QApplicationest un slot prédéfini. Il en existe d'autres, commeaboutQt()qui affiche une fenêtre « À propos de Qt ».
Parfois, pour ne pas dire souvent, les slots prédéfinis par Qt ne nous suffiront pas. Nous apprendrons dans la suite de ce chapitre à créer les nôtres.

Testons notre code ! La fenêtre qui s'ouvre est présentée à la figure suivante :

La fenêtre avec le bouton « Quitter »

Rien de bien extraordinaire à première vue. Sauf que… si vous cliquez sur le bouton « Quitter », le programme s'arrête !
Hourra, on vient de réussir à connecter notre premier signal à un slot !

Des paramètres dans les signaux et slots

La méthode statique connect()est assez originale, vous l'avez vu. Il s'agit justement d'une des particularités de Qt que l'on ne retrouve pas dans les autres bibliothèques.
Ces autres bibliothèques, comme wxWidgets par exemple, utilisent à la place de nombreuses macros et se servent du mécanisme un peu complexe et délicat des pointeurs de fonction (pour indiquer l'adresse de la fonction à appeler en mémoire).

Il y a d'autres avantages à utiliser la méthode connect()avec Qt. On va ici découvrir que les signaux et les slots peuvent s'échanger des paramètres !

Dessin de la fenêtre

Dans un premier temps, nous allons placer de nouveaux widgets dans notre fenêtre.
Vous pouvez enlever le bouton, on ne va plus s'en servir ici.

À la place, je souhaite vous faire utiliser deux nouveaux widgets :

  • QSlider: un curseur qui permet de définir une valeur ;

  • QLCDNumber: un widget qui affiche un nombre.

On va aller un peu plus vite, je vous donne directement le code pour créer cela.
Tout d'abord, le header :

#ifndef DEF_MAFENETRE
#define DEF_MAFENETRE
 
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QLCDNumber>
#include <QSlider>
 
class MaFenetre : public QWidget
{
    public:
    MaFenetre();
 
    private:
    QLCDNumber *m_lcd;
    QSlider *m_slider; 
};
 
#endif

J'ai donc enlevé les boutons, comme vous pouvez le voir, et rajouté unQLCDNumberet unQSlider.
Surtout, n'oubliez pas d'inclure le header de ces classes pour pouvoir les utiliser. J'ai gardé ici l'include duQPushButton, cela ne fait pas de mal de le laisser mais, si vous ne comptez pas le réutiliser, vous pouvez l'enlever sans crainte.

Et le fichier .cpp:

#include "MaFenetre.h"
 
MaFenetre::MaFenetre() : QWidget()
{
    setFixedSize(200, 100);
 
    m_lcd = new QLCDNumber(this);
    m_lcd->setSegmentStyle(QLCDNumber::Flat);
    m_lcd->move(50, 20);
 
    m_slider = new QSlider(Qt::Horizontal, this);
    m_slider->setGeometry(10, 60, 150, 20);
}

Les détails ne sont pas très importants. J'ai modifié le type d'afficheur LCD pour qu'il soit plus lisible (avecsetSegmentStyle). Quant au slider, j'ai rajouté un paramètre pour qu'il apparaisse horizontalement (sinon il est vertical).

Voilà qui est fait. Avec ce code, une petite fenêtre devrait s'afficher :

Un afficheur LCD

Connexion avec des paramètres

Maintenant… connexiooooon !
C'est là que les choses deviennent intéressantes. On veut que l'afficheur LCD change de valeur en fonction de la position du curseur du slider.

On dispose du signal et du slot suivants :

  • Le signal valueChanged(int)du QSlider: il est émis dès que l'on change la valeur du curseur du slider en le déplaçant. La particularité de ce signal est qu'il envoie un paramètre de type int(la nouvelle valeur du slider).

  • Le slot display(int)du QLCDNumber: il affiche la valeur qui lui est passée en paramètre.

La connexion se fait avec le code suivant :

QObject::connect(m_slider, SIGNAL(valueChanged(int)), m_lcd, SLOT(display(int))) ;

Bizarre n'est-ce pas ?
Il suffit d'indiquer le type du paramètre envoyé, ici unint, sans donner de nom à ce paramètre. Qt fait automatiquement la connexion entre le signal et le slot et « transmet » le paramètre au slot.

Le transfert de paramètre se fait comme sur la figure suivante :

Connexion de int à int

Ici, il n'y a qu'un paramètre à transmettre, c'est donc simple. Sachez toutefois qu'il pourrait très bien y avoir plusieurs paramètres.

Résultat : quand on change la valeur du slider, le LCD affiche la valeur correspondante !

Un afficheur LCD génère des évènements

Mais comment je sais, moi, quels sont les signaux et les slots que proposent chacune des classes ? Et aussi, comment je sais qu'un signal envoie uninten paramètre ?

La réponse devrait vous paraître simple, les amis : la doc, la doc, la doc !

Si vous regardez la documentation de la classe QLCDNumber, vous pouvez voir au début la liste de ses propriétés (attributs) et ses méthodes. Un peu plus bas, vous avez la liste des slots (« Public Slots ») et des signaux (« Signals ») qu'elle possède !

Exercice

Pour vous entraîner, je vous propose de réaliser une petite variante du code source précédent.
Au lieu d'afficher le nombre avec un QLCDNumber, affichez-le sous la forme d'une jolie barre de progression :

Slider et progressbar

Je ne vous donne que 3 indications qui devraient vous suffire :

  • La barre de progression est gérée par unQProgressBar.

  • Il faut donner des dimensions à la barre de progression pour qu'elle apparaisse correctement, à l'aide de la méthodesetGeometry()que l'on a déjà vue auparavant.

  • Le slot récepteur duQProgressBarestsetValue(int). Il s'agit d'un de ses slots mais la documentation vous indique qu'il y en a d'autres. Par exemple,reset()remet à zéro la barre de progression. Pourquoi ne pas ajouter un bouton qui remettrait à zéro la barre de progression ?

C'est tout. Bon courage !

Créez ses propres signaux et slots

Voici maintenant une partie très intéressante, bien que plus délicate. Nous allons créer nos propres signaux et slots.

En effet, si en général les signaux et slots par défaut suffisent, il n'est pas rare que l'on se dise « Zut, le signal (ou le slot) dont j'ai besoin n'existe pas ». C'est dans un cas comme celui-là qu'il devient indispensable de créer son widget personnalisé.

Nous allons commencer par créer notre propre slot, puis nous verrons comment créer notre propre signal.

Créez son propre slot

Je vous rappelle tout d'abord qu'un slot n'est rien d'autre qu'une méthode que l'on peut connecter à un signal.
Nous allons donc créer une méthode, mais en suivant quelques règles un peu particulières…

Le but du jeu

Pour nous entraîner, nous allons inventer un cas où le slot dont nous avons besoin n'existe pas.
Je vous propose de conserver le QSlider (je l'aime bien celui-là) et de ne garder que cela sur la fenêtre. Nous allons faire en sorte que le QSlider contrôle la largeur de la fenêtre.

Votre fenêtre doit ressembler à la figure suivante.

Fenêtre avec slider

Nous voulons que le signalvalueChanged(int)duQSliderpuisse être connecté à un slot de notre fenêtre (de typeMaFenetre). Ce nouveau slot aura pour rôle de modifier la largeur de la fenêtre.
Comme il n'existe pas de slotchangerLargeurdans la classeQWidget, nous allons devoir le créer.

Pour créer ce slot, il va falloir modifier un peu notre classeMaFenetre. Commençons par le header.

Le header (MaFenetre.h)

Dès que l'on doit créer un signal ou un slot personnalisé, il est nécessaire de définir une macro dans le header de la classe.

Cette macro porte le nom deQ_OBJECT(tout en majuscules) et doit être placée tout au début de la déclaration de la classe :

class MaFenetre : public QWidget
{
    Q_OBJECT
 
    public:
    MaFenetre();
 
    private:
    QSlider *m_slider;
};

Pour le moment, notre classe ne définit qu'un attribut (leQSlider, privé) et une méthode (le constructeur, public).

La macroQ_OBJECT« prépare » en quelque sorte le compilateur à accepter un nouveau mot-clé : « slot ». Nous allons maintenant pouvoir créer une section « slots », comme ceci :

class MaFenetre : public QWidget
{
    Q_OBJECT
 
    public:
    MaFenetre();
 
    public slots:
    void changerLargeur(int largeur);
 
    private:
    QSlider *m_slider;
};

Vous noterez la nouvelle section « public slots » : je rends toujours mes slots publics. On peut aussi les mettre privés mais ils seront quand même accessibles de l'extérieur car Qt doit pouvoir appeler un slot depuis n'importe quel autre widget.

À part cela, le prototype de notre slot-méthode est tout à fait classique. Il ne nous reste plus qu'à l'implémenter dans le.cpp.

L'implémentation (MaFenetre.cpp)

L'implémentation est d'une simplicité redoutable. Regardez :

void MaFenetre::changerLargeur(int largeur)
{
    setFixedSize(largeur, 100);
}

Le slot prend en paramètre un entier : la nouvelle largeur de la fenêtre.
Il se contente d'appeler la méthodesetFixedSizede la fenêtre et de lui envoyer la nouvelle largeur qu'il a reçue.

Connexion

Bien, voilà qui est fait. Enfin presque : il faut encore connecter notreQSliderau slot de notre fenêtre. Où va-t-on faire cela ? Dans le constructeur de la fenêtre (toujours dansMaFenetre.cpp) :

MaFenetre::MaFenetre() : QWidget()
{
    setFixedSize(200, 100);
 
    m_slider = new QSlider(Qt::Horizontal, this);
    m_slider->setRange(200, 600);
    m_slider->setGeometry(10, 60, 150, 20);
 
    QObject::connect(m_slider, SIGNAL(valueChanged(int)), this, SLOT(changerLargeur(int)));
}

J'ai volontairement modifié les différentes valeurs que peut prendre notre slider pour le limiter entre 200 et 600 avec la méthodesetRange(). Ainsi, on est sûr que la fenêtre ne pourra ni être plus petite que 200 pixels de largeur, ni dépasser 600 pixels de largeur.

La connexion se fait entre le signalvalueChanged(int)de notreQSlideret le slotchangerLargeur(int)de notre classeMaFenetre. Vous voyez là encore un exemple oùthisest indispensable : il faut pouvoir indiquer un pointeur vers l'objet actuel (la fenêtre) et seulthispeut faire cela !

Schématiquement, on a réalisé la connexion présentée à la figure suivante :

Connexion entre le slider et la fenêtre
Connexion entre le slider et la fenêtre

Vous pouvez enfin admirer le résultat :

Le slider élargit la fenêtre

Amusez-vous à redimensionner la fenêtre comme bon vous semble avec le slider. Comme nous avons fixé les limites du slider entre 200 et 600, la largeur de la fenêtre restera comprise entre 200 et 600 pixels.

Exercice : redimensionnez la fenêtre en hauteur

Voici un petit exercice, mais qui va vous forcer à travailler (bande de fainéants, vous me regardez faire depuis tout à l'heure ! ;) ).
Je vous propose de créer un secondQSlider, vertical cette fois, qui contrôlera la hauteur de la fenêtre. Pensez à bien définir des limites appropriées pour les valeurs de ce nouveau slider.

Vous devriez obtenir un résultat qui ressemble à la figure suivante.

Un slider vertical

Si vous voulez « conserver » la largeur pendant que vous modifiez la hauteur, et inversement, vous aurez besoin d'utiliser les méthodes accesseurwidth()(largeur actuelle) etheight()(hauteur actuelle).
Vous comprendrez très certainement l'intérêt de ces informations lorsque vous coderez. Au boulot !

Créez son propre signal

Il est plus rare d'avoir à créer son signal que son slot mais cela peut arriver.

Je vous propose de réaliser le programme suivant : si le slider horizontal arrive à sa valeur maximale (600 dans notre cas), alors on émet un signalagrandissementMax. Notre fenêtre doit pouvoir émettre l'information indiquant qu'elle est agrandie au maximum.
Après, nous connecterons ce signal à un slot pour vérifier que notre programme réagit correctement.

Le header (MaFenetre.h)

Commençons par changer le header :

class MaFenetre : public QWidget
{
    Q_OBJECT
 
    public:
    MaFenetre();
 
    public slots:
    void changerLargeur(int largeur);
 
    signals:
    void agrandissementMax();
 
    private:
    QSlider *m_slider;
};

On a ajouté une section signals. Les signaux se présentent en pratique sous forme de méthodes (comme les slots) à la différence près qu'on ne les implémente pas dans le .cpp. En effet, c'est Qt qui le fait pour nous. Si vous tentez d'implémenter un signal, vous aurez une erreur du genre «Multiple definition of…».

Un signal peut passer un ou plusieurs paramètres. Dans notre cas, il n'en envoie aucun.
Un signal doit toujours renvoyervoid.

L'implémentation (MaFenetre.cpp)

Maintenant que notre signal est défini, il faut que notre classe puisse l'émettre à un moment.
Quand est-ce qu'on sait que la fenêtre a été agrandie au maximum ? Dans le slotchangerLargeur! Il suffit de vérifier dans ce slot si la largeur correspond au maximum (600) et d'émettre alors le signal « Youhou, j'ai été agrandie au maximum ! ».

Retournons dansMaFenetre.cppet implémentons ce test qui émet le signal depuischangerLargeur:

void MaFenetre::changerLargeur(int largeur)
{
    setFixedSize(largeur, height());
 
    if (largeur == 600)
    {
        emit agrandissementMax();
    }
}

Notre méthode s'occupe toujours de redimensionner la fenêtre mais vérifie en plus si la largeur a atteint le maximum (600). Si c'est le cas, elle émet le signal agrandissementMax().
Pour émettre un signal, on utilise le mot-clé emit, là encore un terme inventé par Qt qui n'existe pas en C++. L'avantage est que c'est très lisible.

Connexion

Il ne nous reste plus qu'à connecter notre nouveau signal à un slot. Vous pouvez connecter ce signal au slot que vous voulez. Je propose de le connecter à l'application (à l'aide du pointeur globalqApp) pour provoquer l'arrêt du programme.
Cela n'a pas trop de sens, je suis d'accord, mais c'est juste pour s'entraîner et vérifier que cela fonctionne. Vous aurez l'occasion de faire des connexions plus logiques plus tard, je ne m'en fais pas pour cela.

Dans le constructeur deMaFenetre, je rajoute donc :

QObject::connect(this, SIGNAL(agrandissementMax()), qApp, SLOT(quit()));

Vous pouvez tester le résultat : normalement, le programme s'arrête quand la fenêtre est agrandie au maximum.

Le schéma des signaux qu'on vient d'émettre et de connecter est présenté en figure suivante :

Échange de signaux entre objets
Échange de signaux entre objets

Dans l'ordre, voici ce qui s'est passé :

  1. Le signal valueChangeddu slider a appelé le slot changerLargeurde la fenêtre.

  2. Le slot a fait ce qu'il avait à faire (changer la largeur de la fenêtre) et a vérifié que la fenêtre était arrivée à sa taille maximale. Lorsque cela a été le cas, le signal personnalisé agrandissementMax()a été émis.

  3. Le signal agrandissementMax()de la fenêtre était connecté au slot quit()de l'application, ce qui a provoqué la fermeture du programme.

Et voilà comment le déplacement du slider peut, par réaction en chaîne, provoquer la fermeture du programme !
Bien entendu, ce schéma peut être aménagé et complexifié selon les besoins de votre application.

Maintenant que vous savez créer vos propres signaux et slots, vous avez toute la souplesse nécessaire pour faire ce que vous voulez !

En résumé

  • Qt propose un mécanisme d'évènements de type signaux et slots pour connecter des widgets entre eux. Lorsqu'un widget émet un signal, celui-ci est récupéré par un autre widget dans son slot.

  • On peut par exemple connecter le signal « bouton cliqué » au slot « ouverture d'une fenêtre ».

  • Les signaux et les slots peuvent être considérés comme un nouveau type d'élément au sein des classes, en plus des attributs et méthodes.

  • La connexion entre les éléments se fait à l'aide de la méthode statique connect().

  • Il est possible de créer ses propres signaux et slots.

Exemple de certificat de réussite
Exemple de certificat de réussite