Cet article est la solution que j’avais proposé à l’exercice Qt sur la création d’un ColorPicker (voir le première article de cette série). Dans cette partie, j’utilise le QML pour créer le ColorPicker.
L’objet QML ColorPicker
L’objet ColorPicker permet d’afficher les nuances de gris d’une couleur et de sélectionner une nuance directement en cliquant dans l’item. Les composantes des couleurs sont manipulées directement puisqu’il n’est pas possible d’extraire les composantes d’une couleur donnée en QML.
La couleur principale est définie par les variables main_red, main_green et main_blue. La couleur sélectionnée est récupérée grâce aux variables selected_red, selected_green et selected_blue dans le contexte ColorPickerContext.
La taille de l’item est fixée à 256×256 :
1 2 3 4 5 | import Qt 4.7 Item { width: 256 height: 256 |
Pour dessiner les nuances de gris, on va utiliser deux gradients linéaires :
- le premier, horizontal, va du blanc à la couleur principale ;
- le second, vertical, va du transparent au noir.
Pour créer les gradients, on commence par définir un rectangle ayant les mêmes dimensions et position que l’item :
1 2 3 4 | Rectangle { anchors.fill: parent } |
On utilise ensuite l’objet gradient pour remplir le rectangle avec le dégradé. Par exemple pour le second gradient :
1 2 3 4 5 6 7 8 9 | Rectangle { anchors.fill: parent gradient: Gradient { GradientStop { position: 0.0; color: Qt.rgba(0, 0, 0, 0) } GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 1) } } } |
On obtient ainsi un gradient vertical allant du transparent au noir. Pour le premier gradient, il faut donc effectuer une rotation du rectangle de 90° après l’avoir correctement dimensionné et positionné :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Rectangle { width: parent.height height: parent.width transform: Rotation { angle: 90} x: parent.width y: 0 gradient: Gradient { GradientStop { position: 0.0; color: Qt.rgba(main_red, main_green, main_blue, 1)} GradientStop { position: 1.0; color: "white" } } } |
Pour visualiser la position de la couleur sélectionnée, on va afficher un cercle entourant la dernière position connue. Cependant, dessiner un cercle en QML est un peu complexe (il faut créer des objets path pour dessiner des courbes quadratiques et constituer le cercle). Pour simplifier, on va afficher une image de taille 8×8 pixels représentant un cercle avec le fond transparent :
On crée un objet image en indiquant la source du fichier. Par défaut, la taille de l’image est celle du fichier chargé :
1 2 3 4 | Image { source: "cursor.png" } |
Par défaut, on souhaite que le curseur soit placé au centre de l’item. Pour cela, on utilise les variables x et y :
1 2 3 4 5 6 | Image { x: width/2 y: height/2 source: "cursor.png" } |
Pour déplacer, le curseur en fonction de la position de la souris, il faut pouvoir identifier cette image :
1 2 3 4 5 6 7 | Image { id: cursor x: width/2 y: height/2 source: "cursor.png" } |
On pourra alors déplacer l’image en modifiant les variables x et y, en ajoutant le code suivant n’importe où dans notre item :
1 2 | cursor.x = 100 cursor.y = 100 |
Pour terminer, il faut pouvoir récupérer les évènements souris survenant sur notre item. Cela est réalisé en créer un objet MouseArea de même dimension que l’item :
1 2 3 4 | MouseArea { anchors.fill: parent } |
Il faut ensuite demander à cette MouseArea de récupérer les clics sur le bouton gauche de la souris :
1 2 3 4 5 | MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton } |
On va réagir à deux types d’évènements : un clic sur le bouton gauche, qui déclenche un évènement de type onPressed, et le déplacement de la souris avec le bouton enfoncé, qui déclenche un évènement de type onPositionChanged.
Lors de ces évènements, on récupère la position de la souris grâce aux variables mouseX et mouseY que l’on affecte aux positions de l’image avec cursor.x et cursor.y. Cependant, il ne faut pas oublier que la position de cursor correspond au coin en haut à gauche de l’image alors que la souris correspond au centre de l’image. Il faut donc corriger en fonction des dimensions de l’image :
1 2 | cursor.x = mouseX - 4 cursor.y = mouseY – 4 |
Pour terminer, après un changement de la couleur sélectionnée, il faut recalculer les variables selected_red, selected_green et selected_blue. Pour cela, on appelle un fonction JavaScript updateSelectedColor(), qu’il faudra créer :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton onPressed: { cursor.x = mouseX - 4 cursor.y = mouseY - 4 updateSelectedColor() } onPositionChanged: { cursor.x = mouseX - 4 cursor.y = mouseY - 4 updateSelectedColor() } } |
Pour connaître la couleur sélectionnée, on ne dispose pas de fonction équivalente à Qimage::pixel(). Les seuls éléments que l’on dispose sont : la position (x, y) de la couleur sélectionnée dans le widget de taille (width, height) et les composantes RGB de la couleur principale.
Il faut donc calculer les composantes RGB de la couleur sélectionnée à partir de ces éléments et de la méthode utilisée pour dessiner l’item.
Lorsque l’on dessine le premier gradient, on va du blanc (1, 1, 1) à la couleur principale (R, G, B). Le rapport entre la position cursor.x et la largeur est équivalent au rapport entre la composante de la couleur principale et le blanc :
On obtient alors l’équivalence suivante :
{r – 1} over {X} ~ = ~ {R – 1} over {width} ~ = ~ pente de la droite
Alors :
r ~ = ~ 1 ~ + ~ (R-1) {X} over {width}
De même pour les autres composantes :
g ~ = ~ 1 ~ + ~ (G-1) {X} over {width} newline
b ~ = ~ 1 ~ + ~ (B-1) {X} over {width}
Lorsque l’on dessine le second gradient, on va du transparent (alpha = 0) au noir (0, 0, 0, 1). La couleur final s’écrit :
couleur finale ~ = ~ couleur initiale * (1 – alpha) ~ + ~ noir * alpha ~ = ~ couleur initiale * (1-alpha)
La transparence alpha est liée à la position cursor.y par :
alpha ~ = ~ {Y} over {height}
On obtient donc la formule suivante pour la composante rouge :
r’ ~ = ~ r * (1 – {Y} over {height} ) newline
r’ ~ = ~ (1 + (R-1) {X} over {width}) * (1 – {Y} over {height} )
De même pour les autres composantes :
g’ ~ = ~ (1 + (G-1) {X} over {width}) * (1 – {Y} over {height} ) newline
b’ ~ = ~ (1 + (B-1) {X} over {width}) * (1 – {Y} over {height} )
La fonction JavaScript s’écrit alors, en utilisant ces formules :
1 2 3 4 5 6 7 8 9 10 11 12 | function updateSelectedColor() { ColorPickerContext.selected_red = (1 - (cursor.y / height)) * (1 + (cursor.x / width) * (main_red - 1)) ColorPickerContext.selected_green = (1 - (cursor.y / height)) * (1 + (cursor.x / width) * (main_green - 1)) ColorPickerContext.selected_blue = (1 - (cursor.y / height)) * (1 + (cursor.x / width) * (main_blue - 1)) } |
La classe GradientWidget
Pour utiliser cet item QML dans du code C++, il est nécessaire d’écrire un wrapper pour récupérer les signaux et slots de l’item. Cette classe n’est pas un widget et dérive donc de QObject (pour pouvoir utiliser le système de signaux et slots) :
1 2 3 | class GradientWidget : public QObject { Q_OBJECT |
Pour récupérer la couleur sélectionnée, il faut créer une variable pour chaque composante et utiliser la macro Q_PROPERTY pour la rendre accessible en QML. Il faut également définir les fonctions d’écriture (pour modifier la variable depuis le QML) et de lecture (pour lire la variable à l’extérieur de la classe) :
1 2 3 4 5 6 7 8 9 10 | public: Q_PROPERTY(float selected_red READ selectedRed WRITE setSelectedRed) private: float selected_red; float selectedRed() const { return selected_red; } void setSelectedRed(const float red) { selected_red = red; selectedColorChanged(); } |
Après avoir mis à jour la couleur avec la fonction setSelectedColor(), il faut émettre un signal contenant la couleur créée à partir des composantes :
1 2 3 4 5 6 7 8 | signals: void colorSelected(const QColor &color); private: void selectedColorChanged() { emit colorSelected( QColor::fromRgbF(selected_red, selected_green, selected_blue)); } |
Il faut également définir les variables et fonctions pour les deux autres composantes :
1 2 3 4 5 6 | Q_PROPERTY(float selected_green READ selectedGreen WRITE setSelectedGreen) Q_PROPERTY(float selected_blue READ selectedBlue WRITE setSelectedBlue) |
La création de l’item ColorPicker est réalisée dans le constructeur de la classe :
1 2 | public: GradientWidget(QWidget *parent = 0); |
Dans ce constructeur, il faut créer un objet QDeclarativeView pour afficher l’item QML puis fournir le code QML à l’aide la fonction setSource. La taille est fixée à 256×256, comme dans le code QML. Dans cet exemple, le code QML est fournit dans un fichier .qml et référencé dans un fichier ressource .qrc :
1 2 3 4 5 6 | GradientWidget::GradientWidget(QWidget *parent) : QObject(parent) { view = new QdeclarativeView(); view->resize(256, 256); view->setSource(QUrl("qrc:/qml/colorpicker.qml")); |
Pour permettre au code QML de transmettre les variables contenant les composantes de la couleur sélectionnée, il faut récupérer le contexte par défaut de la vue et définir une propriété ColorPickerContext dans celui-ci. Cette propriété permet d’accéder à l’objet dans le code QML à partir de la variable ColorPickerContext et permettre ainsi d’accéder aux propriétés définies ci-dessus :
1 2 | context = view->rootContext(); context->setContextProperty("ColorPickerContext", this); |
Les variables view et context sont définies dans l’en-tête de la classe par :
1 2 3 | private: QDeclarativeView* view; QDeclarativeContext* context; |
Lors de la création de l’item, on attribue également une couleur principale par défaut à l’aide de la fonction setMainColor() :
1 2 | setMainColor(Qt::red); } |
Cette fonction setMainColor() est un slot ayant un paramètre, la couleur a attribuer :
1 2 | public slots: void setMainColor(const QColor &color); |
Pour transmettre cette couleur au code QML, on utilise la fonction setContextProperty(), qui permet d’attribuer une valeur à une variable QML :
1 2 3 4 5 6 | void GradientWidget::setMainColor(const QColor &color) { context->setContextProperty("main_red", color.redF()); context->setContextProperty("main_green", color.greenF()); context->setContextProperty("main_blue", color.blueF()); } |
Pour terminer, il faut créer les fonctions show() et move() pour pouvoir afficher et déplacer la vue :
1 2 3 4 5 6 7 8 9 10 11 12 | public: void show(); void move(int x, int y); void GradientWidget::show() { view->show(); } void GradientWidget::move(int x, int y) { view->move(x, y); } |
Le résultat final
Voici le rendu obtenu :