ο τρέχουσα τάση στο σχεδιασμό γραφικών είναι να χρησιμοποιείτε πολλές στρογγυλεμένες γωνίες σε όλα τα είδη σχημάτων. Μπορούμε να παρατηρήσουμε αυτό το γεγονός σε πολλές ιστοσελίδες, κινητές συσκευές και εφαρμογές για επιτραπέζιους υπολογιστές. Τα πιο αξιοσημείωτα παραδείγματα είναι τα κουμπιά της εφαρμογής, τα οποία χρησιμοποιούνται για να προκαλέσουν κάποια ενέργεια όταν κάνετε κλικ. Αντί αυστηρά ορθογώνιου σχήματος με γωνίες 90 μοιρών στις γωνίες, συχνά σχεδιάζονται με στρογγυλεμένες γωνίες. Οι στρογγυλεμένες γωνίες κάνουν το περιβάλλον εργασίας χρήστη να αισθάνεται πιο ομαλό και καλύτερο. Δεν είμαι απόλυτα πεπεισμένος για αυτό, αλλά ο φίλος μου σχεδιάστρια μου το λέει.
c corporation vs s corporation
Τα οπτικά στοιχεία των διεπαφών χρήστη δημιουργούνται από σχεδιαστές και ο προγραμματιστής πρέπει να τα τοποθετήσει μόνο στα σωστά μέρη. Αλλά τι συμβαίνει, όταν πρέπει να δημιουργήσουμε ένα σχήμα με στρογγυλεμένες γωνίες εν κινήσει, και δεν μπορούμε να το προφορτώσουμε; Ορισμένες βιβλιοθήκες προγραμματισμού προσφέρουν περιορισμένες δυνατότητες για τη δημιουργία προκαθορισμένων σχημάτων με στρογγυλεμένες γωνίες, αλλά συνήθως, δεν μπορούν να χρησιμοποιηθούν σε πιο περίπλοκες περιπτώσεις. Για παράδειγμα, Πλαίσιο Qt έχει μια τάξη QPainter
, το οποίο χρησιμοποιείται για την κατάρτιση όλων των τάξεων που προέρχονται από QPaintDevice
, συμπεριλαμβανομένων widget, pixmaps και εικόνων. Έχει μια μέθοδο που ονομάζεται drawRoundedRect
, η οποία, όπως υποδηλώνει το όνομα, σχεδιάζει ένα ορθογώνιο με στρογγυλεμένες γωνίες. Αλλά αν χρειαζόμαστε λίγο πιο περίπλοκο σχήμα, πρέπει να το εφαρμόσουμε μόνοι μας. Πώς μπορούμε να το κάνουμε αυτό με ένα πολύγωνο, ένα επίπεδο σχήμα που οριοθετείται από μια ομάδα ευθειών γραμμών; Εάν έχουμε ένα πολύγωνο που έχει σχεδιαστεί με ένα μολύβι σε ένα κομμάτι χαρτί, η πρώτη μου ιδέα θα ήταν να χρησιμοποιήσω μια γόμα και να διαγράψω ένα μικρό μέρος των γραμμών σε κάθε γωνία και στη συνέχεια να συνδέσουμε τα υπόλοιπα άκρα του τμήματος με ένα κυκλικό τόξο. Η όλη διαδικασία μπορεί να απεικονιστεί στο παρακάτω σχήμα.
Κατηγορία QPainter
έχει μερικές υπερφορτωμένες μεθόδους που ονομάζονται drawArc
, οι οποίες μπορούν να σχεδιάσουν κυκλικά τόξα. Όλα αυτά απαιτούν παραμέτρους, οι οποίες καθορίζουν το κέντρο και το μέγεθος του τόξου, τη γωνία εκκίνησης και το μήκος του τόξου. Αν και είναι εύκολο να προσδιοριστούν οι απαραίτητες τιμές αυτών των παραμέτρων για ένα μη περιστρεφόμενο ορθογώνιο, είναι εντελώς διαφορετικό θέμα όταν ασχολούμαστε με πιο πολύπλοκα πολύγωνα. Επιπλέον, θα πρέπει να επαναλάβουμε αυτόν τον υπολογισμό για κάθε κορυφή πολυγώνου. Αυτός ο υπολογισμός είναι μια χρονοβόρα και κουραστική εργασία και οι άνθρωποι είναι επιρρεπείς σε κάθε είδους σφάλματα υπολογισμού στη διαδικασία. Ωστόσο, είναι καθήκον των προγραμματιστών λογισμικού να κάνουν τους υπολογιστές να λειτουργούν για τα ανθρώπινα όντα και όχι το αντίστροφο. Έτσι, εδώ θα δείξω πώς να αναπτύξω μια απλή τάξη, η οποία μπορεί να μετατρέψει ένα πολύπλοκο πολύγωνο σε σχήμα με στρογγυλεμένες γωνίες. Οι χρήστες αυτής της τάξης θα πρέπει να προσθέσουν μόνο κορυφές πολυγώνου και η τάξη θα κάνει τα υπόλοιπα. Το βασικό μαθηματικό εργαλείο που χρησιμοποιώ για αυτήν την εργασία, είναι το Καμπύλη Bezier .
Υπάρχουν πολλά μαθηματικά βιβλία και πόροι στο Διαδίκτυο που περιγράφουν τη θεωρία των καμπυλών Bezier, οπότε θα περιγράψω εν συντομία τις σχετικές ιδιότητες.
Εξ ορισμού, η καμπύλη Bezier είναι μια καμπύλη μεταξύ δύο σημείων σε μια δισδιάστατη επιφάνεια, η τροχιά της οποίας διέπεται από ένα ή περισσότερα σημεία ελέγχου. Ακριβώς μιλώντας, μια καμπύλη μεταξύ δύο σημείων χωρίς πρόσθετα σημεία ελέγχου, είναι επίσης μια καμπύλη Bezier. Ωστόσο, καθώς αυτό οδηγεί σε ευθεία γραμμή μεταξύ των δύο σημείων, δεν είναι ιδιαίτερα ενδιαφέρον, ούτε χρήσιμο.
Οι καμπύλες Quadratic Bezier έχουν ένα σημείο ελέγχου. Η θεωρία λέει ότι μια τετραγωνική καμπύλη Bezier μεταξύ σημείων Π0 και Π2 με σημείο ελέγχου Πένας ορίζεται ως εξής:
B (t) = (1 - t)2Π0+ 2t (1 - t) Pένας+ τ2Π2, όπου 0 ≤ t ≤ 1 (ένας)
Οπότε πότε τ είναι ίσο με 0 , Β (τ) θα αποδώσει Π0 , πότε τ είναι ίσο με ένας , Β (τ) θα αποδώσει Π2 , αλλά σε κάθε άλλη περίπτωση, η τιμή του Β (τ) εξαρτάται επίσης από Πένας . Από την έκφραση 2t (1 - t) έχει μέγιστη τιμή σε t = 0,5 , εκεί είναι η επιρροή του Πένας επί Β (τ) θα είναι το μεγαλύτερο. Μπορούμε να σκεφτούμε Πένας ως μια φανταστική πηγή βαρύτητας, η οποία τραβά την τροχιά της λειτουργίας προς τον εαυτό της. Το παρακάτω σχήμα δείχνει μερικά παραδείγματα τετραγωνικών καμπυλών Bezier με τα σημεία έναρξης, τέλους και ελέγχου.
Λοιπόν, πώς επιλύουμε το πρόβλημά μας χρησιμοποιώντας καμπύλες Bezier; Το παρακάτω σχήμα παρέχει μια εξήγηση.
Εάν φανταστούμε τη διαγραφή μιας κορυφής πολυγώνου και ενός μικρού τμήματος συνδεδεμένων τμημάτων γραμμής στο περιβάλλον του, μπορούμε να σκεφτούμε ένα άκρο τμήματος γραμμής από Π0 , το τέλος του άλλου τμήματος γραμμής από Π2 και η διαγραμμένη κορυφή από Πένας . Εφαρμόζουμε μια τετραγωνική καμπύλη Bezier σε αυτό το σύνολο σημείων και voila, υπάρχει η επιθυμητή στρογγυλεμένη γωνία.
Κατηγορία QPainter
δεν έχει τρόπο να σχεδιάσετε τετραγωνικές καμπύλες Bezier. Ενώ είναι αρκετά εύκολο να το εφαρμόσετε από το μηδέν μετά την εξίσωση (1), η βιβλιοθήκη Qt προσφέρει μια καλύτερη λύση. Υπάρχει μια άλλη ισχυρή κλάση για 2D σχέδιο: QPainterPath
. Κατηγορία QPainterPath
είναι μια συλλογή γραμμών και καμπυλών που μπορούν να προστεθούν και να χρησιμοποιηθούν αργότερα με το QPainter
αντικείμενο. Υπάρχουν μερικές υπερφορτωμένες μέθοδοι που προσθέτουν καμπύλες Bezier σε μια τρέχουσα συλλογή. Συγκεκριμένα, οι μέθοδοι quadTo
θα προσθέσει μια τετραγωνική καμπύλη Bezier. Η καμπύλη θα ξεκινήσει από την τρέχουσα QPainterPath
σημείο ( Π0 ), ενώ Πένας και Π2 πρέπει να περάσουν στο quadTo
ως παράμετροι.
QPainter
Μέθοδος drawPath
χρησιμοποιείται για να σχεδιάσει μια συλλογή γραμμών και καμπυλών από QPainterPath
αντικείμενο, το οποίο πρέπει να δοθεί ως παράμετρος, με ενεργό στυλό και πινέλο.
Ας δούμε λοιπόν τη δήλωση τάξης:
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; };
Αποφάσισα να υποκατηγορία QPolygon
έτσι ώστε να μην χρειάζεται να κάνω μόνη μου την προσθήκη κορυφών και άλλων. Εκτός από τον κατασκευαστή, ο οποίος θέτει απλώς την ακτίνα σε κάποια λογική αρχική τιμή, αυτή η τάξη έχει δύο άλλες δημόσιες μεθόδους:
SetRadius
Η μέθοδος ορίζει την ακτίνα σε μια δεδομένη τιμή. Η ακτίνα είναι το μήκος μιας ευθείας γραμμής (σε εικονοστοιχεία) κοντά σε κάθε κορυφή, η οποία θα διαγραφεί (ή, πιο συγκεκριμένα, δεν σχεδιάζεται) για τη στρογγυλεμένη γωνία.GetPath
είναι όπου πραγματοποιούνται όλοι οι υπολογισμοί. Θα επιστρέψει το QPainterPath
αντικείμενο που δημιουργήθηκε από τα σημεία πολυγώνου που προστέθηκαν στο RoundedPolygon
.Οι μέθοδοι από το ιδιωτικό μέρος είναι απλώς βοηθητικές μέθοδοι που χρησιμοποιούνται από το GetPath
.
Ας δούμε την εφαρμογή και θα ξεκινήσω με τις ιδιωτικές μεθόδους:
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); }
Όχι πολύ να εξηγήσουμε εδώ, η μέθοδος επιστρέφει την ευκλείδια απόσταση μεταξύ των δεδομένων δύο σημείων.
χρησιμοποιώντας μακροεντολές στα φύλλα Google
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; }
Μέθοδος GetLineStart
υπολογίζει τη θέση του σημείου Π2 από το τελευταίο σχήμα, εάν τα σημεία προστίθενται στο πολύγωνο προς τη φορά των δεικτών του ρολογιού. Πιο συγκεκριμένα, θα επιστρέψει ένα σημείο, το οποίο είναι m_uiRadius
pixel μακριά από την i
-th κορυφή στην κατεύθυνση προς την (i+1)
-th κορυφή. Κατά την πρόσβαση στην κορυφή | (i+1)
-th, πρέπει να θυμόμαστε ότι στο πολύγωνο, υπάρχει επίσης ένα τμήμα γραμμής μεταξύ της τελευταίας και της πρώτης κορυφής, το οποίο το καθιστά κλειστό σχήμα, επομένως η έκφραση (i+1)%count()
. Αυτό αποτρέπει επίσης τη μέθοδο να μην είναι εκτός εμβέλειας και να έχει πρόσβαση στο πρώτο σημείο αντ 'αυτού. Μεταβλητή fRat
διατηρεί την αναλογία μεταξύ της ακτίνας και του μήκους τμήματος της γραμμής i
Υπάρχει επίσης ένας έλεγχος που αποτρέπει fRat
από την τιμή πάνω από 0.5
. Εάν fRat
είχε τιμή πάνω από 0.5
, τότε οι δύο διαδοχικές στρογγυλεμένες γωνίες θα επικαλύπτονταν, γεγονός που θα προκαλούσε κακό οπτικό αποτέλεσμα.
Όταν ταξιδεύετε από σημείο Πένας προς το Π2 σε ευθεία γραμμή και συμπληρώνοντας το 30 τοις εκατό της απόστασης, μπορούμε να προσδιορίσουμε την τοποθεσία μας χρησιμοποιώντας τον τύπο 0,7 • Σένας+ 0,3 • Ρ2 . Σε γενικές γραμμές, εάν επιτύχουμε ένα κλάσμα της πλήρους απόστασης, και α = 1 υποδηλώνει πλήρη απόσταση, η τρέχουσα τοποθεσία είναι (1 - α) • P1 + α • P2 .
Έτσι το GetLineStart
Η μέθοδος καθορίζει τη θέση του σημείου που είναι m_uiRadius
pixel μακριά από την i
-th κορυφή στην κατεύθυνση (i+1)
-th.
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; }
Αυτή η μέθοδος είναι πολύ παρόμοια με την GetLineStart
. Υπολογίζει τη θέση του σημείου Π0 για την κορυφή (i+1)
-th, όχι i
-th. Με άλλα λόγια, αν σχεδιάσουμε μια γραμμή από GetLineStart(i)
έως GetLineEnd(i)
για κάθε i
μεταξύ 0
και n-1
, όπου n
είναι ο αριθμός κορυφών στο πολύγωνο, θα έχουμε το πολύγωνο με σβησμένες κορυφές και το κοντινό τους περιβάλλον.
Και τώρα, η κύρια μέθοδος τάξης:
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; }
Σε αυτήν τη μέθοδο, χτίζουμε το QPainterPath
αντικείμενο. Εάν το πολύγωνο δεν έχει τουλάχιστον τρεις κορυφές, δεν ασχολούμαστε πλέον με ένα σχήμα 2D, και σε αυτήν την περίπτωση, η μέθοδος εκδίδει μια προειδοποίηση και επιστρέφει την κενή διαδρομή. Όταν υπάρχουν αρκετά σημεία, βγαίνουμε σε όλα τα τμήματα ευθείας γραμμής του πολυγώνου (ο αριθμός των τμημάτων γραμμής είναι, φυσικά, ίσος με τον αριθμό των κορυφών), υπολογίζοντας την αρχή και το τέλος κάθε τμήματος ευθείας γραμμής μεταξύ της στρογγυλεμένης γωνίες. Βάζουμε μια ευθεία γραμμή μεταξύ αυτών των δύο σημείων και μια τετραγωνική καμπύλη Bezier μεταξύ του τέλους του προηγούμενου τμήματος γραμμής και της έναρξης του ρεύματος, χρησιμοποιώντας τη θέση της τρέχουσας κορυφής ως σημείο ελέγχου. Μετά το βρόχο, πρέπει να κλείσουμε τη διαδρομή με μια καμπύλη Bezier μεταξύ των τμημάτων της τελευταίας και της πρώτης γραμμής, επειδή στον βρόχο σχεδιάσαμε μια ευθεία γραμμή περισσότερο από τις καμπύλες Bezier.
RoundedPolygon
χρήση και αποτελέσματαΤώρα ήρθε η ώρα να δούμε πώς να χρησιμοποιήσετε αυτό το μάθημα στην πράξη.
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');
Αυτό το κομμάτι του πηγαίου κώδικα είναι αρκετά απλό. Μετά την προετοιμασία δύο QPixmaps
και τους QPainters
, δημιουργούμε ένα RoundedPolygon
αντικείμενο και γεμίστε το με σημεία. Ζωγράφος P1
σχεδιάζει το κανονικό πολύγωνο, ενώ P2
σχεδιάζει το QPainterPath
με στρογγυλεμένες γωνίες, που παράγονται από το πολύγωνο. Και τα δύο προκύπτοντα pixmaps αποθηκεύονται στα αρχεία τους και τα αποτελέσματα έχουν ως εξής:
Έχουμε δει ότι η δημιουργία ενός σχήματος με στρογγυλεμένες γωνίες από ένα πολύγωνο δεν είναι τελικά τόσο δύσκολη, ειδικά αν χρησιμοποιούμε ένα καλό πλαίσιο προγραμματισμού όπως το Qt. Αυτή η διαδικασία μπορεί να αυτοματοποιηθεί από την τάξη που έχω περιγράψει σε αυτό το blog ως απόδειξη της ιδέας. Ωστόσο, υπάρχει ακόμη πολύ περιθώριο βελτίωσης, όπως:
RoundedPolygon
για να δημιουργήσετε bitmaps, οι οποίοι μπορούν να χρησιμοποιηθούν ως μάσκα widget φόντο για την παραγωγή τρελών σχήματος widget.RoundedPolygon
η κλάση δεν έχει βελτιστοποιηθεί για ταχύτητα εκτέλεσης. Το άφησα όπως είναι για ευκολότερη κατανόηση της έννοιας. Η βελτιστοποίηση μπορεί να περιλαμβάνει τον υπολογισμό πολλών ενδιάμεσων τιμών κατά την προσθήκη μιας νέας κορυφής στο πολύγωνο. Επίσης, όταν GetPath
πρόκειται να επιστρέψει μια αναφορά στο παραγόμενο QPainterPath
, θα μπορούσε να ορίσει μια σημαία, υποδεικνύοντας ότι το αντικείμενο είναι ενημερωμένο. Η επόμενη κλήση στο GetPath
θα είχε ως αποτέλεσμα την επιστροφή μόνο του ίδιου QPainterPath
αντικείμενο, χωρίς επανυπολογισμό τίποτα. Ο προγραμματιστής, ωστόσο, θα πρέπει να διασφαλίσει ότι αυτή η σημαία διαγράφεται σε κάθε αλλαγή σε οποιαδήποτε από τις κορυφές του πολυγώνου, καθώς και σε κάθε νέα κορυφή, κάτι που με κάνει να πιστεύω ότι η βελτιστοποιημένη κλάση θα ήταν καλύτερα να αναπτυχθεί από το μηδέν και να μην προέρχεται από QPolygon
. Τα καλά νέα είναι ότι αυτό δεν είναι τόσο δύσκολο όσο ακούγεται.Συνολικά, το RoundedPolygon
τάξη, όπως είναι, μπορεί να χρησιμοποιηθεί ως εργαλείο όποτε θέλουμε να προσθέσουμε ένα σχεδιαστής αφής στο GUI μας εν κινήσει, χωρίς να προετοιμάζουμε pixmaps ή σχήματα εκ των προτέρων.