Croaking Kero

Handling Keyboard and Mouse Input with Win32 in C

This tutorial is also available as a video. In this tutorial I’ll show you how to handle keyboard and mouse input events with the Win32 native library, including storing the keyboard state and tracking mouse movement and buttons. Here's a clip of the final program running: Note: I’m compiling this code with GCC and not Microsoft’s C compiler. To compile with cl you’ll need to remove the PRINT_ERROR macro and also link to user32. Note: Click any of the hyperlinked words to visit the MSDN documentation page for them. Note: I've dimmed all of the code not specific to the subject of the tutorial. main.c #define UNICODE #define _UNICODE #include <windows.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #define PRINT_ERROR(a, args...) printf("ERROR %s() %s Line %d: " a, __FUNCTION__, __FILE__, __LINE__, ##args); #if RAND_MAX == 32767 #define rand32() ((rand() << 15) + (rand() << 1) + (rand() & 1)) #else #define rand32() rand() #endif bool quit = false; HWND window_handle; BITMAPINFO bitmap_info; HBITMAP bitmap; HDC bitmap_device_context; struct { union { int w, width; }; union { int h, height; }; uint32_t *pixels; } frame = {0}; bool keyboard[256] = {0}; struct { int x, y; uint8_t buttons; } mouse; enum { MOUSE_LEFT = 0b1, MOUSE_MIDDLE = 0b10, MOUSE_RIGHT = 0b100, MOUSE_X1 = 0b1000, MOUSE_X2 = 0b10000 }; LRESULT CALLBACK WindowProcessMessage(HWND window_handle, UINT message, WPARAM wParam, LPARAM lParam); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR pCmdLine, int nCmdShow) { const wchar_t window_class_name[] = L"Window Class"; static WNDCLASS window_class = { 0 }; window_class.lpfnWndProc = WindowProcessMessage; window_class.hInstance = hInstance; window_class.lpszClassName = window_class_name; RegisterClass(&window_class); bitmap_info.bmiHeader.biSize = sizeof(bitmap_info.bmiHeader); bitmap_info.bmiHeader.biPlanes = 1; bitmap_info.bmiHeader.biBitCount = 32; bitmap_info.bmiHeader.biCompression = BI_RGB; bitmap_device_context = CreateCompatibleDC(0); window_handle = CreateWindow(window_class_name, L"Learn to Program Windows", WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); if(window_handle == NULL) { PRINT_ERROR("CreateWindow() failed. Returned NULL.\n"); return -1; } while(!quit) { static MSG message = { 0 }; while(PeekMessage(&message, NULL, 0, 0, PM_REMOVE)) { DispatchMessage(&message); } static int keyboard_x = 0, keyboard_y = 0; if(keyboard[VK_RIGHT] || keyboard['D']) ++keyboard_x; if(keyboard[VK_LEFT] || keyboard['A']) --keyboard_x; if(keyboard[VK_UP] || keyboard['W']) ++keyboard_y; if(keyboard[VK_DOWN] || keyboard['S']) --keyboard_y; if(keyboard_x < 0) keyboard_x = 0; if(keyboard_x > frame.w-1) keyboard_x = frame.w-1; if(keyboard_y < 0) keyboard_y = 0; if(keyboard_y > frame.h-1) keyboard_y = frame.h-1; for(int i = 0; i < 1000; ++i) frame.pixels[rand32() % (frame.w * frame.h)] = 0; frame.pixels[keyboard_x + keyboard_y*frame.w] = 0x00ffffff; if(mouse.buttons & MOUSE_LEFT) frame.pixels[mouse.x + mouse.y*frame.w] = 0x00ffffff; InvalidateRect(window_handle, NULL, FALSE); UpdateWindow(window_handle); } return 0; } LRESULT CALLBACK WindowProcessMessage(HWND window_handle, UINT message, WPARAM wParam, LPARAM lParam) { static bool has_focus = true; switch(message) { case WM_QUIT: case WM_DESTROY: { quit = true; } break; case WM_PAINT: { static PAINTSTRUCT paint; static HDC device_context; device_context = BeginPaint(window_handle, &paint); BitBlt(device_context, paint.rcPaint.left, paint.rcPaint.top, paint.rcPaint.right - paint.rcPaint.left, paint.rcPaint.bottom - paint.rcPaint.top, bitmap_device_context, paint.rcPaint.left, paint.rcPaint.top, SRCCOPY); EndPaint(window_handle,&paint); } break; case WM_SIZE: { frame.w = bitmap_info.bmiHeader.biWidth = LOWORD(lParam); frame.h = bitmap_info.bmiHeader.biHeight = HIWORD(lParam); if(bitmap) DeleteObject(bitmap); bitmap = CreateDIBSection(NULL, &bitmap_info, DIB_RGB_COLORS, (void**)&frame.pixels, 0, 0); SelectObject(bitmap_device_context, bitmap); } break; case WM_KILLFOCUS: { has_focus = false; memset(keyboard, 0, 256 * sizeof(keyboard[0])); mouse.buttons = 0; } break; case WM_SETFOCUS: has_focus = true; break; case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM_KEYDOWN: case WM_KEYUP: { if(has_focus) { static bool key_is_down, key_was_down; key_is_down = ((lParam & (1 << 31)) == 0); key_was_down = ((lParam & (1 << 30)) != 0); if(key_is_down != key_was_down) { keyboard[(uint8_t)wParam] = key_is_down; if(key_is_down) { switch(wParam) { case VK_ESCAPE: quit = true; break; } } } } } break; case WM_MOUSEMOVE: { mouse.x = LOWORD(lParam); mouse.y = frame.h - 1 - HIWORD(lParam); } break; case WM_LBUTTONDOWN: mouse.buttons |= MOUSE_LEFT; break; case WM_LBUTTONUP: mouse.buttons &= ~MOUSE_LEFT; break; case WM_MBUTTONDOWN: mouse.buttons |= MOUSE_MIDDLE; break; case WM_MBUTTONUP: mouse.buttons &= ~MOUSE_MIDDLE; break; case WM_RBUTTONDOWN: mouse.buttons |= MOUSE_RIGHT; break; case WM_RBUTTONUP: mouse.buttons &= ~MOUSE_RIGHT; break; case WM_XBUTTONDOWN: { if(GET_XBUTTON_WPARAM(wParam) == XBUTTON1) { mouse.buttons |= MOUSE_X1; } else { mouse.buttons |= MOUSE_X2; } } break; case WM_XBUTTONUP: { if(GET_XBUTTON_WPARAM(wParam) == XBUTTON1) { mouse.buttons &= ~MOUSE_X1; } else { mouse.buttons &= ~MOUSE_X2; } } break; case WM_MOUSEWHEEL: { printf("%s\n", wParam & 0b10000000000000000000000000000000 ? "Down" : "Up"); } break; default: return DefWindowProc(window_handle, message, wParam, lParam); } return 0; } build.bat gcc main.c -lgdi32

Code Walkthrough

bool keyboard[256] = {0}; We have a 256-element boolean array to store the state of each keyboard key, with false meaning not pressed and true meaning pressed. struct { int x, y; uint8_t buttons; } mouse; enum { MOUSE_LEFT = 0b1, MOUSE_MIDDLE = 0b10, MOUSE_RIGHT = 0b100, MOUSE_X1 = 0b1000, MOUSE_X2 = 0b10000 }; This mouse structure stores the cursor position within our window and the state of the standard mouse buttons, which Windows considers to be left, middle, right, “x1” and “x2”. I made this enumerator to assign each of these buttons to a different bit, which is how we can store all 5 buttons in a single byte in the structure. case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM_KEYDOWN: case WM_KEYUP: { if(has_focus) { static bool key_is_down, key_was_down; key_is_down = ((lParam & (1 << 31)) == 0); key_was_down = ((lParam & (1 << 30)) != 0); if(key_is_down != key_was_down) { keyboard[(uint8_t)wParam] = key_is_down; if(key_is_down) { switch(wParam) { case VK_ESCAPE: quit = true; break; } } } } } break; We can handle all 4 keyboard events in one block of code. If our window has focus when it processes the event, we check the current and previous state of the key by extracting the 31st and 32nd bits of lParam. We do this to ignore key repeat events, where you hold down a key and after a few seconds it starts sending repeated key events. wParam stores the key index, which we use to set the corresponding element in the keyboard array to the new value. This results in an automatically updating array holding the keyboard state. I’ve also demonstrated how you could use a switch statement to immediately respond to the press of any keys here. Latching the keyboard events like this does cause the issue that, if you press and hold a key, tab out of the window, then release the key, you won’t receive a key release event so in your application the key will still be considered pressed. That’s why in many older games you can tab out, then when you tab back in find that your character has walked straight over a cliff. case WM_KILLFOCUS: { has_focus = false; memset(keyboard, 0, 256 * sizeof(keyboard[0])); mouse.buttons = 0; } break; case WM_SETFOCUS: has_focus = true; break; To solve this we clear the keyboard array whenever we receive the “WM_KILLFOCUS” event. We can also use this “has_focus” variable just because Windows can in some circumstances send you keyboard events even when your window doesn’t have focus. static int keyboard_x = 0, keyboard_y = 0; if(keyboard[VK_RIGHT] || keyboard['D']) ++keyboard_x; if(keyboard[VK_LEFT] || keyboard['A']) --keyboard_x; if(keyboard[VK_UP] || keyboard['W']) ++keyboard_y; if(keyboard[VK_DOWN] || keyboard['S']) --keyboard_y; In the main program loop we can check the state of any key using the “virtual key codes”, listed in Microsoft’s documentation. The upper-case ASCII character values of all the letters correspond to their virtual key codes so use 'A' to reference that keyboard key. Here I’m checking the WASD and arrow keys to move a dot around the screen. case WM_MOUSEMOVE: { mouse.x = LOWORD(lParam); mouse.y = frame.h - 1 - HIWORD(lParam); } break; “WM_MOUSEMOVE” tells us the position of the cursor relative to our window, where the top-left is 0,0 and the bottom-right is width-1, height-1. X and Y are the low and high two bytes of lParam. Since our drawing code uses the opposite y axis, we subtract the mouse y coordinate from height-1 to invert it. case WM_LBUTTONDOWN: mouse.buttons |= MOUSE_LEFT; break; case WM_LBUTTONUP: mouse.buttons &= ~MOUSE_LEFT; break; case WM_MBUTTONDOWN: mouse.buttons |= MOUSE_MIDDLE; break; case WM_MBUTTONUP: mouse.buttons &= ~MOUSE_MIDDLE; break; case WM_RBUTTONDOWN: mouse.buttons |= MOUSE_RIGHT; break; case WM_RBUTTONUP: mouse.buttons &= ~MOUSE_RIGHT; break; Here we handle the left, middle and right mouse buttons. When they’re pressed we use bitwise OR to set the correct bit in the mouse.buttons byte to true, and on release we bitwise AND the inverse to set the correct bit to false and leave all the other bits as whatever value they already were. case WM_XBUTTONDOWN: { if(GET_XBUTTON_WPARAM(wParam) == XBUTTON1) { mouse.buttons |= MOUSE_X1; } else { mouse.buttons |= MOUSE_X2; } } break; case WM_XBUTTONUP: { if(GET_XBUTTON_WPARAM(wParam) == XBUTTON1) { mouse.buttons &= ~MOUSE_X1; } else { mouse.buttons &= ~MOUSE_X2; } } break; The X1 and X2 buttons are handled from the same WM_XBUTTONDOWN and UP events and we extract whether it was X1 or X2 from wParam using the GET_XBUTTON_WPARAM macro. case WM_MOUSEWHEEL: { printf("%s\n", wParam & 0b10000000000000000000000000000000 ? "Down" : "Up"); } break; In the MOUSEWHEEL event the scroll direction is stored in the 32nd bit of wParam. case WM_KILLFOCUS: { has_focus = false; memset(keyboard, 0, 256 * sizeof(keyboard[0])); mouse.buttons = 0; } break; Like the keyboard, whenever the window loses focus we set mouse.buttons to 0. if(mouse.buttons & MOUSE_LEFT) frame.pixels[mouse.x + mouse.y*frame.w] = 0x00ffffff; In the main program loop we check if the left button is pressed, then set the pixel at the mouse position to white. Now when we run the program we get two white dots: one that zips around as we use WASD or the arrow keys, and another which only appears when holding the left mouse button. That’s how to handle input with the Windows native library in C. In my next tutorials I’ll show you how to play real-time audio, load and play sound files and load and display images.
If you've got questions about any of the code feel free to e-mail me or comment on the youtube video. I'll try to answer them, or someone else might come along and help you out. If you've got any extra tips about how this code can be better or just more useful info about the code, let me know so I can update the tutorial. Thanks to Froggie717 for criticisms and correcting errors in this tutorial. Cheers.