ヌメロン製作講座第8回:キーボード入力処理
今回はキーボード入力処理の一部を実装します。
ウィンドウズアプリケーションでは特にユーザインタフェース処理が複雑になりがちです*1。本アプリケーションも例外ではなく、キーボード入力処理がもっとも複雑な処理になります。そこで今回は、とりあえずカーソルキーによるカーソル移動処理のみを実装します。
実装すると以下のような画面になります。
ボタンセル選択時の画面
その他の編集セル選択時の画面
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 ); ~省略~ };
- select
- 選択されたカーソル位置を表すゼロから始まるセル番号です。
- selectLR
- 左右のカーソル位置を表すゼロから始まるセル番号です。これはカーソル上下キーでカーソル移動したときに、左右のカーソル位置をもっとも自然に決定するためのギミックとして必要となります。
- 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; }
セルが選択状態のときにだけカーソルを描画します。ボタンとボタン以外で処理が異なります。
以上です。
次回は設定データ編集機能の実装です。