基于OpenGL编写一个简易的2D渲染框架-07 鼠标事件和键盘事件

这次为程序添加鼠标事件和键盘事件

  当检测到鼠标事件和键盘事件的信息时,捕获其信息并将信息传送到需要信息的对象处理。为此,需要一个可以分派信息的对象,这个对象能够正确的把信息交到正确的对象。

实现思路:

  要实现以上的功能,需要几个对象:

    事件分派器: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");
        }
    }
};

运行程序的结果

源码下载:http://pan.baidu.com/s/1skOmP21

时间: 2024-10-13 12:29:29

基于OpenGL编写一个简易的2D渲染框架-07 鼠标事件和键盘事件的相关文章

基于OpenGL编写一个简易的2D渲染框架-05 渲染文本

阅读文章前需要了解的知识:文本渲染 https://learnopengl-cn.github.io/06%20In%20Practice/02%20Text%20Rendering/ 简要步骤: 获取要绘制的字符的 Unicode 码,使用 FreeType 库获取对应的位图数据,添加到字符表中(后面同样的字符可以再表中直接索引),将字符表上的字符填充到一张纹理上.计算每个字符的纹理坐标,使用渲染器绘制 注意的问题: 对于中英文混合的字符串,使用 char 存储时,英文字符占 1 个字节,而中

基于OpenGL编写一个简易的2D渲染框架01——创建窗口

最近正在学习OpenGL,我认为学习的最快方法就是做一个小项目了. 如果对OpenGL感兴趣的话,这里推荐一个很好的学习网站 https://learnopengl-cn.github.io/ 我用的是 vs2013,使用C++语言编写项目.这个小项目叫Simple2D,意味着简易的2D框架.最终的目的是可以渲染几何图形和图片,最后尝试加上一个2D粒子系统和Box2D物理引擎,并编译一个简单的游戏. 第一步,就是创建一个Win32项目. 接下来,生成一个窗口.编写一个RenderWindow类,

基于OpenGL编写一个简易的2D渲染框架02——搭建OpenGL环境

由于没有使用GLFW库,接下来得费一番功夫. 阅读这篇文章前请看一下这个网页:https://learnopengl-cn.github.io/01%20Getting%20started/02%20Creating%20a%20window/ 以下,我摘取了一点片段 Windows上的OpenGL库 如果你是Windows平台,opengl32.lib已经包含在Microsoft SDK里了,它在Visual Studio安装的时候就默认安装了.由于这篇教程用的是VS编译器,并且是在Windo

基于OpenGL编写一个简易的2D渲染框架-08 重构渲染器-整体架构

事实上,前面编写的渲染器 Renderer 非常简陋,虽然能够进行一些简单的渲染,但是它并不能满足我们的要求. 当渲染粒子系统时,需要开启混合模式,但渲染其他顶点时却不需要开启混合模式.所以同时渲染粒子系统和其他纹理时会得不到想要的结果,渲染器还存在许多的不足: 1.当渲染许多透明图形时,没有对其进行排序,使得本应透明的图形没有透明. 2.不能对不同的顶点使用不同的状态进行渲染. 渲染器要做的东西很简单,就是 1.传递数据到 GPU 2.设置 OpenGL 状态信息(Alpha测试.模板测试.深

基于OpenGL编写一个简易的2D渲染框架-04 绘制图片

阅读文章前需要了解的知识,纹理:https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/ 过程简述:利用 FreeImage 库加载图像数据,再创建 OpenGL 纹理,通过 Canvas2D 画布绘制,最后又 Renderer 渲染器渲染 本来想用 soil 库加载图像数据的,虽然方便,但是加载有些格式的图像文件时会出现一些问题.最后,改用 FreeImage 库来加载图像了. 添加 FreeImage 库到工

基于OpenGL编写一个简易的2D渲染框架-09 重构渲染器-Shader

Shader 只是进行一些简单的封装,主要功能: 1.编译着色程序 2.绑定 Uniform 数据 3.根据着色程序的顶点属性传递顶点数据到 GPU 着色程序的编译 GLuint Shader::createShaderProgram(const char* vsname, const char* psname) { std::string vShaderSource, fShaderSource; std::ifstream vShaderFile, fShaderFile; vShaderF

基于OpenGL编写一个简易的2D渲染框架-11 重构渲染器-Renderer

假如要渲染一个纯色矩形在窗口上,应该怎么做? 先确定顶点的格式,一个顶点应该包含位置信息 vec3 以及颜色信息 vec4,所以顶点的结构体定义可以这样: struct Vertex { Vec3 position; Vec4 color; }; 然后填充矩形四个顶点是数据信息: Vertex* data = ( Vertex* ) malloc(sizeof( Vertex ) * 4); data[0].position.set(0, 0, 0); data[1].position.set(

基于OpenGL编写一个简易的2D渲染框架-13 使用例子

这是重构渲染器的最后一部分了,将会给出一个 demo,测试模板测试.裁剪测试.半透明排序等等: 上图是本次 demo 的效果图,中间的绿色图形展现的是模板测试. 模板测试 void init(Pass*& p1, Pass*& p2) { p1 = new Pass; p2 = new Pass; Shader* s1 = new Shader("Shader/defaultGeometryShader.vs", "Shader/defaultGeometry

基于OpenGL编写一个简易的2D渲染框架-12 重构渲染器-BlockAllocator

BlockAllocator 的内存管理情况可以用下图表示 整体思路是,先分配一大块内存 Chunk,然后将 Chunk 分割成小块 Block.由于 Block 是链表的一个结点,所以可以通过链表的形式把未使用的 Block 连接起来,并保存到 pFreeLists 中.当我们向 BlockAllocator 申请一块内存时,BlockAllocator 会通过 pFreeLists 表索引出一块空闲的 Block,并返回其地址.当我们不断申请内存的时候,BlockAllocator 会不断返