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:
- If the
MSG
isWM_NCCREATE
we will useSetWindowLongPtr
to associate a pointer to this instance of ourGLWindow
class to the window. - If the pointer has been set correctly then we retrieve it and use to call the non-static windows procedure method.
- Default to calling
DefWinProc
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
andl_param
contain additional information pertaining to theMSG
, 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 wholeMSG
struct passed through anymore, just the members that Win32 has decided we are allowed. In the case ofWM_NCCREATE
thew_param
is empty, and thel_param
contains an instance ofCREATESTRUCT
, all we need to know aboutCREATESTRUCT
is that it’s members are identical to the parameters ofCreateWindow
. So we have access to all the values passed in toCreateWindow
when we recieve theWM_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:
HWND hWnd
- The handle to the window you want to assign data for.
int nIndex
- The offset to the value to be assigned (we are using
GWLP_USERDATA
)
- The offset to the value to be assigned (we are using
LONG_PTR swNewLong
- 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 GLWindow
class, 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.