キーコンフィグの実装

概要:ダイアログ&ウィンドウによるキーコンフィグの実装

キーコンフィグとは操作するキーの割り当てを変更する機能の事です。

キーコンフィグの実装方法は色々考えられます。
一番簡単なのはテキストファイルを使う方法です。
でも一般ユーザーは戸惑うこと間違いなしですし、ユーザーの操作ミスが怖いです。

もうちょっと現実的でありそうなのは(コンボボックス等の)一覧から選択させる方法です。
ただし一覧にないキーには割り当てられないため、
割り当てを許可するキーは全て登録しておかなければなりません。
制限がありますし、打ち込みが面倒ですし、プログラムは無様です。

最良の解はウィンドウからキー入力を取得することです。
インターフェースの構築が大変ですが、
本格的ゲームに実装するならこれしかないでしょう。

ここで解説するのはちょっと妥協したダイアログ&ウィンドウのコンビネーションによる方法です。
インターフェースはダイアログに任せて、キー入力の取得をウィンドウが担当します。

■ダイアログはキー入力を取得できない

ダイアログはキー入力を取得できません(メッセージが届かない)。
必ずどれかのコントロール(子ウィンドウ)にフォーカスが設定される為です。
メッセージはフォーカスのあるウィンドウのプロシージャに送られます。
またコントロールを全て除外しても特定のキーは取得できません。
ダイアログ自体のプロシージャも特殊だからです。

■今回作るもの(実行ファイル)

メインウィンドウからキーコンフィグダイアログを呼び出します(左)。
キーコンフィグダイアログの各ボタンを押すと入力キー取得ウィンドウが出現します(右)。

変更可能項目は上下左右への移動とキーコンフィグダイアログを呼び出すキーです。
メインウィンドウでは上下左右キーを使って円を移動させる簡単なテストを行います。

ソースファイルへのリンクは最後にあります。



■キーに対応する文字列を得る

キー入力を普通のウィンドウで取得する方針が固まったら
ポイントになるのはユーザーが入力したキーに対応する文字列を得る方法です。
現在どのキーが割り当てられているのか?
さっき押したキーは正しく認識されたのか?
コンピューターには数字があるから不要ですが、人間には文字列が必要です。

キーコードに対応する文字列一覧を用意しておく……というまたまた非現実的な解が浮かぶところですが、
ちゃんと対応する文字列を得る関数が用意されています。

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);
}

★☆ ダウンロード ☆★


戻る / ホーム