
مقدمة
إحدى ميزات لغة C ++ التي يتم توجيه النقد إليها غالبًا هي الافتقار إلى آلية معالجة الأحداث في المعيار. وفي الوقت نفسه ، تعد هذه الآلية إحدى الطرق الرئيسية لتفاعل بعض مكونات البرامج مع مكونات البرامج والأجهزة الأخرى ، ويتم تنفيذها على مستوى نظام تشغيل معين. بطبيعة الحال ، كل منصة لها الفروق الدقيقة في تنفيذ الآلية الموصوفة.
فيما يتعلق بكل ما سبق ، عند التطوير في C ++ ، هناك حاجة لتنفيذ معالجة الأحداث بطريقة أو بأخرى ، ويتم حلها باستخدام مكتبات وإطارات عمل تابعة لجهات خارجية. يوفر إطار عمل Qt المعروف آلية للإشارات والفتحات ، مما يسمح بتنظيم تفاعل الطبقات الموروثة من QObject. تنفيذ الأحداث موجود أيضا في مكتبة دفعة. وبالطبع ، لم يستطع محرك OpenSceneGraph الاستغناء عن "الدراجة" الخاصة به ، والتي ستتم مناقشتها في المقال.
OSG هي مكتبة رسومات مجردة. من ناحية ، فإنه يستخلص من الواجهة الإجرائية لبرنامج OpenGL ، ويزود المطور بمجموعة من الفئات التي تغلف كل آليات OpneGL API. من ناحية أخرى ، فإنه يستخلص أيضًا من واجهة مستخدم رسومية معينة ، نظرًا لاختلاف طرق تطبيقها في الأنظمة الأساسية المختلفة ولديها ميزات حتى داخل نفس النظام الأساسي (MFC ، Qt ، .Net for Windows ، على سبيل المثال).
بغض النظر عن النظام الأساسي ، من وجهة نظر التطبيق ، فإن تفاعل المستخدم مع الواجهة الرسومية يتلخص في إنشاء سلسلة من الأحداث من خلال عناصرها ، والتي يتم معالجتها بعد ذلك داخل التطبيق. تستخدم معظم الأطر الرسومية هذا النهج ، لكن حتى داخل نفس المنصة ، فهي ، للأسف ، غير متوافقة مع بعضها البعض.
لهذا السبب ، توفر OSG واجهة أساسية خاصة بها لمعالجة أحداث عنصر واجهة المستخدم وإدخال المستخدم استنادًا إلى فئة osgGA :: GUIEventHandler. يمكن إرفاق معالج هذا العارض عن طريق استدعاء الأسلوب addEventHandler () وإزالته بواسطة أسلوب removeEventHandler (). وبطبيعة الحال ، يجب أن ترث فئة معالج الخرسانة من فئة osgGA :: GUIEventHandler ، ويجب إعادة تعريف طريقة المقبض () فيها. يقبل هذا الأسلوب الوسيطتين: osgGA :: GUIEventAdapter ، والذي يحتوي على قائمة انتظار الأحداث من واجهة المستخدم الرسومية و osg :: GUIActionAdepter ، المستخدمة للتعليقات. نموذجي في التعريف هو مثل هذا التصميم
bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdepter &aa) {
تتيح المعلمة osgGA :: GUIActionAdapter للمطور أن يطلب من واجهة المستخدم الرسومية اتخاذ بعض الإجراءات استجابة لهذا الحدث. في معظم الحالات ، يتأثر العارض من خلال هذه المعلمة ، وهو مؤشر يمكن الحصول عليه عن طريق تحويل المؤشر الديناميكي
osgViewer::Viewer* viewer = dynamic_cast<osgViewer::Viewer *>(&aa);
1. لوحة المفاتيح والحدث التعامل مع الماوس
تدير فئة osgGA :: GUIEventAdapter () جميع أنواع الأحداث التي تدعمها OSG ، مما يوفر بيانات لإعداد المعلمات واستعادتها. إرجاع الأسلوب getEventType () الحدث GUI الحالي المضمنة في قائمة انتظار الحدث. في كل مرة ، عند تجاوز طريقة المؤشر () الخاصة بالمعالج ، عند استدعاء هذه الطرق ، يجب عليك استخدام هذا getter لتلقي الحدث وتحديد نوعه.
يصف الجدول التالي جميع الأحداث المتاحة.
نوع الحدث | الوصف | طرق استرجاع بيانات الحدث |
---|
دفع / الإفراج / DOUBLECLICK | انقر / الإصدار وانقر نقرًا مزدوجًا فوق أزرار الماوس | getX () ، getY () - الحصول على موضع المؤشر. getButton () - رمز الزر المضغوط (LEFT_MOUSE_BUTTON، RIGHT_MOUSE_BUTTON، MIDDLE_MOUSE_BUTTON |
سكرول | التمرير عجلة الماوس (ق) | getScrollingMotion () - إرجاع SCROOL_UP ، SCROLL_DOWN ، SCROLL_LEFT ، SCROLL_RIGHT |
السحب | جر الماوس | getX () ، getY () - وضع المؤشر ؛ getButtonMask () - قيم مشابهة لـ getButton () |
تحرك | تحرك الماوس | getX () ، getY () - موضع المؤشر |
كيونداون / كيوب | الضغط على / تحرير مفتاح على لوحة المفاتيح | getKey () - رمز ASCII للمفتاح المضغوط أو قيمة العداد Key_Symbol (على سبيل المثال ، KEY_BackSpace) |
الإطار | حدث تم إنشاؤه عند تقديم إطار | لا المدخلات |
المستخدم | حدث معرف المستخدم | getUserDataPointer () - بإرجاع مؤشر إلى مخزن مؤقت لبيانات المستخدم (يتم التحكم في المخزن المؤقت بواسطة مؤشر ذكي) |
هناك أيضًا طريقة getModKeyMask () لاسترداد معلومات حول مفتاح معدل مضغوط (تُرجع قيم النموذج MODKEY_CTRL و MODKEY_SHIFT و MODKEY_ALT وما إلى ذلك) ، مما يتيح لك معالجة مجموعات المفاتيح التي تستخدم المعدلات
if (ea.getModKeyMask() == osgGA::GUIEventAdapter::MODKEY_CTRL) {
ضع في اعتبارك أن أساليب setter مثل setX () ، setY () ، setEventType () ، إلخ. لا تستخدم في معالج () يتم استدعاؤها بواسطة نظام النوافذ الرسومية ذي المستوى المنخفض في OSG للوقوف في قائمة الأحداث.
2. نحن نتحكم سيسنا من لوحة المفاتيح
نحن نعرف بالفعل كيفية تحويل كائنات المشهد من خلال فئات osg :: MatrixTransform. درسنا أنواعًا مختلفة من الرسوم المتحركة باستخدام فصول osg :: AnimationPath و osg :: Animation. ولكن بالنسبة للتفاعل بين تطبيق ما (على سبيل المثال ، لعبة) ، من الواضح أن الرسوم المتحركة والتحولات ليست كافية. تتمثل الخطوة التالية في التحكم في موضع الكائنات على المسرح من أجهزة إدخال المستخدم. دعونا نحاول ربط الإدارة بسيسنا الحبيب.
مثال لوحة المفاتيحالرئيسية #ifndef MAIN_H #define MAIN_H #include <osg/MatrixTransform> #include <osgDB/ReadFile> #include <osgGA/GUIEventHandler> #include <osgViewer/Viewer> #endif
main.cpp #include "main.h"
لحل هذه المشكلة ، نكتب فئة معالج حدث إدخال
class ModelController : public osgGA::GUIEventHandler { public: ModelController( osg::MatrixTransform *node ) : _model(node) {} virtual bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa); protected: osg::ref_ptr<osg::MatrixTransform> _model; };
عند إنشاء هذه الفئة ، كمعلمة ، يتم تمريرها مؤشرًا إلى عقدة التحويل ، والتي سنعمل عليها في المعالج. يتم إعادة تعريف أسلوب معالج handle () نفسه على النحو التالي
bool ModelController::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) { (void) aa; if (!_model.valid()) return false; osg::Matrix matrix = _model->getMatrix(); switch (ea.getEventType()) { case osgGA::GUIEventAdapter::KEYDOWN: switch (ea.getKey()) { case 'a': case 'A': matrix *= osg::Matrix::rotate(-0.1, osg::Z_AXIS); break; case 'd': case 'D': matrix *= osg::Matrix::rotate( 0.1, osg::Z_AXIS); break; case 'w': case 'W': matrix *= osg::Matrix::rotate(-0.1, osg::X_AXIS); break; case 's': case 'S': matrix *= osg::Matrix::rotate( 0.1, osg::X_AXIS); break; default: break; } _model->setMatrix(matrix); break; default: break; } return false; }
من بين التفاصيل الأساسية لتنفيذه ، تجدر الإشارة إلى أنه يجب علينا أولاً الحصول على مصفوفة التحويل من العقدة التي نتحكم فيها
osg::Matrix matrix = _model->getMatrix();
بعد ذلك ، يقوم عبارتان من مفاتيح التبديل المتداخلة () بتحليل نوع الحدث (ضغط المفاتيح) ورمز المفتاح المضغوط. اعتمادًا على رمز المفتاح المضغوط ، يتم ضرب مصفوفة التحويل الحالية بمصفوفة دوران إضافية حول المحور المقابل
case 'a': case 'A': matrix *= osg::Matrix::rotate(-0.1, osg::Z_AXIS); break;
- اقلب المستوى بزاوية من 0،1 راديان عند الضغط على مفتاح "A".
بعد معالجة ضغطات المفاتيح ، لا تنسَ تطبيق مصفوفة تحويل جديدة على عقدة التحويل
_model->setMatrix(matrix);
في الوظيفة الرئيسية () ، قم بتحميل طراز الطائرة وإنشاء عقدة تحويل أصلية له ، مضيفًا الرسم البياني الناتج إلى عقدة الجذر للمشهد
osg::ref_ptr<osg::Node> model = osgDB::readNodeFile("../data/cessna.osg"); osg::ref_ptr<osg::MatrixTransform> mt = new osg::MatrixTransform; mt->addChild(model.get()); osg::ref_ptr<osg::Group> root = new osg::Group; root->addChild(mt.get());
إنشاء وتهيئة معالج إدخال المستخدم
osg::ref_ptr<ModelController> mcontrol = new ModelController(mt.get());
أنشئ عارضًا بإضافة معالجنا إليه
osgViewer::Viewer viewer; viewer.addEventHandler(mcontrol.get());
قم بإعداد مصفوفة عرض الكاميرا
viewer.getCamera()->setViewMatrixAsLookAt( osg::Vec3(0.0f, -100.0f, 0.0f), osg::Vec3(), osg::Z_AXIS );
لا تسمح للكاميرا بتلقي الأحداث من أجهزة الإدخال
viewer.getCamera()->setAllowEventFocus(false);
إذا لم يتم ذلك ، فسيقوم المعالج المعلق بالكاميرا باعتراض كل مدخلات المستخدم ويتداخل مع معالجنا. قمنا بتعيين بيانات المشهد إلى العارض وتشغيله
viewer.setSceneData(root.get()); return viewer.run();
الآن ، بعد إطلاق البرنامج ، سنتمكن من التحكم في اتجاه الطائرة في الفضاء عن طريق الضغط على المفاتيح A و D و W و S.

سؤال مثير للاهتمام هو ما هي الطريقة التي يجب أن تعود بها المقبض () عند الخروج منها. إذا تم إرجاع true ، فسنشير إلى OSG ، ثم قمنا بمعالجة أحداث الإدخال بالفعل ولن تكون هناك حاجة إلى مزيد من المعالجة. في أغلب الأحيان ، لن يناسبنا هذا السلوك ، لذلك من الجيد أن نعود إلى false من المعالج حتى لا نوقف معالجة الأحداث من قبل معالجات آخرين إذا كانت مرتبطة بعقد أخرى في المشهد.
3. استخدام الزوار في معالجة الحدث
على نحو مشابه لكيفية تنفيذه عند اجتياز رسم بياني للمشهد عند تحديثه ، يدعم OSG عمليات الاسترجاعات لمعالجة الأحداث التي يمكن أن ترتبط بالعقد والكائنات الهندسية. لهذا ، يتم استخدام استدعاءات setEventCallback () و addEventCallback () ، والتي تأخذ كمعلمة مؤشر إلى التابع osg :: NodeCallback. لتلقي الأحداث في عامل التشغيل () ، يمكننا تحويل المؤشر الذي تم تمريره إليه إلى زائر الموقع إلى مؤشر إلى osgGA :: EventVisitor ، على سبيل المثال مثل هذا
#include <osgGA/EventVisitor> ... void operator()( osg::Node *node, osg::NodeVisitor *nv ) { std::list<osg::ref_ptr<osgGA::GUIEventAdapter>> events; osgGA::EventVisitor *ev = dynamic_cast<osgGA::EventVisitor *>(nv); if (ev) { events = ev->getEvents(); // } }
4. إنشاء ومعالجة الأحداث المخصصة
يستخدم OSG قائمة انتظار أحداث داخلية (FIFO). تتم معالجة الأحداث في بداية قائمة الانتظار وحذفها منه. يتم وضع الأحداث التي تم إنشاؤها حديثًا في نهاية قائمة الانتظار. سيتم تنفيذ الأسلوب handle () لكل معالج حدث عدة مرات كما توجد أحداث في قائمة الانتظار. يتم وصف قائمة انتظار الأحداث بواسطة فئة osgGA :: EventQueue ، والتي ، من بين أشياء أخرى ، تسمح لك بوضع حدث في قائمة الانتظار في أي وقت عن طريق استدعاء الأسلوب addEvent (). الوسيطة إلى هذه الطريقة هي مؤشر إلى osgGA :: GUIEventAdapter ، والتي يمكن تعيينها إلى سلوك معين باستخدام أساليب setEventType () وما إلى ذلك.
أحد أساليب فئة osgGA :: EventQueue هي userEvent () ، والذي يقوم بتعيين حدث مستخدم من خلال ربطه ببيانات المستخدم ، وهو مؤشر يتم تمريره إليها كمعلمة. يمكن استخدام هذه البيانات لتمثيل أي حدث مخصص.
غير قادر على إنشاء مثيل لقائمة انتظار الأحداث. تم بالفعل إنشاء هذا المثيل وإرفاقه بمثيل العارض ، بحيث يمكنك فقط الحصول على مؤشر لهذا المفرد
viewer.getEventQueue()->userEvent(data);
بيانات المستخدم هي كائن الوريث من osg :: Referenced ، أي أنه يمكنك إنشاء مؤشر ذكي لها.
عند تلقي حدث مخصص ، يمكن للمطور استخراج البيانات منه عن طريق استدعاء الأسلوب getUserData () ومعالجته بالطريقة التي يراها مناسبة.
5. تنفيذ توقيت المستخدم
توفر العديد من المكتبات وإطارات العمل التي تقوم بتطبيق واجهة المستخدم الرسومية مطورًا للفئة لتطبيق أجهزة ضبط الوقت التي تنشئ حدثًا بعد فترة زمنية محددة. لا يحتوي OSG على وسائل منتظمة لتنفيذ أجهزة ضبط الوقت ، لذلك دعونا نحاول تنفيذ بعض أنواع المؤقت من تلقاء نفسه ، باستخدام الواجهة لإنشاء أحداث مخصصة.
ما الذي يمكننا الاعتماد عليه عند حل هذه المشكلة؟ بالنسبة لحدث دوري معين يتم إنشاؤه باستمرار بواسطة التجسيد ، على سبيل المثال ، على FRAME ، حدث رسم الإطار التالي. لهذا نستخدم نفس المثال مع تبديل نموذج سيسنا من الطبيعي إلى الحرق.
مثال الموقتالرئيسية #ifndef MAIN_H #define MAIN_H #include <osg/Switch> #include <osgDB/ReadFile> #include <osgGA/GUIEventHandler> #include <osgViewer/Viewer> #include <iostream> #endif
main.cpp #include "main.h"
أولاً ، دعونا نحدد تنسيق البيانات المرسلة في رسالة المستخدم ، ونعرّفها على أنها بنية
struct TimerInfo : public osg::Referenced { TimerInfo(unsigned int c) : _count(c) {} unsigned int _count; };
ستحتوي المعلمة _count على عدد صحيح بالمللي ثانية التي انقضت من لحظة بدء تشغيل البرنامج حتى استلام حدث المؤقت التالي. يرث الهيكل من الفئة osg :: Referenced بحيث يمكن التحكم به من خلال مؤشرات OSG الذكية. الآن إنشاء معالج الحدث
class TimerHandler : public osgGA::GUIEventHandler { public: TimerHandler(osg::Switch *sw, unsigned int interval = 1000) : _switch(sw) , _count(0) , _startTime(0.0) , _interval(interval) , _time(0) { } virtual bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa); protected: osg::ref_ptr<osg::Switch> _switch; unsigned int _count; double _startTime; unsigned int _interval; unsigned int _time; };
يحتوي هذا المعالج على عدة أعضاء محميين محددين. يشير متغير _switch إلى عقدة تبديل نماذج الطائرة؛ _count - العد التنازلي النسبي للوقت المنقضي منذ الجيل الأخير لحدث المؤقت ، يعمل على حساب الفواصل الزمنية ؛ _startTime - متغير مؤقت لتخزين العد التنازلي السابق ، الذي أجراه العارض ؛ _time - إجمالي وقت تشغيل البرنامج بالميلي ثانية. يقبل مُنشئ الفصل عقدة التبديل كمعلمة ، واختيارياً ، الفاصل الزمني المطلوب لتشغيل مؤقت التبديل.
في هذه الفئة ، نتجاوز طريقة المقبض ()
bool TimerHandler::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) { switch (ea.getEventType()) { case osgGA::GUIEventAdapter::FRAME: { osgViewer::Viewer *viewer = dynamic_cast<osgViewer::Viewer *>(&aa); if (!viewer) break; double time = viewer->getFrameStamp()->getReferenceTime(); unsigned int delta = static_cast<unsigned int>( (time - _startTime) * 1000.0); _startTime = time; if ( (_count >= _interval) || (_time == 0) ) { viewer->getEventQueue()->userEvent(new TimerInfo(_time)); _count = 0; } _count += delta; _time += delta; break; } case osgGA::GUIEventAdapter::USER: if (_switch.valid()) { const TimerInfo *ti = dynamic_cast<const TimerInfo *>(ea.getUserData()); std::cout << "Timer event at: " << ti->_count << std::endl; _switch->setValue(0, !_switch->getValue(0)); _switch->setValue(1, !_switch->getValue(1)); } break; default: break; } return false; }
هنا نقوم بتحليل نوع الرسالة المستلمة. إذا كانت FRAME ، فيتم تنفيذ الإجراءات التالية:
- الحصول على مؤشر للمشاهد
osgViewer::Viewer *viewer = dynamic_cast<osgViewer::Viewer *>(&aa);
- عند استلام المؤشر الصحيح ، اقرأ الوقت المنقضي منذ بدء البرنامج
double time = viewer->getFrameStamp()->getReferenceTime();
حساب مقدار الوقت الذي يقضيه تقديم إطار بالمللي ثانية
unsigned int delta = static_cast<unsigned int>( (time - _startTime) * 1000.0);
وتذكر عدد الوقت الحالي
_startTime = time;
إذا تجاوزت قيمة العداد _count الفاصل الزمني المطلوب (أو كانت هذه هي المكالمة الأولى عندما لا يزال _time يساوي الصفر) ، فسنضع رسالة المستخدم في قائمة الانتظار ، ويمر وقت البرنامج بالهيكل المذكور أعلاه بالمللي ثانية. تتم إعادة تعيين عداد _count إلى صفر
if ( (_count >= _interval) || (_time == 0) ) { viewer->getEventQueue()->userEvent(new TimerInfo(_time)); _count = 0; }
بغض النظر عن قيمة _count ، يجب أن نزيدها ووقتها بمقدار التأخير المطلوب لرسم إطار
_count += delta; _time += delta;
هذه هي الطريقة التي سيتم إنشاء الحدث الموقت. يتم تنفيذ معالجة الأحداث على النحو التالي
case osgGA::GUIEventAdapter::USER: if (_switch.valid()) { const TimerInfo *ti = dynamic_cast<const TimerInfo *>(ea.getUserData()); std::cout << "Timer event at: " << ti->_count << std::endl; _switch->setValue(0, !_switch->getValue(0)); _switch->setValue(1, !_switch->getValue(1)); } break;
نحن هنا نتحقق من صلاحية المؤشر إلى عقدة التبديل ، ونطرح البيانات من الحدث ، بدءًا من بنية TimerInfo ، ونعرض محتويات الهيكل على الشاشة ونحول حالة العقدة.
يشبه الكود الموجود في الوظيفة main () الكود الموجود في المثالين السابقين للتبديل ، مع اختلاف أننا في هذه الحالة نعلق معالج حدث على العارض
viewer.addEventHandler(new TimerHandler(root.get(), 1000));
تمرير المؤشر إلى عقدة الجذر والفاصل الزمني للتبديل المطلوب بالميلي ثانية إلى مُنشئ المعالج. عند تشغيل المثال ، سنرى أن النماذج تتحول على فترات زمنية ثانية ، وفي وحدة التحكم ، نجد ناتج الأوقات التي حدث فيها التبديل
Timer event at: 0 Timer event at: 1000 Timer event at: 2009 Timer event at: 3017 Timer event at: 4025 Timer event at: 5033
يمكن إنشاء حدث مخصص في أي وقت أثناء تنفيذ البرنامج ، وليس فقط عند تلقي حدث FRAME ، مما يتيح آلية مرنة للغاية لتبادل البيانات بين أجزاء البرنامج ، مما يجعل من الممكن معالجة الإشارات من أجهزة الإدخال غير القياسية ، مثل قفازات التحكم أو قفازات VR ، على سبيل المثال.
أن تستمر ...