วิธีรับรูปร่างมุมโค้งมนใน C ++ โดยใช้ Bezier Curves และ QPainter: คำแนะนำทีละขั้นตอน
เผยแพร่แล้ว: 2022-03-11บทนำ
แนวโน้มในการออกแบบกราฟิกในปัจจุบันคือการใช้มุมโค้งมนจำนวนมากในรูปทรงต่างๆ เราสามารถสังเกตข้อเท็จจริงนี้ได้จากหน้าเว็บ อุปกรณ์เคลื่อนที่ และแอปพลิเคชันเดสก์ท็อปจำนวนมาก ตัวอย่างที่โดดเด่นที่สุดคือปุ่มกดของแอปพลิเคชัน ซึ่งใช้เพื่อทริกเกอร์การทำงานบางอย่างเมื่อคลิก แทนที่จะเป็นรูปสี่เหลี่ยมผืนผ้าอย่างเคร่งครัดโดยมีมุม 90 องศาที่มุม มักถูกวาดด้วยมุมมน มุมโค้งมนทำให้ส่วนต่อประสานกับผู้ใช้รู้สึกนุ่มนวลและสวยงามยิ่งขึ้น ฉันไม่ค่อยมั่นใจเกี่ยวกับเรื่องนี้ แต่เพื่อนดีไซเนอร์ของฉันบอกฉันอย่างนั้น
องค์ประกอบที่มองเห็นได้ของอินเทอร์เฟซผู้ใช้สร้างขึ้นโดยนักออกแบบ และโปรแกรมเมอร์ต้องวางไว้ในที่ที่เหมาะสมเท่านั้น แต่จะเกิดอะไรขึ้น เมื่อเราต้องสร้างรูปร่างที่มีมุมโค้งมนในทันที และเราไม่สามารถโหลดล่วงหน้าได้ ไลบรารีโปรแกรมมิ่งบางตัวมีความสามารถจำกัดสำหรับการสร้างรูปร่างที่กำหนดไว้ล่วงหน้าที่มีมุมโค้งมน แต่โดยปกติแล้ว จะไม่สามารถใช้ได้ในกรณีที่ซับซ้อนกว่านี้ ตัวอย่างเช่น เฟรมเวิร์ก Qt มีคลาส QPainter
ซึ่งใช้ในการวาดบนคลาสทั้งหมดที่ได้รับจาก QPaintDevice
รวมถึงวิดเจ็ต pixmaps และรูปภาพ มีวิธีการที่เรียกว่า drawRoundedRect
ซึ่งวาดสี่เหลี่ยมที่มีมุมโค้งมนตามที่ชื่อแนะนำ แต่ถ้าเราต้องการรูปร่างที่ซับซ้อนกว่านี้อีกหน่อย เราต้องปรับใช้มันเอง เราจะทำอย่างนั้นได้อย่างไรด้วยรูปหลายเหลี่ยม รูปทรงระนาบที่ล้อมรอบด้วยกลุ่มของส่วนของเส้นตรง ถ้าเราวาดรูปหลายเหลี่ยมด้วยดินสอบนแผ่นกระดาษ ความคิดแรกของฉันคือการใช้ยางลบและลบส่วนเล็ก ๆ ของเส้นในแต่ละมุมแล้วต่อส่วนที่เหลือจะจบลงด้วยส่วนโค้งวงกลม กระบวนการทั้งหมดสามารถแสดงได้ในรูปด้านล่าง
Class QPainter
มีวิธีการโอเวอร์โหลดชื่อ drawArc
ซึ่งสามารถวาดส่วนโค้งเป็นวงกลมได้ พารามิเตอร์ทั้งหมดต้องการพารามิเตอร์ ซึ่งกำหนดศูนย์กลางและขนาดส่วนโค้ง มุมเริ่มต้น และความยาวส่วนโค้ง แม้ว่าจะกำหนดค่าที่จำเป็นของพารามิเตอร์เหล่านี้สำหรับสี่เหลี่ยมผืนผ้าที่ไม่หมุนได้ง่าย แต่ก็เป็นเรื่องที่แตกต่างอย่างสิ้นเชิงเมื่อเราจัดการกับรูปหลายเหลี่ยมที่ซับซ้อนมากขึ้น นอกจากนี้ เราจะต้องคำนวณซ้ำสำหรับจุดยอดของรูปหลายเหลี่ยมทุกจุด การคำนวณนี้เป็นงานที่ใช้เวลานานและน่าเบื่อหน่าย และมนุษย์มีแนวโน้มที่จะเกิดข้อผิดพลาดในการคำนวณทุกประเภทในกระบวนการนี้ อย่างไรก็ตาม เป็นหน้าที่ของนักพัฒนาซอฟต์แวร์ในการทำให้คอมพิวเตอร์ทำงานสำหรับมนุษย์ ไม่ใช่ในทางกลับกัน ฉันจะแสดงวิธีพัฒนาคลาสง่าย ๆ ซึ่งจะเปลี่ยนรูปหลายเหลี่ยมที่ซับซ้อนให้กลายเป็นรูปร่างที่มีมุมโค้งมน ผู้ใช้คลาสนี้จะต้องต่อท้ายจุดยอดของรูปหลายเหลี่ยม และชั้นเรียนจะจัดการที่เหลือ เครื่องมือทางคณิตศาสตร์ที่สำคัญที่ฉันใช้สำหรับงานนี้คือเส้นโค้งเบซิเยร์
เส้นโค้งเบซิเยร์
มีหนังสือคณิตศาสตร์และแหล่งข้อมูลทางอินเทอร์เน็ตมากมายที่อธิบายทฤษฎีของเส้นโค้งเบซิเยร์ ดังนั้นฉันจะสรุปคุณสมบัติที่เกี่ยวข้องโดยสังเขป
ตามคำจำกัดความ เส้นโค้งเบซิเยร์เป็นเส้นโค้งระหว่างจุดสองจุดบนพื้นผิวสองมิติ ซึ่งวิถีโคจรถูกควบคุมโดยจุดควบคุมหนึ่งจุดหรือมากกว่า พูดอย่างเคร่งครัด เส้นโค้งระหว่างจุดสองจุดโดยไม่มีจุดควบคุมเพิ่มเติม ก็เป็นเส้นโค้งเบซิเยร์เช่นกัน อย่างไรก็ตาม เนื่องจากผลลัพธ์เป็นเส้นตรงระหว่างจุดสองจุด จึงไม่น่าสนใจและไม่มีประโยชน์เป็นพิเศษ
เส้นโค้งเบซิเยร์กำลังสอง
เส้นโค้งควอดราติกเบซิเยร์มีจุดควบคุมหนึ่งจุด ทฤษฎีกล่าวว่าเส้นโค้ง Bezier กำลังสองระหว่างจุด P 0 และ P 2 ที่มีจุดควบคุม P 1 ถูกกำหนดดังนี้:
B(t) = (1 - t) 2 P 0 + 2t(1 - t)P 1 + t 2 P 2 , โดยที่ 0 ≤ t ≤ 1 (1)
ดังนั้นเมื่อ t เท่ากับ 0 , B(t) จะให้ผล P 0 เมื่อ t เท่ากับ 1 , B(t) จะให้ผล P 2 แต่ในทุกกรณี ค่าของ B(t) จะขึ้นอยู่กับ ป 1 . เนื่องจากนิพจน์ 2t(1 - t) มีค่าสูงสุดที่ t = 0.5 นั่นคือจุดที่อิทธิพลของ P 1 ต่อ B(t) จะมากที่สุด เราสามารถนึกถึง P 1 ว่าเป็นแหล่งกำเนิดแรงโน้มถ่วงในจินตภาพ ซึ่งดึงฟังก์ชันวิถีโคจรเข้าหาตัวมันเอง รูปด้านล่างแสดงตัวอย่างบางส่วนของเส้นโค้ง Bezier กำลังสองที่มีจุดเริ่มต้น จุดสิ้นสุด และจุดควบคุม
เราจะแก้ปัญหาโดยใช้เส้นโค้ง Bezier ได้อย่างไร รูปด้านล่างมีคำอธิบาย
หากเราจินตนาการถึงการลบจุดยอดรูปหลายเหลี่ยมและส่วนสั้น ๆ ของส่วนของเส้นตรงที่เชื่อมต่ออยู่โดยรอบ เราสามารถนึกถึงส่วนของเส้นตรงที่สิ้นสุดที่ P 0 อีกส่วนของเส้นตรงจะสิ้นสุดที่ P 2 และจุดยอดที่ถูกลบเมื่อ P 1 เราใช้เส้นโค้งเบซิเยร์กำลังสองกับชุดของคะแนนและ voila มีมุมโค้งมนที่ต้องการ
การใช้งาน C++/Qt โดยใช้ QPainter
Class QPainter
ไม่มีวิธีวาดเส้นโค้ง Bezier กำลังสอง แม้ว่าจะใช้งานได้ง่ายตั้งแต่เริ่มต้นตามสมการ (1) แต่ไลบรารี Qt ก็เสนอวิธีแก้ปัญหาที่ดีกว่า มีคลาสที่ทรงพลังอีกคลาสสำหรับการวาดภาพ 2 มิติ: QPainterPath
Class QPainterPath
คือชุดของเส้นและส่วนโค้งที่สามารถเพิ่มและใช้ภายหลังกับวัตถุ QPainter
มีวิธีการโอเวอร์โหลดบางอย่างที่เพิ่มเส้นโค้งเบซิเยร์ให้กับคอลเล็กชันปัจจุบัน โดยเฉพาะอย่างยิ่ง เมธอด quadTo
จะเพิ่มเส้นโค้งเบซิเยร์กำลังสอง เส้นโค้งจะเริ่มที่จุด QPainterPath
ปัจจุบัน ( P 0 ) ในขณะที่ต้องส่ง P 1 และ P 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); }
ไม่มีอะไรจะอธิบายมากในที่นี้ วิธีการคืนค่าระยะทางแบบยุคลิเดียนระหว่างจุดสองจุดที่กำหนด

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
คำนวณตำแหน่งของจุด P 2 จากตัวเลขสุดท้าย หากเพิ่มจุดลงในรูปหลายเหลี่ยมในทิศทางตามเข็มนาฬิกา แม่นยำยิ่งขึ้น มันจะส่งคืนจุด ซึ่งเป็น m_uiRadius
พิกเซล ห่างจากจุดยอดที่ i
-th ในทิศทางไปยังจุดยอด (i+1)
-th เมื่อเข้าถึงจุดยอดที่ (i+1)
- เราต้องจำไว้ว่าในรูปหลายเหลี่ยมยังมีส่วนของเส้นตรงระหว่างจุดยอดสุดท้ายและจุดยอดแรก ซึ่งทำให้รูปร่างปิด ดังนั้นนิพจน์ (i+1)%count()
. นอกจากนี้ยังป้องกันไม่ให้วิธีการอยู่นอกขอบเขตและเข้าถึงจุดแรกแทน ตัวแปร fRat
ถืออัตราส่วนระหว่างรัศมีและความยาวของส่วนของเส้นที่ i
นอกจากนี้ยังมีการตรวจสอบที่ป้องกันไม่ให้ fRat
มีค่ามากกว่า 0.5
หาก fRat
มีค่ามากกว่า 0.5
มุมโค้งมนสองมุมที่ต่อเนื่องกันจะทับซ้อนกัน ซึ่งจะทำให้ได้ภาพที่ไม่ดี
เมื่อเดินทางจากจุด P 1 ถึง P 2 เป็นเส้นตรงและเมื่อครบ 30 เปอร์เซ็นต์ของระยะทาง เราสามารถระบุตำแหน่งของเราได้โดยใช้สูตร 0.7 • P 1 + 0.3 • P 2 โดยทั่วไป หากเราได้เศษเสี้ยวของระยะทางทั้งหมด และ α = 1 หมายถึงระยะทางเต็ม ตำแหน่งปัจจุบันจะอยู่ที่ (1 - α) • P1 + α • P2
นี่คือวิธีที่เมธอด GetLineStart
กำหนดตำแหน่งของจุดที่ m_uiRadius
พิกเซลอยู่ห่างจากจุดยอดที่ i
ในทิศทางของ (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
มาก มันคำนวณตำแหน่งของจุด P 0 สำหรับจุดยอด (i+1)
- ไม่ใช่ 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 อีกต่อไป และในกรณีนี้ เมธอดจะออกคำเตือนและส่งคืนพาธว่าง เมื่อมีจุดเพียงพอ เราจะวนซ้ำทุกส่วนของเส้นตรงของรูปหลายเหลี่ยม (แน่นอนว่าจำนวนส่วนของเส้นตรงเท่ากับจำนวนจุดยอด) คำนวณจุดเริ่มต้นและจุดสิ้นสุดของแต่ละส่วนของเส้นตรงระหว่างจุดมน มุม เราวางเส้นตรงระหว่างจุดสองจุดนี้กับเส้นโค้งเบซิเยร์กำลังสองระหว่างจุดสิ้นสุดของส่วนของเส้นตรงก่อนหน้าและจุดเริ่มต้นของกระแส โดยใช้ตำแหน่งของจุดยอดปัจจุบันเป็นจุดควบคุม หลังจากวนซ้ำ เราต้องปิดเส้นทางด้วยเส้นโค้งเบซิเยร์ระหว่างส่วนของบรรทัดสุดท้ายและบรรทัดแรก เพราะในลูป เราวาดเส้นตรงมากกว่าเส้นโค้งเบซิเยร์
การใช้ Class 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
และเติมด้วยคะแนน Painter P1
วาดรูปหลายเหลี่ยมปกติ ในขณะที่ P2
วาด QPainterPath
ด้วยมุมมน ซึ่งสร้างจากรูปหลายเหลี่ยม pixmaps ที่เป็นผลลัพธ์ทั้งสองจะถูกบันทึกลงในไฟล์ของพวกเขา และผลลัพธ์จะเป็นดังนี้:
บทสรุป
เราได้เห็นแล้วว่าการสร้างรูปร่างที่มีมุมโค้งมนจากรูปหลายเหลี่ยมนั้นไม่ยากเลย โดยเฉพาะอย่างยิ่งถ้าเราใช้เฟรมเวิร์กการเขียนโปรแกรมที่ดี เช่น Qt กระบวนการนี้สามารถดำเนินการโดยอัตโนมัติโดยชั้นเรียนที่ฉันอธิบายไว้ในบล็อกนี้เป็นการพิสูจน์แนวคิด อย่างไรก็ตาม ยังมีช่องว่างให้ปรับปรุงอีกมาก เช่น:
- ทำมุมโค้งมนเฉพาะที่จุดยอดที่เลือกเท่านั้น อย่าให้มุมโค้งมนเลย
- ทำมุมโค้งมนด้วยรัศมีต่างกันที่จุดยอดต่างกัน
- ใช้วิธีการซึ่งสร้างเส้นหลายเหลี่ยมที่มีมุมโค้งมน (เส้นในคำศัพท์ Qt เหมือนกับรูปหลายเหลี่ยม ยกเว้นว่าไม่ใช่รูปร่างปิดเพราะขาดส่วนของเส้นตรงระหว่างจุดยอดสุดท้ายและจุดสุดยอดแรก)
- ใช้
RoundedPolygon
เพื่อสร้างบิตแมป ซึ่งสามารถใช้เป็นมาสก์วิดเจ็ตพื้นหลังเพื่อสร้างวิดเจ็ตที่มีรูปร่างแปลกประหลาด - คลาส
RoundedPolygon
ไม่ได้รับการปรับให้เหมาะสมกับความเร็วในการดำเนินการ ฉันปล่อยให้มันเป็นเพื่อให้เข้าใจแนวคิดได้ง่ายขึ้น การเพิ่มประสิทธิภาพอาจรวมถึงการคำนวณค่ากลางจำนวนมากเมื่อผนวกจุดยอดใหม่เข้ากับรูปหลายเหลี่ยม นอกจากนี้ เมื่อGetPath
กำลังจะส่งคืนการอ้างอิงไปยังQPainterPath
ที่สร้างขึ้น มันสามารถตั้งค่าสถานะ ซึ่งบ่งชี้ว่าอ็อบเจ็กต์นั้นเป็นข้อมูลล่าสุด การเรียกGetPath
ครั้งถัดไปจะส่งผลให้ส่งคืนอ็อบเจ็กต์QPainterPath
เดียวกันเท่านั้น โดยไม่ต้องคำนวณอะไรใหม่ อย่างไรก็ตาม นักพัฒนาซอฟต์แวร์จะต้องตรวจสอบให้แน่ใจว่าแฟล็กนี้ถูกล้างในทุกการเปลี่ยนแปลงของจุดยอดรูปหลายเหลี่ยมใดๆ รวมถึงจุดยอดใหม่ทุกจุด ซึ่งทำให้ฉันคิดว่าคลาสที่ปรับให้เหมาะสมควรได้รับการพัฒนาตั้งแต่เริ่มต้นและไม่ได้รับ จากQPolygon
ข่าวดีก็คือว่ามันไม่ได้ยากอย่างที่คิด
โดยรวมแล้ว คลาส RoundedPolygon
สามารถใช้เป็นเครื่องมือได้ทุกเมื่อที่เราต้องการเพิ่มสัมผัสของนักออกแบบให้กับ GUI ของเราได้ทันที โดยไม่ต้องเตรียม pixmaps หรือรูปร่างล่วงหน้า