مقدمة
توضح المقالة التنفيذ QObject
الأشجار في C ++ باستخدام coroutines والنطاقات باستخدام مثال تحسين الواجهة للعمل مع الأطفال من فئة QObject
من إطار عمل Qt. يتم إنشاء طريقة عرض مخصصة للعمل مع العناصر الفرعية بالتفصيل ، ويتم تقديم تطبيقات كسولة وكلاسيكية. في نهاية المقال ، يوجد رابط للمستودع بكود المصدر الكامل.
عن المؤلف
أعمل كمطور رئيسي في المكتب النرويجي لشركة Qt. لقد تم تطوير عناصر واجهة المستخدم وعناصر QtQuick ، ومؤخرا كيو تي كور. يمكنني استخدام C ++ وقليلا مهتمة في البرمجة الوظيفية. في بعض الأحيان أقوم بتقديم تقارير وأكتب مقالات.
ما هو كيو تي
Qt عبارة عن إطار عمل عبر النظام الأساسي لإنشاء واجهات المستخدم الرسومية (GUIs). بالإضافة إلى الوحدات النمطية لإنشاء واجهة المستخدم الرسومية ، يحتوي Qt على العديد من الوحدات النمطية لتطوير برامج التطبيق. تم تصميم الإطار بشكل أساسي بلغة برمجة C ++ ، بعض المكونات تستخدم QML و JavaScript .
فئة QObject
QObject هي الفئة التي تم بناء نموذج كائن Qt حولها. يمكن استخدام الفئات الموروثة من QObject
في نموذج إشارة الفتحة وحلقة الحدث. بالإضافة إلى ذلك ، يسمح لك QObject
بالوصول إلى معلومات فئة كائن التعريف وتنظيم الكائنات في هياكل شجرة.
هيكل شجرة QObject
باستخدام بنية شجرة يعني أن كل كائن QObject
يمكن أن يكون له QObject
واحد أو صفر أو أكثر من الأطفال. يتحكم الكائن الأصل في عمر الكائنات التابعة. في المثال التالي ، سيتم حذف طفلين تلقائيًا:
auto parent = std::make_unique<QObject>(); auto onDestroyed = [](auto obj){ qDebug("Object %p destroyed.", obj); }; QObject::connect(new QObject(parent.get()), &QObject::destroyed, onDestroyed); QObject::connect(new QObject(parent.get()), &QObject::destroyed, onDestroyed);
لسوء الحظ ، حتى الآن لا تعمل معظم واجهة برمجة تطبيقات Qt إلا مع المؤشرات الأولية. نحن نعمل على ذلك ، وربما سيتغير الوضع قريبًا إلى الأفضل جزئيًا على الأقل.
تسمح لك واجهة فئة QObject
بالحصول على قائمة بجميع الكائنات الفرعية والبحث حسب بعض المعايير. النظر في مثال الحصول على قائمة بجميع الكائنات التابعة:
auto parent = std::make_unique<QObject>();
إرجاع الأسلوب QObject::children
قائمة بجميع الأطفال للكائن المحدد. ومع ذلك ، غالبًا ما يكون البحث مطلوبًا بين كامل الشجرة الفرعية للكائنات وفقًا لبعض المعايير:
auto children = parent->findChildren<QObject>(QRegularExpression("0$")); qDebug() << children.count();
يوضح المثال أعلاه كيفية الحصول على قائمة بجميع العناصر الفرعية من نوع QObject
الذي ينتهي اسمه بـ 0. على عكس طريقة children
، فإن طريقة findChildren
تعبر الشجرة بشكل متكرر ، أي أنها تبحث في التسلسل الهرمي الكامل للكائنات. يمكن تغيير هذا السلوك بتمرير علامة Qt::FindDirectChildrenOnly
.
عيوب الواجهة للعمل مع العناصر الفرعية
للوهلة الأولى ، قد يبدو أن واجهة العمل مع الأطفال مدروسة ومرنة. ومع ذلك ، فهو لا يخلو من العيوب. دعونا نفكر في بعضها:
- واجهة زائدة عن الحاجة
هناك طريقتان مختلفتان لـ findChildren
(لم تكن هناك منذ ثلاثة وقت طويل): طريقة findChild
لإيجاد عنصر واحد وطريقة الأطفال. كل منهم يتداخل جزئيا. - الواجهة صعبة التغيير
تضمن Qt التوافق الثنائي والتوافق على مستوى الكود المصدري في إصدار رئيسي واحد. لذلك ، لا يمكنك فقط تغيير توقيع طريقة أو إضافة طرق جديدة. - من الصعب توسيع الواجهة
بالإضافة إلى انتهاك التوافق ، من المستحيل ، على سبيل المثال ، الحصول على قائمة بالعناصر التابعة وفقًا لمعيار محدد. لإضافة هذه الوظيفة ، يجب عليك انتظار الإصدار التالي أو إنشاء طريقة أخرى. - أكثر من نسخ جميع العناصر
غالبًا ما تحتاج فقط إلى الاطلاع على قائمة بجميع العناصر الفرعية التي تمت تصفيتها وفقًا لمعايير معينة. للقيام بذلك ، ليس من الضروري إرجاع حاوية مؤشرات إلى كل هذه العناصر. - انتهاك SRP ممكن
هذه مسألة مثيرة للجدل إلى حد ما ، ومع ذلك ، فإن الحاجة إلى تغيير واجهة الفصل لتغيير ، على سبيل المثال ، طريقة لعبور الأطفال تبدو غريبة.
باستخدام range-v3 لإصلاح بعض العيوب
range-v3 هي مكتبة توفر مكونات للعمل مع نطاقات من العناصر. في الواقع ، هذه طبقة إضافية من التجريد على التكرارات الكلاسيكية ، والتي تتيح لك تكوين العمليات والاستفادة من العمليات الحسابية الكسولة.
يتم استخدام مكتبة تابعة لجهة خارجية لأنه في وقت كتابة هذا التقرير ، لم يكن هناك مترجمين معروفين للمؤلف مع دعم مضمن لهذه الوظيفة. ربما سيتغير الوضع قريبًا.
بالنسبة إلى QObject
سيسمح لنا استخدام هذا النهج بفصل عمليات اجتياز شجرة العناصر الفرعية من الفصل وإنشاء واجهة مرنة للبحث عن الكائنات وفقًا لمعايير معينة ، والتي يمكن تعديلها بسهولة.
نطاقات v3 سبيل المثال
للبدء ، فكر في مثال بسيط لاستخدام المكتبة. قبل المتابعة إلى المثال ، نقدم رمزًا مختصراً لمساحات الأسماء:
namespace r = ranges; namespace v = r::views; namespace a = r::actions;
الآن خذ بعين الاعتبار مثال لبرنامج يقوم بطباعة مكعبات من جميع الأرقام الفردية في الفاصل الزمني [1 ، 10) بالترتيب العكسي:
auto is_odd = [](int n) { return n % 2 != 0; }; auto pow3 = [](int n) { return std::pow(n, 3); };
تجدر الإشارة إلى أن جميع الحسابات تحدث بتكاسل ، أي مجموعات البيانات المؤقتة لا يتم إنشاؤها أو نسخها. البرنامج أعلاه مكافئ لهذا ، باستثناء تنسيق الإخراج:
كما ترون من المثال أعلاه ، فإن المكتبة تتيح لك تكوين العديد من العمليات بأمان. يمكن العثور على المزيد من أمثلة الاستخدام في tests
examples
الدلائل الخاصة بمستودع range-v3 .
فئة لتمثيل سلسلة من الأطفال
توفر مكتبة range-v3
فصولاً مساعدة لإنشاء فئات مجمعة مخصصة مختلفة ؛ من بينها فئات من فئة view
. تم تصميم هذه الفئات لتمثيل سلسلة من العناصر بطريقة معينة دون تحويل ونسخ التسلسل نفسه. في المثال السابق ، تم استخدام فئة filter
للنظر فقط في عناصر التسلسل التي تطابق المعايير المحددة.
لإنشاء مثل هذه الفئة للعمل مع عناصر تابعة لـ QObject ، يجب أن تكون موروثة من ranges::view_facade
المساعدة ranges::view_facade
:
namespace qt::detail { template <class T = QObject> class children_view : public r::view_facade<children_view<T>> {
تجدر الإشارة إلى أن الفصل يحدد تلقائيًا طريقة end_cursor
، والتي تُرجع علامة نهاية التسلسل. إذا لزم الأمر ، يمكن تجاوز هذه الطريقة.
بعد ذلك ، نحدد فئة المؤشر نفسها. يمكن القيام بذلك داخل الفصل الدراسي الخاص children_view
وخارجهم:
struct cursor {
المؤشر المحدد أعلاه هو تمرير واحد. هذا يعني أن التسلسل يُسمح له بالتحرك في اتجاه واحد ومرة واحدة فقط. لهذا التنفيذ ، وهذا ليس ضروريا ، لأن نقوم بتخزين سلسلة من جميع الكائنات الفرعية ويمكننا المرور خلالها في أي اتجاه كما تشاء. للإشارة إلى أنه يمكنك متابعة تسلسل عدة مرات ، يجب تطبيق الطريقة التالية في فئة المؤشر:
auto equal(const cursor &that) const { return current_index == that.current_index; }
تحتاج الآن إلى إضافة للتأكد من أن طريقة العرض التي تم إنشاؤها يمكن تضمينها في التكوين. للقيام بذلك ، استخدم ranges::make_pipeable
الوظائف المساعدة ranges::make_pipeable
:
namespace qt { constexpr auto children = r::make_pipeable([](auto &&o) { return detail::children_view(o); }); constexpr auto find_children(Qt::FindChildOptions opts = Qt::FindChildrenRecursively) { return r::make_pipeable([opts](auto &&o) { return detail::children_view(o, opts); }); } }
الآن يمكنك كتابة هذا الكود:
for (auto &&c : root | qt::children) {
تطبيق وظائف فئة QObject الحالية
بعد تنفيذ فئة العرض التقديمي ، يمكنك بسهولة تنفيذ جميع الوظائف للعمل مع الأطفال. للقيام بذلك ، تحتاج إلى تنفيذ ثلاث وظائف:
namespace qt { template <class T> const auto with_type = v::filter([](auto &&o) { using ObjType = std::remove_cv_t<std::remove_pointer_t<T>>; return ObjType::staticMetaObject.cast(o); }) | v::transform([](auto &&o){ return static_cast<T>(o); }); auto by_name(const QString &name) { return v::filter([name](auto &&obj) { return obj->objectName() == name; }); } auto by_re(const QRegularExpression &re) { return v::filter([re](auto &&obj) { return re.match(obj->objectName()).hasMatch(); }); } }
كمثال للاستخدام ، خذ بعين الاعتبار الكود التالي:
for (auto &&c : root | qt::children | qt::with_type<Foo*>) {
استنتاجات وسيطة
كما يمكن الحكم عليه بواسطة الكود ، أصبح الآن من السهل للغاية توسيع الوظائف دون تغيير واجهة الفصل. بالإضافة إلى ذلك ، يتم تمثيل جميع العمليات بوظائف منفصلة ويمكن ترتيبها بالتسلسل المطلوب. يعمل هذا ، من بين أشياء أخرى ، على تحسين إمكانية قراءة التعليمات البرمجية ويتجنب استخدام الوظائف مع العديد من المعلمات في واجهة الفئة. تجدر الإشارة أيضًا إلى تفريغ واجهة الفصل وتقليل عدد أسباب تغييرها.
في الواقع ، هذا التطبيق يلغي بالفعل جميع العيوب المدرجة في الواجهة ، باستثناء أنه لا يزال يتعين علينا نسخ جميع الأطفال إلى الحاوية. طريقة واحدة لحل هذه المشكلة هي استخدام coroutines.
كسول تنفيذ اجتياز شجرة الكائن باستخدام coroutines
يسمح لك Coroutines (coroutines) بإيقاف الوظيفة مؤقتًا واستئنافها لاحقًا. يمكنك اعتبار هذه التكنولوجيا بمثابة نوع من آلة الحالة المحدودة.
في وقت كتابة هذا التقرير ، كانت المكتبة القياسية تفتقر إلى العديد من العناصر المهمة الضرورية للاستخدام المريح للكوروتينات. لذلك ، يُقترح استخدام مكتبة cppcoro تابعة لجهة خارجية ، والتي من المحتمل أن تدخل المعيار بشكل أو بآخر.
بادئ ذي بدء ، سنكتب الوظائف التي ستعيد الطفل التالي عند الطلب:
namespace qt::detail { cppcoro::recursive_generator<QObject*> takeChildRecursivelyImpl( const QObjectList &children, Qt::FindChildOptions opts) { for (QObject *c : children) { if (opts == Qt::FindChildrenRecursively) { co_yield takeChildRecursivelyImpl(c->children(), opts); } co_yield c; } } cppcoro::recursive_generator<QObject*> takeChildRecursively( QObject *root, Qt::FindChildOptions opts = Qt::FindChildrenRecursively) { if (root) { co_yield takeChildRecursivelyImpl(root->children(), opts); } } }
تقوم التعليمة co_yield
بإرجاع القيمة إلى رمز الاستدعاء co_yield
مؤقتًا.
الآن دمج هذا الرمز في فئة children_view
. تعرض التعليمة البرمجية التالية العناصر التي تغيرت فقط:
يجب أيضًا تعديل المؤشر:
template <class T> struct children_view<T>::cursor { cppcoro::recursive_generator<QObject*>::iterator it; decltype(auto) read() const { return *it; } void next() { ++it; } auto equal(ranges::default_sentinel_t) const { return it == cppcoro::recursive_generator<QObject*>::iterator(nullptr); } explicit cursor(cppcoro::recursive_generator<QObject*>::iterator it): it(it) {} cursor() = default; };
المؤشر هنا ببساطة بمثابة غلاف حول التكرار العادية. يمكن استخدام باقي الكود كما هو ، دون تغييرات إضافية.
مخاطر المشي كسول شجرة
تجدر الإشارة إلى أن اجتياز شجرة الأطفال البطيئة ليس آمنًا دائمًا. يتعلق هذا بشكل أساسي بتجاوز التسلسلات الهرمية المعقدة لعناصر الرسوم ، على سبيل المثال ، عناصر واجهة التعامل. والحقيقة هي أنه في عملية التقاطع ، يمكن إعادة بناء التسلسل الهرمي ، وتتم إزالة بعض العناصر بالكامل. إذا كنت تستخدم الحل البديل في هذه الحالة ، فيمكنك الحصول على نتائج ممتعة للغاية وغير متوقعة للبرنامج.
هذا يعني أنه في بعض الحالات يكون من المفيد نسخ جميع العناصر في حاوية. للقيام بذلك ، يمكنك استخدام وظيفة المساعد التالية:
auto children = ranges::to<std::vector>(root | qt::children);
بالمعنى الدقيق للكلمة ، في هذه الحالة ليست هناك حاجة لاستخدام coroutines ويمكنك استخدام طريقة العرض من التكرار الأول.
هل سيكون في كيو تي
ربما ، ولكن ليس في الإصدار التالي. هناك عدة أسباب لهذا:
- سيتطلب الإصدار الرئيسي التالي ، Qt 6 ، دعم C ++ 17 رسميًا ودعمه ، ولكن ليس أعلى.
- لا توجد طريقة لتطبيقها بدون مكتبات الجهات الخارجية.
- سيكون من الصعب نسبيًا تكييف قاعدة الشفرة الحالية.
على الأرجح ، سيعودون إلى هذه المشكلة كجزء من إصدار Qt 7.
استنتاج
التنفيذ المقترح لاجتياز شجرة العناصر الفرعية يجعل من السهل إضافة وظائف جديدة. نظرًا لفصل العمليات ، يتم تحقيق رمز نظافة وإزالة العناصر غير الضرورية من واجهة الفصل.
تجدر الإشارة إلى أن كلا من المكتبات المستخدمة (range-v3 و cpp-coro) يتم توفيرهما كملفات للرأس ، مما يبسط عملية الإنشاء. في المستقبل ، سيكون من الممكن الاستغناء عن مكتبات الطرف الثالث على الإطلاق.
ومع ذلك ، فإن النهج الموصوف لديه بعض العيوب. من بينها ، يمكن للمرء أن يلاحظ بناء الجملة غير العادي للعديد من المطورين ، والتعقيد النسبي للتنفيذ والكسل ، والذي يمكن أن يكون خطيرًا في بعض الحالات.
بالإضافة إلى ذلك
شفرة المصدر
شكر خاص لـ Misha Svetkin (Trilla) لمساهمته في تنفيذ ومناقشة المشروع.