So I have a program that is using multiple keyboards as input using raw input and I want to mess with the indicator lights of caps lock, scroll lock, and num lock per keyboard.
So my approach was to RegisterRawInputDevices for keyboards using the RIDEV_DEVNOTIFY flag. Then when I get a WM_INPUT_DEVICE_CHANGE message with a GIDC_ARRIVAL wParam. I would save off the lParam like HANDLE hDevice = (HANDLE)Message.lParam; then later when I get a WM_INPUT I can act on the key to mess with the indicator lights using DeviceIoControl with the handle in the RAWINPUT struct from GetRawInputData on the lParam. However DeviceIoControl does not like the header.hDevice in the RAWINPUT struct. How would I use DeviceIoControl per keyboard with the raw input handles?
Here is a program that demos the above trying to make the caps lock indicator light per keyboard
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
//windows
#include <windows.h>
#include <dbt.h>
#include <Ntddkbd.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
typedef uint8_t u8;
typedef uint32_t u32;
typedef int64_t s64;
typedef float f32;
#define DEFAULT_SCREEN_WIDTH 1280
#define DEFAULT_SCREEN_HEIGHT 720
#define MINIMUM_SCREEN_WIDTH 300
#define MINIMUM_SCREEN_HEIGHT 300
typedef struct GameState
{
u8 hwIsRunning;
} GameState;
typedef struct Renderer
{
//ScreenGraphics State
u8 hwIsMinimized;
//Win32 Screen Variables
u32 dwScreenWidth = DEFAULT_SCREEN_WIDTH;
u32 dwScreenHeight = DEFAULT_SCREEN_HEIGHT;
HWND MainWindowHandle;
const char *szWindowName = "FPS Camera Basic";
} Renderer;
Renderer sRENDERER;
GameState sGAMESTATE;
u32 max( u32 a, u32 b )
{
return a > b ? a : b;
}
int logWindowsError(const char* msg)
{
LPVOID lpMsgBuf;
DWORD dw = GetLastError();
FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, dw, MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ), (LPTSTR)&lpMsgBuf, 0, NULL );
OutputDebugStringA( msg );
OutputDebugStringA( (LPCTSTR)lpMsgBuf );
LocalFree( lpMsgBuf );
return -1;
}
inline
void CloseProgram()
{
sGAMESTATE.hwIsRunning = 0;
}
LRESULT CALLBACK
Win32MainWindowCallback(
HWND Window,
UINT Message,
WPARAM WParam,
LPARAM LParam)
{
LRESULT Result = 0;
switch (Message)
{
case WM_SYSCHAR:
break;
case WM_SIZE:
{
u32 dwTempScreenWidth = LOWORD( LParam );
u32 dwTempScreenHeight = HIWORD( LParam );
if( WParam == SIZE_MINIMIZED || dwTempScreenWidth == 0 || dwTempScreenHeight == 0 )
{
sRENDERER.hwIsMinimized = 1;
}
else
{
sRENDERER.hwIsMinimized = 0;
}
sRENDERER.dwScreenWidth = max( 1, dwTempScreenWidth );
sRENDERER.dwScreenHeight = max( 1, dwTempScreenHeight );
}break;
case WM_GETMINMAXINFO:
{
LPMINMAXINFO lpMMI = (LPMINMAXINFO)LParam;
lpMMI->ptMinTrackSize.x = MINIMUM_SCREEN_WIDTH;
lpMMI->ptMinTrackSize.y = MINIMUM_SCREEN_HEIGHT;
}break;
case WM_CLOSE: //when user clicks on the X button on the window
{
CloseProgram();
} break;
case WM_ACTIVATE:
{
switch(WParam)
{
//WM_MOUSEACTIVATE
case WA_ACTIVE:
case WA_CLICKACTIVE:
case WA_INACTIVE:
default:
{
break;
}
}
} break;
default:
Result = DefWindowProc( Window, Message, WParam, LParam ); //call windows to handle default behavior of things we don't handle
}
return Result;
}
inline
void InitRawInput( HWND WindowHandle )
{
RAWINPUTDEVICE Rid[2];
Rid[0].usUsagePage = (USHORT) 0x01;
Rid[0].usUsage = (USHORT) 0x02;
Rid[0].dwFlags = RIDEV_INPUTSINK|RIDEV_DEVNOTIFY; //RIDEV_INPUTSINK: If set, this enables the caller to receive the input even when the caller is not in the foreground
Rid[0].hwndTarget = WindowHandle;
Rid[1].usUsagePage = (USHORT) 0x01;
Rid[1].usUsage = (USHORT) 0x06;
Rid[1].dwFlags = RIDEV_INPUTSINK | RIDEV_DEVNOTIFY | RIDEV_NOLEGACY; //RIDEV_INPUTSINK: If set, this enables the caller to receive the input even when the caller is not in the foreground
Rid[1].hwndTarget = WindowHandle;
RegisterRawInputDevices( Rid, 2, sizeof( Rid[0] ) );
}
inline
void InitWin32Window()
{
WNDCLASSEX WindowClass;
WindowClass.cbSize = sizeof( WNDCLASSEX );
WindowClass.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW; //https://devblogs.microsoft.com/oldnewthing/20060601-06/?p=31003
WindowClass.lpfnWndProc = Win32MainWindowCallback;
WindowClass.cbClsExtra = 0;
WindowClass.cbWndExtra = 0;
WindowClass.hInstance = GetModuleHandle( NULL );
WindowClass.hIcon = LoadIcon( 0, IDI_APPLICATION ); //IDI_APPLICATION: Default application icon, 0 means use a default Icon
WindowClass.hCursor = LoadCursor( 0, IDC_ARROW ); //IDC_ARROW: Standard arrow, 0 means used a predefined Cursor
WindowClass.hbrBackground = NULL;
WindowClass.lpszMenuName = NULL; // No menu
WindowClass.lpszClassName = "WindowTestClass"; //name our class
WindowClass.hIconSm = NULL; //can also do default Icon here? will NULL be default automatically?
if ( !RegisterClassEx( &WindowClass ) )
{
logWindowsError( "Failed to Register Window Class:\n" );
sRENDERER.MainWindowHandle = 0;
return;
}
HWND WindowHandle = CreateWindowEx( 0, WindowClass.lpszClassName, sRENDERER.szWindowName,
WS_OVERLAPPEDWINDOW | WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, sRENDERER.dwScreenWidth, sRENDERER.dwScreenHeight, //if fullscreen get monitor width and height
0, 0, WindowClass.hInstance, NULL );
if ( !WindowHandle )
{
logWindowsError( "Failed to Instantiate Window Class:\n" );
sRENDERER.MainWindowHandle = 0;
return;
}
sRENDERER.MainWindowHandle = WindowHandle;
InitRawInput( sRENDERER.MainWindowHandle );
}
inline
void InitStartingGameState()
{
sGAMESTATE.hwIsRunning = 1;
}
u32 dwNumHandles = 0;
typedef struct DoubleHandle
{
HANDLE hHandle0;
HANDLE hHandle1;
} DoubleHandle;
DoubleHandle handles[32];
//Use subsystem console when compiling
int main()
{
InitStartingGameState();
InitWin32Window();
if( !sRENDERER.MainWindowHandle )
{
return -1;
}
while( sGAMESTATE.hwIsRunning )
{
MSG Message;
while( PeekMessage( &Message, 0, 0, 0, PM_REMOVE ) )
{
switch( Message.message )
{
case WM_QUIT:
{
CloseProgram();
break;
}
case WM_SYSKEYDOWN:
case WM_SYSKEYUP:
case WM_KEYDOWN:
case WM_KEYUP:
{
break;
}
case WM_INPUT_DEVICE_CHANGE:
{
HANDLE hDevice = (HANDLE)Message.lParam;
RID_DEVICE_INFO deviceInfo;
deviceInfo.cbSize = sizeof(RID_DEVICE_INFO);
u32 dwSize = sizeof(RID_DEVICE_INFO);
GetRawInputDeviceInfo(hDevice,RIDI_DEVICEINFO,&deviceInfo,&dwSize);
u32 dwNameSize = 0;
u32 res = GetRawInputDeviceInfo(hDevice, RIDI_DEVICENAME, nullptr, &dwNameSize);
char *pName = new char[dwNameSize+1];
res = GetRawInputDeviceInfo(hDevice, RIDI_DEVICENAME, pName, &dwNameSize);
pName[dwNameSize] = 0;
switch(Message.wParam)
{
case GIDC_ARRIVAL:
{
switch(deviceInfo.dwType)
{
case RIM_TYPEKEYBOARD:
{
u32 dwKeyBoard = dwNumHandles++;
handles[dwKeyBoard].hHandle0 = hDevice;
} break;
default:
{
} break;
}
printf("GIDC_ARRIVAL %p %d %s\n",hDevice,deviceInfo.dwType,pName);
} break;
case GIDC_REMOVAL:
{
switch(deviceInfo.dwType)
{
case RIM_TYPEKEYBOARD:
{
} break;
default:
{
} break;
}
printf("GIDC_REMOVAL %p %d %s\n",hDevice,deviceInfo.dwType,pName);
} break;
default:
{
} break;
}
delete [] pName;
} break;
case WM_INPUT:
{
UINT dwSize = sizeof( RAWINPUT );
static BYTE lpb[sizeof( RAWINPUT )];
GetRawInputData( (HRAWINPUT)Message.lParam, RID_INPUT, lpb, &dwSize, sizeof( RAWINPUTHEADER ) );
RAWINPUT* raw = (RAWINPUT*)lpb;
if(raw->header.dwType == RIM_TYPEKEYBOARD )
{
bool bIsUp = (raw->data.keyboard.Flags & RI_KEY_BREAK) != 0;
u32 dwScanCode = raw->data.keyboard.MakeCode;
//capslock
if( 0x3A == dwScanCode)
{
for( u32 dwKeyBoard = 0; dwKeyBoard < dwNumHandles; ++dwKeyBoard )
{
if( hDevice != handles[dwKeyBoard].hHandle0)
{
KEYBOARD_INDICATOR_PARAMETERS InputBuffer; // Input buffer for DeviceIoControl
KEYBOARD_INDICATOR_PARAMETERS OutputBuffer; // Output buffer for DeviceIoControl
UINT LedFlagsMask;
BOOL Toggle;
ULONG DataLength = sizeof(KEYBOARD_INDICATOR_PARAMETERS);
ULONG ReturnedLength; // Number of bytes returned in output buffer
InputBuffer.UnitId = 0;
OutputBuffer.UnitId = 0;
UINT LedFlag = KEYBOARD_CAPS_LOCK_ON;
if (DeviceIoControl(handles[dwKeyBoard].hHandle0, IOCTL_KEYBOARD_QUERY_INDICATORS,
&InputBuffer, DataLength,
&OutputBuffer, DataLength,
&ReturnedLength, NULL))
{
LedFlagsMask = (OutputBuffer.LedFlags & (~LedFlag));
Toggle = (OutputBuffer.LedFlags & LedFlag);
Toggle ^= 1;
InputBuffer.LedFlags = (LedFlagsMask | (LedFlag * Toggle));
DeviceIoControl(handles[dwKeyBoard].hHandle0, IOCTL_KEYBOARD_SET_INDICATORS,
&InputBuffer, DataLength,
NULL, 0, &ReturnedLength, NULL);
}
else
{
logWindowsError("failed to get indicators\n");
}
}
}
}
}
else
{
TranslateMessage( &Message );
DispatchMessage( &Message );
}
break;
}
default:
{
TranslateMessage( &Message );
DispatchMessage( &Message );
break;
}
}
}
}
return 0;
}
RAWINPUT.header.hDevicecannot be directly used withDeviceIoControl.You have to do a call to
GetRawInputDeviceInfowith aRIDI_DEVICENAMEto obtain device interface path. After that you need to call toCreateFileto open this interface and obtain its handle. This handle can be used withDeviceIoControl.Here is some code from my test repo:
You cannot open keyboard handle for writing and do a
DeviceIoControl(..., IOCTL_KEYBOARD_SET_INDICATORS, ...)from a user mode without some kind of hacks described here. It is undocumented and may break at any time.Bonus chatter: Windows organizes devices as virtual files almost like Linux doing it under
/dev/filesystem path. These virtual files cannot be listed via usual APIs. But there is WinObj tool exist that uses undocumented APIs and can show the inner workings. It can help to understand how Windows works under the hood.