Cum să obțineți forme de colț rotunjite în C++ folosind Bezier Curves și QPainter: un ghid pas cu pas

Publicat: 2022-03-11

Introducere

Tendința actuală în designul grafic este de a folosi o mulțime de colțuri rotunjite în tot felul de forme. Putem observa acest fapt pe multe pagini web, dispozitive mobile și aplicații desktop. Cele mai notabile exemple sunt butoanele aplicației, care sunt folosite pentru a declanșa anumite acțiuni atunci când se da clic. În loc de formă strict dreptunghiulară cu unghiuri de 90 de grade în colțuri, acestea sunt adesea desenate cu colțuri rotunjite. Colțurile rotunjite fac ca interfața cu utilizatorul să se simtă mai netedă și mai plăcută. Nu sunt pe deplin convins de asta, dar prietenul meu designer îmi spune așa.

Colțurile rotunjite fac ca interfața cu utilizatorul să se simtă mai netedă și mai plăcută

Colțurile rotunjite fac ca interfața cu utilizatorul să se simtă mai netedă și mai plăcută.
Tweet

Elementele vizuale ale interfețelor utilizator sunt create de designeri, iar programatorul trebuie doar să le pună în locurile potrivite. Dar ce se întâmplă când trebuie să generăm din mers o formă cu colțuri rotunjite și nu o putem preîncărca? Unele biblioteci de programare oferă capacități limitate pentru a crea forme predefinite cu colțuri rotunjite, dar, de obicei, nu pot fi utilizate în cazuri mai complicate. De exemplu, cadrul Qt are o clasă QPainter , care este folosită pentru a desena toate clasele derivate din QPaintDevice , inclusiv widget-uri, hărți pixeli și imagini. Are o metodă numită drawRoundedRect , care, așa cum sugerează și numele, desenează un dreptunghi cu colțuri rotunjite. Dar dacă avem nevoie de o formă puțin mai complexă, trebuie să o implementăm singuri. Cum am putea face asta cu un poligon, o formă plană delimitată de un grup de segmente de linie dreaptă? Dacă avem un poligon desenat cu un creion pe o bucată de hârtie, prima mea idee ar fi să folosesc o gumă de șters și să șterg o mică parte din liniile de la fiecare colț și apoi să conectez capetele segmentelor rămase cu un arc circular. Întregul proces poate fi ilustrat în figura de mai jos.

Cum să creați manual colțuri rotunjite

Clasa QPainter are câteva metode supraîncărcate numite drawArc , care pot desena arce circulare. Toate necesită parametri, care definesc centrul și dimensiunea arcului, unghiul de pornire și lungimea arcului. Deși este ușor să determinați valorile necesare ale acestor parametri pentru un dreptunghi nerotat, este o problemă cu totul diferită atunci când avem de-a face cu poligoane mai complexe. În plus, ar trebui să repetăm ​​acest calcul pentru fiecare vârf de poligon. Acest calcul este o sarcină lungă și obositoare, iar oamenii sunt predispuși la tot felul de erori de calcul în acest proces. Cu toate acestea, este datoria dezvoltatorilor de software să facă computerele să funcționeze pentru ființe umane, și nu invers. Deci, aici voi arăta cum să dezvolt o clasă simplă, care poate transforma un poligon complex într-o formă cu colțuri rotunjite. Utilizatorii acestei clase vor trebui doar să atașeze vârfuri de poligon, iar clasa va face restul. Instrumentul matematic esențial pe care îl folosesc pentru această sarcină este curba Bezier.

curbe Bezier

Există o mulțime de cărți de matematică și resurse de internet care descriu teoria curbelor Bezier, așa că voi sublinia pe scurt proprietățile relevante.

Prin definiție, curba Bezier este o curbă între două puncte de pe o suprafață bidimensională, a cărei traiectorie este guvernată de unul sau mai multe puncte de control. Strict vorbind, o curbă între două puncte fără puncte de control suplimentare este, de asemenea, o curbă Bezier. Cu toate acestea, deoarece acest lucru are ca rezultat o linie dreaptă între cele două puncte, nu este deosebit de interesant și nici util.

Curbe Bezier cuadratice

Curbele Bezier pătratice au un singur punct de control. Teoria spune că o curbă Bezier pătratică între punctele P 0 și P 2 cu punctul de control P 1 este definită după cum urmează:

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

Deci, când t este egal cu 0 , B(t) va da P 0 , când t este egal cu 1 , B(t) va da P2 , dar în orice alt caz, valoarea lui B(t) va depinde și de P 1 . Deoarece expresia 2t(1 - t) are o valoare maximă la t = 0,5 , acolo influența lui P 1 asupra B(t) va fi cea mai mare. Ne putem gândi la P 1 ca la o sursă imaginară de gravitație, care trage traiectoria funcției spre sine. Figura de mai jos prezintă câteva exemple de curbe Bezier pătratice cu punctele lor de început, sfârșit și de control.

Curbe Bezier cuadratice

Deci, cum ne rezolvăm problema folosind curbele Bezier? Figura de mai jos oferă o explicație.

Cum să creați colțuri rotunjite folosind codul

Dacă ne imaginăm ștergerea unui vârf de poligon și a unei părți scurte din segmentele de linie conectate în împrejurimile lui, ne putem gândi la un capăt de segment de linie ca de P 0 , celălalt capăt de segment de linie ca de P 2 și vârful șters ca de P 1 . Aplicăm o curbă Bezier pătratică acestui set de puncte și voilà, există colțul rotunjit dorit.

Implementarea C++/Qt folosind QPainter

Clasa QPainter nu are o modalitate de a desena curbe Bezier pătratice. Deși este destul de ușor să-l implementați de la zero urmând ecuația (1), biblioteca Qt oferă o soluție mai bună. Există o altă clasă puternică pentru desenul 2D: QPainterPath . Clasa QPainterPath este o colecție de linii și curbe care pot fi adăugate și utilizate ulterior cu obiectul QPainter . Există câteva metode supraîncărcate care adaugă curbe Bezier la o colecție curentă. În special, metodele quadTo vor adăuga curbe Bezier pătratice. Curba va începe la punctul curent QPainterPath ( P 0 ), în timp ce P 1 și P 2 trebuie să fie transmise la quadTo ca parametri.

Metoda QPainter drawPath este folosită pentru a desena o colecție de linii și curbe din obiectul QPainterPath , care trebuie dat ca parametru, cu creion și pensulă active.

Deci, să vedem declarația clasei:

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

Am decis să subclasez QPolygon , astfel încât să nu fiu nevoit să implementez singur adăugarea de vârfuri și alte lucruri. Pe lângă constructor, care doar setează raza la o valoare inițială sensibilă, această clasă are alte două metode publice:

  • Metoda SetRadius setează raza la o valoare dată. Raza este lungimea unei linii drepte (în pixeli) lângă fiecare vârf, care va fi ștearsă (sau, mai precis, nedesenată) pentru colțul rotunjit.
  • GetPath este locul unde au loc toate calculele. Va returna obiectul QPainterPath generat din punctele poligonului adăugate la RoundedPolygon .

Metodele din partea privată sunt doar metode auxiliare utilizate de GetPath .

Să vedem implementarea și voi începe cu metodele private:

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

Nu sunt multe de explicat aici, metoda returnează distanța euclidiană dintre cele două puncte date.

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

Metoda GetLineStart calculează locația punctului P 2 din ultima cifră, dacă punctele sunt adăugate la poligon în sensul acelor de ceasornic. Mai precis, va returna un punct, care este m_uiRadius pixeli depărtare de i --lea vârf în direcția către (i+1) --lea vârf. La accesarea vârfului (i+1) --lea, trebuie să ne amintim că în poligon există și un segment de linie între ultimul și primul vârf, ceea ce îl face o formă închisă, deci expresia (i+1)%count() . Acest lucru împiedică, de asemenea, metoda să iasă din interval și accesează în schimb primul punct. Variabila fRat deține raportul dintre rază și lungimea i -lea a segmentului de linie. Există, de asemenea, o verificare care împiedică fRat să aibă o valoare peste 0.5 . Dacă fRat ar avea o valoare peste 0.5 , atunci cele două colțuri rotunjite consecutive s-ar suprapune, ceea ce ar provoca un rezultat vizual slab.

Când călătorim de la punctul P 1 la P 2 în linie dreaptă și completând 30 la sută din distanță, putem determina locația noastră folosind formula 0,7 • P 1 + 0,3 • P 2 . În general, dacă obținem o fracțiune din distanța completă, iar α = 1 indică distanța completă, locația curentă este la (1 - α) • P1 + α • P2 .

Acesta este modul în care metoda GetLineStart determină locația punctului care este m_uiRadius pixeli distanță de i --lea vârf în direcția (i+1) --lea.

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

Această metodă este foarte asemănătoare cu GetLineStart . Acesta calculează locația punctului P 0 pentru (i+1) --lea vârf, nu i --lea. Cu alte cuvinte, dacă tragem o linie de la GetLineStart(i) la GetLineEnd(i) pentru fiecare i între 0 și n-1 , unde n este numărul de vârfuri din poligon, am obține poligonul cu vârfuri șterse și lor în apropierea împrejurimilor.

Și acum, metoda clasei 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; }

În această metodă, construim obiectul QPainterPath . Dacă poligonul nu are cel puțin trei vârfuri, nu mai avem de-a face cu o formă 2D, iar în acest caz, metoda emite un avertisment și returnează calea goală. Când sunt disponibile suficiente puncte, trecem peste toate segmentele de linie dreaptă ale poligonului (numărul de segmente de linie este, desigur, egal cu numărul de vârfuri), calculând începutul și sfârșitul fiecărui segment de linie dreaptă dintre cele rotunjite. colțuri. Punem o linie dreaptă între aceste două puncte și o curbă Bezier pătratică între sfârșitul segmentului de linie anterior și începutul curentului, folosind locația vârfului curent ca punct de control. După buclă, trebuie să închidem traseul cu o curbă Bezier între ultimul și primul segment de linie pentru că în buclă am tras cu o linie dreaptă mai mult decât curbele Bezier.

Utilizarea și rezultatele clasei RoundedPolygon

Acum este timpul să vedem cum să folosiți această clasă în practică.

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

Această bucată de cod sursă este destul de simplă. După inițializarea a două QPixmaps și QPainters lor, creăm un obiect RoundedPolygon și îl umplem cu puncte. Painter P1 desenează poligonul obișnuit, în timp ce P2 desenează QPainterPath cu colțuri rotunjite, generate din poligon. Ambele hărți pixeli rezultate sunt salvate în fișierele lor, iar rezultatele sunt următoarele:

Colțuri rotunjite folosind QPainter

Concluzie

Am văzut că generarea unei forme cu colțuri rotunjite dintr-un poligon nu este atât de dificilă până la urmă, mai ales dacă folosim un cadru de programare bun, cum ar fi Qt. Acest proces poate fi automatizat de clasa pe care am descris-o în acest blog ca o dovadă de concept. Cu toate acestea, există încă mult loc de îmbunătățire, cum ar fi:

  • Faceți colțuri rotunjite numai la vârfurile selectate și deloc.
  • Faceți colțuri rotunjite cu raze diferite la vârfuri diferite.
  • Implementați o metodă, care generează o polilinie cu colțuri rotunjite (polilinia în terminologia Qt este la fel ca poligonul, cu excepția faptului că nu este o formă închisă, deoarece îi lipsește segmentul de linie dintre ultimul și primul vârf).
  • Utilizați RoundedPolygon pentru a genera hărți de biți, care pot fi utilizate ca mască de widget de fundal pentru a produce widget-uri în formă nebună.
  • Clasa RoundedPolygon nu este optimizată pentru viteza de execuție; L-am lăsat așa cum este pentru a înțelege mai ușor conceptul. Optimizarea poate include calcularea multor valori intermediare la adăugarea unui nou vârf la poligon. De asemenea, atunci când GetPath este pe cale să returneze o referință la QPainterPath generat, ar putea seta un steag, care indică faptul că obiectul este actualizat. Următorul apel la GetPath ar avea ca rezultat returnarea aceluiași obiect QPainterPath , fără a recalcula nimic. Dezvoltatorul ar trebui, totuși, să se asigure că acest steag este șters la fiecare modificare în oricare dintre nodurile poligonului, precum și pe fiecare vârf nou, ceea ce mă face să cred că clasa optimizată ar fi mai bine dezvoltată de la zero și nu derivată. de la QPolygon . Vestea bună este că acest lucru nu este atât de dificil pe cât pare.

În total, clasa RoundedPolygon , așa cum este, poate fi folosită ca instrument oricând dorim să adăugăm o notă de designer la GUI-ul nostru din mers, fără a pregăti hărți pixeli sau forme în avans.

Înrudit: Cum să înveți limbajele C și C++: Lista finală