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.hDevice
cannot be directly used with DeviceIoControl
.
You have to do a call to GetRawInputDeviceInfo
with a RIDI_DEVICENAME
to obtain device interface path. After that you need to call to CreateFile
to open this interface and obtain its handle. This handle can be used with DeviceIoControl
.
Here is some code from my test repo:
inline ScopedHandle OpenDeviceInterface(const std::string& deviceInterface, bool readOnly = false)
{
DWORD desired_access = readOnly ? 0 : (GENERIC_WRITE | GENERIC_READ);
DWORD share_mode = FILE_SHARE_READ | FILE_SHARE_WRITE;
HANDLE handle = ::CreateFileW(utf8::widen(deviceInterface).c_str(), desired_access, share_mode, 0, OPEN_EXISTING, 0, 0);
return ScopedHandle(handle);
}
bool RawInputDevice::QueryRawInputDeviceInfo()
{
DCHECK(IsValidHandle(m_Handle));
UINT size = 0;
UINT result = ::GetRawInputDeviceInfoW(m_Handle, RIDI_DEVICENAME, nullptr, &size);
if (result == static_cast<UINT>(-1))
{
//PLOG(ERROR) << "GetRawInputDeviceInfo() failed";
return false;
}
DCHECK_EQ(0u, result);
std::wstring buffer(size, 0);
result = ::GetRawInputDeviceInfoW(m_Handle, RIDI_DEVICENAME, buffer.data(), &size);
if (result == static_cast<UINT>(-1))
{
//PLOG(ERROR) << "GetRawInputDeviceInfo() failed";
return false;
}
DCHECK_EQ(size, result);
m_InterfacePath = utf8::narrow(buffer);
m_InterfaceHandle = OpenDeviceInterface(m_InterfacePath);
if (!IsValidHandle(m_InterfaceHandle.get()))
{
/* System devices, such as keyboards and mice, cannot be opened in
read-write mode, because the system takes exclusive control over
them. This is to prevent keyloggers. However, feature reports
can still be sent and received. Retry opening the device, but
without read/write access. */
m_InterfaceHandle = OpenDeviceInterface(m_InterfacePath, true);
if (IsValidHandle(m_InterfaceHandle.get()))
m_IsReadOnlyInterface = true;
}
return !m_InterfacePath.empty();
}
bool RawInputDeviceKeyboard::ExtendedKeyboardInfo::QueryInfo(const ScopedHandle& interfaceHandle)
{
// https://docs.microsoft.com/windows/win32/api/ntddkbd/ns-ntddkbd-keyboard_extended_attributes
KEYBOARD_EXTENDED_ATTRIBUTES extended_attributes{ KEYBOARD_EXTENDED_ATTRIBUTES_STRUCT_VERSION_1 };
DWORD len = 0;
if (!DeviceIoControl(interfaceHandle.get(), IOCTL_KEYBOARD_QUERY_EXTENDED_ATTRIBUTES, nullptr, 0, &extended_attributes, sizeof(extended_attributes), &len, nullptr))
return false;
DCHECK_EQ(len, sizeof(extended_attributes));
FormFactor = extended_attributes.FormFactor;
KeyType = extended_attributes.IETFLanguageTagIndex;
PhysicalLayout = extended_attributes.PhysicalLayout;
VendorSpecificPhysicalLayout = extended_attributes.VendorSpecificPhysicalLayout;
IETFLanguageTagIndex = extended_attributes.IETFLanguageTagIndex;
ImplementedInputAssistControls = extended_attributes.ImplementedInputAssistControls;
return true;
}
I want to mess with the indicator lights of caps lock, scroll lock, and num lock per keyboard
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.