Mesoscopic Programming

タコさんプログラミング専門

ヌメロン製作講座第8回:キーボード入力処理

今回はキーボード入力処理の一部を実装します。
ウィンドウズアプリケーションでは特にユーザインタフェース処理が複雑になりがちです*1。本アプリケーションも例外ではなく、キーボード入力処理がもっとも複雑な処理になります。そこで今回は、とりあえずカーソルキーによるカーソル移動処理のみを実装します。
実装すると以下のような画面になります。

ボタンセル選択時の画面

f:id:hidakas1961:20120919153229j:plain

その他の編集セル選択時の画面

f:id:hidakas1961:20120919153234j:plain

修正ソースファイル
  1. main.h
  2. main.cpp

今回は既存クラスおよび構造体に対する追加修正のみで、新たに追加されたクラスや構造体はありません。

class Application

アプリケーションクラスに追加されたのはキーダウンメッセージ処理だけです。

class Application : public Window
{
    ~省略~
    VOID    OnKeyDown();
    ~省略~
};
VOID Application :: OnKeyDown()
VOID Application :: OnKeyDown()
{
    UINT vkey = LOWORD( wparam );

    setGrid.OnKeyDown( vkey );

    lresult = 0;
}

メインウィンドウであるアプリケーションクラスがキーダウンメッセージを受け取ったら、仮想キーコードをグリッドクラスに渡すだけです。

class Grid

グリッドクラスに追加されたメンバ変数とメソッドは以下の通りです。

class Grid
{
    ~省略~
    SHORT   select;
    SHORT   selectLR;
    BOOL    cursor;
    ~省略~
    virtual BOOL    CanSelect( int n );
    virtual VOID    Cursor( BOOL sw );
    virtual VOID    Select( int n );
    virtual VOID    SelectFirst();
    virtual VOID    SelectLast();
    virtual VOID    SelectNext();
    virtual VOID    SelectPrev();
    virtual VOID    OnKeyDown( UINT vkey );
    ~省略~
};
  1. select
    • 選択されたカーソル位置を表すゼロから始まるセル番号です。
  2. selectLR
    • 左右のカーソル位置を表すゼロから始まるセル番号です。これはカーソル上下キーでカーソル移動したときに、左右のカーソル位置をもっとも自然に決定するためのギミックとして必要となります。
  3. cursor
    • カーソル有効/無効フラグです。
VOID Grid :: Init()
VOID Grid :: Init( HWND hWnd )
{
    ~省略~
    SelectFirst();
    Cursor( TRUE );
}

初期化関数でカーソル初期化処理を追加しました。選択可能な最初のセルを選択してカーソルを有効にします。

BOOL Grid :: CanSelect()
BOOL Grid :: CanSelect( int n )
{
    return CanSelectCell( GetCells()[ n ] );
}

選択可能なセルをゼロから始まるセル番号で指定して判定します。

VOID Grid :: Cursor()
VOID Grid :: Cursor( BOOL sw )
{
    Cell    * cells = GetCells();
    RECT    rc;

    cursor = sw;

    if ( cursor )
    {
        cells[ select ].state |= STATE_SELECT;
    }
    else
    {
        cells[ select ].state &= ~ STATE_SELECT;
    }

    cells[ select ].GetRect( rc, offset );

    InvalidateRect( hwnd, & rc, FALSE );
}

カーソルのオン/オフを切り替えます。セルの選択フラグビットを操作して InvalidateRect() で再描画させます。

VOID Grid :: Select()
VOID Grid :: Select( int n )
{
    BOOL cursor0 = cursor;

    Cursor( FALSE );

    select = n;

    Cursor( cursor0 );
}

カーソル選択位置をゼロから始まるセル番号で指定します。直前の選択位置の選択フラグを解除して新しいセルの選択フラグを有効化*2しています。

VOID Grid :: SelectFirst()
VOID Grid :: SelectFirst()
{
    for( int n = 0; n < maxCell; n++ )
    {
        if ( CanSelect( n ) )
        {
            Select( n );

            selectLR = select;

            break;
        }
    }
}

セル定義データの先頭から選択可能なセルを検索して最初に見つけたセルを選択します。

VOID Grid :: SelectLast()
VOID Grid :: SelectLast()
{
    for( int n = maxCell - 1; n >= 0; n-- )
    {
        if ( CanSelect( n ) )
        {
            Select( n );

            selectLR = select;

            break;
        }
    }
}

セル定義データの最後尾から選択可能なセルを逆順に検索して最初に見つけたセルを選択します。

VOID Grid :: SelectNext()
VOID Grid :: SelectNext()
{
    for( int n = select + 1; n < maxCell; n++ )
    {
        if ( CanSelect( n ) )
        {
            Select( n );

            selectLR = select;

            break;
        }
    }
}

現在のカーソル位置の次の位置から選択可能なセルを検索して最初に見つけたセルを選択します。

VOID Grid :: SelectPrev()
VOID Grid :: SelectPrev()
{
    for( int n = select - 1; n >= 0; n-- )
    {
        if ( CanSelect( n ) )
        {
            Select( n );

            selectLR = select;

            break;
        }
    }
}

現在のカーソル位置の1つ前から選択可能なセルを逆順で検索して最初に見つけたセルを選択します。

VOID Grid :: OnKeyDown()
VOID Grid :: OnKeyDown( UINT vkey )
{
    Cell    * cells     = GetCells();
    Cell    & cell0     = cells[ select ];
    Cell    & cellLR    = cells[ selectLR ];
    int     n;
    int     n2;
    int     left0;
    int     right0;
    int     left;
    int     right;
    int     left2;
    int     right2;

    switch ( vkey )
    {
    case VK_HOME :

        SelectFirst();

        return;

    case VK_END :

        SelectLast();

        return;

    case VK_TAB :

        if ( ( GetKeyState( VK_SHIFT ) & 0x8000 ) == 0 )
        {
            SelectNext();
        }
        else
        {
            SelectPrev();
        }

        return;

    case VK_LEFT :
    case VK_RIGHT :

        n = select;

        for ( int i = 0; i < maxCell - 1; i++ )
        {
            if ( vkey == VK_RIGHT )
            {
                n = ( n + 1 ) % maxCell;
            }
            else if ( vkey == VK_LEFT )
            {
                n = ( n + maxCell - 1 ) % maxCell;
            }

            Cell & cell = cells[ n ];

            if ( CanSelectCell( cell ) )
            {
                if ( cell.top == cell0.top )
                {
                    Select( n );

                    selectLR = n;

                    return;
                }
            }
        }

        break;

    case VK_UP :
    case VK_DOWN :

        left0   = cellLR.left;
        right0  = cellLR.left + cellLR.width;
        n       = select;
        n2      = n;

        for ( int i = 0; i < maxCell - 1; i++ )
        {
            if ( vkey == VK_DOWN )
            {
                n = ( n + 1 ) % maxCell;
            }
            else if ( vkey == VK_UP )
            {
                n = ( n + maxCell - 1 ) % maxCell;
            }

            Cell    & cell  = cells[ n ];
            Cell    & cell2 = cells[ n2 ];

            if ( CanSelectCell( cell ) )
            {
                if ( cell2.top == cell0.top )
                {
                    n2 = n;
                }
                else if ( cell.top != cell0.top )
                {
                    left    = cell.left;
                    right   = cell.left  + cell.width;
                    left2   = cell2.left;
                    right2  = cell2.left + cell2.width;

                    if ( cell.top != cell2.top )
                    {
                        Select( n2 );

                        return;
                    }
                    else if ( left >= left0 && right <= right0 )
                    {
                        if ( abs( left - left0 ) < abs( left2 - left0 ) )
                        {
                            n2 = n;
                        }
                    }
                    else if ( left < left0 && right > right0 )
                    {
                        n2 = n;
                    }
                    else if ( min( abs( left - left0 ), abs( right - right0 ) ) < min( abs( left2 - left0 ), abs( right2 - right0 ) ) )
                    {
                        n2 = n;
                    }
                }
            }
        }

        break;
    }
}

カーソル上下左右キー、ホームキー、エンドキー、タブキーにそれぞれ対応します。
カーソルキー以外の処理は単純ですが、カーソルキー処理はちょっと複雑です。グリッドは任意のセル配置に対応*3しているので、左右キーで移動した場合と上下キーで移動した場合にもっとも自然に思えるセルを選択しなければなりません。設定画面をご覧になればお分かりのように、選択可能セルの配置は左右方向はきれいに並んでいますが上下方向はまちまちです。従って左右の選択はまあ普通に、上下の選択に関してはユーザが意図した左右位置を選択するという設計思想で実装した結果がこれです。具体的にはユーザが最後に左右キーで選択した位置を左右位置として selectLR に保存し、そこから上下キーで縦に1周したときにちゃんと元の左右位置に戻るということです。何しろ試行錯誤による実装なので言葉で説明するのは難しいです。

struct Cell

セル構造体に追加されたメンバ変数とメソッドは以下の2つです。
あとセルの状態フラグも追加されました。

struct Cell
{
    ~省略~
    WORD    state;
    ~省略~
    BOOL    DrawCursor( HDC hdc, RECT rc );
    ~省略~
};

enum StateMask
{
    STATE_NORMAL    = 0x0000,
    STATE_SELECT    = 0x0001,
};
VOID Cell :: Draw()
VOID Cell :: Draw( HDC hdc, POINT offset )
{
        ~省略~
        DrawCursor( hdc, rc );
    }
}

セル描画関数にカーソル描画処理を追加しました。

BOOL Cell :: DrawFrame()
BOOL Cell :: DrawFrame( HDC hdc, RECT rc )
{
    if ( cid >= CELL_LABEL )
    {
        if ( cid == CELL_BUTTON )
        {
            if ( ( state & STATE_SELECT ) != 0 )
            {
                FrameRect( hdc, & rc, app.hbrBlack );

                rc.left++;
                rc.top++;
                rc.right--;
                rc.bottom--;
            }
            ~省略~
}

フレーム描画関数にボタンの選択時の描画処理を追加しました。

BOOL Cell :: DrawCursor()
BOOL Cell :: DrawCursor( HDC hdc, RECT rc )
{
    if ( cid >= CELL_LABEL )
    {
        if ( ( state & STATE_SELECT ) != 0 )
        {
            rc.left     +=3;
            rc.top      +=3;
            rc.right    -=4;
            rc.bottom   -=4;

            if ( cid == CELL_BUTTON )
            {
                rc.left++;
                rc.top++;
                rc.right--;
                rc.bottom--;
            }

            SelectObject( hdc, app.hpenDot );

            MoveToEx( hdc, rc.left, rc.top, NULL );
            LineTo( hdc, rc.right, rc.top );
            LineTo( hdc, rc.right, rc.bottom );
            LineTo( hdc, rc.left, rc.bottom );
            LineTo( hdc, rc.left, rc.top );

            if ( cid != CELL_BUTTON )
            {
                rc.left++;
                rc.top++;

                InvertRect( hdc, & rc );
            }
        }

        return TRUE;
    }

    return FALSE;
}

セルが選択状態のときにだけカーソルを描画します。ボタンとボタン以外で処理が異なります。

以上です。
次回は設定データ編集機能の実装です。

*1: Windows あるある早く言いたい~♪

*2: ただし直前のカーソルフラグに従います

*3: 任意と言ってもセル定義データは一応左上から定義する必要がありますけど