So erhalten Sie abgerundete Eckenformen in C++ mit Bezier-Kurven und QPainter: Eine Schritt-für-Schritt-Anleitung

Veröffentlicht: 2022-03-11

Einführung

Der aktuelle Trend im Grafikdesign geht dahin, viele abgerundete Ecken in allen möglichen Formen zu verwenden. Wir können diese Tatsache auf vielen Webseiten, mobilen Geräten und Desktop-Anwendungen beobachten. Die bemerkenswertesten Beispiele sind die Anwendungsschaltflächen, die verwendet werden, um eine Aktion auszulösen, wenn sie angeklickt werden. Statt streng rechteckiger Form mit 90-Grad-Winkeln in den Ecken werden sie oft mit abgerundeten Ecken gezeichnet. Abgerundete Ecken sorgen dafür, dass sich die Benutzeroberfläche glatter und schöner anfühlt. Ich bin davon nicht ganz überzeugt, aber mein Designerfreund sagt es mir.

Abgerundete Ecken sorgen dafür, dass sich die Benutzeroberfläche glatter und schöner anfühlt

Abgerundete Ecken sorgen dafür, dass sich die Benutzeroberfläche glatter und schöner anfühlt.
Twittern

Die visuellen Elemente von Benutzeroberflächen werden von Designern erstellt, und der Programmierer muss sie nur an den richtigen Stellen platzieren. Aber was passiert, wenn wir spontan eine Form mit abgerundeten Ecken erzeugen müssen und diese nicht vorab laden können? Einige Programmierbibliotheken bieten begrenzte Möglichkeiten zum Erstellen vordefinierter Formen mit abgerundeten Ecken, aber normalerweise können sie in komplizierteren Fällen nicht verwendet werden. Beispielsweise hat das Qt-Framework eine Klasse QPainter , die verwendet wird, um auf alle von QPaintDevice abgeleiteten Klassen zuzugreifen, einschließlich Widgets, Pixmaps und Bilder. Es hat eine Methode namens drawRoundedRect , die, wie der Name schon sagt, ein Rechteck mit abgerundeten Ecken zeichnet. Aber wenn wir eine etwas komplexere Form brauchen, müssen wir sie selbst umsetzen. Wie könnten wir das mit einem Polygon machen, einer ebenen Form, die von einer Gruppe gerader Liniensegmente begrenzt wird? Wenn wir ein Polygon mit einem Bleistift auf ein Blatt Papier gezeichnet haben, wäre meine erste Idee, mit einem Radiergummi an jeder Ecke einen kleinen Teil der Linien zu löschen und dann die verbleibenden Segmentenden mit einem Kreisbogen zu verbinden. Der gesamte Prozess kann in der folgenden Abbildung dargestellt werden.

So erstellen Sie manuell abgerundete Ecken

Die Klasse QPainter hat einige überladene Methoden namens drawArc , die Kreisbögen zeichnen können. Alle erfordern Parameter, die den Bogenmittelpunkt und -größe, den Startwinkel und die Bogenlänge definieren. Während es für ein nicht gedrehtes Rechteck leicht ist, die notwendigen Werte dieser Parameter zu ermitteln, sieht es bei komplexeren Polygonen ganz anders aus. Außerdem müssten wir diese Berechnung für jeden Polygoneckpunkt wiederholen. Diese Berechnung ist eine langwierige und lästige Aufgabe, und Menschen sind dabei anfällig für alle möglichen Rechenfehler. Es ist jedoch die Aufgabe der Softwareentwickler, Computer für Menschen arbeiten zu lassen und nicht umgekehrt. Hier werde ich also zeigen, wie man eine einfache Klasse entwickelt, die ein komplexes Polygon in eine Form mit abgerundeten Ecken umwandeln kann. Benutzer dieser Klasse müssen nur Polygonscheitel anhängen, und die Klasse erledigt den Rest. Das wesentliche mathematische Werkzeug, das ich für diese Aufgabe verwende, ist die Bezier-Kurve.

Bezier-Kurven

Es gibt viele mathematische Bücher und Internetressourcen, die die Theorie der Bezier-Kurven beschreiben, daher werde ich die relevanten Eigenschaften kurz skizzieren.

Per Definition ist die Bezier-Kurve eine Kurve zwischen zwei Punkten auf einer zweidimensionalen Oberfläche, deren Bahn durch einen oder mehrere Kontrollpunkte bestimmt wird. Genau genommen ist auch eine Kurve zwischen zwei Punkten ohne zusätzliche Kontrollpunkte eine Bezier-Kurve. Da dies jedoch zu einer geraden Linie zwischen den beiden Punkten führt, ist dies weder besonders interessant noch nützlich.

Quadratische Bezier-Kurven

Quadratische Bezier-Kurven haben einen Kontrollpunkt. Die Theorie besagt, dass eine quadratische Bezier-Kurve zwischen den Punkten P 0 und P 2 mit Kontrollpunkt P 1 wie folgt definiert ist:

B(t) = (1 - t) 2 P 0 + 2t(1 - t)P 1 + t 2 P 2 , wobei 0 ≤ t ≤ 1 (1)

Wenn also t gleich 0 ist, ergibt B(t) P 0 , wenn t gleich 1 ist, ergibt B(t) P 2 , aber in jedem anderen Fall hängt der Wert von B(t) auch davon ab P1 . Da der Ausdruck 2t(1 - t) einen Maximalwert bei t = 0,5 hat, ist dort der Einfluss von P 1 auf B(t) am größten. Wir können uns P 1 als eine imaginäre Gravitationsquelle vorstellen, die die Funktionsbahn zu sich hinzieht. Die folgende Abbildung zeigt einige Beispiele quadratischer Bezier-Kurven mit ihren Start-, End- und Kontrollpunkten.

Quadratische Bezier-Kurven

Wie lösen wir also unser Problem mit Bezier-Kurven? Die folgende Abbildung bietet eine Erklärung.

So erstellen Sie abgerundete Ecken mit dem Code

Wenn wir uns vorstellen, einen Polygoneckpunkt und einen kurzen Teil verbundener Liniensegmente in seiner Umgebung zu löschen, können wir uns das eine Liniensegmentende bei P 0 vorstellen, das andere Liniensegmentende bei P 2 und den gelöschten Eckpunkt bei P 1 . Wir wenden eine quadratische Bezier-Kurve auf diese Menge von Punkten an und voila, da ist die gewünschte abgerundete Ecke.

C++/Qt-Implementierung mit QPainter

Die Klasse QPainter hat keine Möglichkeit, quadratische Bezier-Kurven zu zeichnen. Während es ziemlich einfach ist, es von Grund auf neu zu implementieren, indem man die Gleichung (1) befolgt, bietet die Qt-Bibliothek eine bessere Lösung. Es gibt eine weitere leistungsstarke Klasse für das 2D-Zeichnen: QPainterPath . Die Klasse QPainterPath ist eine Sammlung von Linien und Kurven, die hinzugefügt und später mit dem QPainter Objekt verwendet werden können. Es gibt einige überladene Methoden, die Bezier-Kurven zu einer aktuellen Sammlung hinzufügen. Insbesondere die Methoden quadTo fügen quadratische Bezier-Kurven hinzu. Die Kurve beginnt am aktuellen QPainterPath Punkt ( P 0 ), während P 1 und P 2 als Parameter an quadTo werden müssen.

Die Methode QPainter von drawPath wird verwendet, um eine Sammlung von Linien und Kurven aus dem QPainterPath Objekt, das als Parameter angegeben werden muss, mit aktivem Stift und Pinsel zu zeichnen.

Sehen wir uns also die Klassendeklaration an:

 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; };

Ich habe mich entschieden, QPolygon zu unterteilen, damit ich das Hinzufügen von Scheitelpunkten und anderen Dingen nicht selbst implementieren muss. Neben dem Konstruktor, der den Radius einfach auf einen vernünftigen Anfangswert setzt, hat diese Klasse zwei weitere öffentliche Methoden:

  • SetRadius Methode setzt den Radius auf einen bestimmten Wert. Der Radius ist die Länge einer geraden Linie (in Pixel) in der Nähe jedes Scheitelpunkts, die für die abgerundete Ecke gelöscht (oder genauer gesagt nicht gezeichnet) wird.
  • GetPath ist der Ort, an dem alle Berechnungen stattfinden. Sie gibt das QPainterPath Objekt zurück, das aus den zu RoundedPolygon hinzugefügten Polygonpunkten generiert wurde.

Die Methoden aus dem privaten Teil sind nur Hilfsmethoden, die von GetPath verwendet werden.

Sehen wir uns die Implementierung an und beginne mit den privaten Methoden:

 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); }

Hier gibt es nicht viel zu erklären, die Methode gibt den euklidischen Abstand zwischen den gegebenen zwei Punkten zurück.

 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; }

Die Methode GetLineStart berechnet die Lage des Punktes P 2 aus der letzten Figur, wenn die Punkte im Uhrzeigersinn zum Polygon hinzugefügt werden. Genauer gesagt gibt es einen Punkt zurück, der m_uiRadius Pixel vom i -ten Scheitelpunkt in Richtung des (i+1) -ten Scheitelpunkts entfernt ist. Beim Zugriff auf den (i+1) -ten Eckpunkt müssen wir daran denken, dass es im Polygon auch ein Liniensegment zwischen dem letzten und dem ersten Eckpunkt gibt, was es zu einer geschlossenen Form macht, daher der Ausdruck (i+1)%count() . Dadurch wird auch verhindert, dass die Methode den Bereich verlässt und stattdessen auf den ersten Punkt zugreift. Die Variable fRat enthält das Verhältnis zwischen dem Radius und der Länge des i -ten Liniensegments. Es gibt auch eine Prüfung, die verhindert, dass fRat einen Wert über 0.5 hat. Wenn fRat einen Wert über 0.5 hätte, würden sich die beiden aufeinanderfolgenden abgerundeten Ecken überlappen, was zu einem schlechten visuellen Ergebnis führen würde.

Wenn wir von Punkt P 1 nach P 2 in gerader Linie reisen und 30 Prozent der Strecke zurücklegen, können wir unseren Standort mit der Formel 0,7 • P 1 + 0,3 • P 2 bestimmen. Wenn wir im Allgemeinen einen Bruchteil der vollen Entfernung erreichen und α = 1 die volle Entfernung bezeichnet, befindet sich die aktuelle Position bei (1 - α)·P1 + α·P2 .

Auf diese Weise bestimmt die GetLineStart Methode die Position des Punktes, der m_uiRadius Pixel vom i -ten Scheitelpunkt in Richtung (i+1) -th entfernt ist.

 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; }

Diese Methode ist GetLineStart sehr ähnlich. Es berechnet den Ort des Punktes P 0 für den (i+1) -ten Eckpunkt, nicht den i -ten. Mit anderen Worten, wenn wir für jedes i zwischen 0 und n-1 eine Linie von GetLineStart(i) nach GetLineEnd(i) , wobei n die Anzahl der Eckpunkte im Polygon ist, erhalten wir das Polygon mit gelöschten Eckpunkten und deren nähere Umgebung.

Und jetzt die Methode der Hauptklasse:

 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; }

In dieser Methode erstellen wir das QPainterPath Objekt. Wenn das Polygon nicht mindestens drei Scheitelpunkte hat, haben wir es nicht mehr mit einer 2D-Form zu tun, und in diesem Fall gibt die Methode eine Warnung aus und gibt den leeren Pfad zurück. Wenn genügend Punkte verfügbar sind, durchlaufen wir alle geraden Liniensegmente des Polygons (die Anzahl der Liniensegmente ist natürlich gleich der Anzahl der Scheitelpunkte) und berechnen den Anfang und das Ende jedes geraden Liniensegments zwischen den abgerundeten Ecken. Wir setzen eine gerade Linie zwischen diesen beiden Punkten und eine quadratische Bezier-Kurve zwischen dem Ende des vorherigen Liniensegments und dem Start des aktuellen, wobei wir die Position des aktuellen Scheitelpunkts als Kontrollpunkt verwenden. Nach der Schleife müssen wir den Pfad mit einer Bezier-Kurve zwischen dem letzten und dem ersten Liniensegment schließen, da wir in der Schleife eine gerade Linie mehr gezeichnet haben als die Bezier-Kurven.

Verwendung und Ergebnisse der Klasse RoundedPolygon

Jetzt ist es an der Zeit zu sehen, wie man diese Klasse in der Praxis verwendet.

 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");

Dieses Stück Quellcode ist ziemlich einfach. Nachdem wir zwei QPixmaps und ihre QPainters initialisiert haben, erstellen wir ein RoundedPolygon Objekt und füllen es mit Punkten. Painter P1 zeichnet das regelmäßige Polygon, während P2 den aus dem Polygon generierten QPainterPath mit abgerundeten Ecken zeichnet. Beide resultierenden Pixmaps werden in ihren Dateien gespeichert und die Ergebnisse sind wie folgt:

Abgerundete Ecken mit dem QPainter

Fazit

Wir haben gesehen, dass das Generieren einer Form mit abgerundeten Ecken aus einem Polygon doch nicht so schwierig ist, insbesondere wenn wir ein gutes Programmier-Framework wie Qt verwenden. Dieser Prozess kann durch die Klasse automatisiert werden, die ich in diesem Blog als Proof of Concept beschrieben habe. Es gibt jedoch noch viel Raum für Verbesserungen, wie zum Beispiel:

  • Machen Sie abgerundete Ecken nur an ausgewählten Scheitelpunkten und nicht an allen.
  • Machen Sie abgerundete Ecken mit unterschiedlichen Radien an verschiedenen Scheitelpunkten.
  • Implementieren Sie eine Methode, die eine Polylinie mit abgerundeten Ecken erzeugt (Polylinie in Qt-Terminologie ist genau wie Polygon, außer dass es keine geschlossene Form ist, weil ihr das Liniensegment zwischen dem letzten und ersten Scheitelpunkt fehlt).
  • Verwenden Sie RoundedPolygon , um Bitmaps zu generieren, die als Hintergrund-Widget-Maske verwendet werden können, um verrückt geformte Widgets zu erzeugen.
  • Die RoundedPolygon -Klasse ist nicht auf Ausführungsgeschwindigkeit optimiert; Ich habe es so gelassen, wie es ist, um das Konzept besser zu verstehen. Die Optimierung kann das Berechnen vieler Zwischenwerte beim Anhängen eines neuen Scheitelpunkts an das Polygon umfassen. Wenn GetPath im Begriff ist, einen Verweis auf den generierten QPainterPath , könnte es außerdem ein Flag setzen, das anzeigt, dass das Objekt auf dem neuesten Stand ist. Der nächste Aufruf von GetPath würde dazu führen, dass nur dasselbe QPainterPath Objekt zurückgegeben wird, ohne dass etwas neu berechnet wird. Der Entwickler müsste jedoch sicherstellen, dass dieses Flag bei jeder Änderung in einem der Polygoneckpunkte sowie bei jedem neuen Eckpunkt gelöscht wird, was mich denken lässt, dass die optimierte Klasse besser von Grund auf neu entwickelt und nicht abgeleitet werden sollte von QPolygon . Die gute Nachricht ist, dass dies nicht so schwierig ist, wie es sich anhört.

Insgesamt kann die RoundedPolygon -Klasse, so wie sie ist, jederzeit als Werkzeug verwendet werden, wenn wir unserer GUI im Handumdrehen einen Designer-Touch hinzufügen möchten, ohne Pixmaps oder Formen im Voraus vorzubereiten.

Siehe auch: So lernen Sie die Sprachen C und C++: Die ultimative Liste