Un ColorPicker avec Qt – Version Qt

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.

Conception

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 :

  1. 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.
  2. 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.
  3. 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.
  4. 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 ?

  1. L’affichage des nuances de gris sera réalisée dans la fonction paintEvent(), qui prend en charge le dessin du widget. Il suffit de dessiner les nuances de gris puis la position de la couleur sélectionnée par un petit cercle.
  2. La création des nuances de gris peut être réalisée directement dans paintEvent(). Cependant, cela implique de redessiner l’image à chaque déplacement de la souris, ce qui peut rendre le widget non fluide. De plus, la récupération de la couleur sélectionnée utilisera la fonction QImage::pixel(). Au final, il est donc préférable de dessiner les nuances de gris dans une QImage et d’afficher cette QImage dans la fonction paintEvent(). Cette image sera recalculée que lors d’un changement de la couleur principale ou lors du redimensionnement du widget.
  3. La fonction resizeEvent() sera utilisée pour mettre à jour l’image contenant les nuances de gris.
    Les évènements souris seront gérés par les fonctions mousePressEvent(), mouseMoveEvent() et mouseReleaseEvent().
  4. Le changement de couleur principale et la sélection d’une couleur utilisera le système de signaux et slots, pour faciliter l’interaction entre les widgets.

Pour dessiner les différentes nuances de gris d’un couleur, deux méthodes sont envisageables :

  1. Dessiner deux gradients superposés : un premier gradient linéaire, horizontal, allant du blanc (à gauche) à la couleur principale (à droite) ; un second gradient linéaire, horizontal, allant du transparent (en haut) au noir (en bas).
  2. Faire varier les composantes S (saturation) et V (valeur) d’une couleur en fonction des coordonnées (x, y) d’un pixel. La saturation permet d’aller du blanc (S=0) à la couleur (S=255) et correspond donc à l’axe horizontal. La valeur permet d’aller du noir (V=0) à la couleur (V=255) et correspond donc à l’axe vertical.

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.

Interface

Pour créer l’interface de notre classe, on crée une classe GradientWidget héritant de Qwidget, avec son constructeur :

1
2
3
4
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 :

1
2
3
4
5
6
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 :

1
2
3
4
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 :

1
2
3
4
5
6
7
8
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 :

1
2
3
4
    // private membres
    void    updateGradientImage();
    void    updateSelectedColor();
};

Implémentation

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 :

1
2
3
4
5
6
void GradientWidget::updateSelectedColor()
{
    m_selected_color = m_gradient_image.pixel(m_cursor_position);
    emit colorSelected(m_selected_color);
    update();
}

Création des nuances avec QLinearGradient

La première version de la fonction updateGradientImage() utilise l’approche à deux gradients :

1
2
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 :

1
2
3
    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) :

1
2
3
    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 :

1
2
    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) :

1
2
3
4
5
    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 :

1
    m_gradient_image = pixmap.toImage();

Pour finir, puisse que le gradient à été mis à jour, on met également à jour la couleur sélectionnée :

1
2
    updateSelectedColor();
}

Création des nuances avec HSV

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 :

1
2
3
4
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 :

1
    float h = m_main_color.hsvHueF();

On parcourt l’ensemble des pixels de l’image à l’aide de deux boucles imbriquées :

1
2
3
    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 :

1
2
3
4
    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 :

1
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 :

1
2
3
4
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 :

1
2
3
4
5
        m_tracking = true;
        m_cursor_position = event->pos();
        updateSelectedColor();
    }
}

Lors d’un évènement de type mouseReleaseEvent, on déssactive le suivi de la souris et on met à jour la couleur sélectionnée :

1
2
3
4
5
6
7
8
9
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 :

1
2
3
4
5
6
7
8
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 :

1
2
3
4
5
6
void GradientWidget::resizeEvent(QResizeEvent *event)
{
    Q_UNUSED(event)
    m_cursor_position = rect().center();
    updateGradientImage();
}

Il ne reste plus qu’a implémenter le constructeur :

1
2
3
4
5
6
7
8
9
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() :

1
2
3
4
5
void GradientWidget::setMainColor(const QColor &color)
{
    m_main_color = color;
    updateGradientImage();
}

Le résultat final

Voici le rendu obtenu :

null

Télécharger les sources de cet article.