PROJET AUTOBLOG


Portail Yosko.net

Site original : Portail Yosko.net

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

Qt : création dynamique de widgets

lundi 3 septembre 2012 à 17:25
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);

Ce slot fera tout simplement un affichage du nom du bouton dans une boite de message

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(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 (this->parentWidget());
emit papa->signalDePapa();

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.

Pixel Art en ombres et lumières

mardi 28 août 2012 à 12:00
Vous êtes vous déjà demandé ce qui faisait la richesse des couleurs d'une image, ou au contraire ce qui vous donnait l'impression qu'elle était plate ou vide ?

En réfléchissant au problème, je suis arrivé à des conclusions quelque peu inattendues et intéressantes sur l'expression des ombres et lumières dans le Pixel Art.

Ombres et lumières

Pour qu'une image donne une impression de profondeur et de réalisme, elle se doit de rendre des effets d'ombre et de lumière.

Ces ombres et lumières apparaissent sous forme de dégradés plus ou moins marqués en fonction des zones. Sur un véritable dessin ou une photo, il y aurait une "infinité" de variantes de couleurs sur ces dégradés. Dans mon exemple ci-dessous, en pixel art, il m'a paru préférable de ne conserver au maximum que 6 variantes d'une même couleur pour garder un effet "jeu vidéo à l'ancienne", et surtout pour fonctionner dans un espace discret, plutôt que continu.


L'image de droite comporte une variation de saturation et de luminosité, contrairement à celle de gauche

Comme on peut le voir, l'image de gauche parait franchement "plate", aucun volume n'est exprimé, et elle semble un peu vide.
Dans la seconde, on constate bien les variantes (sur le violet des cheveux par exemple), et on a un peu plus l'impression d'avoir une expression de la troisième dimension et de la lumière.

Les variantes ont été constituées de manière très basique (en reprenant le modèle HSL déjà évoqué plus tôt) : on conserve ici la même teinte, et on fait varier la luminosité (ce qui semble logique, puisqu'on cherche à exprimer une variation de lumière sur une même surface) et la saturation.

Mais il semble encore manquer quelque chose...
En effet, cette méthode est trop simpliste, et ça se voit. Les effets de lumière paraissent étranges, voire "plats", encore une fois...

Couleurs des ombres et lumières

Il y a deux éléments primordiaux qui n'ont pas été pris en compte jusqu'ici :
  1. La lumière a une couleur. Toute source de lumière tend vers une couleur, le Soleil y compris (en réalité, le Soleil émet une lumière blanche, mais, après être passée dans l’atmosphère, le bleu s'est dispersé dans le ciel, et on voit surtout du jaune). De ce fait, une surface bleue éclairée par une lampe jaune devrait paraitre un peu plus verte dans les zones éclairées, et plus bleue dans les zones d'ombre
  2. Les ombres aussi comportent une couleur. En effet, si vous vous mettez à l'ombre d'un bâtiment orange, votre peau paraitra plus orange. Ce ne sont pas les ombres elles-mêmes qui génèrent la couleur. En réalité, l'élément qui entraine l'ombre reflète aussi une infime partie de la lumière dans sa couleur.

Un bon exemple de ces deux points : la peau parait "rose" du fait de la couleur rouge du sang. Et souvent, les sources de lumière artificielles ont tendance à tendre vers le jaune.
On constatera que les photos et dessins de peau (je ne vous pousse pas à aller regarde du nu, mais c'est le meilleur moyen de le constater) ont tendance à accentuer les rouges dans les ombres, et à accentuer le jaune/orange dans les zones de lumière.
C'est particulièrement visible sur ce dessin et un peu moins sur cette photo

Méthode empirique

Pour simplifier, on pourrait partir du principe que toutes les sources de lumière sont jaunes, et que plus un objet est éclairé, plus il tend vers cette couleur, et plus il est dans l'ombre, plus il tend à refléter sa propre couleur.

C'est un peu comme ça que je le voyais, jusqu'à ce que j'analyse les couleurs utilisées dans divers sprites issus de jeux-vidéo différents (Ragnarok online, Jump Superstars, et divers autres). En prenant divers exemples, je me suis rendu compte que les 6 variantes de couleur avaient tendance à se construire sur le même principe, c'est à dire que les variations de teinte, de saturation et de luminosité entre les variantes d'une même couleur pouvait très souvent être exprimées avec la même formule mathématique.

Note : il n'y a aucune correspondance de couleur entre les graphiques, ni avec la couleur affichée
Les variantes de couleurs vont de 0 (la plus sombre) à 5 (la plus claire)

Si on met de côté le drôle d'écart de la couleur 4, on a semble-t-il quelque chose d'assez linéaire. Chaque droite semble avoir une pente équivalente, mais un offset légèrement différent.

Ici, on constate que chacune des courbes se découpe en trois parties : une pente normale sur les premiers points, une pente forte au milieu et une pente faible sur la fin. Cela laisse imaginer une équation d'ordre 3 ou plus. Ici encore, les variations entre chaque courbe peuvent probablement s'exprimer par un offset, ou au pire par un facteur (ça reste franchement empirique et approximatif, mais avons-nous réellement besoin de plus de précision ?).

Bref, jusqu'ici, on constate que plus on est dans les ombres, plus la saturation est forte et la luminosité faible. Ca n'a rien de surprenant. Le plus intéressant arrive :

Clairement, la variation de de teinte se présente sous forme linéaire. Mais les pentes varient beaucoup. Il y a donc une logique, mais laquelle ?

Elles semblent plus ou moins converger vers un point. Et ce point de convergence est {X=15, Y=75}. En bref, elles tendent toutes vers la couleur "75", qui correspond à un vert un peu jaune !

J'en entend dans le fond me dire que ce n'est pas le cas de la couleur 3, qui diverge (verge !). Sauf que... si !
En réalité, les valeurs que nous manipulons ici sont des angles exprimés en degrés. Si on calcule où semble tendre la couleur 3, on tombe sur 435°, qui équivaut en fait à 75° (rappel : 360° = 0°).

Si on applique cette logique à notre image du début, cela donne :


L'image de droite comporte une variation de teinte, contrairement à celle de gauche

Je vous l'accorde, la différence ne saute pas aux yeux (elle est surtout visible dans les cheveux et le vêtement. La peau est un peu ratée, ici...). Et pourtant, c'est un élément crucial pour obtenir un effet plus réaliste.

Traduction mathématique

Après quelques recherches et interpolations, j'en viens à cette description du comportement des variations de couleurs sur les sprites de jeu vidéo entre ombre et lumière, que les graphistes les aient calculées ou choisies "au feeling", il semblerait qu'ils ont toujours tendance à suivre la même logique mathématique, qui peut s'exprimer comme suit :

Teinte

y = x + ( 75 - x ) 15

(x = 0) étant la teinte appliquée à la première variante, la plus sombre.
On pourrait très bien faire varier le diviseur pour tendre plus ou moins rapidement vers le vert à 75°.

Saturation

y = 55 - 7,2 x

Une droite relativement simple à calculer (comme la plupart de mes calculs, il s'agit d'une conclusion empirique, bien sûr).

Luminosité

y = 95 + 51,58 x - 18,55 x 2 + 2,92 x 3

Merci à l'outil d'interpolation de WolframAlpha, qui m'a permis d'arriver à un résultat exploitable, que j'ai ensuite simplifié un peu.

Pistes de réflexion

Maintenant que nous avons un moyen mathématique de calculer des variantes de couleur qui donneront un certain réalisme à notre image, que reste-t-il en suspens ?

Tout d'abord, pourquoi le vert 75° ? Ce vert-jaune vers lequel semblent tendre toutes les couleurs dans leurs variantes claires pourrait-il avoir un lien avec l'un des éléments suivants ?
Autre question : à partir de quel point la droite d'une teinte devrait diverger ? J'ai deux hypothèses là-dessus, mais aucune preuve concrète pour l'étayer :

EDIT : et je compte réutiliser toute cette logique dans une variante de l'Anime Girl Generator, réalisée en C++/Qt, cette fois-ci.
Des nouvelles sur le sujet d'ici quelques jours.

Error happened! 0 - Call to a member function query() on null In: /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php:200 http://www.ecirtam.net/autoblogs/autoblogs/wwwyoskonet_39296e46d42dce00b6c8fda59e3c365e3bea6524/?5 #0 /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php(414): VroumVroum_Config->setDisabled() #1 /var/www/ecirtam.net/autoblogs/autoblogs/autoblog.php(999): VroumVroum_Blog->update() #2 /var/www/ecirtam.net/autoblogs/autoblogs/wwwyoskonet_39296e46d42dce00b6c8fda59e3c365e3bea6524/index.php(1): require_once('...') #3 {main}