Cet article est la solution que j’avais proposé à l’exercice Qt sur la création d’un ColorPicker (voir l’article précédent du blog). Il décrit la création d’un widget permettant d’afficher et sélectionner les nuances de gris d’une couleur.
Le widget devra simplement afficher un fond (les nuances de gris d’une couleur) et gérer les événements de la souris (clique et déplacement pour sélectionner une nuance de gris).
Il est possible de partir de plusieurs classes différentes pour créer le nouveau widget. Voyons les avantages et inconvénients de chacune :
QWidget
est la classe parent de tous les widgets de Qt. Elle fournit l’interface de base commune de tous les objets visibles. Par défaut, elle n’affiche rien et ne fait rien.QScrollArea
est destinée à afficher le contenu d’un widget dans un autre, en gérant les barres de déplacement horizontale et verticale. Dans cet exercice, la présence de barre de déplacement n’est pas souhaitée et il est préférable d’ajuster la taille de la zone de dessin à la taille du widget.QLabel
permet d’afficher un texte ou une image sans interaction avec l’utilisateur. Même s’il est possible de surcharger les fonctions de gestion des événements souris, utiliser cette classe pour créer noter widget revient à ne pas respecter la conception de QLabel.QpushButton
permet également d’afficher des images mais gère aussi les événements souris. Mais cette classe n’est pas destiner à gérer la position précise des événements souris et de réagir différemment en fonction de celle-ci.Au final, même si certaine classe présentent des fonctionnalités qui seraient intéressantes (affichage d’images, gestion des clics), seule l’utilisation de QWidget respecte la conception des objets Qt.
En partant de QWidget, quels seront les fonctions qu’il faudra implémenter ?
Pour dessiner les différentes nuances de gris d’un couleur, deux méthodes sont envisageables :
Ces deux méthodes sont identique visuellement (mais présentent en réalité des petites différences) et permettent toute deux d’afficher toutes les nuances de gris d’une couleur.
Pour créer l’interface de notre classe, on crée une classe GradientWidget héritant de Qwidget, avec son constructeur :
class GradientWidget : public QWidget { public: explicit GradientWidget(QWidget *parent = 0);
Les fonctions de gestion des évènements surchargent les fonctions de même nom de QWidget :
protected: void mouseMoveEvent(QMouseEvent *event); void mousePressEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); void paintEvent(QPaintEvent *event); void resizeEvent(QResizeEvent *event);
La connexion avec les autres widgets est assurée par le signal envoyé lors de la sélection d’une couleur et le slot reçu lors du changement de couleur principale :
signals: void colorSelected(const QColor &color); public slots: void setMainColor(const QColor &color);
Les variables privées permettent de conserver les couleurs principale et sélectionnée, l’image dans laquelle on dessine les nuances de gris, la position du curseur et une variable boolean indiquant si la mouvement de la souris correspond à un clic ou non :
private: QColor m_main_color; QColor m_selected_color; QImage m_gradient_image; QPen m_cursor_pen; int m_cursor_diameter; QPoint m_cursor_position; bool m_tracking;
Les variab Pour finir, deux fonctions privées, permettant de redessiner les nuances de gris et de mettre à jour la couleur sélectionnée :
// private membres void updateGradientImage(); void updateSelectedColor(); };
Commençons par l’implémentation de la fonction updateSelectedColor(). Celle-ci récupère simplement la couleur du pixel de m_gradient_image à la position m_cursor_position, émet le signal colorSelected() puis appelle la fonction update() pour mettre ajour le widget :
void GradientWidget::updateSelectedColor() { m_selected_color = m_gradient_image.pixel(m_cursor_position); emit colorSelected(m_selected_color); update(); }
La première version de la fonction updateGradientImage() utilise l’approche à deux gradients :
void GradientWidget::updateGradientImage() {
On crée une QPixmap de la taille du widget puis un QPainter pour dessiner dedans. Par défaut, nous n’avons pas besoin de dessiner des traits donc on supprime le QPen :
QPixmap pixmap(size()); QPainter painter(&pixmap); painter.setPen(QPen(Qt::NoPen));
Pour dessiner des gradients linéaires, Qt propose une classe QLinearGradient. Il suffit donc de préciser les positions des points du gradient et les couleurs de ces points. Pour le premier gradient, les couleurs doivent aller du blanc (Qt::white) à la position (0, 0) à la couleur m_main_color à la position (with, 0) :
QLinearGradient h_gradient(QPointF(0.0, 0.0), QPointF(width(), 0.0)); h_gradient.setColorAt(0, Qt::white); h_gradient.setColorAt(1, m_main_color);
Puis on dessine un rectangle de la taille du widget :
painter.setBrush(QBrush(h_gradient)); painter.drawRect(rect());
Pour le second gradient, les couleurs doivent aller du transparent (Qt::transparent) à la position (0, 0) au noir (Qt::black) couleur m_main_color à la position (0, height) :
QLinearGradient v_gradient(QPointF(0.0, 0.0), QPointF(0.0, height())); v_gradient.setColorAt(0, Qt::transparent); v_gradient.setColorAt(1, Qt::black); painter.setBrush(QBrush(v_gradient)); painter.drawRect(rect());
On convertit ensuite la Qpixmap en Qimage et on la conserve dans m_gradient_image :
m_gradient_image = pixmap.toImage();
Pour finir, puisse que le gradient à été mis à jour, on met également à jour la couleur sélectionnée :
updateSelectedColor(); }
La seconde version de updateGradientImage() utilise deux boucles imbriquées qui parcourent l’ensemble des pixels de l’image et qui calcul la couleur en faisant varier la saturation et la valeur en fonction de la position (x, y). Pour commencer, il faut redimensionner l’image de destination si celle-ci n’est pas identique aux dimensions du widget :
void GradientWidget::updateGradientImage() { if (m_gradient_image.rect() != rect()) m_gradient_image = QImage(size(), Qimage::Format_RGB32);
La teinte est obtenue à partir de la couleur principale :
float h = m_main_color.hsvHueF();
On parcourt l’ensemble des pixels de l’image à l’aide de deux boucles imbriquées :
for (int s=0; s<width(); ++s) { for (int v=0; v(height()/3)) ? Qt::white : Qt::black);
Il reste plus qu’a dessiner un cercle à la position m_cursor_position :
painter.setPen(m_cursor_pen); painter.setBrush(QBrush(Qt::NoBrush)); painter.drawEllipse(m_cursor_position, defaut_diameter, defaut_diameter); }
Le diamètre du cercle est une constante définie en début du fichier d’implémentation :
const int defaut_diameter = 5;
Lors d’un évènement souris de type mousePressEvent, on teste si l’utilisateur a cliqué sur le bouton gauche :
void GradientWidget::mousePressEvent(QMouseEvent *event) { if (event->button() = Qt::LeftButton) {
Si c’est le cas, on active le suivi des mouvements de la souris, on récupère la positon de la souris dans m_cursor_position puis on met à jour la couleur sélectionnée :
m_tracking = true; m_cursor_position = event->pos(); updateSelectedColor(); } }
Lors d’un événement de type mouseReleaseEvent, on désactive le suivi de la souris et on met à jour la couleur sélectionnée :
void GradientWidget::mouseReleaseEvent(QMouseEvent *event) { if (event->button() = Qt::LeftButton) { m_tracking = false; m_cursor_position = event->pos(); updateSelectedColor(); } }
Les mouvements de la souris sont détectés par l’évènement mouseMoveEvent. Pour que ces événements soient pris en compte pour le widget, il faut les activer en utilisant la fonction setMouseTracking(true) dans le constructeur. Ces événements mouseMoveEvent sont activés même lorsqu’aucun bouton n’est appuyé. C’est la fonction de la variable m_tracking d’indiquer si on déplace la souris en conservant un bouton cliqué ou non. Lorsque l’on maintient appuyé un bouton de la souris et que l’on déplace celle-ci en dehors du widget, des événements mouseMoveEvent continuent d’être envoyé. Si on prend en compte ces évènements, on risque de demander la couleur de pixels en dehors de la taille de m_gradient_image, ce qui provoquera des erreurs. Il faut donc tester si la position de la souris est dans le widget. Au final, le code de mouseMoveEvent sera :
void GradientWidget::mouseMoveEvent(QMouseEvent *event) { if (m_tracking && rect().contains(event->pos())) { m_cursor_position = event->pos(); updateSelectedColor(); } }
Lors d’un changement de taille du widget, il suffit de remettre à jour le gradient :
void GradientWidget::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) m_cursor_position = rect().center(); updateGradientImage(); }
Il ne reste plus qu’a implémenter le constructeur :
GradientWidget::GradientWidget(QWidget *parent) : QWidget(parent), m_cursor_position(rect().center()), m_tracking(false) { setAttribute(Qt::WA_OpaquePaintEvent); setMouseTracking(true); updateGradientImage(); }
Ainsi que le slot setMainColor() :
void GradientWidget::setMainColor(const QColor &color) { m_main_color = color; updateGradientImage(); }