
引言
经常被批评的C ++语言的特征之一是标准中缺乏事件处理机制。 同时,该机制是某些软件组件与其他软件组件和硬件交互的主要方式之一,并且是在特定OS级别上实现的。 自然,每个平台在实现所描述的机制时都有其自己的细微差别。
结合以上所有内容,当使用C ++开发时,需要以一种或另一种方式来实现事件处理,这可以通过使用第三方库和框架来解决。 众所周知的Qt框架提供了一种信号和时隙机制,用于组织从QObject继承的类的交互。 事件的实现也存在于boost库中。 当然,OpenSceneGraph引擎离不开它自己的“自行车”,其应用将在本文中进行讨论。
OSG是一个抽象的图形库。 一方面,它从OpenGL过程接口中抽象出来,为开发人员提供了一组类,这些类封装了OpneGL API的整个机制。 另一方面,它也从特定的图形用户界面中抽象出来,因为其实现方法因不同平台而异,甚至在同一平台内也具有功能(例如,对于Windows来说是MFC,Qt,.Net)。
不管平台如何,从应用程序的角度来看,用户与图形界面的交互归结为通过其元素生成一系列事件,然后在应用程序内部对其进行处理。 大多数图形框架都使用这种方法,但是不幸的是,即使在同一平台内,它们也不兼容。
因此,OSG提供了自己的基本接口,用于基于osgGA :: GUIEventHandler类处理小部件小部件事件和用户输入。 可以通过调用addEventHandler()方法将该处理程序附加到查看器,并通过removeEventHandler()方法将其删除。 自然,具体的处理程序类应从osgGA :: GUIEventHandler类继承,并应在其中重新定义handle()方法。 此方法接受两个参数:osgGA :: GUIEventAdapter,它包含来自GUI的事件队列;以及osg :: GUIActionAdepter,用于反馈。 定义中典型的就是这样的设计
bool handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdepter &aa) {
osgGA :: GUIActionAdapter参数允许开发人员要求GUI响应该事件采取一些措施。 在大多数情况下,查看者会受到此参数的影响,该参数可以通过动态指针转换获得指向的指针
osgViewer::Viewer* viewer = dynamic_cast<osgViewer::Viewer *>(&aa);
1.键盘和鼠标事件处理
osgGA :: GUIEventAdapter()类管理OSG支持的所有类型的事件,提供用于设置和检索其参数的数据。 getEventType()方法返回事件队列中包含的当前GUI事件。 每次重写处理程序的handle()方法时,在调用此方法时,都应使用此getter来接收事件并确定其类型。
下表描述了所有可用事件。
活动类型 | 内容描述 | 事件数据检索方法 |
---|
按钮/发布/双重点击 | 单击/释放并双击鼠标按钮 | 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()等。 未在handle()处理函数中使用。 它们由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();
接下来,两个嵌套的switch()语句分析事件的类型(击键)和按下的键的代码。 根据按下的键的代码,当前变换矩阵乘以围绕相应轴的附加旋转矩阵
case 'a': case 'A': matrix *= osg::Matrix::rotate(-0.1, osg::Z_AXIS); break;
-按下“ A”键时,以-0.1弧度的偏航角旋转飞机。
在处理了击键之后,请不要忘记将新的转换矩阵应用于转换节点
_model->setMatrix(matrix);
在main()函数中,加载飞机模型并为其创建父变换节点,然后将结果子图添加到场景的根节点
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键来控制飞机在太空中的方向。

一个有趣的问题是handle()方法在退出时应返回什么。 如果返回true,则表示OSG,则我们已经处理了输入事件,不需要进一步处理。 通常,这种行为不适合我们,因此,优良作法是从处理程序中返回false,以免在其他处理程序附加到场景中的其他节点时不中断其他处理程序对事件的处理。
3.在事件处理中使用访客
与在更新场景图时遍历场景图的方式类似,OSG支持回调以处理可与节点和几何对象关联的事件。 为此,使用对setEventCallback()和addEventCallback()的调用,这些调用将指向子osg :: NodeCallback的指针作为参数。 要在operator()运算符中接收事件,我们可以将传递给它的指向站点访问者的指针转换为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 ::引用的继承人的对象,也就是说,您可以创建指向它的智能指针。
收到自定义事件后,开发人员可以通过调用getUserData()方法从中提取数据并按其认为适当的方式对其进行处理。
5.用户计时器的实现
许多实现GUI的库和框架都提供了一个类开发人员,用于实现在特定时间间隔后生成事件的计时器。 OSG不包含实现计时器的常规方法,因此让我们尝试使用该接口创建自定义事件来自行实现某种计时器。
解决这个问题时我们可以依靠什么? 对于由渲染不断生成的某个定期事件,例如在FRAME上,绘制下一个帧的事件。 为此,我们使用相同的示例将cessna模型从正常切换为燃烧。
计时器示例主文件 #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-程序的总运行时间(以毫秒为单位)。 类的构造函数接受一个开关节点作为参数,并可选地接受开关计时器运行所需的时间间隔。
在此类中,我们重写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; }
在这里,我们分析收到的消息的类型。 如果是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的值如何,我们都必须将其和_time增大绘制框架所需的延迟量
_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手套)的信号。
待续...