关键词:
这次为程序添加鼠标事件和键盘事件
当检测到鼠标事件和键盘事件的信息时,捕获其信息并将信息传送到需要信息的对象处理。为此,需要一个可以分派信息的对象,这个对象能够正确的把信息交到正确的对象。
实现思路:
要实现以上的功能,需要几个对象:
事件分派器:EventDispatcher,负责将 BaseEvent 分派给 EventListener 对象
事件监听器:EventListener,这只是一个接口类,接受 BaseEvent 的对象,真正的处理在它的子类中实现
事件:BaseEvent,储存用户数据,事件信息载体
假设我要分派一个 BaseEvent, 那么我应该将 BaseEvent 分派给哪个监听器 EventListener ?可以在 BaseEvent 上添加一个 ID,通过这个 ID 将 BaseEvent 分派到对应 ID 的监听器。
有这样一个场景,有 A、B、C、D 四个监听器,需要把 ID 为 5 的 BaseEvent 分派给 A、B 监听器,而 C、D 监听器不需要接受这个 BaseEvent。
这时可以创建一个映射表,存储有 ID 和 监听器之间的联系信息
typedef std::map<int, std::list<EventListener*>> ListenerGroup;
A、B 需要监听 ID 为 5 的 BaseEvent,就把 A、B 注册到这个表中,表中就有了 5-A、B 这样的信息。事件分派器就能根据这个表将 ID 为 5 的 BaseEvent 分派到需要监听这个 BaseEvent 的监听器 A 和 B。对于 C、D 监听器,只能监听到对应 ID 的 BaseEvent,实现思路就这样。
代码实现:
BaseEvent 结构如下
struct BaseEvent { int nEventID; /* 事件 ID */ int nParams[MAX_EVENT_PARAM]; /* 自定义参数 */ void* pUserData; /* 用户数据 */ };
nParams 用来储存几个自定义参数,对于其他数据就用 void 指针储存,需要时转换一下就可以了。
事件分派器有两个属性,分别是 事件池 和 ID-监听器表,事件池主要是用来储存所有要分派的事件
std::list<BaseEvent> vEventPool;
ListenerGroup listenerGroup;
接下来是监听器的实现
class DLL_export EventListener { friend class EventDispatcher; public: EventListener(); virtual ~EventListener() {} protected: void appendListener(int eventID, EventListener* listener); void removeListener(int eventID, EventListener* listener); virtual void handleEvent(const BaseEvent& event) = 0; private: static unsigned int nIDCounter; unsigned int nID; };
主要有三个函数,用于将监听器注册到 ID-监听器表和从 ID-监听器表中移除监听器,最后一个是处理 BaseEvent 的函数,这是一个抽象函数,表示在子类中实现处理函数。
将监听器注册到表中,需要一个监听器要监听的 BaseEvent ID 以及监听器本身
void EventListener::appendListener(int eventID, EventListener* new_listener) { auto listenerList = pDispatcher->listenerGroup.find(eventID); /* 事件 ID 没有监听列表?为 ID 创建监听列表,添加 eListener */ if ( listenerList == pDispatcher->listenerGroup.end() ) { std::list<EventListener*> newListenerList; newListenerList.push_back(new_listener); pDispatcher->listenerGroup.insert(std::make_pair(eventID, newListenerList)); } else { /* 如果监听列表中没有监听器,添加监听器到列表中 */ std::list<EventListener*>::iterator listener_it; for ( listener_it = listenerList->second.begin(); listener_it != listenerList->second.end(); ++listener_it ) { if ( (*listener_it)->nID == new_listener->nID ) return; } if ( listener_it == listenerList->second.end() ) { listenerList->second.push_back(new_listener); } } }
先判断该 ID 的 BaseEvent 是否有一张表了,如果没有就新建表,然后将监听器添加到表中。
将监听器中表中移除
void EventListener::removeListener(int eventID, EventListener* listener) { auto listenerList = pDispatcher->listenerGroup.find(eventID); if ( listenerList == pDispatcher->listenerGroup.end() ) return; /* 从监听列表中移除监听器 */ for ( auto it = listenerList->second.begin(); it != listenerList->second.end(); ++it ) { if ( (*it)->nID == listener->nID ) { listenerList->second.erase(it); break; } } /* 移除空监听列表 */ if ( listenerList->second.empty() ) { pDispatcher->listenerGroup.erase(listenerList); } }
如果要分派一个 BaseEvent,先将其添加到分派器中
void EventDispatcher::dispatchEvent(const BaseEvent& event) { /* 只是暂时添加事件到事件池中,并没有立即分派事件,避免递归分派错误 */ vEventPool.push_back(event); }
这里没有立即将 BaseEvent 交给对应的监听器处理,是因为如果处理函数中有将 BaseEvent 添加到事件分派器中的操作,会发生递归错误。所以就将 BaseEvent 添加到一个事件池中,稍后在函数 flushEvent 中统一分派
void EventDispatcher::flushEvent() { if ( vEventPool.empty() ) return; /* 分派事件池中的所有事件 */ for ( auto& event : vEventPool ) { this->realDispatchEvent(event); } vEventPool.clear(); }
分派每一个 BaseEvent,需要找到其对应的监听表,再交给表中的监听器处理
void EventDispatcher::realDispatchEvent(const BaseEvent& event) { auto listenerList_it = listenerGroup.find(event.nEventID); if ( listenerList_it != listenerGroup.end() ) { std::list<EventListener*>& listenerList = listenerList_it->second; for ( auto listener_it : listenerList ) { listener_it->handleEvent(event); } } }
以上就实现了一个事件分派模块,费如此大的一番功夫,是为了让它不仅仅分派鼠标和键盘事件,还可以分派其他需要的事件。
鼠标事件和键盘事件处理
为鼠标事件和键盘事件分别定义事件 ID
enum EventType { ET_UNKNOWN, /* 未知事件 */ ET_MOUSE, /* 鼠标事件 */ ET_KEY /* 按键事件 */ };
先实现鼠标事件的处理,定义一个鼠标监听器类,继承于事件监听器
class DLL_export MouseEventListener : public EventListener { public: MouseEventListener(); virtual ~MouseEventListener(); virtual void mouseMove(const MouseEvent& event) {} virtual void mousePress(const MouseEvent& event) {} virtual void mouseRelease(const MouseEvent& event) {} virtual void mouseDoubleClick(const MouseEvent& event) {} virtual void mouseWheel(const MouseEvent& event) {} void handleEvent(const BaseEvent& event); };
在构造函数和析构函数中,主要是注册监听器到事件分派器和从事件分派器中移除监听器
MouseEventListener::MouseEventListener() { this->appendListener(EventType::ET_MOUSE, this); } MouseEventListener::~MouseEventListener() { this->removeListener(EventType::ET_MOUSE, this); }
鼠标事件分别有按键按下、释放、双击、鼠标移动和滚轮滑动等动作
enum EventAction { ACT_MOVE, /* 移动 */ ACT_PRESS, /* 按压 */ ACT_RELAESE, /* 释放 */ ACT_DUBBLE_CLICK, /* 双击 */ ACT_SCROLL /* 滚动 */ };
以及按钮类型,左键、右键和中键
enum ButtonType { LEFT_BUTTON, /* 鼠标左键 */ RIGHT_BUTTON, /* 鼠标右键 */ MIDDLE_BUTTON /* 鼠标中键 */ };
对于一个鼠标事件,需要的数据信息如下
/* 鼠标事件 */ struct MouseEvent { EventAction eventAction; ButtonType buttonType; int nDelta; int nX, nY; };
动作类型、按钮类型、滚轮滚动数据和坐标数据。
为了捕捉窗口程序的鼠标信息,定义一个窗口信息处理类
//------------------------------------------------------------------ // WinMsgHandle // 窗口信息处理 //------------------------------------------------------------------ class WinMsgHandle { public: WinMsgHandle(); void handleMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); private: BaseEvent baseEvent; KeyEvent keyEvent; MouseEvent mouseEvent; };
函数 handleMessage 主要捕捉窗口信息
void WinMsgHandle::handleMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { baseEvent.nEventID = ET_UNKNOWN; /* 鼠标事件信息 */ if ( msg >= WM_MOUSEMOVE && msg <= WM_MBUTTONDBLCLK || msg == WM_MOUSEWHEEL ) { switch ( msg ) { case WM_LBUTTONDOWN: mouseEvent.buttonType = ButtonType::LEFT_BUTTON; mouseEvent.eventAction = EventAction::ACT_PRESS; break; case WM_LBUTTONUP: mouseEvent.buttonType = ButtonType::LEFT_BUTTON; mouseEvent.eventAction = EventAction::ACT_RELAESE; break; case WM_LBUTTONDBLCLK: mouseEvent.buttonType = ButtonType::LEFT_BUTTON; mouseEvent.eventAction = EventAction::ACT_DUBBLE_CLICK; break; case WM_MBUTTONDOWN: mouseEvent.buttonType = ButtonType::MIDDLE_BUTTON; mouseEvent.eventAction = EventAction::ACT_PRESS; break; case WM_MBUTTONUP: mouseEvent.buttonType = ButtonType::MIDDLE_BUTTON; mouseEvent.eventAction = EventAction::ACT_RELAESE; break; case WM_MBUTTONDBLCLK: mouseEvent.buttonType = ButtonType::MIDDLE_BUTTON; mouseEvent.eventAction = EventAction::ACT_DUBBLE_CLICK; break; case WM_RBUTTONDOWN: mouseEvent.buttonType = ButtonType::RIGHT_BUTTON; mouseEvent.eventAction = EventAction::ACT_PRESS; break; case WM_RBUTTONUP: mouseEvent.buttonType = ButtonType::RIGHT_BUTTON; mouseEvent.eventAction = EventAction::ACT_RELAESE; break; case WM_RBUTTONDBLCLK: mouseEvent.buttonType = ButtonType::RIGHT_BUTTON; mouseEvent.eventAction = EventAction::ACT_DUBBLE_CLICK; break; case WM_MOUSEMOVE: mouseEvent.eventAction = EventAction::ACT_MOVE; break; case WM_MOUSEWHEEL: mouseEvent.eventAction = EventAction::ACT_SCROLL; mouseEvent.nDelta = ( short ) HIWORD(wParam); break; } mouseEvent.nX = ( short ) LOWORD(lParam); mouseEvent.nY = ( short ) HIWORD(lParam); baseEvent.nEventID = ET_MOUSE; baseEvent.pUserData = &mouseEvent; EventDispatcher::getInstance()->dispatchEvent(baseEvent); } }
主要是获取鼠标事件数据 MouseEvent,然后将数据附加到 BaseEvent 上,设置其 ID 为 鼠标事件ID——ET_MOUSE,最后由事件分派器分派 BaseEvent。
当鼠标事件监听器处理 BaseEvent 时,需要获取 MouseEvent 数据,然后根据按钮类型和动作类型调用相应函数
void MouseEventListener::handleEvent(const BaseEvent& event) { if ( event.nEventID != EventType::ET_MOUSE && event.pUserData ) return; MouseEvent* mouseEvent = static_cast<MouseEvent*>(event.pUserData); switch ( mouseEvent->eventAction ) { case Simple2D::ACT_MOVE: this->mouseMove(*mouseEvent); break; case Simple2D::ACT_PRESS: this->mousePress(*mouseEvent); break; case Simple2D::ACT_RELAESE: this->mouseRelease(*mouseEvent); break; case Simple2D::ACT_SCROLL: this->mouseWheel(*mouseEvent); break; case Simple2D::ACT_DUBBLE_CLICK: this->mouseDoubleClick(*mouseEvent); break; } }
当然这些函数都没有具体的实现,具体的实现由子类完成。
对于键盘事件,只有两个按键动作按压和释放,及事件的数据结构体
/* 按键事件 */ struct KeyEvent { EventAction eventAction; bool keys[256]; KeyType keyType; };
bool 类型的按键数组 keys 储存哪一个按键被按下的信息,当同时有多个按键按压时也可以检测。而 KeyType 就记录了当前按压的按键类型,这里并不包括键盘上的所有按键,只包含字母键、数字键和其它常用按键。
/* * VK_0 - VK_9 are the same as ASCII '0' - '9' (0x30 - 0x39) * 0x40 : unassigned * VK_A - VK_Z are the same as ASCII 'A' - 'Z' (0x41 - 0x5A) */ enum KeyType { Key_Unknown, Key_Space = 0x20, Key_Prior, Key_Next, Key_End, Key_Home, Key_Left, Key_Up, Key_Right, Key_Down, Key_Select, Key_Print, Key_Execute, Key_Snapshot, Key_Insert, Key_Delete, Key_Help, /* 主键盘上的数字键 */ Key_0 = 0x30, Key_1, Key_2, Key_3, Key_4, Key_5, Key_6, Key_7, Key_8, Key_9, Key_A = 0x41, Key_B, Key_C, Key_D, Key_E, Key_F, Key_G, Key_H, Key_I, Key_J, Key_K, Key_L, Key_M, Key_N, Key_O, Key_P, Key_Q, Key_R, Key_S, Key_T, Key_U, Key_V, Key_W, Key_X, Key_Y, Key_Z, /* 小键盘上的数字 */ Key_NumPad_0 = 0x60, Key_NumPad_1, Key_NumPad_2, Key_NumPad_3, Key_NumPad_4, Key_NumPad_5, Key_NumPad_6, Key_NumPad_7, Key_NumPad_8, Key_NumPad_9, Key_F1 = 0x70, Key_F2, Key_F3, Key_F4, Key_F5, Key_F6, Key_F7, Key_F8, Key_F9, Key_F10, Key_F11, Key_F12, Key_F13, Key_F14, Key_F15, Key_F16, Key_F17, Key_F18, Key_F19, Key_F20, Key_F21, Key_F22, Key_F23, Key_F24, };
键盘事件监听器定义
class DLL_export KeyEventListener : public EventListener { public: KeyEventListener(); virtual ~KeyEventListener(); virtual void keyPress(const KeyEvent& event) {} virtual void keyRelease(const KeyEvent& event) {} void handleEvent(const BaseEvent& event); };
对于按键信息的捕捉,和鼠标事件一样在 handleMessage 函数中,这里只截取了键盘事件
void WinMsgHandle::handleMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { baseEvent.nEventID = ET_UNKNOWN;/* 键盘按键事件信息 */ if ( msg == WM_KEYDOWN || msg == WM_KEYUP ) { keyEvent.eventAction = (msg == WM_KEYDOWN) ? EventAction::ACT_PRESS : EventAction::ACT_RELAESE; keyEvent.keyType = keyMap(( UINT ) wParam); keyEvent.keys[( UINT ) wParam] = (msg == WM_KEYDOWN) ? true : false; baseEvent.nEventID = ET_KEY; baseEvent.pUserData = &keyEvent; EventDispatcher::getInstance()->dispatchEvent(baseEvent); } }
和鼠标事件一样,获取按键数据 KeyEvent,然后附加到 BaseEvent 中,设置其 ID 为 ET_KEY,最后由分派器分派事件。按键事件监听器处理 BaseEvent 时,根据动作类型调用相应函数,其函数有子类实现。
void KeyEventListener::handleEvent(const BaseEvent& event) { if ( event.nEventID != EventType::ET_KEY && event.pUserData ) return; KeyEvent* keyEvent = static_cast<KeyEvent*>(event.pUserData); switch ( keyEvent->eventAction ) { case Simple2D::ACT_PRESS: this->keyPress(*keyEvent); break; case Simple2D::ACT_RELAESE: this->keyRelease(*keyEvent); break; } }
最后在窗口的 proc 函数中
/* 处理鼠标和按键事件 */ if ( self ) { self->winMsgHandle.handleMessage(wnd, msg, wParam, lParam); }
主循环中分派所有事件
if ( PeekMessage(&msg, 0, 0, 0, PM_REMOVE) ) { TranslateMessage(&msg); DispatchMessage(&msg); EventDispatcher::getInstance()->flushEvent(); }
新建一个测试类,继承与鼠标事件监听器和按键事件监听器,实现监听器中的函数,输出到输出窗口
class EventTest : public MouseEventListener, public KeyEventListener { public: //void mouseMove(const MouseEvent& event) //{ // log("mouse move"); // log("x:%d - y:%d", event.nX, event.nY); //} void mousePress(const MouseEvent& event) { if ( event.buttonType == ButtonType::LEFT_BUTTON ) { log("left button press"); } else if ( event.buttonType == ButtonType::MIDDLE_BUTTON ) { log("middle button press"); } else if ( event.buttonType == ButtonType::RIGHT_BUTTON ) { log("right button press"); } log("x:%d - y:%d", event.nX, event.nY); } void mouseRelease(const MouseEvent& event) { log("mouse release"); log("x:%d - y:%d", event.nX, event.nY); } void mouseDoubleClick(const MouseEvent& event) { log("mouse double click"); log("x:%d - y:%d", event.nX, event.nY); } void mouseWheel(const MouseEvent& event) { log("mouse wheel"); log("delta: %d", event.nDelta); } void keyPress(const KeyEvent& event) { if ( event.keys[KeyType::Key_A] && event.keys[KeyType::Key_S] ) { log("同时按下 AS"); } } void keyRelease(const KeyEvent& event) { if ( event.keyType == KeyType::Key_NumPad_1 ) { log("释放键 1"); } } };
运行程序的结果
基于opengl编写一个简易的2d渲染框架-04绘制图片
阅读文章前需要了解的知识,纹理:https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/ 过程简述:利用FreeImage库加载图像数据,再创建OpenGL纹理,通过Canvas2D画布绘制,最后又Renderer渲染器渲染 本来想用soil库... 查看详情
基于opengl编写一个简易的2d渲染框架-09重构渲染器-shader
Shader只是进行一些简单的封装,主要功能: 1、编译着色程序 2、绑定Uniform数据 3、根据着色程序的顶点属性传递顶点数据到GPU 着色程序的编译GLuintShader::createShaderProgram(constchar*vsname,constchar*p... 查看详情
基于opengl编写一个简易的2d渲染框架-05渲染文本
阅读文章前需要了解的知识:文本渲染 https://learnopengl-cn.github.io/06%20In%20Practice/02%20Text%20Rendering/ 简要步骤:获取要绘制的字符的Unicode码,使用FreeType库获取对应的位图数据,添加到字符表中(后面同样的字符可以再表中... 查看详情
基于opengl编写一个简易的2d渲染框架02——搭建opengl环境
由于没有使用GLFW库,接下来得费一番功夫。阅读这篇文章前请看一下这个网页:https://learnopengl-cn.github.io/01%20Getting%20started/02%20Creating%20a%20window/ 以下,我摘取了一点片段 Windows上的OpenGL库 如果你是Windows平台,opengl32.l... 查看详情
基于opengl编写一个简易的2d渲染框架-13使用例子
这是重构渲染器的最后一部分了,将会给出一个demo,测试模板测试、裁剪测试、半透明排序等等: 上图是本次demo的效果图,中间的绿色图形展现的是模板测试。 模板测试voidinit(Pass*&p1,Pass*&p2){p1=newPass;p2=newPass;... 查看详情
基于opengl编写一个简易的2d渲染框架-12重构渲染器-blockallocator
BlockAllocator的内存管理情况可以用下图表示 整体思路是,先分配一大块内存Chunk,然后将Chunk分割成小块Block。由于Block是链表的一个结点,所以可以通过链表的形式把未使用的Block连接起来,并保存到pFreeLists中。当... 查看详情
基于opengl编写一个简易的2d渲染框架-08重构渲染器-整体架构
事实上,前面编写的渲染器Renderer非常简陋,虽然能够进行一些简单的渲染,但是它并不能满足我们的要求。 当渲染粒子系统时,需要开启混合模式,但渲染其他顶点时却不需要开启混合模式。所以同时渲染粒子系统和... 查看详情
基于opengl编写一个简易的2d渲染框架-11重构渲染器-renderer
假如要渲染一个纯色矩形在窗口上,应该怎么做?先确定顶点的格式,一个顶点应该包含位置信息vec3以及颜色信息vec4,所以顶点的结构体定义可以这样:structVertex{Vec3position;Vec4color;};然后填充矩形四个顶点是数据信息:Verte... 查看详情
OpenGL 2d矩形没有被渲染
】OpenGL2d矩形没有被渲染【英文标题】:OpenGL2drectanglenotbeingrendered【发布时间】:2016-12-2200:07:43【问题描述】:我正在尝试在屏幕上渲染一个矩形。程序运行时,只显示清晰的颜色,没有矩形显示。代码如下:glClearColor(0.0,0.0,0.0,... 查看详情
使用 opencl 将软件渲染到 opengl 2d 视图 [关闭]
】使用opencl将软件渲染到opengl2d视图[关闭]【英文标题】:softwarerenderingwithopenclontoopengl2dview[closed]【发布时间】:2011-07-0606:59:44【问题描述】:我想构建一个小的软件渲染库,因为我喜欢体素的想法,以及其他可能的替代渲染方... 查看详情
基于opengles的深度学习框架编写
基于OpenGLES的深度学习框架编写背景与工程定位背景项目组基于深度学习实现了视频风格化和人像抠图的功能,但这是在PC/服务端上跑的,现在需要移植到移动端,因此需要一个移动端的深度学习的计算框架。同类型的库caffe-andr... 查看详情
OpenGL:如何存储/渲染 2D 四边形?
】OpenGL:如何存储/渲染2D四边形?【英文标题】:OpenGL:Howshould2Dquadsbestored/rendered?【发布时间】:2015-07-0919:30:11【问题描述】:我将模型数据存储在VBO/IBO中,并使用VAO渲染它们,据我所知,这与通常的做法差不多。但我有为2D精... 查看详情
opengl绘制三角形(代码片段)
...对象:ElementBufferObject,EBO或IndexBufferObject,IBO渲染管线在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由Ope... 查看详情
OpenGL 2D像素完美渲染
】OpenGL2D像素完美渲染【英文标题】:OpenGL2Dpixelperfectrendering【发布时间】:2013-12-2115:04:10【问题描述】:我正在尝试渲染2D图像,以便它完全覆盖整个窗口。对于我的测试,我设置了一个窗口,使客户区正好是320x240,纹理也是... 查看详情
OpenGL:如何在 3d 模式下优化多层相互重叠的 2d 渲染?
】OpenGL:如何在3d模式下优化多层相互重叠的2d渲染?【英文标题】:OpenGL:Howtooptimize2drenderingwithmultiplelayersoverlappingeachotherin3dmode?【发布时间】:2011-06-0612:36:31【问题描述】:我知道如何通过简单地首先渲染最近的平面来加速3d渲... 查看详情
opengl工作流程
在OpenGL中,一切事物都在3D空间中,但我们的屏幕坐标确实2D像素数组,OpenGL大部分工作就是把3D坐标转换成适应屏幕的2D像素。3D坐标转换成2D屏幕坐标的过程是有OpenGL的图形渲染管线管理的。图形渲染管线的工作可以被划分... 查看详情
opengl-渲染流程
参考技术A在OpenGL中,任何事物都处于3D空间中,而屏幕和窗口却都是2D像素数组,这就导致了OpenGL大部分工作都是关于把3D坐标转变为适配你屏幕的2D像素,3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(指的是一堆原始图... 查看详情
iOS 中基于矢量的实时 OSM 渲染器(使用 OpenGL ES)
】iOS中基于矢量的实时OSM渲染器(使用OpenGLES)【英文标题】:Realtimevector-basedOSMrendereriniOS(usingOpenGLES)【发布时间】:2013-04-2415:30:09【问题描述】:我正在寻找一种解决方案,它允许使用OpenStreetMap数据在iOS中渲染基于矢量的2D俯... 查看详情