Comment obtenir des formes de coins arrondis en C++ à l'aide des courbes de Bézier et de QPainter : un guide étape par étape
Publié: 2022-03-11introduction
La tendance actuelle dans la conception graphique consiste à utiliser de nombreux coins arrondis dans toutes sortes de formes. Nous pouvons observer ce fait sur de nombreuses pages Web, appareils mobiles et applications de bureau. Les exemples les plus notables sont les boutons-poussoirs d'application, qui sont utilisés pour déclencher une action lorsqu'ils sont cliqués. Au lieu d'une forme strictement rectangulaire avec des angles de 90 degrés dans les coins, ils sont souvent dessinés avec des coins arrondis. Les coins arrondis rendent l'interface utilisateur plus lisse et plus agréable. Je n'en suis pas entièrement convaincu, mais mon ami designer me le dit.
Les éléments visuels des interfaces utilisateur sont créés par les concepteurs, et le programmeur n'a qu'à les placer aux bons endroits. Mais que se passe-t-il lorsque nous devons générer une forme avec des coins arrondis à la volée et que nous ne pouvons pas la précharger ? Certaines bibliothèques de programmation offrent des capacités limitées pour créer des formes prédéfinies avec des coins arrondis, mais généralement, elles ne peuvent pas être utilisées dans des cas plus compliqués. Par exemple, le framework Qt a une classe QPainter
, qui est utilisée pour dessiner sur toutes les classes dérivées de QPaintDevice
, y compris les widgets, les pixmaps et les images. Il a une méthode appelée drawRoundedRect
, qui, comme son nom l'indique, dessine un rectangle avec des coins arrondis. Mais si nous avons besoin d'une forme un peu plus complexe, nous devons l'implémenter nous-mêmes. Comment pourrions-nous faire cela avec un polygone, une forme plane délimitée par un groupe de segments de droite ? Si nous avons un polygone dessiné avec un crayon sur une feuille de papier, ma première idée serait d'utiliser une gomme et de supprimer une petite partie des lignes à chaque coin, puis de relier les extrémités du segment restant avec un arc de cercle. L'ensemble du processus peut être illustré dans la figure ci-dessous.
La classe QPainter
a des méthodes surchargées nommées drawArc
, qui peuvent dessiner des arcs de cercle. Tous nécessitent des paramètres qui définissent le centre et la taille de l'arc, l'angle de départ et la longueur de l'arc. S'il est facile de déterminer les valeurs nécessaires de ces paramètres pour un rectangle non pivoté, il en va tout autrement lorsqu'il s'agit de polygones plus complexes. De plus, nous devrions répéter ce calcul pour chaque sommet de polygone. Ce calcul est une tâche longue et fastidieuse, et les humains sont sujets à toutes sortes d'erreurs de calcul dans le processus. Cependant, c'est le travail des développeurs de logiciels de faire fonctionner les ordinateurs pour les êtres humains, et non l'inverse. Donc, ici, je vais montrer comment développer une classe simple, qui peut transformer un polygone complexe en une forme avec des coins arrondis. Les utilisateurs de cette classe n'auront qu'à ajouter des sommets de polygone, et la classe fera le reste. L'outil mathématique essentiel que j'utilise pour cette tâche est la courbe de Bézier.
Courbes de Bézier
Il existe de nombreux livres mathématiques et ressources Internet décrivant la théorie des courbes de Bézier, je vais donc brièvement décrire les propriétés pertinentes.
Par définition, la courbe de Bézier est une courbe entre deux points sur une surface à deux dimensions, dont la trajectoire est régie par un ou plusieurs points de contrôle. À proprement parler, une courbe entre deux points sans points de contrôle supplémentaires est également une courbe de Bézier. Cependant, comme cela se traduit par une ligne droite entre les deux points, ce n'est pas particulièrement intéressant, ni utile.
Courbes de Bézier quadratiques
Les courbes de Bézier quadratiques ont un point de contrôle. La théorie dit qu'une courbe de Bézier quadratique entre les points P 0 et P 2 avec le point de contrôle P 1 est définie comme suit :
B(t) = (1 - t) 2 P 0 + 2t(1 - t)P 1 + t 2 P 2 , où 0 ≤ t ≤ 1 (1)
Donc quand t est égal à 0 , B(t) donnera P 0 , quand t est égal à 1 , B(t) donnera P 2 , mais dans tous les autres cas, la valeur de B(t) dépendra aussi de P1 . Puisque l'expression 2t(1 - t) a une valeur maximale à t = 0,5 , c'est là que l'influence de P 1 sur B(t) sera la plus grande. On peut considérer P 1 comme une source imaginaire de gravité, qui tire la trajectoire de la fonction vers elle-même. La figure ci-dessous montre quelques exemples de courbes de Bézier quadratiques avec leurs points de départ, de fin et de contrôle.
Alors, comment résolvons-nous notre problème en utilisant les courbes de Bézier ? La figure ci-dessous propose une explication.
Si nous imaginons supprimer un sommet de polygone et une courte partie de segments de ligne connectés dans son environnement, nous pouvons penser à une extrémité de segment de ligne à partir de P 0 , l'autre extrémité de segment de ligne à partir de P 2 et le sommet supprimé à partir de P 1 . Nous appliquons une courbe de Bézier quadratique à cet ensemble de points et le tour est joué, voilà le coin arrondi désiré.
Implémentation C++/Qt avec QPainter
La classe QPainter
ne permet pas de dessiner des courbes de Bézier quadratiques. Bien qu'il soit assez facile de l'implémenter à partir de zéro en suivant l'équation (1), la bibliothèque Qt offre une meilleure solution. Il existe une autre classe puissante pour le dessin 2D : QPainterPath
. La classe QPainterPath
est une collection de lignes et de courbes qui peuvent être ajoutées et utilisées ultérieurement avec l'objet QPainter
. Certaines méthodes surchargées ajoutent des courbes de Bézier à une collection actuelle. En particulier, les méthodes quadTo
ajouteront une courbe de Bézier quadratique. La courbe commencera au point QPainterPath
actuel ( P 0 ), tandis que P 1 et P 2 doivent être passés à quadTo
en tant que paramètres.
La méthode QPainter
de drawPath
est utilisée pour dessiner une collection de lignes et de courbes à partir de l'objet QPainterPath
, qui doit être donné en paramètre, avec un stylo et un pinceau actifs.
Voyons donc la déclaration de classe :
class RoundedPolygon : public QPolygon { public: RoundedPolygon() { SetRadius(10); } void SetRadius(unsigned int iRadius) { m_iRadius = iRadius; } const QPainterPath& GetPath(); private: QPointF GetLineStart(int i) const; QPointF GetLineEnd(int i) const; float GetDistance(QPoint pt1, QPoint pt2) const; private: QPainterPath m_path; unsigned int m_iRadius; };
J'ai décidé de sous- QPolygon
afin de ne pas avoir à implémenter l'ajout de sommets et d'autres éléments par moi-même. Outre le constructeur, qui définit simplement le rayon sur une valeur initiale raisonnable, cette classe a deux autres méthodes publiques :
- La méthode
SetRadius
définit le rayon sur une valeur donnée. Le rayon est la longueur d'une ligne droite (en pixels) près de chaque sommet, qui sera supprimée (ou, plus précisément, non dessinée) pour le coin arrondi. -
GetPath
est l'endroit où tous les calculs ont lieu. Il renverra l'objetQPainterPath
généré à partir des points de polygone ajoutés àRoundedPolygon
.
Les méthodes de la partie privée ne sont que des méthodes auxiliaires utilisées par GetPath
.

Voyons l'implémentation et je commencerai par les méthodes privées :
float RoundedPolygon::GetDistance(QPoint pt1, QPoint pt2) const { float fD = (pt1.x() - pt2.x())*(pt1.x() - pt2.x()) + (pt1.y() - pt2.y()) * (pt1.y() - pt2.y()); return sqrtf(fD); }
Pas grand chose à expliquer ici, la méthode renvoie la distance euclidienne entre les deux points donnés.
QPointF RoundedPolygon::GetLineStart(int i) const { QPointF pt; QPoint pt1 = at(i); QPoint pt2 = at((i+1) % count()); float fRat = m_uiRadius / GetDistance(pt1, pt2); if (fRat > 0.5f) fRat = 0.5f; pt.setX((1.0f-fRat)*pt1.x() + fRat*pt2.x()); pt.setY((1.0f-fRat)*pt1.y() + fRat*pt2.y()); return pt; }
La méthode GetLineStart
calcule l'emplacement du point P 2 à partir du dernier chiffre, si les points sont ajoutés au polygone dans le sens des aiguilles d'une montre. Plus précisément, il renverra un point, qui est m_uiRadius
pixels du i
-ème sommet dans la direction vers le (i+1)
-ème sommet. Lors de l'accès au (i+1)
-ième sommet, il faut se rappeler que dans le polygone, il y a aussi un segment de ligne entre le dernier et le premier sommet, ce qui en fait une forme fermée, d'où l'expression (i+1)%count()
. Cela empêche également la méthode de sortir de la plage et accède au premier point à la place. La variable fRat
contient le rapport entre le rayon et la longueur du i
segment de ligne. Il existe également une vérification qui empêche fRat
d'avoir une valeur supérieure à 0.5
. Si fRat
avait une valeur supérieure à 0.5
, les deux coins arrondis consécutifs se chevaucheraient, ce qui entraînerait un mauvais résultat visuel.
Lorsque nous voyageons du point P 1 à P 2 en ligne droite et en parcourant 30 % de la distance, nous pouvons déterminer notre emplacement à l'aide de la formule 0,7 • P 1 + 0,3 • P 2 . En général, si nous atteignons une fraction de la distance totale et que α = 1 indique la distance totale, l'emplacement actuel est à (1 - α) • P1 + α • P2 .
C'est ainsi que la méthode GetLineStart
détermine l'emplacement du point situé à m_uiRadius
pixels du i
-ème sommet dans la direction de (i+1)
-ème.
QPointF RoundedPolygon::GetLineEnd(int i) const { QPointF pt; QPoint pt1 = at(i); QPoint pt2 = at((i+1) % count()); float fRat = m_uiRadius / GetDistance(pt1, pt2); if (fRat > 0.5f) fRat = 0.5f; pt.setX(fRat*pt1.x() + (1.0f - fRat)*pt2.x()); pt.setY(fRat*pt1.y() + (1.0f - fRat)*pt2.y()); return pt; }
Cette méthode est très similaire à GetLineStart
. Il calcule l'emplacement du point P 0 pour le (i+1)
-ème sommet, et non le i
-ème. En d'autres termes, si nous traçons une ligne de GetLineStart(i)
à GetLineEnd(i)
pour chaque i
entre 0
et n-1
, où n
est le nombre de sommets dans le polygone, nous obtiendrions le polygone avec les sommets effacés et leur environs proches.
Et maintenant, la méthode de la classe principale :
const QPainterPath& RoundedPolygon::GetPath() { m_path = QPainterPath(); if (count() < 3) { qWarning() << "Polygon should have at least 3 points!"; return m_path; } QPointF pt1; QPointF pt2; for (int i = 0; i < count(); i++) { pt1 = GetLineStart(i); if (i == 0) m_path.moveTo(pt1); else m_path.quadTo(at(i), pt1); pt2 = GetLineEnd(i); m_path.lineTo(pt2); } // close the last corner pt1 = GetLineStart(0); m_path.quadTo(at(0), pt1); return m_path; }
Dans cette méthode, nous construisons l'objet QPainterPath
. Si le polygone n'a pas au moins trois sommets, on n'a plus affaire à une forme 2D, et dans ce cas, la méthode émet un avertissement et renvoie le chemin vide. Lorsque suffisamment de points sont disponibles, on boucle sur tous les segments de droite du polygone (le nombre de segments de droite est, bien sûr, égal au nombre de sommets), en calculant le début et la fin de chaque segment de droite entre les arrondis coins. Nous plaçons une ligne droite entre ces deux points et une courbe de Bézier quadratique entre la fin du segment de ligne précédent et le début du courant, en utilisant l'emplacement du sommet actuel comme point de contrôle. Après la boucle, nous devons fermer le chemin avec une courbe de Bézier entre le dernier et le premier segment de ligne car dans la boucle nous avons tracé une ligne droite de plus que les courbes de Bézier.
Utilisation et résultats de la classe RoundedPolygon
Il est maintenant temps de voir comment utiliser cette classe dans la pratique.
QPixmap pix1(300, 200); QPixmap pix2(300, 200); pix1.fill(Qt::white); pix2.fill(Qt::white); QPainter P1(&pix1); QPainter P2(&pix2); P1.setRenderHints(QPainter::Antialiasing); P2.setRenderHints(QPainter::Antialiasing); P1.setPen(QPen(Qt::blue, 2)); P1.setBrush(Qt::red); P2.setPen(QPen(Qt::blue, 2)); P2.setBrush(Qt::red); RoundedPolygon poly; poly << QPoint(147, 187) << QPoint(95, 187) << QPoint(100, 175) << QPoint(145, 165) << QPoint(140, 95) << QPoint(5, 85) << QPoint(5, 70) << QPoint(140, 70) << QPoint(135, 45) << QPoint(138, 25) << QPoint(145, 5) << QPoint(155, 5) << QPoint(162, 25) << QPoint(165, 45) << QPoint(160, 70) << QPoint(295, 70) << QPoint(295, 85) << QPoint(160, 95) << QPoint(155, 165) << QPoint(200, 175) << QPoint(205, 187) << QPoint(153, 187) << QPoint(150, 199); P1.drawPolygon(poly); P2.drawPath(poly.GetPath()); pix1.save("1.png"); pix2.save("2.png");
Ce morceau de code source est assez simple. Après avoir initialisé deux QPixmaps
et leurs QPainters
, nous créons un objet RoundedPolygon
et le remplissons de points. Painter P1
dessine le polygone régulier, tandis que P2
dessine le QPainterPath
avec des coins arrondis, généré à partir du polygone. Les deux pixmaps résultants sont enregistrés dans leurs fichiers et les résultats sont les suivants :
Conclusion
Nous avons vu que générer une forme avec des coins arrondis à partir d'un polygone n'est finalement pas si difficile, surtout si nous utilisons un bon framework de programmation tel que Qt. Ce processus peut être automatisé par la classe que j'ai décrite dans ce blog comme une preuve de concept. Cependant, il y a encore beaucoup de place à l'amélioration, comme :
- Faire des coins arrondis uniquement aux sommets sélectionnés et pas du tout.
- Faites des coins arrondis avec différents rayons à différents sommets.
- Implémentez une méthode qui génère une polyligne avec des coins arrondis (la polyligne dans la terminologie Qt est comme un polygone, sauf que ce n'est pas une forme fermée car il manque le segment de ligne entre le dernier et le premier sommet).
- Utilisez
RoundedPolygon
pour générer des bitmaps, qui peuvent être utilisés comme masque de widget d'arrière-plan pour produire des widgets aux formes folles. - La classe
RoundedPolygon
n'est pas optimisée pour la vitesse d'exécution ; Je l'ai laissé tel quel pour faciliter la compréhension du concept. L'optimisation peut inclure le calcul de nombreuses valeurs intermédiaires lors de l'ajout d'un nouveau sommet au polygone. De plus, lorsqueGetPath
est sur le point de renvoyer une référence auQPainterPath
généré, il peut définir un indicateur indiquant que l'objet est à jour. Le prochain appel àGetPath
ne renverrait que le même objetQPainterPath
, sans rien recalculer. Le développeur devrait cependant s'assurer que cet indicateur est effacé à chaque modification de l'un des sommets du polygone, ainsi qu'à chaque nouveau sommet, ce qui me fait penser que la classe optimisée serait mieux développée à partir de zéro et non dérivée deQPolygon
. La bonne nouvelle est que ce n'est pas aussi difficile qu'il y paraît.
Dans l'ensemble, la classe RoundedPolygon
, telle qu'elle est, peut être utilisée comme un outil chaque fois que nous voulons ajouter une touche de design à notre interface graphique à la volée, sans préparer de pixmaps ou de formes à l'avance.