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-11

introduction

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 coins arrondis rendent l'interface utilisateur plus lisse et plus agréable

Les coins arrondis rendent l'interface utilisateur plus lisse et plus agréable.
Tweeter

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.

Comment créer des coins arrondis manuellement

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.

Courbes de Bézier quadratiques

Alors, comment résolvons-nous notre problème en utilisant les courbes de Bézier ? La figure ci-dessous propose une explication.

Comment créer des coins arrondis en utilisant le code

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'objet QPainterPath 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 :

Coins arrondis avec le QPainter

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, lorsque GetPath est sur le point de renvoyer une référence au QPainterPath 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 objet QPainterPath , 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 de QPolygon . 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.

Connexe : Comment apprendre les langages C et C++ : La liste ultime