كيفية الحصول على أشكال زوايا مستديرة في C ++ باستخدام Bezier Curves و QPainter: دليل خطوة بخطوة

نشرت: 2022-03-11

مقدمة

الاتجاه الحالي في تصميم الرسومات هو استخدام الكثير من الزوايا الدائرية في جميع أنواع الأشكال. يمكننا ملاحظة هذه الحقيقة على العديد من صفحات الويب والأجهزة المحمولة وتطبيقات سطح المكتب. أبرز الأمثلة هي الأزرار الانضغاطية للتطبيق ، والتي تُستخدم لتشغيل بعض الإجراءات عند النقر فوقها. بدلاً من الشكل المستطيل بدقة بزوايا 90 درجة في الزوايا ، غالبًا ما يتم رسمها بزوايا مستديرة. الزوايا الدائرية تجعل واجهة المستخدم أكثر سلاسة وأجمل. لست مقتنعًا تمامًا بهذا ، لكن صديقي المصمم يخبرني بذلك.

الزوايا الدائرية تجعل واجهة المستخدم أكثر سلاسة وأجمل

الزوايا الدائرية تجعل واجهة المستخدم أكثر سلاسة وأجمل.
سقسقة

يتم إنشاء العناصر المرئية لواجهات المستخدم بواسطة المصممين ، ولا يتعين على المبرمج إلا وضعها في الأماكن الصحيحة. ولكن ماذا يحدث ، عندما يتعين علينا إنشاء شكل بزوايا مستديرة أثناء الطيران ، ولا يمكننا تحميله مسبقًا؟ تقدم بعض مكتبات البرمجة إمكانات محدودة لإنشاء أشكال محددة مسبقًا بزوايا دائرية ، ولكن عادةً لا يمكن استخدامها في الحالات الأكثر تعقيدًا. على سبيل المثال ، يحتوي إطار عمل Qt على فئة QPainter ، والتي تُستخدم للرسم على جميع الفئات المشتقة من QPaintDevice ، بما في ذلك الأدوات ، والخرائط البيكسية ، والصور. لها طريقة تسمى drawRoundedRect ، والتي ، كما يوحي الاسم ، ترسم مستطيلاً بزوايا دائرية. ولكن إذا احتجنا إلى شكل أكثر تعقيدًا ، فعلينا أن نطبقه بأنفسنا. كيف يمكننا فعل ذلك باستخدام مضلع ، شكل مستو يحده مجموعة من مقاطع الخط المستقيم؟ إذا كان لدينا مضلع مرسوم بقلم رصاص على قطعة من الورق ، فستكون فكرتي الأولى هي استخدام ممحاة وحذف جزء صغير من الخطوط في كل زاوية ثم توصيل الأطراف المتبقية بقوس دائري. يمكن توضيح العملية برمتها في الشكل أدناه.

كيفية إنشاء زوايا دائرية يدويًا

تحتوي الفئة QPainter على بعض الطرق المحملة بشكل زائد باسم drawArc ، والتي يمكنها رسم أقواس دائرية. تتطلب كل منهم معلمات تحدد مركز القوس وحجمه وزاوية البداية وطول القوس. في حين أنه من السهل تحديد القيم الضرورية لهذه المعلمات لمستطيل غير مستدير ، فإن الأمر مختلف تمامًا عندما نتعامل مع مضلعات أكثر تعقيدًا. بالإضافة إلى ذلك ، علينا تكرار هذه العملية الحسابية لكل رأس مضلع. هذا الحساب هو مهمة طويلة ومرهقة ، والبشر عرضة لجميع أنواع الأخطاء الحسابية في هذه العملية. ومع ذلك ، فإن مهمة مطوري البرمجيات هي جعل أجهزة الكمبيوتر تعمل من أجل البشر ، وليس العكس. لذلك ، سأقوم هنا بتوضيح كيفية تطوير فئة بسيطة ، والتي يمكنها تحويل مضلع معقد إلى شكل بزوايا مستديرة. سيتعين على مستخدمي هذه الفئة فقط إلحاق رؤوس المضلع ، وسيقوم الفصل بالباقي. الأداة الرياضية الأساسية التي أستخدمها لهذه المهمة هي منحنى بيزير.

منحنيات بيزير

هناك الكثير من الكتب الرياضية ومصادر الإنترنت التي تصف نظرية منحنيات بيزيير ، لذلك سأوجز بإيجاز الخصائص ذات الصلة.

بحكم التعريف ، منحنى بيزير هو منحنى بين نقطتين على سطح ثنائي الأبعاد ، يتحكم مساره بنقطة تحكم واحدة أو أكثر. بالمعنى الدقيق للكلمة ، فإن المنحنى بين نقطتين بدون نقاط تحكم إضافية ، هو أيضًا منحنى بيزير. ومع ذلك ، نظرًا لأن هذا يؤدي إلى خط مستقيم بين النقطتين ، فإنه ليس مثيرًا للاهتمام ولا مفيدًا بشكل خاص.

منحنيات بيزير التربيعية

منحنيات بيزير التربيعية لها نقطة تحكم واحدة. تقول النظرية أن منحنى بيزير التربيعي بين النقطتين 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 كمصدر وهمي للجاذبية ، يسحب مسار الوظيفة نحو نفسه. يوضح الشكل أدناه بعض الأمثلة على منحنيات بيزير التربيعية بنقاط البداية والنهاية والتحكم.

منحنيات بيزير التربيعية

إذن ، كيف نحل مشكلتنا باستخدام منحنيات بيزير؟ يقدم الشكل أدناه تفسيرا.

كيفية إنشاء زوايا مستديرة باستخدام الكود

إذا تخيلنا حذف رأس مضلع وجزء قصير من مقاطع الخط المتصلة في المناطق المحيطة به ، فيمكننا التفكير في نهاية قطعة خطية واحدة على أنها P 0 ، ونهاية قطعة مستقيمة أخرى كما في P 2 والرأس المحذوف اعتبارًا من P 1 . نطبق منحنى بيزير تربيعيًا على هذه المجموعة من النقاط وفويلا ، هناك الزاوية المستديرة المرغوبة.

تنفيذ C ++ / Qt باستخدام QPainter

لا تملك فئة QPainter طريقة لرسم منحنيات بيزيير من الدرجة الثانية. في حين أنه من السهل جدًا تنفيذه من البداية باتباع المعادلة (1) ، فإن مكتبة Qt تقدم حلاً أفضل. هناك فئة أخرى قوية للرسم ثنائي الأبعاد: QPainterPath . فئة 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 pixels بعيدًا عن رأس i -th في الاتجاه نحو الرأس (i+1) . عند الوصول إلى الرأس (i+1) -th ، علينا أن نتذكر أنه في المضلع ، يوجد أيضًا مقطع خطي بين الرأس الأخير والأول ، مما يجعله شكلًا مغلقًا ، وبالتالي التعبير (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 pixels عن رأس 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 . تحسب موقع النقطة P 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 . إذا لم يكن للمضلع ثلاثة رؤوس على الأقل ، فإننا لم نعد نتعامل مع شكل ثنائي الأبعاد ، وفي هذه الحالة ، تصدر الطريقة تحذيرًا وتعيد المسار الفارغ. عندما تتوفر نقاط كافية ، نقوم بعمل حلقة فوق جميع مقاطع الخط المستقيم للمضلع (عدد مقاطع الخط ، بالطبع ، يساوي عدد الرؤوس) ، ونحسب بداية ونهاية كل مقطع خط مستقيم بين المقطع المستدير زوايا. وضعنا خطًا مستقيمًا بين هاتين النقطتين ومنحنى بيزير تربيعيًا بين نهاية مقطع الخط السابق وبداية التيار ، مستخدمين موقع الرأس الحالي كنقطة تحكم. بعد الحلقة ، يتعين علينا إغلاق المسار بمنحنى بيزير بين مقطعي الخط الأخير والأول لأننا في الحلقة رسمنا خطًا مستقيمًا واحدًا أكثر من منحنيات بيزير.

استخدام فئة 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 دائرية ناتجة عن المضلع. يتم حفظ كلتا الصورتين البيكسلتين الناتجتين في ملفاتهما ، وتكون النتائج كما يلي:

زوايا مدورة باستخدام QPainter

خاتمة

لقد رأينا أن إنشاء شكل بزوايا مستديرة من مضلع ليس بالأمر الصعب على الإطلاق ، خاصة إذا استخدمنا إطار عمل برمجة جيد مثل Qt. يمكن أتمتة هذه العملية بواسطة الفصل الذي وصفته في هذه المدونة كدليل على المفهوم. ومع ذلك ، لا يزال هناك مجال كبير للتحسين ، مثل:

  • قم بعمل زوايا مستديرة عند الرؤوس المحددة فقط وليس جميعها.
  • اصنع زوايا دائرية بنصف قطر مختلف عند رؤوس مختلفة.
  • نفِّذ طريقة تُنشئ خطًا متعدد الخطوط بزوايا دائرية (متعدد الخطوط في مصطلحات Qt يشبه المضلع تمامًا ، إلا أنه ليس شكلًا مغلقًا لأنه يفتقد قطعة الخط بين الرأس الأخير والأول).
  • استخدم RoundedPolygon لإنشاء صور نقطية ، والتي يمكن استخدامها كقناع عنصر واجهة مستخدم في الخلفية لإنتاج عناصر واجهة مستخدم مجنونة الشكل.
  • لم يتم تحسين فئة RoundedPolygon لسرعة التنفيذ ؛ لقد تركتها كما هي لتسهيل فهم المفهوم. قد يتضمن التحسين حساب عدد كبير من القيم الوسيطة عند إلحاق رأس جديد بالمضلع. أيضًا ، عندما يكون GetPath على وشك إرجاع مرجع إلى QPainterPath الذي تم إنشاؤه ، يمكنه تعيين علامة تشير إلى أن الكائن محدث. سيؤدي الاستدعاء التالي لـ GetPath إلى إرجاع نفس كائن QPainterPath فقط ، دون إعادة حساب أي شيء. ومع ذلك ، سيتعين على المطور التأكد من مسح هذه العلامة عند كل تغيير في أي من رؤوس المضلع ، وكذلك في كل قمة جديدة ، مما يجعلني أعتقد أنه من الأفضل تطوير الفئة المحسّنة من نقطة الصفر وليس اشتقاقها من QPolygon . النبأ السار هو أن هذا ليس بالصعوبة التي يبدو عليها.

إجمالاً ، يمكن استخدام فئة RoundedPolygon ، كما هي ، كأداة في أي وقت نريد إضافة لمسة مصمم إلى واجهة المستخدم الرسومية الخاصة بنا أثناء التنقل ، دون إعداد خرائط بكسل أو أشكال مسبقًا.

الموضوعات ذات الصلة: كيفية تعلم لغات C و C ++: القائمة النهائية