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