キーコンフィグとは操作するキーの割り当てを変更する機能の事です。
キーコンフィグの実装方法は色々考えられます。
一番簡単なのはテキストファイルを使う方法です。
でも一般ユーザーは戸惑うこと間違いなしですし、ユーザーの操作ミスが怖いです。
もうちょっと現実的でありそうなのは(コンボボックス等の)一覧から選択させる方法です。
ただし一覧にないキーには割り当てられないため、
割り当てを許可するキーは全て登録しておかなければなりません。
制限がありますし、打ち込みが面倒ですし、プログラムは無様です。
最良の解はウィンドウからキー入力を取得することです。
インターフェースの構築が大変ですが、
本格的ゲームに実装するならこれしかないでしょう。
ここで解説するのはちょっと妥協したダイアログ&ウィンドウのコンビネーションによる方法です。
インターフェースはダイアログに任せて、キー入力の取得をウィンドウが担当します。
■ダイアログはキー入力を取得できない
ダイアログはキー入力を取得できません(メッセージが届かない)。
必ずどれかのコントロール(子ウィンドウ)にフォーカスが設定される為です。
メッセージはフォーカスのあるウィンドウのプロシージャに送られます。
またコントロールを全て除外しても特定のキーは取得できません。
ダイアログ自体のプロシージャも特殊だからです。
■今回作るもの(実行ファイル)
メインウィンドウからキーコンフィグダイアログを呼び出します(左)。
キーコンフィグダイアログの各ボタンを押すと入力キー取得ウィンドウが出現します(右)。
変更可能項目は上下左右への移動とキーコンフィグダイアログを呼び出すキーです。
メインウィンドウでは上下左右キーを使って円を移動させる簡単なテストを行います。
ソースファイルへのリンクは最後にあります。


■キーに対応する文字列を得る
キー入力を普通のウィンドウで取得する方針が固まったら
ポイントになるのはユーザーが入力したキーに対応する文字列を得る方法です。
現在どのキーが割り当てられているのか?
さっき押したキーは正しく認識されたのか?
コンピューターには数字があるから不要ですが、人間には文字列が必要です。
キーコードに対応する文字列一覧を用意しておく……というまたまた非現実的な解が浮かぶところですが、
ちゃんと対応する文字列を得る関数が用意されています。
GetKeyNameText
キーの名前を表す文字列を取得します。
int GetKeyNameText(
LONG lParam, // キーボードメッセージの第 2 パラメータ
LPTSTR lpString, // キー名を保持するバッファへのポインタ
int nSize // キー名を表す文字列の最大サイズ
);
GetKeyNameText 関数は WM_KEYDOWN などの LPARAM から対応する文字列を得る関数です。
こんな都合のいい関数も探せばあるもんですなぁ……。
ただ残念なのはキーコードからではなく LPARAM から文字列を生成するという点。
従ってプログラムからキーコードを指定して文字列を得ることはできません。
あくまでもユーザーの入力を待つ必要があります。
■フォーカスの制御
入力キー取得ウィンドウはキーコンフィグダイアログをオーナーとします。
入力キー取得ウィンドウ表示中にキーコンフィグダイアログを操作されては非常に都合が悪いので
キーコンフィグダイアログにフォーカスが移らないようにする必要があります。
モーダルダイアログなら簡単なことですが、ウィンドウでやろうと思うと大変です。
ちゃんとした解法はわかりませんでしたが、
ちょっと反則な方法としてキーコンフィグダイアログを
入力キー取得ウィンドウで覆い隠してしまうという方法を思いつきました。
表示直後は入力キー取得ウィンドウにフォーカスが設定されるので
選択できないキーコンフィグダイアログには決してフォーカスが移らないという寸法です。
例外としてキーコンフィグダイアログをタスクバーに表示させるように設定すると
タスクバーを選択することでフォーカスが移ってしまいます。
この設定は無効にしておいて下さい(初期状態では無効)。
■WinMain 関数
メインウィンドウの他に入力キー取得ウィンドウのクラスも登録しておきます。
int WINAPI WinMain(
HINSTANCE hInstance,HINSTANCE hPrevInstance,PSTR lpCmdLine,int nCmdShow)
{
WNDCLASS wc;
MSG msg;
hInst=hInstance;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL,IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL,IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = __FILE__;
if(!RegisterClass(&wc)) return 0;
/// 入力キー取得ウィンドウ
wc.lpfnWndProc = InputWindowProc;
wc.lpszClassName = "Input";
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
if(!RegisterClass(&wc)) return 0;
HWND hWnd=CreateWindow(
__FILE__,"キーコンフィグ",
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT,CW_USEDEFAULT,
CW_USEDEFAULT,CW_USEDEFAULT,
NULL,NULL,hInstance,NULL);
if(hWnd==NULL) return 0;
BOOL bRet;
while((bRet=GetMessage(&msg,NULL,0,0))!=0){
if(bRet==-1) break;
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
■キーコンフィグダイアログ
KEY DIALOGEX 0, 0, 186, 132
STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION |
WS_SYSMENU
CAPTION "キーコンフィグ"
FONT 8, "MS Shell Dlg", 400, 0, 0x1
BEGIN
DEFPUSHBUTTON "OK",IDOK,36,108,50,14
PUSHBUTTON "キャンセル",IDCANCEL,102,108,50,14
PUSHBUTTON "上移動",IDC_BMU,12,12,162,14
PUSHBUTTON "下移動",IDC_BMD,12,30,162,14
PUSHBUTTON "左移動",IDC_BML,12,48,162,14
PUSHBUTTON "右移動",IDC_BMR,12,66,162,14
PUSHBUTTON "キーコンフィグ",IDC_BKC,12,84,162,14
END
■メインウィンドウのプロシージャ
WM_KEYDOWN の処理に注目して下さい。
普通なら switch で振り分けますが、
if でどのキーが押されたのか判定しています。
これは switch の case には定数しか指定できない為です。
しかし if の方が柔軟な処理が可能です。
キーコードは Key 配列に格納していきます。
KEY 列挙定数は配列の各要素の添字の役割を果たします。
#include<windows.h>
#include"resource.h"
typedef enum{MU=0,MD,ML,MR,KC,KEYNUM}KEY; /// キー操作定数
WPARAM Key[KEYNUM]={'E','X','A','F',VK_SPACE}; /// 操作キー
HINSTANCE hInst; /// インスタンスハンドル
/// 各種プロシージャ
LRESULT CALLBACK KeyDlgProc(HWND hDlg,UINT uMsg,WPARAM wParam,LPARAM lParam);
LRESULT CALLBACK InputWindowProc(HWND hDlg,UINT uMsg,WPARAM wParam,LPARAM lParam);
LRESULT CALLBACK WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
static POINT pt={100,100}; //円の中心座標
const int R=50; //円の半径
switch(uMsg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
hdc=BeginPaint(hWnd,&ps);
Ellipse(hdc,pt.x-R,pt.y-R,pt.x+R,pt.y+R);
EndPaint(hWnd,&ps);
return 0;
case WM_KEYDOWN:
if(wParam==Key[MU]) pt.y-=5;
else if(wParam==Key[MD]) pt.y+=5;
else if(wParam==Key[ML]) pt.x-=5;
else if(wParam==Key[MR]) pt.x+=5;
else if(wParam==Key[KC]) DialogBox(hInst,"KEY",hWnd,(DLGPROC)KeyDlgProc);
else return 0; //関係ない入力
InvalidateRect(hWnd,NULL,TRUE);
return 0;
}
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}
■キーコンフィグダイアログのプロシージャ
変数から見ていきましょう。
keyText 配列はキーに対応する文字列を格納していく配列です。
初期値を与えているのはキーコードから文字列を得ることができないからです。
この初期値は操作キーの初期値から私が判断して与えたものです。
あと重要なのは button 変数です。
KEY 列挙定数型であることにも注目して下さい。
button 変数にはどのコントロールボタンが押されたかを記録しておきます。
入力キー取得ウィンドウから返ってきた時に
どのボタンに対する操作だったのか覚えておかなければキー割り当ての変更などができません。
入力キー取得ウィンドウはそれぞれのボタンを押すことで呼び出されます。
キーコンフィグダイアログの大きさを取得して
同じ大きさの入力キー取得ウィンドウを作ります。
これですっぽりと覆い被さるウィンドウのできあがりです。
入力キー取得ウィンドウの消滅は WM_INPUT メッセージが知らせます。
WM_INPUT メッセージの wParam , lParam には入力キー取得ウィンドウに送られた
WM_KEYDOWN メッセージの wParam , lParam が入っています。
入力キー取得ウィンドウに表示する文字列も必要です。
今どの操作のキー割り当てを変更しようとしているのか?
現在割り当てられているキーは何か?
表示する必要があるでしょう。
入力キー取得ウィンドウに情報を渡すには CreateWindow 関数の最後の引数を使います。
/// 入力キー取得ウィンドウを閉じる時にキーコンフィグダイアログに送るメッセージ
/// ( wParam , lParam には WM_KEYDOWN のそれが入る)
#define WM_INPUT WM_APP
/// キーコンフィグダイアログボックスのプロシージャ
LRESULT CALLBACK KeyDlgProc(HWND hDlg,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
static KEY button;
static WPARAM wp[KEYNUM];
static char tempKeyText[KEYNUM][32];
static char keyText[KEYNUM][32]={"E","X","A","F","Space"};
const char ope[KEYNUM][32]={"上移動","下移動","左移動","右移動","キーコンフィグ"};
const DWORD IDC[KEYNUM]={IDC_BMU,IDC_BMD,IDC_BML,IDC_BMR,IDC_BKC};
char str[64];
RECT rc;
int i,k;
switch(uMsg){
case WM_INITDIALOG:
for(i=0;i<KEYNUM;i++){
wp[i]=Key[i]; strcpy(tempKeyText[i],keyText[i]);
wsprintf(str,"%s : %s",ope[i],tempKeyText[i]);
SetWindowText(GetDlgItem(hDlg,IDC[i]),str);
}
return TRUE;
case WM_COMMAND:
switch(LOWORD(wParam)){
case IDOK:
for(i=0;i<KEYNUM-1;i++){ /// 一つのキーが複数の操作に
for(k=i+1;k<KEYNUM;k++){ /// 割り当てられていないか?
if(wp[i]==wp[k]){
MessageBox(hDlg,"同じキーが複数の操作に割り当てられています",NULL,MB_OK);
return TRUE;
}
}
}
/// 異常なし
for(i=0;i<KEYNUM;i++){Key[i]=wp[i]; strcpy(keyText[i],tempKeyText[i]);}
EndDialog(hDlg,IDOK); return TRUE;
case IDCANCEL: EndDialog(hDlg,IDCANCEL); return TRUE;
case IDC_BMU: button=MU; break;
case IDC_BMD: button=MD; break;
case IDC_BML: button=ML; break;
case IDC_BMR: button=MR; break;
case IDC_BKC: button=KC; break;
default: return FALSE;
}
wsprintf(str,"%s\n\n現在のキー:%s",ope[button],tempKeyText[button]);
GetWindowRect(hDlg,&rc);
CreateWindow("Input",NULL,WS_POPUP | WS_VISIBLE,
rc.left,rc.top,rc.right-rc.left,rc.bottom-rc.top,
hDlg,NULL,hInst,str);
return TRUE;
case WM_INPUT:
wp[button]=wParam;
GetKeyNameText((LONG)lParam,tempKeyText[button],sizeof(tempKeyText[button]));
wsprintf(str,"%s : %s",ope[button],tempKeyText[button]);
SetWindowText(GetDlgItem(hDlg,IDC[button]),str);
return TRUE;
}
return FALSE;
}
■入力キー取得ウィンドウのプロシージャ
キーが押されたらキーコンフィグダイアログに WM_INPUT を送って直ぐに終了します。
見た目が寂しかったので文字列の明滅アニメーションを実装しました。
ここで一つ困った問題があります。
タスクバーから他のウィンドウを選択されると入力キー取得ウィンドウがフォーカスを失うことです。
再びフォーカスを得るにはマウスでクリックするだけですが、
のっぺらぼうなウィンドウですからフォーカスを失ったことに気付かない可能性も十分考えられます。
そこで再びフォアグラウンドウィンドウになった時にフォーカスを得る処理を追加しています。
これはこれで問題があるのですが、謎のウィンドウで操作不能の方が避けたい事態だと判断しました。
/// 入力キー取得ウィンドウのプロシージャ
LRESULT CALLBACK InputWindowProc(HWND hWnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
static RECT rc;
static BOOL paint;
char str[128];
static char params[64];
const char *explain="新たに割り当てたい\nキーを押して下さい";
switch(uMsg){
case WM_CREATE:
strcpy(params,(char*)(((LPCREATESTRUCT)lParam)->lpCreateParams));
GetClientRect(hWnd,&rc); rc.top=50;
paint=TRUE; SetTimer(hWnd,1,500,NULL);
return 0;
case WM_TIMER:
paint=!paint;
InvalidateRect(hWnd,NULL,TRUE);
return 0;
case WM_PAINT:
hdc=BeginPaint(hWnd,&ps);
SetTextColor(hdc,RGB(0,255,0)); SetBkColor(hdc,RGB(0,0,0));
if(paint) wsprintf(str,"%s\n\n%s",params,explain);
else strcpy(str,params);
DrawText(hdc,str,-1,&rc,DT_CENTER);
EndPaint(hWnd,&ps); /// ↓タスクバーの項目をクリックする事で
return 0; /// ↓再び最前面になった場合はフォーカスを失っている
case WM_WINDOWPOSCHANGED: SetForegroundWindow(hWnd); return 0;
case WM_KEYDOWN:
PostMessage(GetWindow(hWnd,GW_OWNER),WM_INPUT,wParam,lParam);
KillTimer(hWnd,1); DestroyWindow(hWnd);
return 0;
}
return DefWindowProc(hWnd,uMsg,wParam,lParam);
}
★☆ ダウンロード ☆★