You can register for Raw Input in a separate thread. But first you need to create invisible window in that thread. Also to receive input in that thread you need to provide RIDEV_INPUTSINK to your RegisterRawInputDevices() call.
Here is my code that doing that:
void RawInputDeviceManager::RawInputManagerImpl::ThreadRun()
{
m_WakeUpEvent = ::CreateEventExW(nullptr, nullptr, 0, EVENT_ALL_ACCESS);
CHECK(IsValidHandle(m_WakeUpEvent));
HINSTANCE hInstance = ::GetModuleHandleW(nullptr);
m_hWnd = ::CreateWindowExW(0, L"Static", nullptr, 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, hInstance, 0);
CHECK(IsValidHandle(m_hWnd));
SUBCLASSPROC subClassProc = [](HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uIdSubclass*/, DWORD_PTR dwRefData) -> LRESULT
{
auto manager = reinterpret_cast<RawInputManagerImpl*>(dwRefData);
CHECK(manager);
switch (uMsg)
{
case WM_CHAR:
{
wchar_t ch = LOWORD(wParam);
DBGPRINT("WM_CHAR: `%s` (U+%04X %s)\n", GetUnicodeCharacterForPrint(ch).c_str(), ch, GetUnicodeCharacterName(ch).c_str());
return 0;
}
case WM_INPUT_DEVICE_CHANGE:
{
CHECK(wParam == GIDC_ARRIVAL || wParam == GIDC_REMOVAL);
HANDLE deviceHandle = reinterpret_cast<HANDLE>(lParam);
bool isConnected = (wParam == GIDC_ARRIVAL);
manager->OnInputDeviceConnected(deviceHandle, isConnected);
return 0;
}
case WM_INPUT:
{
HRAWINPUT dataHandle = reinterpret_cast<HRAWINPUT>(lParam);
manager->OnInputMessage(dataHandle);
return 0;
}
}
return ::DefSubclassProc(hWnd, uMsg, wParam, lParam);
};
CHECK(::SetWindowSubclass(m_hWnd, subClassProc, 0, reinterpret_cast<DWORD_PTR>(this)));
CHECK(Register());
// enumerate devices before start
EnumerateDevices();
// main message loop
while (m_Running)
{
// wait for new messages
::MsgWaitForMultipleObjectsEx(1, &m_WakeUpEvent, INFINITE, QS_ALLEVENTS, MWMO_INPUTAVAILABLE);
MSG msg;
while (::PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
{
m_Running = false;
break;
}
::TranslateMessage(&msg);
::DispatchMessageW(&msg);
}
}
CHECK(Unregister());
CHECK(::RemoveWindowSubclass(m_hWnd, subClassProc, 0));
CHECK(::DestroyWindow(m_hWnd));
m_hWnd = nullptr;
}
bool RawInputDeviceManager::RawInputManagerImpl::Register()
{
RAWINPUTDEVICE rid[] =
{
{
HID_USAGE_PAGE_GENERIC,
0,
RIDEV_DEVNOTIFY | RIDEV_INPUTSINK | RIDEV_PAGEONLY,
m_hWnd
}
};
return ::RegisterRawInputDevices(rid, static_cast<UINT>(std::size(rid)), sizeof(RAWINPUTDEVICE));
}
You even can make WM_CHAR to work in your thread by posting WM_KEYDOWN from WM_INPUT keyboard messages:
void RawInputDeviceManager::RawInputManagerImpl::OnKeyboardEvent(const RAWKEYBOARD& keyboard) const
{
if (keyboard.VKey >= 0xff/*VK__none_*/)
return;
// Sync keyboard layout with parent thread
HKL keyboardLayout = ::GetKeyboardLayout(m_ParentThreadId);
if (keyboardLayout != m_KeyboardLayout)
{
m_KeyboardLayout = keyboardLayout;
// This will post WM_INPUTLANGCHANGE
::ActivateKeyboardLayout(m_KeyboardLayout, 0);
}
// To be able to receive WM_CHAR in our thread we need WM_KEYDOWN/WM_KEYUP messages.
// But we wouldn't have them in invisible unfocused window that we have there.
// Just emulate them from RawInput message manually.
uint16_t keyFlags = LOBYTE(keyboard.MakeCode);
if (keyboard.Flags & RI_KEY_E0)
keyFlags |= KF_EXTENDED;
if (keyboard.Message == WM_SYSKEYDOWN || keyboard.Message == WM_SYSKEYUP)
keyFlags |= KF_ALTDOWN;
if (keyboard.Message == WM_KEYUP || keyboard.Message == WM_SYSKEYUP)
keyFlags |= KF_REPEAT;
if (keyboard.Flags & RI_KEY_BREAK)
keyFlags |= KF_UP;
::PostMessageW(m_hWnd, keyboard.Message, keyboard.VKey, MAKELONG(1/*repeatCount*/, keyFlags));
}
Another more common approach is to push WM_INPUT events in queue (possibly lockless) and process them in some input worker thread that could emit input events etc to other parts of your program.