Qt fournit au programmeur des fonctionnalités très riches, mais l'ensemble de widgets est limité. Si aucun des éléments disponibles ne convient, vous devez dessiner quelque chose de votre choix. La manière la plus simple - d'utiliser des images prêtes à l'emploi - présente de sérieux inconvénients: la nécessité de stocker des images dans un fichier ou des ressources, des problèmes d'évolutivité et de portabilité des formats d'image. Ce qui suit décrit l'utilisation des principes des graphiques vectoriels sans utiliser les images vectorielles réelles.
Préambule
Tout a commencé avec le fait qu'une fois qu'une indication de signes à un bit était nécessaire. Certaines applications reçoivent des données sur un port, le package doit être démonté et affiché à l'écran. Ce serait bien en même temps d'imiter le tableau de bord familier. Pour afficher les données numériques, Qt propose «prête à l'emploi» la classe QLCDNumber, similaire aux indicateurs familiers à sept segments, mais quelque chose n'est pas visible dans les lampes simples.
L'utilisation de drapeaux (ce sont des cases à cocher) et de commutateurs (ce sont des boutons radio) à ces fins est mauvaise, et voici une liste de raisons:
- C'est faux sémantiquement. Boutons - ce sont des boutons qui sont destinés à la saisie de l'utilisateur et non à lui montrer quoi que ce soit.
- Cela implique la seconde: l'utilisateur s'efforce de pousser sur ces boutons. Si, en même temps, la mise à jour des informations n'est pas particulièrement rapide, l'indication s'allongera et l'utilisateur signalera un programme défectueux en riant méchamment.
- Si vous verrouillez le bouton pour appuyer sur (setEnabled (false)), il devient alors gris moche. Je me souviens qu'à Delphi, dans la région de la version 6, il y avait une telle feinte avec des oreilles: on pouvait mettre un drapeau sur le panneau et désactiver la disponibilité du panneau, pas le drapeau, alors le drapeau n'était ni gris ni actif. Cette astuce ne fonctionne pas ici.
- Les boutons ont le focus d'entrée. Par conséquent, s'il y a des éléments d'entrée dans la fenêtre et que l'utilisateur les parcourt à l'aide de la touche Tab, il devra parcourir les éléments de sortie, ce qui est gênant et laid.
- En fin de compte, ces boutons sont tout simplement esthétiques, surtout à côté du sept-segment.
Conclusion: vous devez dessiner vous-même une ampoule.
Farine de choix
J'ai d'abord cherché des solutions toutes faites. En ce temps lointain, quand j'ai utilisé Delphi, vous ne pouviez trouver qu'une quantité gigantesque de composants finis, à la fois d'entreprises sérieuses et de fabricants amateurs. Qt a beaucoup de mal avec ça. QWT comporte certains éléments, mais pas cela. Je n'ai pas du tout vu d'amateurisme. Probablement, si vous creusez correctement sur Github, vous pouvez trouver quelque chose, mais je le ferai probablement plus rapidement moi-même.
La première chose qui s'est suggérée par rapport à celle faite maison était d'utiliser deux fichiers d'images avec des images de la lumière allumées et éteintes. Mauvais:
- Il faut trouver de bonnes images (ou dessiner, mais je ne suis pas artiste);
- La question de principe est la suivante: attacher n'est pas bon, même des images, même couché sous vos pieds;
- Ils doivent être stockés quelque part. Les fichiers sont très mauvais: effacés accidentellement - et il n'y a pas de boutons. Les ressources sont meilleures, mais je n'ai pas non plus l'impression de pouvoir m'en sortir;
- Aucune évolutivité;
- La personnalisation (couleurs, par exemple) n'est obtenue qu'en ajoutant des fichiers. Autrement dit, exigeant en ressources et rigide.
La deuxième chose qui découle de la première est d'utiliser des images vectorielles au lieu d'images. De plus, Qt peut rendre SVG. Ici, c'est déjà un peu plus facile avec la recherche de l'image elle-même: il y a beaucoup de leçons sur les graphiques vectoriels dans le réseau, vous pouvez trouver quelque chose de plus ou moins adapté et l'adapter à vos besoins. Mais la question reste de stockage et de personnalisation, et le rendu n'est pas gratuit pour les ressources. Des sous, bien sûr, mais quand même ...
Et le troisième découle du second: vous pouvez utiliser les principes des graphiques vectoriels pour des images auto-dessinées! Un fichier image vectorielle sous forme de texte indique quoi et comment dessiner. Je peux spécifier le même code à l'aide de didacticiels vectoriels. Heureusement, l'objet QPainter dispose des outils nécessaires: un stylo, un pinceau, un dégradé et des primitives de dessin, voire un remplissage de texture. Oui, les outils sont loin de tout: il n'y a pas de masques, de modes de fusion, mais absolument aucun photoréalisme n'est requis.
J'ai cherché quelques exemples sur le net. Il a suivi la première leçon qui a suivi: «Nous dessinons un bouton dans l'éditeur graphique d'Inkscape» sur le site «C'est facile à dessiner». Le bouton de cette leçon ressemble beaucoup plus à une ampoule qu'à un bouton, ce qui me convient parfaitement. Je fais un brouillon: au lieu d'Inkscape, un projet dans Qt.
Test de plumes
Je crée un nouveau projet. Je choisis le nom du projet (car je veux faire quelque chose comme une LED RGB) et le chemin vers celui-ci. Je choisis la classe de base QWidget et le nom RgbLed, je refuse de créer un fichier formulaire. Le projet par défaut après le lancement fait une fenêtre vide, il est toujours sans intérêt.
Préparation au dessin
Il y a un blanc. Vous devez maintenant obtenir les membres privés de la classe, qui détermineront la géométrie de l'image. Un avantage essentiel des graphiques vectoriels est leur évolutivité, donc il devrait y avoir un minimum de nombres constants, et ils ne fixent que les proportions. Les dimensions seront recalculées dans l'événement resizeEvent (), qui devra être redéfini.
Dans le didacticiel de dessin utilisé, les dimensions sont spécifiées en pixels au fur et à mesure. Je dois déterminer à l'avance ce que j'utiliserai et comment recompter.
Une image dessinée se compose des éléments suivants:
- bague extérieure (inclinée vers l'extérieur, partie de la jante convexe)
- bague intérieure (inclinée vers l'intérieur)
- Boîtier de lampe à LED, "verre"
- ombre sur le bord du verre
- haut point culminant
- évasement en bas
Les cercles concentriques, c'est-à-dire tout sauf l'éblouissement, sont déterminés par la position du centre et du rayon. L'éblouissement est déterminé par le centre, la largeur et la hauteur, et la position des X centres d'éblouissement coïncide avec la position du centre X de l'image entière.
Pour calculer les éléments de la géométrie, vous devez déterminer laquelle est la plus grande - la largeur ou la hauteur, car l'ampoule est ronde et doit s'insérer dans un carré avec un côté égal à la plus petite des deux dimensions. J'ajoute donc les membres privés correspondants au fichier d'en-tête.
codeprivate: int height; int width; int minDim; int half; int centerX; int centerY; QRect drawingRect; int outerBorderWidth; int innerBorderWidth; int outerBorderRadius; int innerBorderRadius; int topReflexY; int bottomReflexY; int topReflexWidth; int topReflexHeight; int bottomReflexWidth; int bottomReflexHeight;
Ensuite, je redéfinis la fonction protégée qui est appelée lorsque le widget est redimensionné.
code protected: void resizeEvent(QResizeEvent *event); void RgbLed::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); this->height = this->size().height(); this->width = this->size().width(); this->minDim = (height > width) ? width : height; this->half = minDim / 2; this->centerX = width / 2; this->centerY = height / 2; this->outerBorderWidth = minDim / 10; this->innerBorderWidth = minDim / 14; this->outerBorderRadius = half - outerBorderWidth; this->innerBorderRadius = half - (outerBorderWidth + innerBorderWidth); this->topReflexY = centerY - (half - outerBorderWidth - innerBorderWidth) / 2; this->bottomReflexY = centerY + (half - outerBorderWidth - innerBorderWidth) / 2; this->topReflexHeight = half / 5; this->topReflexWidth = half / 3; this->bottomReflexHeight = half / 5; this->bottomReflexWidth = half / 3; drawingRect.setTop((height - minDim) / 2); drawingRect.setLeft((width - minDim) / 2); drawingRect.setHeight(minDim); drawingRect.setWidth(minDim); }
Ici, le côté du carré dans lequel l'ampoule est inscrite est calculé, le centre de ce carré, le rayon de la jante occupant la zone maximale possible, la largeur de la jante, dont la partie extérieure doit être 1/10 du diamètre et le 1/14 intérieur. Ensuite, la position de l'éblouissement, qui sont situés au milieu des rayons supérieur et inférieur, est calculée, la largeur et la hauteur sont sélectionnées à l'œil nu.
De plus, dans les champs protégés, j'ajouterai immédiatement un ensemble de couleurs à utiliser.
code QColor ledColor; QColor lightColor; QColor shadowColor; QColor ringShadowDarkColor; QColor ringShadowMedColor; QColor ringShadowLightColor; QColor topReflexUpColor; QColor topReflexDownColor; QColor bottomReflexCenterColor; QColor bottomReflexSideColor;
Par les noms, il est à peu près clair que ce sont les couleurs de l'ampoule, la partie claire de l'ombre, la partie sombre de l'ombre, les trois couleurs de l'ombre annulaire autour de l'ampoule et les couleurs des gradients d'éblouissement.
Les couleurs doivent être initialisées, je vais donc compléter la pièce du designer.
code RgbLed::RgbLed(QWidget *parent) : QWidget(parent), ledColor(Qt::green), lightColor(QColor(0xE0, 0xE0, 0xE0)), shadowColor(QColor(0x70, 0x70, 0x70)), ringShadowDarkColor(QColor(0x50, 0x50, 0x50, 0xFF)), ringShadowMedColor(QColor(0x50, 0x50, 0x50, 0x20)), ringShadowLightColor(QColor(0xEE, 0xEE, 0xEE, 0x00)), topReflexUpColor(QColor(0xFF, 0xFF, 0xFF, 0xA0)), topReflexDownColor(QColor(0xFF, 0xFF, 0xFF, 0x00)), bottomReflexCenterColor(QColor(0xFF, 0xFF, 0xFF, 0x00)), bottomReflexSideColor(QColor(0xFF, 0xFF, 0xFF, 0x70)) { }
N'oubliez pas non plus d'insérer dans le fichier d'en-tête les inclusions de classes qui seront nécessaires lors du dessin.
code #include <QPainter> #include <QPen> #include <QBrush> #include <QColor> #include <QGradient>
Ce code se compile avec succès, mais rien n'a changé dans la fenêtre du widget. Il est temps de commencer à dessiner.
Dessin
J'entre dans une fonction fermée
void drawLed(const QColor &color);
et redéfinir la fonction protégée
void paintEvent(QPaintEvent *event);
L'événement redessiner provoquera le dessin réel, auquel la couleur du "verre" est transmise comme paramètre.
code void RgbLed::paintEvent(QPaintEvent *event) { QWidget::paintEvent(event); this->drawLed(ledColor); }
Jusqu'à présent. Et nous commençons à remplir progressivement la fonction de dessin.
code void RgbLed::drawLed(const QColor &color) { QPainter p(this); QPen pen; pen.setStyle(Qt::NoPen); p.setPen(pen); }
Tout d'abord, un objet d'artiste est créé, qui sera engagé dans le dessin. Ensuite, un crayon est créé qui est nécessaire pour qu'il n'y ait pas de crayon: dans cette image, le trait de contour n'est pas simplement nécessaire, mais pas du tout nécessaire.
Ensuite, le premier cercle est tracé approximativement conformément à la leçon sur les graphiques vectoriels: un grand cercle, rempli d'un dégradé radial. Le dégradé a un point d'ancrage clair en haut, mais pas au bord même, et un point sombre en bas, mais pas non plus au bord même. Un pinceau est créé sur la base du dégradé, ce peintre au pinceau peint un cercle (c'est-à-dire une ellipse inscrite dans un carré). Il s'avère qu'un tel code
code QRadialGradient outerRingGradient(QPoint(centerX, centerY - outerBorderRadius - (outerBorderWidth / 2)), minDim - (outerBorderWidth / 2))
L'environnement met l'accent sur le paramètre de couleur de la fonction drawLed car il n'est pas utilisé. Laissez-le tolérer, il n'est pas encore nécessaire, mais il en aura bientôt besoin. Le projet lancé produit le résultat suivant:
Ajoutez un autre lot de code.
code QRadialGradient innerRingGradient(QPoint(centerX, centerY + innerBorderRadius + (innerBorderWidth / 2)), minDim - (innerBorderWidth / 2))
Presque le même cercle, mais de taille plus petite et à l'envers. Nous obtenons l'image suivante:
Enfin, vous avez besoin de la couleur du verre:
code QColor dark(color.darker(120)); QRadialGradient glassGradient(QPoint(centerX, centerY), innerBorderRadius); glassGradient.setColorAt(0, color); glassGradient.setColorAt(1, dark); QBrush glassBrush(glassGradient); p.setBrush(glassBrush); p.drawEllipse(QPoint(centerX, centerY), innerBorderRadius, innerBorderRadius);
Ici, en utilisant la fonction plus sombre de la couleur transmise, la même couleur est obtenue, mais plus sombre, pour organiser le dégradé. Coefficient 120 sélectionné à l'œil. Voici le résultat:
Ajoutez une ombre annulaire autour du verre. Cela se fait dans la leçon sur les graphiques vectoriels, et cela devrait ajouter du volume et du réalisme:
code QRadialGradient shadowGradient(QPoint(centerX, centerY), innerBorderRadius); shadowGradient.setColorAt(0, ringShadowLightColor); shadowGradient.setColorAt(0.85, ringShadowMedColor); shadowGradient.setColorAt(1, ringShadowDarkColor); QBrush shadowBrush(shadowGradient); p.setBrush(shadowBrush); p.drawEllipse(QPoint(centerX, centerY), innerBorderRadius, innerBorderRadius);
Il y a un dégradé en trois étapes, de sorte que l'ombre est plus épaisse jusqu'au bord et pâlit vers le centre. Il se présente comme ceci:
Ajoutez des surbrillances à la fois. La surbrillance supérieure, contrairement à la partie inférieure (et à tous les autres éléments), est un dégradé linéaire. L'artiste de moi est moyen, je prends ma parole pour l'auteur de la leçon. Peut-être y a-t-il une part de vérité à cela, je ne vais pas expérimenter avec différents types de dégradés.
code QLinearGradient topTeflexGradient(QPoint(centerX, (innerBorderWidth + outerBorderWidth)), QPoint(centerX, centerY))
En fait, c'est tout, une ampoule prête à l'emploi, comme sur KDPV.
La visibilité de l'éblouissement et du renflement du verre est affectée par la couleur, ou plutôt par son obscurité. Il peut être judicieux d'ajouter un ajustement pour la luminosité de l'éblouissement et le coefficient de variation dans la fonction plus sombre en fonction de l'obscurité, mais c'est du perfectionnisme, je pense.
Voici un exemple d'utilisation dans une fenêtre de programme.
Choyer
Pour le plaisir, vous pouvez jouer avec des fleurs. Par exemple, remplacer un événement de clic de souris protégé
void mousePressEvent(QMouseEvent *event);
de cette façon:
code void RgbLed::mousePressEvent(QMouseEvent *event) { static int count = 0; if (event->button() == Qt::LeftButton) { switch (count) { case 0: ledColor = Qt::red; count++; break; case 1: ledColor = Qt::green; count++; break; case 2: ledColor = Qt::blue; count++; break; case 3: ledColor = Qt::gray; count++; break; default: ledColor = QColor(220, 30, 200); count = 0; break; } this->repaint(); } QWidget::mousePressEvent(event); }
sans oublier d'ajouter des événements de souris à l'en-tête:
#include <QMouseEvent>
Maintenant, un clic de souris sur le composant changera la couleur de l'ampoule: rouge, vert, bleu, gris et une lumière aléatoire de la lampe.
Épilogue
Quant au dessin, c’est tout. Et le widget devrait ajouter des fonctionnalités. Dans mon cas, j'ai ajouté un champ booléen «use state», un autre champ booléen qui définit l'état On ou Off et les couleurs par défaut de ces états, ainsi que des getters et setters ouverts pour tout cela. Ces champs sont utilisés dans la fonction paintEvent () pour sélectionner la couleur transmise à drawLed () en tant que paramètre. Par conséquent, vous pouvez désactiver l'utilisation des états et définir l'ampoule sur n'importe quelle couleur, ou activer les états et activer ou désactiver l'ampoule en fonction des événements. Il est particulièrement pratique de faire du sélecteur d'état un emplacement ouvert et conjoint que ce soit avec le signal qui doit être surveillée.
L'utilisation de mousePressEvent démontre qu'un widget peut être créé non seulement un indicateur, mais aussi un bouton, ce qui le rend enfoncé, relâché, plié, tordu, coloré et tout ce que vous voulez pour les événements de pointage, de clic et de relâchement.
Mais ce n'est plus fondamental. L'objectif était de montrer où vous pouvez prendre des modèles lorsque vous dessinez vos propres widgets et comment ce dessin est facile à implémenter sans utiliser d'images raster ou vectorielles, dans des ressources ou des fichiers.