Tutorial 2 (Dealing With User Input)

|

Background

When approaching this tutorial I hummed and hawed over whether it should build on Tutorial1 or not. Technically it has nothing to do with OpenGL, but user input is so closely tied to creating a window that in the end I decided that I would.
Bearing that in mind the starting point for this tutorial more or less picks up at the end of Tutorial1. If you haven’t worked through it, then the source and Visual Studio project files for it are available on Bitbucket here.

Getting Started

So assuming we have a working copy of the source from Tutorial1 lets get started. The first thing you might notice is that there are a lot of warnings being generated when we compile the project. This can easily be fixed by defining GLEW_BUILD, this can either be done using #define GLEW_BUILD before including the GLEW headers, but I personally prefer to add it to the preprocessor definitions.

Now that that’s taken care off, we can look at how to properly handle inputs.
In the previous tutorial we defined a basic GL::run method that made sure that all messages were being handled, but I kind of glossed over the details, mainly because I was just concerned with getting the OpenGL context created.
In Windows applications are event driven, I’m not going to go into loads of detail here (it’s quite dull), but the MSDN page is quite informative. To get started all you really need to know is that whenever an input event occurs a MSG is sent to the application. A MSG is defined as:

typedef struct tagMSG {
  HWND   hwnd;          // The window handle that should receive the message
  UINT   message;       // The message ID (What kind of message it is)
  WPARAM wParam;        // Additional info (Message Specific)
  LPARAM lParam;        // Additional info (Message Specific)
  DWORD  time;          // Time the message was posted
  POINT  pt;            // Screen coordinates of the cursor when the message was posted.
} MSG, *PMSG, *LPMSG;

These messages are processed by the window procedure of the window that the OpenGL context was attached to. Currently the windows procedure is a default one that is called to handle any messages that are generated for the window. You can see this being set up where the window is created (in GLWindow::createApplicationWindow) the WNDCLASSEX::lpfnWndProc member is a function pointer and we currently assign DefWinProc to it. The type of WNDCLASSEX::lpfnWndProc is WNDPROC which is a typedef for a function in the form:

typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
//     |  type |        name      |       parameters          |

So rather than use DefWinProc we need to define our own, the over eager may just go ahead and define it as:

class GLWindow
{
    GLWindow();
    ~GLWindow()
    // other methods...
    LRESULT CALLBACK MyWinProc(
        HWND handle,
        UINT message,
        WPARAM w_param,
        LPARAM l_param
    );
}

The more observant may notice that WNDPROC can’t be an instance method so we have to declare it as a static.

class GLWindow
{
    GLWindow();
    ~GLWindow()
    // other methods...

private:
    // other methods...
    static LRESULT CALLBACK staticWindowsProcedure(
        HWND handle,
        UINT message,
        WPARAM w_param,
        LPARAM l_param
    );
}

The problem with this is that we may (almost certaintly) want to access instance members/methods which can’t be done in a static method.

The solution is fairly standard, and multiple versions of it can be found elswhere on the web, but here it is in one of it’s simplest forms.

Add the above method decleration in the GLWindow.h file (it can be a private method), and then in the .cpp file start to define it as:

LRESULT CALLBACK GLWindow::staticWindowsProcedure(
    HWND handle,
    UINT message,
    WPARAM w_param,
    LPARAM l_param
) {

}

This method is going to do three things:

  1. If the MSG is WM_NCCREATE we will use SetWindowLongPtr to associate a pointer to this instance of our GLWindow class to the window.
  2. If the pointer has been set correctly then we retrieve it and use to call the non-static windows procedure method.
  3. Default to callingDefWinProc if neither of the two options succeed.

The first thing we check is the value of the message parameter, if it is WM_NCCREATE, then we can use the l_param to access the data we need.

As you may remember w_param and l_param contain additional information pertaining to the MSG, the kinds of information they contain is different according the type of the message, by the time our windows procedure is called we aren’t getting the whole MSG struct passed through anymore, just the members that Win32 has decided we are allowed. In the case of WM_NCCREATE the w_param is empty, and the l_param contains an instance of CREATESTRUCT, all we need to know about CREATESTRUCT is that it’s members are identical to the parameters of CreateWindow. So we have access to all the values passed in to CreateWindow when we recieve the WM_NCCREATE message.

Update the staticWindowsProcedure so it looks like this:

 #!c++
 LRESULT CALLBACK GLWindow::staticWindowsProcedure(
    HWND handle,
    UINT message,
    WPARAM w_param,
    LPARAM l_param
) {
    if(message == WM_NCCREATE)
    {
        // Cast the l_param to a CREATESTRUCT
        CREATESTRUCT* create_struct = reinterpret_cast<CREATESTRUCT*>(l_param);

        // cast the lpCreateParams member to a LONG_PTR
        LONG_PTR this_window_ptr = reinterpret_cast<LONG_PTR>(create_struct->lpCreateParams);

        // Set the pointer to the GLWindow instance as the user data for the window
        SetWindowLongPtr(handle, GWLP_USERDATA, this_window_ptr);
    }

    return DefWindowProc(handle, message, w_param, l_param);
}

This will now catch any WM_NCCREATE messages associate with the window and make sure that a pointer to the correct instance of GLWindow is associated with it. To do this it first casts the l_param to CREATESTRUCT, accesses the lpCreateParams member of the CREATESTRUCT struct, casts it to a LONG_PTR and then assigns it using SetWindowLongPtr.
The SetWindowLongPtr method takes three parameters:

  1. HWND hWnd
    1. The handle to the window you want to assign data for.
  2. int nIndex
    1. The offset to the value to be assigned (we are using GWLP_USERDATA)
  3. LONG_PTR swNewLong
    1. The data being assigned.

In order for this to work correctly though we need to alter the code inside GLWindow::createApplicationWindow. First we change the line:

wcex.lpfnWndProc = DefWinProc;

to:

wcex.lpfnWndProc = staticWindowsProcedure;

and then we change the last parameter of the call to CreateWindow from NULL to this:

m_window_handle = CreateWindow(
    L"tutorial_2_window",
    L"Tutorial 2",
    WS_BORDER | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX,
    200,
    200,
    600,
    400,
    NULL,
    NULL,
    instance,
    this
);

I also took this opportunity to update the lpszClassName and window title to reflect the fact this was tutorial 2.

Next we need to define a non-static windows procedure that will do all the actual heavy lifting. In the GLWindow header declare a method:

LRESULT winProc(
    HWND hWnd,
    UINT message,
    WPARAM wParam,
    LPARAM lParam
);

and in the .cpp just declare it as:

LRESULT CALLBACK GLWindow::windowsProcedure(
    HWND handle,
    UINT message,
    WPARAM w_param,
    LPARAM l_param
) {
    return DefWindowProc(handle, message, w_param, l_param);
}

For the moment we will just leave it returning DefWinProc and will come back to it in a bit.

Inside GLWindow::staticWinProc we have already dealt with the WM_NCCREATE message, but now we want to forward any other messages to our non-static method.
To do this we get the pointer to the GLWindow instance that we stored on the WM_NCCREATE message. This uses the method GetWindowLongPtr which is the complimentery method to SetWindowLongPtr, we pass in the handle to the window and the offset to the data we want, we assigned to GWLP_USERDATA so we fetch from there as well. The method GetWindowLongPtr has the following signature:

LONG_PTR WINAPI GetWindowLongPtr(
  HWND hWnd,  // The window handle we want to get info from (we pass in the one passed into the procedure)
  int  nIndex  // The offset (GWLP_USERDATA) in our case
);

The LONG_PTR it returns is the data we are interested in, if the method returns 0 then the method failed for whatever reason. To use it add the following condition into the GLWindow::staticWindowsProcedure method directly after the check for WM_CREATE:

if(message == WM_NCCREATE)
{
    // ...
}
if(auto user_data = GetWindowLongPtr(handle, GWLP_USERDATA)) // `=` returns value assigned if method fails this will be 0(false)
{
    // Once it's fully created pass on messages to non-static member  function.
    auto this_window = reinterpret_cast<GLWindow*>(user_data);
    return this_window->windowsProcedure(handle, message, w_param, l_param);
}

Keyboard Support

Now we can add a simple test to the non static windows procedure just to make sure everything is working.
Add:

switch(message)
{
    // Key-down event
    case WM_KEYDOWN:
    {
        std::cout << " Key: " << char(w_param) << " pressed" << std::endl;
        break;
    }
    // Key-up event
    case WM_KEYUP:
    {
        std::cout << " Key: " << char(w_param) << " released" << std::endl;
        break;
    }
        default:
            break;
    }
}

to the start of GLWindow::windowsProcedure. If we run the program now and tap the ‘Q’ key, we’ll see something like:

If you keep the key pressed then release it you’ll see:

Obviously this isn’t entirely correct. Ideally we want to be able to differentiate between key-down and key-repeat events. The reason we get what we currently get is that WM_KEYDOWN will auto-repeat if the key is held long enough. Luckily the l_param of a WM_KEYDOWN message can tell us the previous keystate. For WM_KEYDOWN the l_param will hold the following values:

| Bits | Data | |–|–| | 0-15 | The repeat count for auto-repeat | | 16-23 | The scan code (Don’t worry about this) | | 24 | 1 if its an extended key (i.e Right-Ctrl, or Right-Alt) | | 25-28 | Reserved | | 29 | Context Code (Always 0) | | 30 | Previous key state; 1 if key was already down otherwise 0 | | 31 | Transition state (Always 0) |

Looking at this it’s clear we can check the bit associated with the previous key state to check for key-repeats. We could do this with some wonderful bit arithmetic but I prefer to use the std::bitset functionality. So we change the WM_KEYDOWN case to look like:

// Key-down event
case WM_KEYDOWN:
{
    if(std::bitset<32>(l_param).test(30))
    {
        std::cout << " Key: " << char(w_param) << " repeating" << std::endl;
    }
    else
    {
        std::cout << " Key: " << char(w_param) << " pressed" << std::endl;
    }
    break;
}

If we run the program now we should see something like:

Obviously we don’t actually want to put all our input handling inside the GLWindowclass, so we need a way to inform the rest of the application about the inputs that are recieved. To do this we’ll store each input we care about and pass it through to the rest of the application. One way to do this is to change the update-loop in our main to look something like:

while(test_window.run())
{
    // Process Inputs
    InputEvent e;
    while (test_window.popEvent(e))
    {
        // Do things with 'e'
        printEvent(e);
    }
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glClearColor(1.0f, 1.0f, 0.0f, 1.0f);
    // Your OpenGLCode here...
    test_window.swapBuffers();
}

What is an InputEvent? An InputEvent is a struct that we will define which will store input events in a way more suited to our purposes than raw windows messages.

To define InputEvent first create a header file called InputEvent.h and define a struct like:

struct InputEvent
{

}

For the moment we are going to limit ourselves to keyboard events, so all the struct needs is a member to hold the key identifier, and a member holding the action (pressed, repeating or released). To hold these we are going to define two enums. One for holding the key value (which key was pressed) and the other for the state. So in InputEvent.h add the following before the struct:

enum class key_state
{
    e_pressed,
    e_repeat,
    e_released
};

enum class key_code
{
    e_unknown = 0,
    A,
    B,
    C,
    D,
    E,
    F,
    G,
    H,
    I,
    J,
    K,
    L,
    M,
    N,
    O,
    P,
    Q,
    R,
    S,
    T,
    U,
    V,
    W,
    X,
    Y,
    Z,
    Num0,
    Num1,
    Num2,
    Num3,
    Num4,
    Num5,
    Num6,
    Num7,
    Num8,
    Num9,
    Escape,
    LControl,
    LShift,
    LAlt,
    LSystem,
    RControl,
    RShift,
    RAlt,
    RSystem,
    Menu,
    LBracket,
    RBracket,
    SemiColon,
    Comma,
    Period,
    Quote,
    Slash,
    BackSlash,
    Tilde,
    Equal,
    Dash,
    Space,
    Return,
    BackSpace,
    Tab,
    PageUp,
    PageDown,
    End,
    Home,
    Insert,
    Delete,
    Add,
    Subtract,
    Multiply,
    Divide,
    Left,
    Right,
    Up,
    Down,
    Numpad0,
    Numpad1,
    Numpad2,
    Numpad3,
    Numpad4,
    Numpad5,
    Numpad6,
    Numpad7,
    Numpad8,
    Numpad9,
    F1,
    F2,
    F3,
    F4,
    F5,
    F6,
    F7,
    F8,
    F9,
    F10,
    F11,
    F12,
    F13,
    F14,
    F15,
    Pause
};

and then update the struct to look like:

struct InputEvent
{
    key_code key;
    key_state state;
};

And thats our basic event defined. (It will get more complex later).

In GLWindow.h, add:

std::deque<InputEvent> m_events;

to the private members (remember to add update the #includes), and add two method declerations:


// In the public visibility section
bool popEvent(
    InputEvent& e
);


// In the private section
key_code readKey(
    WPARAM key,
    LPARAM flags
);

In the source file define GLWindow::popEvent as:

bool GLWindow::popEvent(InputEvent& e) {

    if(m_events.empty())
    {
        return false;
    }
    else
    {
        e = m_events.front();
        m_events.pop_front();
    }
    return true;
}

This method will now allow access to any input events the window has stored.

The main concern now is that we have to translate the windows keycodes into our own enum (key_code). The reason we do this is that windows keycodes are akward to work with and also gives us a head start if we want to start porting our projects to other platforms. To do this we define the GLWindow::readKey as:

key_code GLWindow::readKey(
    WPARAM key,
    LPARAM flags // We will use this parameter later
) {
    switch(key)
    {
        case VK_LWIN: return key_code::LSystem;
        case VK_RWIN: return key_code::RSystem;
        case VK_APPS: return key_code::Menu;
        case VK_OEM_1: return key_code::SemiColon;
        case VK_OEM_2: return key_code::Slash;
        case VK_OEM_PLUS: return key_code::Equal;
        case VK_OEM_MINUS: return key_code::Dash;
        case VK_OEM_4: return key_code::LBracket;
        case VK_OEM_6: return key_code::RBracket;
        case VK_OEM_COMMA: return key_code::Comma;
        case VK_OEM_PERIOD: return key_code::Period;
        case VK_OEM_7: return key_code::Quote;
        case VK_OEM_5: return key_code::BackSlash;
        case VK_OEM_3: return key_code::Tilde;
        case VK_ESCAPE: return key_code::Escape;
        case VK_SPACE: return key_code::Space;
        case VK_RETURN: return key_code::Return;
        case VK_BACK: return key_code::BackSpace;
        case VK_TAB: return key_code::Tab;
        case VK_PRIOR: return key_code::PageUp;
        case VK_NEXT: return key_code::PageDown;
        case VK_END: return key_code::End;
        case VK_HOME: return key_code::Home;
        case VK_INSERT: return key_code::Insert;
        case VK_DELETE: return key_code::Delete;
        case VK_ADD: return key_code::Add;
        case VK_SUBTRACT: return key_code::Subtract;
        case VK_MULTIPLY: return key_code::Multiply;
        case VK_DIVIDE: return key_code::Divide;
        case VK_PAUSE: return key_code::Pause;
        case VK_F1: return key_code::F1;
        case VK_F2: return key_code::F2;
        case VK_F3: return key_code::F3;
        case VK_F4: return key_code::F4;
        case VK_F5: return key_code::F5;
        case VK_F6: return key_code::F6;
        case VK_F7: return key_code::F7;
        case VK_F8: return key_code::F8;
        case VK_F9: return key_code::F9;
        case VK_F10: return key_code::F10;
        case VK_F11: return key_code::F11;
        case VK_F12: return key_code::F12;
        case VK_F13: return key_code::F13;
        case VK_F14: return key_code::F14;
        case VK_F15: return key_code::F15;
        case VK_LEFT: return key_code::Left;
        case VK_RIGHT: return key_code::Right;
        case VK_UP: return key_code::Up;
        case VK_DOWN: return key_code::Down;
        case VK_NUMPAD0: return key_code::Numpad0;
        case VK_NUMPAD1: return key_code::Numpad1;
        case VK_NUMPAD2: return key_code::Numpad2;
        case VK_NUMPAD3: return key_code::Numpad3;
        case VK_NUMPAD4: return key_code::Numpad4;
        case VK_NUMPAD5: return key_code::Numpad5;
        case VK_NUMPAD6: return key_code::Numpad6;
        case VK_NUMPAD7: return key_code::Numpad7;
        case VK_NUMPAD8: return key_code::Numpad8;
        case VK_NUMPAD9: return key_code::Numpad9;
        case 'A': return key_code::A;
        case 'B': return key_code::B;
        case 'C': return key_code::C;
        case 'D': return key_code::D;
        case 'E': return key_code::E;
        case 'F': return key_code::F;
        case 'G': return key_code::G;
        case 'H': return key_code::H;
        case 'I': return key_code::I;
        case 'J': return key_code::J;
        case 'K': return key_code::K;
        case 'L': return key_code::L;
        case 'M': return key_code::M;
        case 'N': return key_code::N;
        case 'O': return key_code::O;
        case 'P': return key_code::P;
        case 'Q': return key_code::Q;
        case 'R': return key_code::R;
        case 'S': return key_code::S;
        case 'T': return key_code::T;
        case 'U': return key_code::U;
        case 'V': return key_code::V;
        case 'W': return key_code::W;
        case 'X': return key_code::X;
        case 'Y': return key_code::Y;
        case 'Z': return key_code::Z;
        case '0': return key_code::Num0;
        case '1': return key_code::Num1;
        case '2': return key_code::Num2;
        case '3': return key_code::Num3;
        case '4': return key_code::Num4;
        case '5': return key_code::Num5;
        case '6': return key_code::Num6;
        case '7': return key_code::Num7;
        case '8': return key_code::Num8;
        case '9': return key_code::Num9;
    }
    return key_code::e_unknown;
}

To store them update the GLWindow::windowsProcedure to look like:

LRESULT CALLBACK GLWindow::windowsProcedure(
    HWND handle,
    UINT message,
    WPARAM w_param,
    LPARAM l_param
) {
    switch(message)
    {
        // Key-down event
        case WM_KEYDOWN:
        {
            InputEvent e;
            if(std::bitset<32>(l_param).test(30))
            {
                e.key = readKey(w_param, l_param);
                e.state = key_state::e_repeat;
            }
            else
            {
                e.key = readKey(w_param, l_param);
                e.state = key_state::e_pressed;
            }
            m_events.push_back(e);
            break;
        }
        // Key-up event
        case WM_KEYUP:
        {
            InputEvent e;
            e.key = readKey(w_param, l_param);
            e.state = key_state::e_released;
            m_events.push_back(e);
            break;
        }
        default:
            break;
    }
    return DefWindowProc(handle, message, w_param, l_param);
}

To test this we will add a method to the main.cpp printEvent which we will define as:

void printEvent(
    const InputEvent& e
) {
    std::string key;
    std::string state;
    switch(e.key)
    {
        case key_code::Q:
            key = "Q";
            break;
        case key_code::A:
            key = "A";
            break;
    }
    switch(e.state)
    {
        case key_state::e_pressed:
            state = "pressed";
            break;
        case key_state::e_released:
            state = "released";
            break;
        case key_state::e_repeat:
            state = "repeating";
            break;
    }
    std::cout << "Key: " << key << " " << state << std::endl;
}

You will need to add #include <string> as well.

So what is the LPARAM parameter in the GLWindow::readKeyState method for? Well it allows us to differentiate between left and right versions of the Ctrl, Shift and Alt keys.

For the Ctrl keys this is as straightforward as adding the following case to the switch statement.


// Check the "extended" flag to distinguish between left and right control
case VK_CONTROL: return (HIWORD(flags) & KF_EXTENDED) ? key_code::RControl : key_code::LControl;

The Shift case is slightly more complex as it doesn’t use the extended flag, rather it uses the scan code. What you need to do is add the following case to the switch:

case VK_SHIFT:
{
    // Check the scancode to distinguish between left and right shift
    // Map the VK_LSHIFT to the scan code for lshift
    static auto lShift = MapVirtualKey(VK_LSHIFT, MAPVK_VK_TO_VSC);

    // Get the bits 16-23 (the scancode) and cast to a UINT
    auto scancode = static_cast<UINT>((flags & (0xFF << 16)) >> 16);

    // If the scancode is equal to the VK_LSHIFT scancode mapping then this is the l-shift
    // otherwise it's the right-shift
    return scancode == lShift ? key_code::LShift : key_code::RShift;
}

The Alt case is slightly more akward again. Like the Ctrl case it takes the simpler form of:

// Check the "extended" flag to distinguish between left and right alt
case VK_MENU: return (HIWORD(flags) & KF_EXTENDED) ? key_code::RAlt : key_code::LAlt;

However Alt keys require a WM_SYSKEYDOWN or WM_SYSKEYUP message to have been generated (F10 also requires these messages), so we need to alter the GLWindow::windowsProcedure method to cater for these.

//...
// Key-down event
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
{
    InputEvent e;
    if(std::bitset<32>(l_param).test(30))
    {
        e.key = readKey(w_param, l_param);
        e.state = key_state::e_repeat;
    }
    else
    {
        e.key = readKey(w_param, l_param);
        e.state = key_state::e_pressed;
    }
    m_events.push_back(e);
    break;
}
// Key-up event
case WM_KEYUP:
case WM_SYSKEYUP:
{
    InputEvent e;
    e.key = readKey(w_param, l_param);
    e.state = key_state::e_released;
    m_events.push_back(e);
    break;
}
//...

Even with this the Alt keys may not be as stable, for instance on keyboards with an Alt Gr key, pressing it will generate a Right-Alt and a Left-Ctrl.
Adding the following will allow the test method in main.cpp to confirm that these are working:

switch(e.key)
{
    ///...
    case key_code::LShift:
        key = "Left Shift";
        break;
    case key_code::RShift:
        key = "Right Shift";
        break;
    case key_code::LAlt:
        key = "Left Alt";
        break;
    case key_code::RAlt:
        key = "Right Alt";
        break;
    case key_code::LControl:
        key = "Left Ctrl";
        break;
    case key_code::RControl:
        key = "Right Ctrl";
        break;
}

Mouse Support

Now we have support for key’s lets look at supporting mouse input as well. The first thing we have to do though is extend our InputEvent struct.
Currently our InputEvent holds a key_code and a key_state, we could just add new mouse specific fields such as mouse position and fill in the relevant sections for each different event time. That would leave us with a structure that was larger than it had to be, and would grow for every event type that we introduced.
Instead we will extract the existing members of InputEvent anto a new struct called key_args and then define a similar struct for the mouse arguments (this gets a bit complex):

//...
//...
struct key_args
{
    key_code key;
    key_state state;
};

struct bit_helper
{
    unsigned short ctrl : 1;
    unsigned short l_button : 1;
    unsigned short m_button : 1;
    unsigned short r_button : 1;
    unsigned short shift : 1;
    unsigned short x1_button : 1;
    unsigned short x2_button : 1;
};

using move_modifier_flag = std::bitset<7>;

struct mouse_move_modifier
{
    union
    {
        move_modifier_flag bits;
        bit_helper named_keys;
    };
};

struct mouse_move_args
{
    int mouse_x;
    int mouse_y;
    mouse_move_modifier move_modifier;
};

InputEvent
{

};
//...
//...

Basically mouse_move_modifier is a struct that contains a bitset that stores modifiers(such as which mouse button is also pressed, or if Ctrl or Shift is pressed). I’ve created it as a union of a std::bitset<7> and bit_helper where bit_helper is just a convenience to access the bits by name.

we will also define an enum called event_type (place it at near the beginning of InputEvent.h withthe other enums):

enum class event_type
{
    e_key_event,
    e_mouse_event
};

Finally we can redefine InputEvent as:

 struct InputEvent
{
    InputEvent() {}; // To stop errorC2280(https://msdn.microsoft.com/en-us/library/bb531344.aspx#BK_compiler)
    event_type m_event_type;
    union
    {
        key_args m_key_args;
        mouse_args m_mouse_args;
    };
};

using a union to hold the event specific values allows us to save space and present a cleaner interface.
In order to differentiate between keyboard and mouse eventsthe GLWindow::windowsProcedure method will also need updated so that it handles the new InputEvent interface.

// Key-down event
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
{
    InputEvent e;
    e.m_event_type = event_type::e_key_event;
    if(std::bitset<32>(l_param).test(30))
    {
        e.m_key_args.key = readKey(w_param, l_param);
        e.m_key_args.state = key_state::e_repeat;
    }
    else
    {
        e.m_key_args.key = readKey(w_param, l_param);
        e.m_key_args.state = key_state::e_pressed;
    }
    m_events.push_back(e);
    break;
}
// Key-up event
case WM_KEYUP:
case WM_SYSKEYUP:
{
    InputEvent e;
    e.m_event_type = event_type::e_key_event;
    e.m_key_args.key = readKey(w_param, l_param);
    e.m_key_args.state = key_state::e_released;
    m_events.push_back(e);
    break;
}

and change the switch statement in the printEvent test method in main.cpp to use the correct value as well:

//...
switch(e.m_key_args.key)
{
//...
//...
}
switch(e.m_key_args.state)
{
//...
//...
}

Now we’ll look at actually dealing with mouse movement.
When the mouse is moved a windows message is geberated with type WM_MOUSEMOVE, like all messages it comes with a WPARAM and an LPARAM. For WM_MOUSEMOVE these parameters hold the following information:

  • WPARAM
    • A Bitset that stores the following state.

| Value | Meaning | |—————–| | MK_CONTROL | Ctrl Key is down | | MK_LBUTTON | Left Mouse Button is down | | MK_MBUTTON | Middle Mouse Button is down | | MK_RBUTTON | Right Mouse Button is down | | MK_SHIFT | Shift Key is down | | MK_XBUTTON1 | First X button is down | | MK_XBUTTON2 | Second X Button is down |

  • LPARAM
    • The low word holds the x-coordinate of the mouse
    • the high word holds the y-coordinate of the mouse

Add the following case to the switch statement in the GLWindow::windowsProcedure method:

// Mouse move event
case WM_MOUSEMOVE :
{
    InputEvent e;
    e.m_event_type = event_type::e_mouse_event;
    e.m_mouse_args.mouse_x = GET_X_LPARAM(l_param);
    e.m_mouse_args.mouse_y = GET_Y_LPARAM(l_param);
    e.m_mouse_args.move_modifier.bits = GetModifier(w_param);
    m_events.push_back(e);
    break;
}

The GetModifier is a helper method that translates the WPARAM into a move_modifier_flag. It is fairly straightforward and as it’s esentially a private helper I have defined it in an anonymous namespace at the top of the cpp file This stops it cluttering up the header file and hides implementation details from the user. It is defined as:

#!C++
namespace
{
    move_modifier_flag GetModifier(WPARAM w_param) {
        move_modifier_flag ret_val;
        if(w_param & MK_CONTROL)
        {
            ret_val.set(0, 1);
        }
        if(w_param & MK_LBUTTON)
        {
            ret_val.set(1, 1);
        }
        if(w_param & MK_MBUTTON)
        {
            ret_val.set(2, 1);
        }
        if(w_param & MK_RBUTTON)
        {
            ret_val.set(3, 1);
        }
        if(w_param & MK_SHIFT)
        {
            ret_val.set(4, 1);
        }
        if(w_param & MK_XBUTTON1)
        {
            ret_val.set(5, 1);
        }
        if(w_param & MK_XBUTTON2)
        {
            ret_val.set(6, 1);
        }

        return ret_val;
    }
}

And in the main.cpp alter the while loop that pops each event to look like:

while (test_window.popEvent(e))
{
    // Do things with 'e'
    switch(e.m_event_type)
    {
        case event_type::e_key_event:
            printEvent(e);
            break;
        case event_type::e_mouse_event:
            testMouse(e);
            break;
    }
}

Define the testMouse method in main as:

void testMouse(
    const InputEvent& e
) {
    if(e.m_mouse_args.move_modifier.named_keys.l_button && e.m_mouse_args.move_modifier.named_keys.r_button)
    {
        std::cout << "Left & Right drag " << "mouse X: " << e.m_mouse_args.mouse_x << "  mouse Y:" << e.m_mouse_args.mouse_y << std::endl;
    }
    else if(e.m_mouse_args.move_modifier.named_keys.l_button && e.m_mouse_args.move_modifier.named_keys.shift)
    {
        std::cout << "Shift & Left drag " << "mouse X: " << e.m_mouse_args.mouse_x << "  mouse Y:" << e.m_mouse_args.mouse_y << std::endl;
    }
    else if(e.m_mouse_args.move_modifier.named_keys.ctrl)
    {
        std::cout << "Ctrl drag " << "mouse X: " << e.m_mouse_args.mouse_x << "  mouse Y:" << e.m_mouse_args.mouse_y << std::endl;
    }
    else if(e.m_mouse_args.move_modifier.named_keys.l_button)
    {
        std::cout << "Left drag " << "mouse X: " << e.m_mouse_args.mouse_x << "  mouse Y:" << e.m_mouse_args.mouse_y << std::endl;
    }
    else if(e.m_mouse_args.move_modifier.named_keys.m_button)
    {
        std::cout << "Middle drag " << "mouse X: " << e.m_mouse_args.mouse_x << "  mouse Y:" << e.m_mouse_args.mouse_y << std::endl;
    }
    else if(e.m_mouse_args.move_modifier.named_keys.r_button)
    {
        std::cout << "Right drag " << "mouse X: " << e.m_mouse_args.mouse_x << "  mouse Y:" << e.m_mouse_args.mouse_y << std::endl;
    }
    else if(e.m_mouse_args.move_modifier.named_keys.shift)
    {
        std::cout << "Shift drag " << "mouse X: " << e.m_mouse_args.mouse_x << "  mouse Y:" << e.m_mouse_args.mouse_y << std::endl;
    }
    else
    {
        std::cout << "mouse X: " << e.m_mouse_args.mouse_x << "  mouse Y:" << e.m_mouse_args.mouse_y << std::endl;
    }
}

If you run the program now you can try various mouse moves/drags and should see something like:

Further Work

This tutorial just covers the bare minimum to capture keyboard and mouse events, your event system could be extended to detect any other message such as when the window gains/loses focus. I’m trying to keep these tutorials as non-specific as possible so that the anyone who comes across them can modify them easily to their own needs.