Cara Mendapatkan Bentuk Sudut Bulat di C++ Menggunakan Kurva Bezier dan QPainter: Panduan Langkah-demi-Langkah
Diterbitkan: 2022-03-11pengantar
Tren desain grafis saat ini adalah menggunakan banyak sudut membulat dalam berbagai bentuk. Kita dapat mengamati fakta ini di banyak halaman web, perangkat seluler, dan aplikasi desktop. Contoh yang paling menonjol adalah tombol tekan aplikasi, yang digunakan untuk memicu beberapa tindakan saat diklik. Alih-alih bentuk persegi panjang dengan sudut 90 derajat di sudut, mereka sering digambar dengan sudut membulat. Sudut membulat membuat antarmuka pengguna terasa lebih halus dan lebih bagus. Saya tidak sepenuhnya yakin tentang ini, tetapi teman desainer saya memberi tahu saya begitu.
Elemen visual antarmuka pengguna dibuat oleh desainer, dan programmer hanya perlu meletakkannya di tempat yang tepat. Tetapi apa yang terjadi, ketika kita harus membuat bentuk dengan sudut membulat dengan cepat, dan kita tidak dapat memuatnya terlebih dahulu? Beberapa pustaka pemrograman menawarkan kemampuan terbatas untuk membuat bentuk yang telah ditentukan sebelumnya dengan sudut membulat, tetapi biasanya, mereka tidak dapat digunakan dalam kasus yang lebih rumit. Misalnya, kerangka kerja Qt memiliki kelas QPainter , yang digunakan untuk menggambar pada semua kelas yang diturunkan dari QPaintDevice , termasuk widget, pixmaps, dan gambar. Ini memiliki metode yang disebut drawRoundedRect , yang, seperti namanya, menggambar persegi panjang dengan sudut membulat. Tetapi jika kita membutuhkan bentuk yang sedikit lebih kompleks, kita harus menerapkannya sendiri. Bagaimana kita bisa melakukannya dengan poligon, bentuk planar yang dibatasi oleh sekelompok segmen garis lurus? Jika kita memiliki poligon yang digambar dengan pensil di selembar kertas, ide pertama saya adalah menggunakan penghapus dan menghapus sebagian kecil garis di setiap sudut dan kemudian menghubungkan ujung segmen yang tersisa dengan busur melingkar. Seluruh proses dapat diilustrasikan pada gambar di bawah ini.
Kelas QPainter memiliki beberapa metode kelebihan beban bernama drawArc , yang dapat menggambar busur lingkaran. Semuanya membutuhkan parameter, yang menentukan pusat dan ukuran busur, sudut awal dan panjang busur. Meskipun mudah untuk menentukan nilai yang diperlukan dari parameter ini untuk persegi panjang yang tidak diputar, ini adalah masalah yang sama sekali berbeda ketika kita berurusan dengan poligon yang lebih kompleks. Plus, kita harus mengulangi perhitungan ini untuk setiap simpul poligon. Perhitungan ini adalah tugas yang panjang dan melelahkan, dan manusia rentan terhadap segala macam kesalahan perhitungan dalam prosesnya. Namun, adalah tugas pengembang perangkat lunak untuk membuat komputer bekerja untuk manusia, dan bukan sebaliknya. Jadi, di sini saya akan menunjukkan bagaimana mengembangkan kelas sederhana, yang dapat mengubah poligon kompleks menjadi bentuk dengan sudut membulat. Pengguna kelas ini hanya perlu menambahkan simpul poligon, dan kelas akan melakukan sisanya. Alat matematika penting yang saya gunakan untuk tugas ini, adalah kurva Bezier.
kurva Bezier
Ada banyak buku matematika dan sumber internet yang menjelaskan teori kurva Bezier, jadi saya akan menguraikan secara singkat sifat-sifat yang relevan.
Menurut definisi, kurva Bezier adalah kurva antara dua titik pada permukaan dua dimensi, yang lintasannya diatur oleh satu atau lebih titik kontrol. Sebenarnya, kurva antara dua titik tanpa titik kontrol tambahan, juga merupakan kurva Bezier. Namun, karena ini menghasilkan garis lurus antara dua titik, itu tidak terlalu menarik, juga tidak berguna.
Kurva Bezier kuadrat
Kurva Bezier kuadrat memiliki satu titik kontrol. Teori mengatakan bahwa kurva Bezier kuadrat antara titik P 0 dan P 2 dengan titik kontrol P 1 didefinisikan sebagai berikut:
B(t) = (1 - t) 2 P 0 + 2t(1 - t)P 1 + t 2 P 2 , di mana 0 t 1 (1)
Jadi ketika t sama dengan 0 , B(t) akan menghasilkan P 0 , ketika t sama dengan 1 , B(t) akan menghasilkan P 2 , tetapi dalam setiap kasus lain, nilai B(t) juga akan tergantung pada P 1 . Karena ekspresi 2t(1 - t) memiliki nilai maksimal pada t = 0,5 , di situlah pengaruh P 1 terhadap B(t) akan paling besar. Kita dapat menganggap P 1 sebagai sumber gravitasi imajiner, yang menarik lintasan fungsi ke arah dirinya sendiri. Gambar di bawah menunjukkan beberapa contoh kurva Bezier kuadrat dengan titik awal, akhir, dan kontrolnya.
Jadi, bagaimana kita memecahkan masalah kita menggunakan kurva Bezier? Gambar di bawah ini memberikan penjelasan.
Jika kita membayangkan menghapus simpul poligon dan bagian pendek dari segmen garis yang terhubung di sekitarnya, kita dapat menganggap satu ruas garis berakhir pada P 0 , ruas garis lainnya berakhir pada P 2 dan simpul yang dihapus pada P 1 . Kami menerapkan kurva Bezier kuadrat ke kumpulan titik ini dan voila, ada sudut bulat yang diinginkan.
Implementasi C++/Qt menggunakan QPainter
Kelas QPainter tidak memiliki cara untuk menggambar kurva Bezier kuadrat. Meskipun cukup mudah untuk mengimplementasikannya dari awal mengikuti persamaan (1), perpustakaan Qt memang menawarkan solusi yang lebih baik. Ada kelas lain yang kuat untuk menggambar 2D: QPainterPath . Kelas QPainterPath adalah kumpulan garis dan kurva yang dapat ditambahkan dan digunakan nanti dengan objek QPainter . Ada beberapa metode kelebihan beban yang menambahkan kurva Bezier ke koleksi saat ini. Secara khusus, metode quadTo akan menambahkan kurva Bezier kuadrat. Kurva akan dimulai pada titik QPainterPath saat ini ( P 0 ), sedangkan P 1 dan P 2 harus diteruskan ke quadTo sebagai parameter.
Metode QPainter 's drawPath digunakan untuk menggambar kumpulan garis dan kurva dari objek QPainterPath , yang harus diberikan sebagai parameter, dengan pena dan kuas aktif.
Jadi mari kita lihat deklarasi kelas:
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; }; Saya memutuskan untuk membuat subkelas QPolygon sehingga saya tidak perlu mengimplementasikan penambahan simpul dan hal-hal lain sendiri. Selain konstruktor, yang hanya menetapkan radius ke beberapa nilai awal yang masuk akal, kelas ini memiliki dua metode publik lainnya:
- Metode
SetRadiusmenetapkan radius ke nilai yang diberikan. Jari-jari adalah panjang garis lurus (dalam piksel) di dekat setiap titik, yang akan dihapus (atau, lebih tepatnya, tidak digambar) untuk sudut yang dibulatkan. -
GetPathadalah tempat semua perhitungan dilakukan. Ini akan mengembalikan objekQPainterPathyang dihasilkan dari titik poligon yang ditambahkan keRoundedPolygon.
Metode dari bagian pribadi hanyalah metode tambahan yang digunakan oleh GetPath .

Mari kita lihat implementasinya dan saya akan mulai dengan metode pribadi:
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); }Tidak banyak yang bisa dijelaskan di sini, metode ini mengembalikan jarak Euclidian antara dua titik yang diberikan.
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; } Metode GetLineStart menghitung lokasi titik P 2 dari gambar terakhir, jika titik ditambahkan ke poligon searah jarum jam. Lebih tepatnya, ia akan mengembalikan sebuah titik, yaitu m_uiRadius piksel menjauh dari i -th vertex ke arah (i+1) -th vertex. Ketika mengakses (i+1) -th vertex, kita harus ingat bahwa di dalam poligon, juga terdapat ruas garis antara vertex terakhir dan pertama, yang membuatnya menjadi bentuk tertutup, sehingga ekspresi (i+1)%count() . Ini juga mencegah metode keluar dari jangkauan dan mengakses titik pertama sebagai gantinya. Variabel fRat memegang rasio antara jari-jari dan panjang segmen garis ke- i . Ada juga pemeriksaan yang mencegah fRat memiliki nilai lebih dari 0.5 . Jika fRat memiliki nilai lebih dari 0.5 , maka dua sudut membulat yang berurutan akan tumpang tindih, yang akan menyebabkan hasil visual yang buruk.
Ketika bepergian dari titik P 1 ke P 2 dalam garis lurus dan dengan menyelesaikan 30 persen jarak, kita dapat menentukan lokasi kita menggunakan rumus 0,7 • P 1 + 0,3 • P 2 . Secara umum, jika kita mencapai sebagian kecil dari jarak penuh, dan = 1 menunjukkan jarak penuh, lokasi saat ini di (1 - ) • P1 + • P2 .
Beginilah cara metode GetLineStart menentukan lokasi titik yang berjarak m_uiRadius piksel dari titik ke- i ke arah (i+1) -ke.
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; } Metode ini sangat mirip dengan GetLineStart . Ini menghitung lokasi titik P 0 untuk (i+1) -th vertex, bukan i -th. Dengan kata lain, jika kita menarik garis dari GetLineStart(i) ke GetLineEnd(i) untuk setiap i antara 0 dan n-1 , di mana n adalah jumlah simpul dalam poligon, kita akan mendapatkan poligon dengan simpul terhapus dan mereka dekat lingkungan.
Dan sekarang, metode kelas utama:
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; } Dalam metode ini, kita membangun objek QPainterPath . Jika poligon tidak memiliki setidaknya tiga simpul, kita tidak lagi berurusan dengan bentuk 2D, dan dalam kasus ini, metode mengeluarkan peringatan dan mengembalikan jalur kosong. Ketika titik yang cukup tersedia, kami mengulang semua segmen garis lurus poligon (jumlah segmen garis, tentu saja, sama dengan jumlah simpul), menghitung awal dan akhir setiap segmen garis lurus antara yang dibulatkan sudut. Kami menempatkan garis lurus antara dua titik ini dan kurva Bezier kuadrat antara ujung segmen garis sebelumnya dan awal arus, menggunakan lokasi titik saat ini sebagai titik kontrol. Setelah loop, kita harus menutup jalur dengan kurva Bezier antara segmen garis terakhir dan pertama karena dalam loop kita menggambar satu garis lurus lebih banyak daripada kurva Bezier.
Penggunaan dan hasil Kelas RoundedPolygon
Sekarang saatnya untuk melihat bagaimana menggunakan kelas ini dalam praktik.
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"); Bagian dari kode sumber ini cukup mudah. Setelah menginisialisasi dua QPixmaps dan QPainters mereka, kami membuat objek RoundedPolygon dan mengisinya dengan poin. Painter P1 menggambar poligon biasa, sedangkan P2 menggambar QPainterPath dengan sudut membulat, yang dihasilkan dari poligon. Kedua pixmaps yang dihasilkan disimpan ke file mereka, dan hasilnya adalah sebagai berikut:
Kesimpulan
Kita telah melihat bahwa menghasilkan bentuk dengan sudut membulat dari poligon tidak begitu sulit, terutama jika kita menggunakan kerangka kerja pemrograman yang baik seperti Qt. Proses ini dapat diotomatisasi oleh kelas yang telah saya jelaskan di blog ini sebagai bukti konsep. Namun, masih ada banyak ruang untuk perbaikan, seperti:
- Buat sudut membulat hanya pada simpul yang dipilih dan tidak sama sekali.
- Buat sudut membulat dengan jari-jari berbeda pada simpul yang berbeda.
- Menerapkan metode, yang menghasilkan polyline dengan sudut membulat (polyline dalam terminologi Qt sama seperti poligon, kecuali itu bukan bentuk tertutup karena tidak ada segmen garis antara simpul terakhir dan pertama).
- Gunakan
RoundedPolygonuntuk menghasilkan bitmap, yang dapat digunakan sebagai topeng widget latar belakang untuk menghasilkan widget berbentuk gila. - Kelas
RoundedPolygontidak dioptimalkan untuk kecepatan eksekusi; Saya membiarkannya apa adanya agar lebih mudah memahami konsepnya. Pengoptimalan mungkin termasuk menghitung banyak nilai antara setelah menambahkan simpul baru ke poligon. Juga, ketikaGetPathakan mengembalikan referensi keQPainterPathyang dihasilkan, itu dapat menetapkan tanda, yang menunjukkan bahwa objek tersebut adalah yang terbaru. Panggilan berikutnya keGetPathhanya akan menghasilkan objekQPainterPathyang sama, tanpa menghitung ulang apa pun. Pengembang harus, bagaimanapun, harus memastikan bahwa tanda ini dihapus pada setiap perubahan di salah satu simpul poligon, serta pada setiap simpul baru, yang membuat saya berpikir bahwa kelas yang dioptimalkan lebih baik dikembangkan dari awal dan tidak diturunkan dariQPolygon. Berita baiknya adalah ini tidak sesulit kedengarannya.
Secara keseluruhan, kelas RoundedPolygon , sebagaimana adanya, dapat digunakan sebagai alat kapan saja kita ingin menambahkan sentuhan desainer ke GUI kita dengan cepat, tanpa menyiapkan pixmap atau bentuk sebelumnya.
