Bezier Eğrileri ve QPainter Kullanarak C++'ta Yuvarlak Köşe Şekilleri Nasıl Elde Edilir: Adım Adım Kılavuz
Yayınlanan: 2022-03-11Tanıtım
Grafik tasarımındaki mevcut eğilim, her türlü şekilde çok sayıda yuvarlatılmış köşe kullanmaktır. Bu gerçeği birçok web sayfasında, mobil cihazda ve masaüstü uygulamasında görebiliyoruz. En dikkate değer örnekler, tıklandığında bazı eylemleri tetiklemek için kullanılan uygulama basma düğmeleridir. Köşelerde 90 derecelik açılara sahip kesinlikle dikdörtgen şekil yerine, genellikle köşeleri yuvarlatılmış olarak çizilirler. Yuvarlatılmış köşeler, kullanıcı arayüzünün daha yumuşak ve daha hoş görünmesini sağlar. Bu konuda tam olarak ikna olmadım ama tasarımcı arkadaşım bana öyle söylüyor.
Kullanıcı arayüzlerinin görsel öğeleri tasarımcılar tarafından oluşturulur ve programcının bunları doğru yerlere koyması yeterlidir. Ancak, anında köşeleri yuvarlatılmış bir şekil oluşturmamız gerektiğinde ve onu önceden yükleyemediğimiz zaman ne olur? Bazı programlama kitaplıkları, köşeleri yuvarlatılmış önceden tanımlanmış şekiller oluşturmak için sınırlı yetenekler sunar, ancak genellikle daha karmaşık durumlarda kullanılamazlar. Örneğin, Qt çerçevesi, parçacıklar, pixmapler ve görüntüler dahil olmak üzere QPaintDevice
türetilen tüm sınıflar üzerinde çizim yapmak için kullanılan bir QPainter
sınıfına sahiptir. Adından da anlaşılacağı gibi, köşeleri yuvarlatılmış bir dikdörtgen çizen drawRoundedRect
adlı bir yöntemi vardır. Ama biraz daha karmaşık bir şekle ihtiyacımız varsa, onu kendimiz uygulamak zorundayız. Bunu, bir grup düz doğru parçasıyla sınırlanmış düzlemsel bir şekil olan bir çokgenle nasıl yapabiliriz? Bir kağıda kurşun kalemle çizilmiş bir çokgenimiz varsa, ilk fikrim bir silgi kullanarak her köşedeki çizgilerin küçük bir kısmını silmek ve ardından kalan segment uçlarını dairesel bir yay ile birleştirmek olacaktır. Tüm süreç aşağıdaki şekilde gösterilebilir.
QPainter
sınıfı, dairesel yaylar çizebilen drawArc
adlı bazı aşırı yüklenmiş yöntemlere sahiptir. Hepsi, yay merkezini ve boyutunu, başlangıç açısını ve yay uzunluğunu tanımlayan parametreler gerektirir. Döndürülmemiş bir dikdörtgen için bu parametrelerin gerekli değerlerini belirlemek kolay olsa da, daha karmaşık çokgenlerle uğraşırken tamamen farklı bir konudur. Ayrıca, bu hesaplamayı her çokgen köşesi için tekrarlamamız gerekir. Bu hesaplama uzun ve yorucu bir iştir ve insanlar bu süreçte her türlü hesaplama hatasına eğilimlidir. Ancak, bilgisayarların insanlar için çalışmasını sağlamak yazılım geliştiricilerin işidir, tersi değil. Bu yüzden, burada karmaşık bir çokgeni köşeleri yuvarlatılmış bir şekle dönüştürebilen basit bir sınıfın nasıl geliştirileceğini göstereceğim. Bu sınıfın kullanıcıları yalnızca çokgen köşeleri eklemek zorunda kalacak ve gerisini sınıf yapacak. Bu görev için kullandığım temel matematiksel araç Bezier eğrisidir.
Bezier eğrileri
Bezier eğrileri teorisini anlatan çok sayıda matematik kitabı ve internet kaynağı var, bu yüzden ilgili özellikleri kısaca özetleyeceğim.
Tanım olarak, Bezier eğrisi, yörüngesi bir veya daha fazla kontrol noktası tarafından yönetilen iki boyutlu bir yüzey üzerindeki iki nokta arasındaki bir eğridir. Kesin konuşmak gerekirse, ek kontrol noktaları olmayan iki nokta arasındaki bir eğri de bir Bezier eğrisidir. Ancak bu, iki nokta arasında düz bir çizgi ile sonuçlandığından, özellikle ilginç ve kullanışlı değildir.
Kuadratik Bezier eğrileri
Kuadratik Bezier eğrilerinin bir kontrol noktası vardır. Teori, P 1 kontrol noktası ile P 0 ve P 2 noktaları arasındaki ikinci dereceden bir Bezier eğrisinin aşağıdaki gibi tanımlandığını söylüyor:
B(t) = (1 - t) 2 P 0 + 2t(1 - t)P 1 + t 2 P 2 , burada 0 ≤ t ≤ 1 (1)
Yani t 0'a eşit olduğunda, B(t) P 0 verecek, t 1'e eşit olduğunda, B(t) P 2 verecek, ancak diğer her durumda, B(t) 'nin değeri aynı zamanda aşağıdakilere bağlı olacaktır. 1 . 2t(1 - t) ifadesi t = 0,5'te maksimum bir değere sahip olduğundan, P 1'in B(t) üzerindeki etkisinin en büyük olacağı yer burasıdır. P 1'i , fonksiyon yörüngesini kendine doğru çeken hayali bir yerçekimi kaynağı olarak düşünebiliriz. Aşağıdaki şekil, başlangıç, bitiş ve kontrol noktalarıyla birlikte birkaç ikinci dereceden Bezier eğrisi örneğini göstermektedir.
Peki Bezier eğrilerini kullanarak problemimizi nasıl çözebiliriz? Aşağıdaki şekil bir açıklama sunmaktadır.
Bir çokgen tepe noktasını ve çevresindeki bağlantılı doğru parçalarının kısa bir bölümünü silmeyi hayal edersek, bir doğru parçasının sonunu P 0 , diğer doğru parçasının sonunu P 2 ve silinen tepe noktasını P 1 olarak düşünebiliriz. Bu nokta kümesine ikinci dereceden bir Bezier eğrisi uyguluyoruz ve işte, istenen yuvarlak köşe var.
QPainter kullanarak C++/Qt uygulaması
QPainter
sınıfı, ikinci dereceden Bezier eğrileri çizmenin bir yoluna sahip değildir. (1) denklemini izleyerek onu sıfırdan uygulamak oldukça kolay olsa da, Qt kitaplığı daha iyi bir çözüm sunar. 2B çizim için başka bir güçlü sınıf daha var: QPainterPath
. QPainterPath
sınıfı, daha sonra QPainter
nesnesiyle eklenebilen ve kullanılabilen bir çizgi ve eğriler topluluğudur. Geçerli bir koleksiyona Bezier eğrileri ekleyen bazı aşırı yüklenmiş yöntemler vardır. Özellikle, quadTo
yöntemleri ikinci dereceden Bezier eğrileri ekleyecektir. Eğri, mevcut QPainterPath
noktasında ( P 0 ) başlarken, P 1 ve P 2'nin parametre olarak quadTo
geçirilmesi gerekir.
QPainter
drawPath
, parametre olarak verilmesi gereken QPainterPath
nesnesinden aktif kalem ve fırça ile bir dizi çizgi ve eğri çizmek için kullanılır.
Öyleyse sınıf bildirimini görelim:
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; };
Köşeleri ve diğer şeyleri kendi başıma eklemek zorunda kalmamak için QPolygon
alt sınıflamaya karar verdim. Yarıçapı mantıklı bir başlangıç değerine ayarlayan yapıcının yanı sıra, bu sınıfın iki genel yöntemi daha vardır:
-
SetRadius
yöntemi, yarıçapı belirli bir değere ayarlar. Yarıçap, yuvarlatılmış köşe için silinecek (veya daha doğrusu çizilmeyecek) her bir tepe noktasına yakın düz bir çizginin (piksel cinsinden) uzunluğudur. -
GetPath
, tüm hesaplamaların yapıldığı yerdir.RoundedPolygon
eklenen çokgen noktalarından oluşturulanQPainterPath
nesnesini döndürür.
Özel bölümdeki yöntemler GetPath
tarafından kullanılan yardımcı yöntemlerdir.

Uygulamayı görelim ve özel yöntemlerle başlayacağım:
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); }
Burada açıklanacak pek bir şey yok, yöntem verilen iki nokta arasındaki Öklid mesafesini döndürür.
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
Yöntemi, noktalar çokgene saat yönünde eklenirse, son şekilden P 2 noktasının konumunu hesaplar. Daha doğrusu, i
-inci tepe noktasından (i+1)
-inci tepe noktasına doğru m_uiRadius
piksel uzaklıkta olan bir noktayı döndürür. (i+1)
-th köşesine erişirken, çokgende, son ve ilk köşe arasında, onu kapalı bir şekil yapan bir çizgi parçası olduğunu, dolayısıyla (i+1)%count()
ifadesinin olduğunu unutmamalıyız. (i+1)%count()
. Bu ayrıca yöntemin aralık dışına çıkmasını önler ve bunun yerine ilk noktaya erişir. Değişken fRat
, yarıçap ile i
-inci doğru parçası uzunluğu arasındaki oranı tutar. Ayrıca fRat
0.5
bir değere sahip olmasını engelleyen bir kontrol de vardır. fRat
0.5
bir değere sahipse, ardışık iki yuvarlak köşe üst üste gelecek ve bu da kötü bir görsel sonuca neden olacaktır.
P 1 noktasından P 2 noktasına düz bir çizgide giderken ve mesafenin yüzde 30'unu tamamlayarak, 0.7 • P 1 + 0.3 • P 2 formülünü kullanarak yerimizi belirleyebiliriz. Genel olarak, tam mesafenin bir kısmını elde edersek ve α = 1 tam mesafeyi gösterirse, mevcut konum (1 - α) • P1 + α • P2'dir .
GetLineStart
yöntemi, i
-inci tepe noktasından (i+1)
-inci yönünde m_uiRadius
piksel uzaklıkta olan noktanın konumunu bu şekilde belirler.
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; }
Bu yöntem GetLineStart
çok benzer. (i+1)
-inci köşe için P 0 noktasının konumunu hesaplar, i
-inci değil. Başka bir deyişle, 0
ile n-1
arasındaki her i
için GetLineEnd(i)
i)'den GetLineStart(i)
'ye bir çizgi çizersek, burada n
çokgendeki köşe sayısıdır, köşeleri silinmiş çokgeni ve bunların yakın çevre.
Ve şimdi, ana sınıf yöntemi:
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; }
Bu yöntemde QPainterPath
nesnesini oluşturuyoruz. Çokgenin en az üç köşesi yoksa, artık 2B bir şekille uğraşmıyoruz ve bu durumda yöntem bir uyarı veriyor ve boş yolu döndürüyor. Yeterli nokta mevcut olduğunda, çokgenin tüm düz çizgi parçaları üzerinde döngü yaparız (doğru parçalarının sayısı, elbette, köşelerin sayısına eşittir), yuvarlatılmış noktalar arasındaki her bir düz doğru parçasının başlangıcını ve sonunu hesaplar. köşeler. Bu iki nokta arasına düz bir çizgi ve bir önceki çizgi parçasının sonu ile akımın başlangıcı arasına ikinci dereceden bir Bezier eğrisi koyarız ve mevcut tepe noktasının konumunu kontrol noktası olarak kullanırız. Döngüden sonra, son ve ilk doğru parçaları arasında bir Bezier eğrisi ile yolu kapatmamız gerekiyor çünkü döngüde Bezier eğrilerinden bir düz çizgi fazla çizdik.
Class RoundedPolygon
kullanımı ve sonuçları
Şimdi bu sınıfın pratikte nasıl kullanılacağını görme zamanı.
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");
Bu kaynak kodu parçası oldukça basittir. İki QPixmaps
ve bunların QPainters
başlattıktan sonra bir RoundedPolygon
nesnesi oluşturuyor ve onu noktalarla dolduruyoruz. Ressam P1
normal çokgeni çizerken, P2
çokgenden oluşturulan köşeleri yuvarlatılmış QPainterPath
çizer. Ortaya çıkan her iki pixmap, dosyalarına kaydedilir ve sonuçlar aşağıdaki gibidir:
Çözüm
Özellikle Qt gibi iyi bir programlama çerçevesi kullanırsak, bir çokgenden köşeleri yuvarlatılmış bir şekil oluşturmanın o kadar da zor olmadığını gördük. Bu süreç, bu blogda bir kavram kanıtı olarak tanımladığım sınıf tarafından otomatikleştirilebilir. Bununla birlikte, iyileştirme için hala çok yer var, örneğin:
- Köşeleri yalnızca seçilen köşelerde yuvarlayın, hiçbirinde değil.
- Farklı köşelerde farklı yarıçaplı yuvarlak köşeler yapın.
- Köşeleri yuvarlatılmış bir çoklu çizgi oluşturan bir yöntem uygulayın (Qt terminolojisindeki çoklu çizgi, son ve ilk tepe noktası arasındaki çizgi parçası eksik olduğu için kapalı bir şekil olmaması dışında, çokgen gibidir).
- Çılgın şekilli widget'lar üretmek için arka plan widget maskesi olarak kullanılabilecek bitmap'ler oluşturmak için
RoundedPolygon
kullanın. -
RoundedPolygon
sınıfı, yürütme hızı için optimize edilmemiştir; Kavramın daha kolay anlaşılması için olduğu gibi bıraktım. Optimizasyon, çokgene yeni bir tepe noktası eklenmesi üzerine çok sayıda ara değerin hesaplanmasını içerebilir. AyrıcaGetPath
, oluşturulanQPainterPath
bir referans döndürmek üzereyken, nesnenin güncel olduğunu belirten bir bayrak ayarlayabilir.GetPath
yapılan bir sonraki çağrı, hiçbir şeyi yeniden hesaplamadan yalnızca aynıQPainterPath
nesnesinin döndürülmesiyle sonuçlanacaktır. Bununla birlikte, geliştirici, bu bayrağın herhangi bir poligon tepe noktasındaki her değişiklikte ve ayrıca her yeni tepe noktasında temizlendiğinden emin olmalıdır, bu da bana optimize edilmiş sınıfın sıfırdan daha iyi geliştirileceğini ve türetilmeyeceğini düşündürüyor.QPolygon
. İyi haber şu ki, bu göründüğü kadar zor değil.
Toplamda, RoundedPolygon
sınıfı, önceden piksel haritaları veya şekiller hazırlamadan, anında GUI'mize tasarımcı bir dokunuş eklemek istediğimizde bir araç olarak kullanılabilir.