
Présentation
L'une des caractéristiques du langage C ++ pour laquelle il est souvent critiqué est l'absence de mécanisme de traitement des événements dans la norme. Pendant ce temps, ce mécanisme est l'un des principaux moyens d'interaction de certains composants logiciels avec d'autres composants logiciels et matériels, et il est mis en œuvre au niveau d'un système d'exploitation spécifique. Naturellement, chaque plate-forme a ses propres nuances de mise en œuvre du mécanisme décrit.
En relation avec tout ce qui précède, lors du développement en C ++, il est nécessaire d'implémenter le traitement des événements d'une manière ou d'une autre, résolu en utilisant des bibliothèques et des frameworks tiers. Le framework Qt bien connu fournit un mécanisme pour les signaux et les slots, qui permet d'organiser l'interaction des classes héritées de QObject. L'implémentation d'événements est également présente dans la bibliothèque boost. Et bien sûr, le moteur OpenSceneGraph ne pouvait se passer de son propre «vélo», dont l'application sera discutée dans l'article.
OSG est une bibliothèque graphique abstraite. D'une part, il fait abstraction de l'interface procédurale d'OpenGL, fournissant au développeur un ensemble de classes qui encapsulent la mécanique entière de l'API OpneGL. D'autre part, il résume également une interface utilisateur graphique spécifique, car les approches de sa mise en œuvre sont différentes pour différentes plates-formes et ont des fonctionnalités même au sein de la même plate-forme (MFC, Qt, .Net pour Windows, par exemple).
Quelle que soit la plateforme, du point de vue de l'application, l'interaction de l'utilisateur avec l'interface graphique se résume à la génération d'une séquence d'événements par ses éléments, qui sont ensuite traités à l'intérieur de l'application. La plupart des cadres graphiques utilisent cette approche, mais même au sein de la même plate-forme, ils ne sont malheureusement pas compatibles les uns avec les autres.
Pour cette raison, OSG fournit sa propre interface de base pour gérer les événements de widget widget et les entrées utilisateur basées sur la classe osgGA :: GUIEventHandler. Ce gestionnaire peut être attaché à la visionneuse en appelant la méthode addEventHandler () et supprimé par la méthode removeEventHandler (). Naturellement, la classe de gestionnaire concret doit être héritée de la classe osgGA :: GUIEventHandler et la méthode handle () doit y être redéfinie. Cette méthode accepte deux arguments: osgGA :: GUIEventAdapter, qui contient la file d'attente des événements de l'interface graphique et osg :: GUIActionAdepter, utilisé pour les commentaires. Typique dans la définition est une telle conception
bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdepter &aa) {
Le paramètre osgGA :: GUIActionAdapter permet au développeur de demander à l'interface graphique d'intervenir en réponse à l'événement. Dans la plupart des cas, une visionneuse est affectée par ce paramètre, un pointeur vers lequel peut être obtenu par conversion de pointeur dynamique
osgViewer::Viewer* viewer = dynamic_cast<osgViewer::Viewer *>(&aa);
1. Gestion des événements du clavier et de la souris
La classe osgGA :: GUIEventAdapter () gère tous les types d'événements pris en charge par OSG, fournissant des données pour la définition et la récupération de leurs paramètres. La méthode getEventType () renvoie l'événement GUI actuel contenu dans la file d'attente d'événements. Chaque fois, en remplaçant la méthode handle () du gestionnaire, lors de l'appel de ces méthodes, vous devez utiliser ce getter pour recevoir l'événement et déterminer son type.
Le tableau suivant décrit tous les événements disponibles.
Type d'événement | La description | Méthodes de récupération des données d'événement |
---|
PUSH / RELEASE / DOUBLECLICK | Cliquez / relâchez et double-cliquez sur les boutons de la souris | getX (), getY () - obtenir la position du curseur. getButton () - code du bouton enfoncé (LEFT_MOUSE_BUTTON, RIGHT_MOUSE_BUTTON, MIDDLE_MOUSE_BUTTON |
Scrol | Molette (s) de la souris | getScrollingMotion () - retourne SCROOL_UP, SCROLL_DOWN, SCROLL_LEFT, SCROLL_RIGHT |
DRAG | Glisser la souris | getX (), getY () - position du curseur; getButtonMask () - valeurs similaires à getButton () |
DÉPLACER | Déplacement de la souris | getX (), getY () - position du curseur |
KEYDOWN / KEYUP | Appuyer / relâcher une touche d'un clavier | getKey () - Code ASCII de la touche enfoncée ou la valeur de l'énumérateur Key_Symbol (par exemple, KEY_BackSpace) |
CADRE | Événement généré lors du rendu d'une image | pas d'entrée |
UTILISATEUR | Événement défini par l'utilisateur | getUserDataPointer () - renvoie un pointeur vers un tampon de données utilisateur (le tampon est contrôlé par un pointeur intelligent) |
Il existe également une méthode getModKeyMask () pour obtenir des informations sur la touche de modification enfoncée (renvoie les valeurs de la forme MODKEY_CTRL, MODKEY_SHIFT, MODKEY_ALT, etc.), vous permettant de traiter les combinaisons de touches utilisant des modificateurs
if (ea.getModKeyMask() == osgGA::GUIEventAdapter::MODKEY_CTRL) {
Gardez à l'esprit que les méthodes de définition comme setX (), setY (), setEventType (), etc. non utilisé dans le gestionnaire handle (). Ils sont appelés par le système de fenêtrage graphique de bas niveau OSG pour mettre l'événement en file d'attente.
2. Nous contrôlons le cessna à partir du clavier
Nous savons déjà comment transformer des objets de scène via les classes osg :: MatrixTransform. Nous avons examiné différents types d'animations à l'aide des classes osg :: AnimationPath et osg :: Animation. Mais pour l'interactivité d'une application (comme un jeu), l'animation et les transformations ne sont clairement pas suffisantes. L'étape suivante consiste à contrôler la position des objets sur la scène à partir des périphériques d'entrée utilisateur. Essayons de fixer la gestion à notre bien-aimé cessna.
Exemple de claviermain.h #ifndef MAIN_H #define MAIN_H #include <osg/MatrixTransform> #include <osgDB/ReadFile> #include <osgGA/GUIEventHandler> #include <osgViewer/Viewer> #endif
main.cpp #include "main.h"
Pour résoudre ce problème, nous écrivons une classe de gestionnaire d'événements d'entrée
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; };
Lors de la construction de cette classe, en tant que paramètre, un pointeur est transmis au nœud de transformation, sur lequel nous agirons dans le gestionnaire. La méthode du gestionnaire handle () elle-même est redéfinie comme suit
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; }
Parmi les détails essentiels de sa mise en œuvre, il convient de noter que nous devons d'abord obtenir la matrice de transformation à partir du nœud que nous contrôlons
osg::Matrix matrix = _model->getMatrix();
Ensuite, deux instructions switch () imbriquées analysent le type d'événement (frappe) et le code de la touche enfoncée. En fonction du code de la touche enfoncée, la matrice de transformation courante est multipliée par une matrice de rotation supplémentaire autour de l'axe correspondant
case 'a': case 'A': matrix *= osg::Matrix::rotate(-0.1, osg::Z_AXIS); break;
- tournez l'avion à des angles de lacet de -0,1 radians lorsque vous appuyez sur la touche "A".
Après avoir traité les frappes, n'oubliez pas d'appliquer une nouvelle matrice de transformation au nœud de transformation
_model->setMatrix(matrix);
Dans la fonction main (), chargez le modèle d'avion et créez un nœud de transformation parent pour celui-ci, en ajoutant le sous-graphique résultant au nœud racine de la scène
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());
Créer et initialiser un gestionnaire de saisie utilisateur
osg::ref_ptr<ModelController> mcontrol = new ModelController(mt.get());
Créez une visionneuse en y ajoutant notre gestionnaire
osgViewer::Viewer viewer; viewer.addEventHandler(mcontrol.get());
Configurer la matrice de vue de la caméra
viewer.getCamera()->setViewMatrixAsLookAt( osg::Vec3(0.0f, -100.0f, 0.0f), osg::Vec3(), osg::Z_AXIS );
Interdire à la caméra de recevoir des événements des périphériques d'entrée
viewer.getCamera()->setAllowEventFocus(false);
Si cela n'est pas fait, le gestionnaire accroché à la caméra interceptera par défaut toutes les entrées utilisateur et interférera avec notre gestionnaire. Nous définissons les données de la scène au spectateur et les exécutons
viewer.setSceneData(root.get()); return viewer.run();
Maintenant, après avoir lancé le programme, nous pourrons contrôler l'orientation de l'avion dans l'espace en appuyant sur les touches A, D, W et S.

Une question intéressante est de savoir ce que la méthode handle () devrait retourner en la quittant. Si true est retourné, alors nous indiquons OSG, alors nous avons déjà traité les événements d'entrée et aucun traitement supplémentaire n'est nécessaire. Le plus souvent, ce comportement ne nous convient pas, il est donc recommandé de renvoyer false du gestionnaire afin de ne pas interrompre le traitement des événements par d'autres gestionnaires s'ils sont attachés à d'autres nœuds de la scène.
3. Utilisation des visiteurs dans le traitement des événements
Semblable à la façon dont il est implémenté lors de la traversée d'un graphe de scène lors de sa mise à jour, OSG prend en charge les rappels pour gérer les événements qui peuvent être associés aux nœuds et aux objets géométriques. Pour cela, des appels à setEventCallback () et addEventCallback () sont utilisés, qui prennent en paramètre un pointeur sur l'osg :: NodeCallback enfant. Pour recevoir des événements dans l'opérateur operator (), nous pouvons convertir le pointeur qui lui est transmis au visiteur du site en un pointeur vers osgGA :: EventVisitor, par exemple comme ceci
#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. Création et traitement d'événements personnalisés
OSG utilise une file d'attente d'événements interne (FIFO). Les événements au début de la file d'attente sont traités et supprimés de celle-ci. Les événements nouvellement générés sont placés à la fin de la file d'attente. La méthode handle () de chaque gestionnaire d'événements sera exécutée autant de fois qu'il y a d'événements dans la file d'attente. La file d'attente d'événements est décrite par la classe osgGA :: EventQueue, qui, entre autres, vous permet de placer un événement dans la file d'attente à tout moment en appelant la méthode addEvent (). L'argument de cette méthode est un pointeur sur osgGA :: GUIEventAdapter, qui peut être défini sur un comportement spécifique à l'aide des méthodes setEventType (), etc.
L'une des méthodes de la classe osgGA :: EventQueue est userEvent (), qui définit un événement utilisateur en l'associant aux données utilisateur, un pointeur vers lequel lui est transmis en tant que paramètre. Ces données peuvent être utilisées pour représenter tout événement personnalisé.
Impossible de créer sa propre instance de la file d'attente d'événements. Cette instance a déjà été créée et attachée à l'instance de la visionneuse, vous ne pouvez donc obtenir qu'un pointeur vers ce singleton
viewer.getEventQueue()->userEvent(data);
Les données utilisateur sont un objet de l'héritier d'osg :: Referenced, c'est-à-dire que vous pouvez y créer un pointeur intelligent.
Lorsqu'un événement personnalisé est reçu, le développeur peut en extraire des données en appelant la méthode getUserData () et le traiter comme bon lui semble.
5. Implémentation du temporisateur utilisateur
De nombreuses bibliothèques et frameworks qui implémentent l'interface graphique fournissent un développeur de classe pour implémenter des temporisateurs qui génèrent un événement après un certain intervalle de temps. OSG ne contient pas de moyens réguliers pour implémenter des minuteries, alors essayons d'implémenter une sorte de minuteur nous-mêmes, en utilisant l'interface pour créer des événements personnalisés.
Sur quoi pouvons-nous compter pour résoudre ce problème? Pour un certain événement périodique qui est constamment généré par le rendu, par exemple sur FRAME, l'événement de dessin de l'image suivante. Pour cela, nous utilisons le même exemple avec le passage du modèle de cessna de normal à brûlant.
Exemple de minuteriemain.h #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"
Déterminons d'abord le format des données envoyées dans le message utilisateur, en le définissant comme une structure
struct TimerInfo : public osg::Referenced { TimerInfo(unsigned int c) : _count(c) {} unsigned int _count; };
Le paramètre _count contiendra le nombre entier de millisecondes qui se sont écoulées entre le moment où le programme a été lancé et le prochain événement de temporisation a été reçu. La structure hérite de la classe osg :: Referenced afin qu'elle puisse être contrôlée via des pointeurs intelligents OSG. Créez maintenant un gestionnaire d'événements
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; };
Ce gestionnaire a plusieurs membres protégés spécifiques. La variable _switch indique un nœud qui change de modèle d'avion; _count - le compte à rebours relatif du temps écoulé depuis la dernière génération de l'événement timer, sert à compter les intervalles de temps; _startTime - une variable temporaire pour stocker le compte à rebours précédent, effectuée par le spectateur; _time - la durée totale de fonctionnement du programme en millisecondes. Le constructeur de classe accepte un nœud de commutation comme paramètre et, facultativement, l'intervalle de temps requis pour que le temporisateur de commutation fonctionne.
Dans cette classe, nous remplaçons la méthode handle ()
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; }
Nous analysons ici le type de message reçu. S'il s'agit de FRAME, les actions suivantes sont effectuées:
- Obtenez un pointeur vers le spectateur
osgViewer::Viewer *viewer = dynamic_cast<osgViewer::Viewer *>(&aa);
- À la réception du pointeur correct, lisez le temps écoulé depuis le démarrage du programme
double time = viewer->getFrameStamp()->getReferenceTime();
calculer le temps passé à restituer une image en millisecondes
unsigned int delta = static_cast<unsigned int>( (time - _startTime) * 1000.0);
et rappelez-vous le compte de temps actuel
_startTime = time;
Si la valeur du compteur _count a dépassé l'intervalle de temps requis (ou s'il s'agit du premier appel lorsque _time est toujours nul), nous mettons le message utilisateur dans la file d'attente, en transmettant dans la structure ci-dessus le temps du programme en millisecondes. Le compteur _count est remis à zéro
if ( (_count >= _interval) || (_time == 0) ) { viewer->getEventQueue()->userEvent(new TimerInfo(_time)); _count = 0; }
Quelle que soit la valeur de _count, nous devons l'augmenter et _time de la quantité de retard nécessaire pour dessiner un cadre
_count += delta; _time += delta;
C'est ainsi que l'événement timer sera généré. La gestion des événements est implémentée comme suit
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;
Ici, nous vérifions la validité du pointeur sur le nœud de commutation, soustrayons les données de l'événement, provenant de la structure TimerInfo, affichons le contenu de la structure à l'écran et changeons l'état du nœud.
Le code de la fonction main () est similaire au code des deux exemples de commutation précédents, à la différence que dans ce cas, nous suspendons un gestionnaire d'événements au visualiseur
viewer.addEventHandler(new TimerHandler(root.get(), 1000));
transmettre le pointeur au nœud racine et l'intervalle de commutation requis en millisecondes au constructeur du gestionnaire. En exécutant l'exemple, nous verrons que les modèles commutent à des intervalles d'une seconde, et dans la console, nous trouvons la sortie des moments auxquels la commutation s'est produite
Timer event at: 0 Timer event at: 1000 Timer event at: 2009 Timer event at: 3017 Timer event at: 4025 Timer event at: 5033
Un événement personnalisé peut être généré à tout moment pendant l'exécution du programme, et pas seulement lors de la réception de l'événement FRAME, ce qui donne un mécanisme très flexible pour l'échange de données entre les parties du programme, permet de traiter les signaux provenant de périphériques d'entrée non standard, tels que des joysticks ou des gants VR, par exemple.
À suivre ...