Aller au contenu

Qt : création dynamique de widgets

Vous êtes-vous déjà retrouvé devant une situation où vous réalisez une interface avec un nombre de boutons ou de listes indéterminées ?

Créer une interface lorsqu'on sait déjà à l'avance comment elle sera constituée est une chose, devoir créer les éléments qui la composent de manière dynamique en est une autre.

Je me suis retrouvé face à ce problème sur Qt récemment et me suis dit qu'il pourrait être intéressant de vous faire profiter de ce que j'ai appris.

Imaginons que nous devons créer une interface avec des boutons (QPushButton). Un nombre indéterminé de boutons. Du moins, un nombre que notre programme ne connait pas à l'avance, qu'il découvrira à l'exécution (récupérant cette information dans un fichier .INI, par exemple).

Ainsi, lorsqu'on lit le fichier .ini, une simple petite boucle nous permet de créer tout ça :

QVBoxLayout *mainLayout = new QVBoxLayout;
this->setLayout(mainLayout);

//Lecture du fichier .ini
QSettings iniFile("DynamicWidgets.ini", QSettings::IniFormat);
iniFile.beginGroup("Buttons");

//Lecture d'un tableau de clés
foreach(QString key, iniFile.childKeys()) {
    //Récupération du nom du bouton
    QString buttonName(iniFile.value(key,"").toString());
    QPushButton *button = new QPushButton;
    button->setText(buttonName);
    mainLayout->addWidget(button);
}
iniFile.endGroup();

Et un petit exemple de fichier .ini pourrait être :

[Buttons]
1=Presse-moi
2=Youpi
3=Je hais les pointeurs
4=Yosko wuz here
5=Bouton n°5

On se retrouve donc avec 5 boutons (alors que rien dans notre programme n'indiquait qu'il y en aurait 5).

Mais maintenant, comment fait-on pour interagir avec ces boutons ? Si on les connectes tous à un slot, comment le slot saura-t-il quel bouton l'a appelé ?

Pour cela, on peut procéder de plusieurs manières. Il nous faut tout d'abord déclarer un slot dans le .h :

public slots:
    void displayButton(QString buttonName);</pre>
<p>
    Ce slot fera tout simplement un affichage du nom du bouton dans une boite de message
<pre class="brush:cpp;">
void MainWindow::displayButton(QString buttonName) {
    QMessageBox::information(this, "Bouton", "Bouton \"" + buttonName + "\" cliqué !");
}

L'erreur

Celle qui nous viendrait naturellement à l'esprit serait de connecter le signal à notre slot en ajoutant la valeur qui nous intéresse (comme le nom du bouton, par exemple) dans les paramètre du slot. Il suffirait théoriquement de faire cette connexion lorsqu'on est dans notre boucle, et ainsi transmettre notre "buttonName" :

QObject::connect(button, SIGNAL(clicked()), this, SLOT(displayButton(buttonName)));

Sauf que... NON ! Il s'agit d'une erreur.

Les signaux et les slots fonctionnent de telle manière qu'il est interdit de transmettre à un slot plus que ce que le signal transmet (note qu'à l'inverse, le slot n'est pas obligé de prendre en compte tous les paramètres envoyé par le signal). Le code ci-dessus ferait donc planter votre programme.

Solution 1 : récupération du pointeur

A l'intérieur même du slot, nous avons un moyen de récupérer le pointeur vers l'objet qui a émit le signal déclencheur. Ici, ce pointeur est un pointeur vers un QPushButton.

Ainsi, si notre slot ne prenait pas de paramètre, il pourrait ressembler à ceci :

void MainWindow::displayButton() {
    //QObject::sender() retourne le pointeur vers l'objet qui a émit le signal
    QPushButton *button = qobject_cast< qpushbutton* >(QObject::sender());

    //On récupère simplement le texte du bouton pour avoir le nom
    QString buttonName = button->text();

    QMessageBox::information(this, "Bouton", QString("Bouton \"" + buttonName + "\" cliqué !"));
}
Solution simple, mais probablement pas la plus judicieuse. Imaginons par exemple que vous ayez envie d'appeler ce slot via des signaux émits par des objets qui n'ont rien à voir avec des QPushButton ? Vous auriez alors un beau plantage.

Solution 2 : mapper les signaux et slot

Cette solution est en réalité le seul véritable moyen de transmettre un paramètre supplémentaire au slot, en plus de ceux issus du signal. Les objets de la classe QSignalMapper s'occupent de décider quel paramètre supplémentaire envoyer au slot en fonction de l'objet émettant le signal.

Avant notre boucle, on déclare le mapper :

QSignalMapper *signalMapper = new QSignalMapper(this);
QObject::connect(signalMapper, SIGNAL(mapped(QString)), this, SLOT(displayButton(QString)));
Le mapper est le seul qui sera directement connecté au slot de destination. En gros, on lui dit qu'il devra transmettre au slot une QString issue de son mappage.

Le mappage en lui même se fait en utilisant la méthode setMapping de QSignalMapper, qui consiste à lui dire "tel bouton correspond à telle QString". Et quand on fait le mappage, on décide complètement de ce que sera cette QString. On pourrait très bien en faire une constante, ou la récupérer d'ailleurs. Ici, on réutilise simplement la même chaîne que celle affichée sur le bouton :

signalMapper->setMapping(button, buttonName);
QObject::connect(button, SIGNAL(clicked()), signalMapper, SLOT(map()));

Et voilà, ça fonctionne. C'est magique !

Vous pouvez récupérer les sources de cet exemple ici : Widgets dynamiques et QSignalMapper.

Solution 3 : widget "wrapper" customisé

Une autre solution pourrait être d'étendre la classe QWidget, et de se faire un widget "customisé" qui contiendrait un bouton et un slot, puis de créer plusieurs instance de ce widget.

Pour l'exemple proposé dans cet article, il s'agit d'une solution un peu lourde, mais dans certains cas, cela peut vite s'avérer utile (si votre interface répète toujours le même "module" avec ses 3 boutons, sa liste déroulante, son label et sa case à cochée + tous les signaux et slots associés).

Je ne vais pas enter dans les détails de cette idée, et vous laisserai chercher par vous-même. Cependant, si vous souhaitez la mettre en place, voici un petit détail qui pourrait vous être utile : si jamais vous souhaitez remonter quelque-chose à la fenêtre ou au widget parent depuis votre widget personnalisé, rien ne vous empêche d'émettre manuellement les signaux du parent :

QWidget *papa = dynamic_cast<qwidget> (this->parentWidget());
emit papa->signalDePapa();</qwidget>

Conclusion

Nous avons vu ici quelques idées sur la manière de gérer la création de widgets de manière dynamique. Il ne s'agit très probablement pas des seules méthodes possible, et je ne vous ai pas non plus complètement prémâché le code. Mais j'espère que cet article saura vous être utile :-)

Si vous avez des questions ou remarques, surtout n'hésitez pas à me contacter ou à répondre dans les commentaires.