通常、キーボードはいくつ接続しても、1つとして認識されてしまいます。
Raw Input API を使うと、その名の通り、接続されているデバイスから“生”の入力を取得することができます。
当然、マウスやキーボードもそのデバイスに含まれます。
ただし、Raw Input API を使用できるのは、Windows XP 以降のようです。
今回は、Raw Input API を使って、2つ以上のキーボードからの入力を別々に扱う方法を、簡単なサンプルとともに説明します。
言語は、C++を用いるので、RawInputというクラスとして設計したいと思います。
ちなみに、描画部分にはDXライブラリを利用しています。
【ソースコード】
► スポイラーを表示
[tabs][tabs:RawInput.h]
コード:
#pragma once
#include <windows.h>
#include <vector>
#include <bitset>
namespace a5ua
{
class RawInput
{
public: // function
// キーの入力状態を表すビット列
typedef std::bitset<256> KeyState;
// コンストラクタ(親ウインドウのハンドルを指定)
RawInput(HWND hWnd);
// デストラクタ
~RawInput();
// 検出されたキーボードの数を返す
std::size_t keyboard_num() const;
// id 番目のキーボードの入力状態を返す
const KeyState &operator[](std::size_t id) const;
private: // function
// WM_INPUT メッセージを受け取るためのウインドウを作成する
void create_dummy_window(HWND parent_window_handle);
// RawInput デバイスを登録する
void register_device();
// ウインドウプロシージャをメンバ関数に転送する
static LRESULT CALLBACK call_message_procedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
// 実際のウインドウプロシージャ
LRESULT message_procedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
// キー入力状態を更新する
bool update(HRAWINPUT hRawInput);
private: // member
// WM_INPUT を受け取るウインドウのハンドル
HWND m_window_handle;
// 入力を受け付ける RawInput デバイスのハンドル
std::vector<HANDLE> m_device_handle;
// 検出された全てのキーボードの入力状態
std::vector<KeyState> m_key_state_table;
};
}
[tabs:RawInput.cpp]
コード:
#include "RawInput.h"
namespace
{
// キーボード登録用
RAWINPUTDEVICE RawInputKeyboard(HWND hWnd)
{
RAWINPUTDEVICE d;
d.usUsagePage = 0x01;
d.usUsage = 0x06;
d.dwFlags = RIDEV_INPUTSINK; // ウインドウが最前面になくとも WM_INPUT を受け取れるようにする
d.hwndTarget = hWnd;
return d;
}
// マウス登録用
/*
RAWINPUTDEVICE RawInputMouse(HWND hWnd)
{
RAWINPUTDEVICE d;
d.usUsagePage = 0x01;
d.usUsage = 0x02;
d.dwFlags = RIDEV_NOLEGACY;
d.hwndTarget = hWnd;
return d;
}
*/
}
namespace a5ua
{
RawInput::RawInput(HWND hWnd)
{
create_dummy_window(hWnd);
register_device();
}
RawInput::~RawInput()
{
DestroyWindow(m_window_handle);
}
void RawInput::create_dummy_window(HWND parent_window_handle)
{
const char *class_name = "RawInputWindow";
HINSTANCE hInstance = GetModuleHandle(0);
//ウインドウクラスの登録
WNDCLASS window_class = {};
window_class.lpszClassName = class_name;
window_class.hInstance = hInstance;
window_class.lpfnWndProc = &RawInput::call_message_procedure;
if (!RegisterClass(&window_class)) {
throw std::exception("ウインドウクラスの登録に失敗");
}
// ウインドウの作成
// thisを渡すことで、WM_CREATE 時にstatic関数内でそのパラメータを参照できる
m_window_handle = CreateWindowEx(
0, "RawInputWindow", "RawInput", WS_CHILD,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
parent_window_handle, 0, hInstance, this
);
if (!m_window_handle) {
throw std::exception("Raw Input 用ウィンドウの作成に失敗");
}
}
void RawInput::register_device()
{
// 現在接続されている入力デバイスの数を取得
UINT device_num = 0;
GetRawInputDeviceList(0, &device_num, sizeof(RAWINPUTDEVICELIST));
// デバイス情報用構造体(の配列)
std::vector<RAWINPUTDEVICELIST> device_list(device_num);
// デバイス情報の取得
if (GetRawInputDeviceList(&device_list[0], &device_num, sizeof(RAWINPUTDEVICELIST)) != device_num) {
throw std::exception("デバイス情報の取得に失敗");
}
// キーボードのデバイスハンドルを記憶
for (int i = 0, N = device_list.size(); i < N; ++i) {
if (device_list[i].dwType == RIM_TYPEKEYBOARD) {
m_device_handle.push_back(device_list[i].hDevice);
}
}
m_key_state_table.resize(m_device_handle.size()); // デバイスの数だけ、入力状態テーブルをつくる
// WM_INPUTで受け取れるように登録
std::vector<RAWINPUTDEVICE> device;
device.push_back(RawInputKeyboard(m_window_handle));
//device.push_back(RawInputMouse(hWnd));
if (!RegisterRawInputDevices(&device[0], device.size(), sizeof device[0])) {
throw std::exception("デバイスの登録に失敗");
}
}
LRESULT CALLBACK RawInput::call_message_procedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
RawInput *self = reinterpret_cast<RawInput *>(GetWindowLongPtr(hWnd, GWL_USERDATA));
// RawInputオブジェクトが得られない場合
if (!self) {
// WM_CREATE なら、CreateWindowEx() 呼び出し時に指定したパラメータを取得し、
if (message == WM_CREATE) {
self = static_cast<RawInput *>(reinterpret_cast<LPCREATESTRUCT>(lParam)->lpCreateParams);
}
// ユーザーデータ部分に設定(以後、selfに有効な値が入る)
if (self) {
SetWindowLongPtr(hWnd, GWL_USERDATA, reinterpret_cast<LONG_PTR>(self));
}
}
// 実際のウインドウプロシージャに転送
if (self) {
return self->message_procedure(hWnd, message, wParam, lParam);
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
LRESULT RawInput::message_procedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if (message == WM_INPUT) {
// 入力状態を更新
update(reinterpret_cast<HRAWINPUT>(lParam));
return 0;
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
bool RawInput::update(HRAWINPUT hRawInput)
{
// RawInputのデータを取得
UINT size;
GetRawInputData(hRawInput, RID_INPUT, 0, &size, sizeof(RAWINPUTHEADER));
std::vector<BYTE> buf(size);
if (GetRawInputData(hRawInput, RID_INPUT, &buf[0], &size, sizeof(RAWINPUTHEADER)) != size) {
return false;
}
RAWINPUT *input = static_cast<RAWINPUT *>(static_cast<void *>(&buf[0]));
// キーボードの入力状態を更新
if (input->header.dwType == RIM_TYPEKEYBOARD) {
for (int i = 0, N = m_device_handle.size(); i < N; ++i) {
if (input->header.hDevice == m_device_handle[i]) {
const RAWKEYBOARD &key = input->data.keyboard;
m_key_state_table[i][key.VKey] = !(key.Flags & RI_KEY_BREAK);
}
}
}
return true;
}
std::size_t RawInput::keyboard_num() const
{
return m_key_state_table.size();
}
const RawInput::KeyState &RawInput::operator[](std::size_t id) const
{
return m_key_state_table[id];
}
}
[tabs:WinMain.cpp]
コード:
// もしかしたら必要かも
//#define WINVER 0x500
//#define _WIN32_WINNT 0x0501
#include <windows.h>
#include <DxLib.h>
#include <vector>
#include <bitset>
#include <set>
#include <string>
#include <sstream>
#include <functional>
#include <algorithm>
#include "RawInput.h"
// DXライブラリを使用
// ・コンストラクタで初期化
// ・デストラクタで終了
class UseDxLibrary
{
public:
UseDxLibrary()
{
ChangeWindowMode(TRUE);
SetMainWindowText("Raw Input API のサンプル");
if (DxLib_Init() != 0) {
throw std::exception("DXライブラリの初期化に失敗");
}
SetDrawScreen(DX_SCREEN_BACK);
}
~UseDxLibrary()
{
DxLib_End();
}
} use_dxlib_application;
// テスト用の適当なクラス
class Unit
{
public:
Unit(int id, int x, int y, const std::bitset<256> &input) : m_id(id), m_x(x), m_y(y), m_input(&input)
{
}
void update()
{
int d = 5;
if (m_input->at(VK_LEFT)) m_x -= d;
if (m_input->at(VK_UP)) m_y -= d;
if (m_input->at(VK_RIGHT)) m_x += d;
if (m_input->at(VK_DOWN)) m_y += d;
}
void draw()
{
DrawCircle(m_x, m_y, 20, GetColor(255, 255, 255), FALSE);
DrawFormatString(m_x, m_y, GetColor(255, 255, 255), "%d", m_id);
}
private:
int m_id;
int m_x, m_y;
const std::bitset<256> *m_input;
};
std::vector<int> GetKeyboardID(const a5ua::RawInput &input, unsigned int request_num)
{
std::set<int> id;
if (input.keyboard_num() < request_num) {
return std::vector<int>();
}
while (ProcessMessage() == 0) {
ClearDrawScreen();
DrawString(0, 0, "任意のキーを押してキーボードを登録してください", GetColor(255, 255, 255));
DrawFormatString(0, 20, GetColor(255, 255, 255), "%d/%d", id.size(), request_num);
for (int i = 0, N = input.keyboard_num(); i < N; ++i) {
if (input[i].any()) {
id.insert(i);
// デバイス数が要求数に達したら終了
if (id.size() == request_num) {
return std::vector<int>(id.begin(), id.end());
}
}
}
ScreenFlip();
}
return std::vector<int>();
}
void WinMain2()
{
a5ua::RawInput KEY(GetMainWindowHandle());
// キーボードの登録
const std::vector<int> KeyboardID = GetKeyboardID(KEY, 2);
if (KeyboardID.empty()) {
throw std::exception("キーボードの登録に失敗"); // 登録に失敗
}
// キーボードの数だけユニットを作る
std::vector<Unit> units;
for (int i = 0, N = KeyboardID.size(); i < N; ++i) {
units.push_back(Unit(i, 0, 0, KEY[KeyboardID[i]]));
}
while (ProcessMessage() == 0) {
// 皆、同時にESC押したら終了
bool escape = true;
for (int i = 0, N = KeyboardID.size(); i < N; ++i) {
if (!KEY[KeyboardID[i]][VK_ESCAPE]) {
escape = false;
break;
}
}
if (escape) {
break;
}
ClearDrawScreen();
std::for_each(units.begin(), units.end(), std::mem_fun_ref(&Unit::update));
std::for_each(units.begin(), units.end(), std::mem_fun_ref(&Unit::draw));
ScreenFlip();
}
}
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
try {
WinMain2();
} catch (const std::exception &e) {
MessageBox(GetMainWindowHandle(), e.what(), "エラー", MB_OK | MB_ICONERROR);
} catch (DWORD e) {
return e;
}
return 0;
}
[/tabs]
まず、ヘッダファイル(RawInput.h)ですが、機能としては、コメントに書いてある通りです。
実装の詳細については、この後、説明します。
また、std::bitset<256> というクラスを使っていますが、ビット列(ここでは256ビット)を簡単に操作するためのクラスです。
次に、実装ファイル(RawInput.cpp)の説明です。
コンストラクタでウインドウを作成していますが、これは、入力を受け付けるためのダミーのウインドウです。
子ウインドウとして作成しますが、表示はしません。
また、ウインドウプロシージャの処理をメンバ関数に記述するテクニック[anchor=a1 goto=ref5][5][/anchor]を使用しています。
RawInputの入力は、WM_INPUTメッセージを捕捉して取得します。
WM_INPUTメッセージを発生させるためには、はじめにデバイスを登録しなければなりません。
登録処理は、RawInput::register_device()にまとまっています。
ここでは、接続されているデバイスを列挙し、キーボードのデバイスハンドルを記憶しています。
登録時に、ウインドウ(上で作成したダミーのウインドウです)が最前面になくても、メッセージを受け取れるように以下のフラグを設定します。[anchor=a2 goto=ref4][4][/anchor]
コード:
d.dwFlags = RIDEV_INPUTSINK;
その他、[anchor=a3 goto=ref1][1][/anchor][anchor=a3 goto=ref2][2][/anchor]などが参考になります。
入力状態の更新処理は、RawInput::update(HRAWINPUT hRawInput)という関数に記述しています。
取得したキーボードのデータとして、仮想キーコード(VK_***)やフラグが格納されています。
コード:
m_key_state_table[i][key.VKey] = !(key.Flags & RI_KEY_BREAK);
この文の意味は、i番目のキーボードのVKeyが押されていたら"1"が、押されていなかったら"0"という意味になります。(参考:[anchor=a4 goto=ref3][3][/anchor])
最後に、RawInputクラスを使ったサンプルを紹介します。
GetKeyboardID関数では、要求した分だけのキーボードからの入力があるまで、入力を待ち続け、キーボードIDの配列を返します。
Unitクラスのオブジェクトは、指定された入力状態に従って上下左右に動きます。
[]演算子がオーバーロードされているので、RawInputのオブジェクトinputに対して、input[x]とすることで、x番目のキーボードの入力状態を得ることができます。
さらに、input[x]はbitsetなので、input[x][VK_LEFT]などとすると、x番目のキーボードの左カーソルキーが押されているかどうかを調べることができます。
ちなみに、input[x].any()とすると、x番目のキーボードのどれかのキーが押されているかを知ることができます。(GetKeyboardID関数で利用しています。)
長くなりましたが、暇な人がいたら、動作報告などしてくれたらうれしいです。
【製作環境】
・Windows XP
・Visual Studio 2008 Express Edition
【参考】
[anchor=ref1][/anchor]
[1] 直接入力について (ウィンドウズの操作まわり) - とやとは なにやつ
http://members.jcom.home.ne.jp/toya.hir ... input.html
[anchor=ref2][/anchor]
[2] RawInputで複数のマウスからの入力を取得する - ゲームプログラマ見習いの日記
http://d.hatena.ne.jp/p-tal/20090902/1251852285
[anchor=ref3][/anchor]
[3] Raw Input - Toymaker
http://www.toymaker.info/Games/html/raw_input.html
[anchor=ref4][/anchor]
[4] RAWINPUTDEVICE Structure (Windows) - MSDN Library
http://msdn.microsoft.com/en-us/library ... 85%29.aspx
[anchor=ref5][/anchor]
[5] ウィンドウのクラス化 - Programmer's Studio
http://members3.jcom.home.ne.jp/progstu ... tips2.html