ヌメロン製作講座第9回:編集処理機能の作成
春はあけぼの、秋はのぼけあ。皆様いかがおすごしでしょうか?
それでは本日のメニューをご紹介いたします。
class Application
アプリケーションクラスには以下に示す2つのメソッド*1が追加されました。
class Application : public Window { ~中略~ BOOL HookMessage( LPMSG pmsg ); ~中略~ VOID OnNotify(); ~中略~ };
HookMessage() 関数はウィンドウズメッセージをメインウィンドウに渡す前にフックするやつです。
なぜこんなものが必要かと言うと、エディットコントロールなどの昔ながらのコントロール系が必要なキー入力通知をよこしてくれないからです。新しいコモンコントロール系はいろいろ返してくれるんですけど、めんどくさいから両方一気にここでフックしちゃいます。
必要なキーとはリターンキーとエスケープキーです。たとえエディットコントロールがフォーカスを持っていて編集作業中でも、これらのキーは本来親が処理したいじゃないですか。フックして当然です。
VOID Application :: Run()
以下のようにメッセージをディスパッチする前に呼んであげます。フック処理が必要無かった時は FALSE なので何事も起こりません。
VOID Application :: Run() { MSG msg; while ( GetMessage( & msg, NULL, 0, 0 ) ) { if ( TranslateAccelerator( hwnd, hAccel, & msg ) == 0 ) { if ( ! HookMessage( & msg ) ) *2 { TranslateMessage( & msg ); DispatchMessage( & msg ); } } } ~中略~ }
BOOL Application :: HookMessage()
フック処理の実装は以下の通り。
BOOL Application :: HookMessage( LPMSG pmsg ) { NMKEY nmk; if ( IsChild( hwnd, pmsg->hwnd ) ) { if ( pmsg->message == WM_KEYDOWN ) { nmk.hdr.hwndFrom = pmsg->hwnd; nmk.hdr.idFrom = GetDlgCtrlID( pmsg->hwnd ); nmk.hdr.code = NM_KEYDOWN; nmk.nVKey = ( UINT ) pmsg->wParam; nmk.uFlags = ( UINT ) pmsg->lParam; return ( BOOL ) SendMessage( hwnd, WM_NOTIFY, ( WPARAM ) nmk.hdr.idFrom, ( LPARAM ) & nmk ); } } return FALSE; }
子ウィンドウであるコントロールにキーダウンメッセージが届いたら、メインウィンドウに NM_NOTIFY メッセージを送りつけます。
VOID Application :: OnNotify()
OnNotify() 関数で受け取ったメッセージをさらにグリッドクラスに転送します。
VOID Application :: OnNotify() { int id = ( int ) wparam; LPNMHDR pnmh = ( LPNMHDR ) lparam; LPNMKEY pnmk = ( LPNMKEY ) pnmh; if ( pnmh->code == NM_KEYDOWN ) { lresult = setGrid.OnNmKeyDown( id, pnmh->hwndFrom, pnmk->nVKey ); } }
アプリケーションクラスの仕事は以上です。
次にグリッドクラスの処理を見てみましょう。
class Grid
グリッドクラスには以下に示す3つのメンバ変数と4つのメソッドが追加されました。
class Grid { ~中略~ HWND hctrl; HWND hctrl2; EditParam edit; ~中略~ virtual BOOL OnNmKeyDown( int id, HWND hWnd, UINT vkey ); virtual VOID OnEditBegin(); virtual VOID OnEditBreak(); virtual VOID OnEditEnd(); };
変数の内容は以下の通りです。
- hctrl
- コントロールハンドル
- hctrl2
- 第2のコントロールハンドル
- edit
- 編集パラメータ
メソッドの内容は以下の通りです。
- OnNmKeyDown()
- NM_KEYDOWN メッセージ処理
- OnEditBegin()
- 編集開始処理
- OnEditBreak()
- 編集中止処理
- OnEditEnd()
- 編集終了処理
クラスの初期化など微細な追加修正もありますが、その辺の当たり前の処理については直接ソースを見てもらうこととし説明は省きます。
VOID Grid :: OnKeyDown()
編集開始のためにキーダウンメッセージ処理にも追加します。
VOID Grid :: OnKeyDown( UINT vkey ) { ~中略~ switch ( vkey ) { case VK_RETURN : if ( cell0.cid >= CELL_EDIT ) { OnEditBegin(); return; } break; ~中略~ } }
リターンキーが押されたら編集を開始するだけです。今回はまだボタンには対応していないのでセルの種類は CELL_EDIT 以降のやつに限定しています。
BOOL Grid :: OnNmKeyDown()
コントロールが処理する前のキーダウンメッセージを受け取ります。
BOOL Grid :: OnNmKeyDown( int id, HWND hWnd, UINT vkey ) { if ( hWnd == hctrl ) { if ( vkey == VK_ESCAPE ) { OnEditBreak(); return TRUE; } else if ( vkey == VK_RETURN ) { OnEditEnd(); return TRUE; } } return FALSE; }
hWnd が自分の管理しているコントロールなのかチェックして、エスケープキーなら編集中止、リターンキーなら編集終了とします。
何も処理しなかった場合は FALSE を返す必要があります。でないとコントロールがキーダウンメッセージを受け取れなくなっちゃいますから。
VOID Grid :: OnEditBegin()
編集開始処理です。
VOID Grid :: OnEditBegin() { HINSTANCE hinstance = GetModuleHandle( NULL ); Cell & cell = GetCells()[ select ]; ParamID pid = cell.GetParamID(); DWORD dwstyle; DWORD dwstyle2; LPCTSTR pszclass; int id; int id2; RECT rc; RECT rc2; if ( pid > PARAM_NULL && pid < MAX_PARAM ) { OnEditBreak(); ZeroMemory( & edit, sizeof edit ); edit.cid = cell.cid; edit.left = cell.left + offset.x; edit.top = cell.top + offset.y; edit.width = cell.width; edit.height = cell.height; cell.GetText( edit.text ); if ( cell.GetEditParam( edit ) ) { Cursor( FALSE ); if ( edit.cid == CELL_COMBO ) { edit.height = edit.height * ( edit.max + 1 ) + 4; pszclass = WC_COMBOBOX; dwstyle = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | CBS_DROPDOWNLIST; id = ID_COMBO; hctrl = CreateWindow( pszclass, NULL, dwstyle, edit.left, edit.top, edit.width, edit.height, hwnd, ( HMENU )( INT_PTR ) id, hinstance, NULL ); SendMessage( hctrl, WM_SETFONT, ( WPARAM ) GetStockObject( DEFAULT_GUI_FONT ), 0 ); SetFocus( hctrl ); for ( int i = 0; i < edit.max; i++ ) { ComboBox_AddString( hctrl, edit.list[ i ] ); } ComboBox_SetCurSel( hctrl, edit.pos ); ComboBox_ShowDropdown( hctrl, TRUE ); } else { if ( edit.cid == CELL_EDIT ) { pszclass = WC_EDIT; dwstyle = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | ES_AUTOHSCROLL; id = ID_EDIT; hctrl = CreateWindowEx( WS_EX_CLIENTEDGE, pszclass, edit.text, dwstyle, edit.left, edit.top, edit.width, edit.height, hwnd, ( HMENU )( INT_PTR ) id, hinstance, NULL ); SendMessage( hctrl, EM_SETSEL, 0, -1 ); } else if ( edit.cid == CELL_NUMBER ) { pszclass = WC_EDIT; dwstyle = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | ES_NUMBER; id = ID_EDIT; hctrl = CreateWindowEx( WS_EX_CLIENTEDGE, pszclass, edit.text, dwstyle, edit.left, edit.top, edit.width, edit.height, hwnd, ( HMENU )( INT_PTR ) id, hinstance, NULL ); SendMessage( hctrl, EM_SETSEL, 0, -1 ); } else if ( edit.cid == CELL_UPDOWN ) { pszclass = WC_EDIT; dwstyle = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | ES_NUMBER; dwstyle2 = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_SETBUDDYINT | UDS_ARROWKEYS; id = ID_EDIT; id2 = ID_UPDOWN; hctrl = CreateWindowEx( WS_EX_CLIENTEDGE, pszclass, edit.text, dwstyle, edit.left, edit.top, edit.width, edit.height, hwnd, ( HMENU )( INT_PTR ) id, hinstance, NULL ); hctrl2 = CreateUpDownControl( dwstyle2, 0, 0, 0, 0, hwnd, id2, hinstance, NULL, 0, 0, 0 ); SendMessage( hctrl2, UDM_SETRANGE, 0, ( LPARAM ) MAKELONG ( ( SHORT ) edit.max, ( SHORT ) edit.min ) ); SendMessage( hctrl2, UDM_SETPOS, 0, ( LPARAM ) MAKELONG ( ( SHORT ) edit.pos, 0 ) ); SendMessage( hctrl, EM_SETSEL, 0, -1 ); GetWindowRect( hctrl, & rc ); GetWindowRect( hctrl2, & rc2 ); ScreenToClient( hwnd, ( LPPOINT ) & rc.left ); ScreenToClient( hwnd, ( LPPOINT ) & rc.right ); ScreenToClient( hwnd, ( LPPOINT ) & rc2.left ); ScreenToClient( hwnd, ( LPPOINT ) & rc2.right ); rc.right -= rc.left; rc.bottom -= rc.top; rc2.right -= rc2.left; rc2.bottom -= rc2.top; if ( edit.width > 20 ) { rc.right = edit.width - rc2.right; } else { rc.right = edit.width; } rc2.left = rc.left + rc.right; MoveWindow( hctrl, rc.left, rc.top, rc.right, rc.bottom, TRUE ); MoveWindow( hctrl2, rc2.left, rc2.top, rc2.right, rc2.bottom, TRUE ); } else { Cursor( TRUE ); return; } SendMessage( hctrl, WM_SETFONT, ( WPARAM ) GetStockObject( DEFAULT_GUI_FONT ), 0 ); SetFocus( hctrl ); } } } }
行が折り返して見にくくなっちゃいましたが必要ならばソースファイルの方をご覧願います。何をやってるかといえば編集目的に見合ったコントロール子ウィンドウを作成しています。セルオブジェクトに対して GetEditParam() 関数を呼び出すことにより、より詳細な編集パラメータを取得するとともに編集可能かどうか問い合わせています。編集可能な場合はセルの種類によって必要なコントロールウィンドウを作成します。編集コントロールの位置とサイズはデフォルトでは対象となるセルと同じですが、必要ならば GetEditParam() 関数中でセル側が表示位置を変更することもできます。
struct EditParam
編集パラメータ構造体です。
struct EditParam { CellID cid; int left; int top; int width; int height; int min; int max; int pos; TCHAR text[ MAX_STRING ]; LPCTSTR list[ MAX_EDITLIST ]; };
各変数の内容は以下の通りです。
- cid
- セルの編集タイプです。GetEditParam() 中で定義データと異なる値を返すこともできます。
- left、top、width、height
- コントロールの位置とサイズです。
- min
- 数字編集の場合の最小値です。
- max、pos
- 数字編集の場合は最大値と初期値です。コンボボックスの場合はリスト数と初期選択番号です。
- text
- 文字列編集の初期文字列です。
- list
- コンボボックスの項目文字列配列です。
VOID Grid :: OnEditBreak()
編集を中止しコントロールウィンドウを破棄します。
VOID Grid :: OnEditBreak() { Cell & cell = GetCells()[ select ]; if ( hctrl != NULL ) { if ( hctrl2 != NULL ) { DestroyWindow( hctrl2 ); hctrl2 = NULL; } DestroyWindow( hctrl ); hctrl = NULL; Cursor( TRUE ); SetFocus( hwnd ); } }
hctrl2 はアップダウンコントロールハンドルで、常にエディットコントロールとペアなので2つ破棄しています。
コントロールがフォーカスを失うのでメインウィンドウにフォーカスを渡しています。
VOID Grid :: OnEditEnd()
編集を終了します。
VOID Grid :: OnEditEnd() { Cell & cell = GetCells()[ select ]; GetWindowText( hctrl, edit.text, sizeof edit.text ); edit.pos = atoi( edit.text ); if ( edit.cid == CELL_EDIT ) { SendMessage( hctrl, EM_SETSEL, 0, -1 ); } else if ( edit.cid == CELL_NUMBER ) { sprintf( edit.text, "%0*d", numer0n.maxColumn, edit.pos ); SetWindowText( hctrl, edit.text ); SendMessage( hctrl, EM_SETSEL, 0, -1 ); edit.pos = atoi( edit.text ); } else if ( edit.cid == CELL_UPDOWN ) { SendMessage( hctrl, EM_SETSEL, 0, -1 ); edit.pos = atoi( edit.text ); } else if ( edit.cid == CELL_COMBO ) { edit.pos = ComboBox_GetCurSel( hctrl ); } if ( cell.SetEditData( edit ) ) { OnEditBreak(); } }
コントロールウィンドウの編集済み文字列を取得し、必要ならば数値に変換します。結果を編集パラメータにセットしたらセルに対して SetEditData() 関数を呼び出します。セル側は編集内容をチェックして問題なければ TRUE を返します。TRUE ならばコントロールを破棄しますが、FALSE だった場合は破棄せずに現在の状態を維持します。
グリッドクラスの任務は以上です。
最後にセル構造体の変更点について説明しましょう。
struct Cell
セル構造体には以下に示す2つのメソッドが追加されました。
struct Cell { ~中略~ BOOL GetEditParam( EditParam & edit ); BOOL SetEditData( EditParam & edit ); };
BOOL Cell :: GetEditParam()
編集パラメータの取得です。
BOOL Cell :: GetEditParam( EditParam & edit ) { ParamID pid = GetParamID(); MoveID move; DigitID digit; ItemID item; switch ( pid ) { case PARAM_TITLE : return TRUE; case PARAM_COLUMN : edit.min = MIN_COLUMN; edit.max = MAX_COLUMN; edit.pos = numer0n.maxColumn; return TRUE; case PARAM_RULE : edit.max = MAX_RULE; edit.pos = numer0n.rule; CopyMemory( & edit.list, ruleNames, sizeof ruleNames ); return TRUE; case PARAM_NAME_FIRST : case PARAM_NAME_SECOND : move = ( MoveID ) ( pid - PARAM_NAME_FIRST ); sprintf( edit.text, "%s", numer0n.players[ move ].name ); return TRUE; case PARAM_TYPE_FIRST : case PARAM_TYPE_SECOND : move = ( MoveID ) ( pid - PARAM_TYPE_FIRST ); edit.max = MAX_TYPE; edit.pos = numer0n.players[ move ].type; CopyMemory( & edit.list, typeNames, sizeof typeNames ); return TRUE; case PARAM_CARD0_FIRST : case PARAM_CARD1_FIRST : case PARAM_CARD2_FIRST : case PARAM_CARD3_FIRST : case PARAM_CARD4_FIRST : case PARAM_CARD5_FIRST : case PARAM_CARD6_FIRST : case PARAM_CARD7_FIRST : case PARAM_CARD8_FIRST : case PARAM_CARD9_FIRST : case PARAM_CARD0_SECOND : case PARAM_CARD1_SECOND : case PARAM_CARD2_SECOND : case PARAM_CARD3_SECOND : case PARAM_CARD4_SECOND : case PARAM_CARD5_SECOND : case PARAM_CARD6_SECOND : case PARAM_CARD7_SECOND : case PARAM_CARD8_SECOND : case PARAM_CARD9_SECOND : move = ( MoveID ) ( ( pid - PARAM_CARD0_FIRST ) / MAX_DIGIT ); digit = ( DigitID ) ( ( pid - PARAM_CARD0_FIRST ) % MAX_DIGIT ); edit.max = 9; edit.pos = numer0n.players[ move ].cards[ digit ]; return TRUE; case PARAM_HIGHLOW_FIRST : case PARAM_DOUBLE_FIRST : case PARAM_TARGET_FIRST : case PARAM_SLASH_FIRST : case PARAM_SHUFFLE_FIRST : case PARAM_CHANGE_FIRST : case PARAM_HIGHLOW_SECOND : case PARAM_DOUBLE_SECOND : case PARAM_TARGET_SECOND : case PARAM_SLASH_SECOND : case PARAM_SHUFFLE_SECOND : case PARAM_CHANGE_SECOND : move = ( MoveID ) ( ( pid - PARAM_HIGHLOW_FIRST ) / MAX_ITEM ); item = ( ItemID ) ( ( pid - PARAM_HIGHLOW_FIRST ) % MAX_ITEM ); edit.max = 9; edit.pos = numer0n.players[ move ].items[ item ]; return TRUE; case PARAM_OPEN_FIRST : case PARAM_OPEN_SECOND : move = ( MoveID ) ( pid - PARAM_OPEN_FIRST ); sprintf( edit.text, "%0*d", numer0n.maxColumn, numer0n.players[ move ].open ); return TRUE; } return FALSE; }
各編集内容によって最大値、最小値などをセットしています。
コンボボックスの場合は表示に必要な文字列配列を list にコピーしています。
BOOL Cell :: SetEditData()
編集終了処理です。
BOOL Cell :: SetEditData( EditParam & edit ) { ParamID pid = GetParamID(); MoveID move; DigitID digit; ItemID item; switch ( pid ) { case PARAM_TITLE : strcpy( numer0n.title, edit.text ); return TRUE; case PARAM_COLUMN : numer0n.maxColumn = ( Column ) edit.pos; return TRUE; case PARAM_RULE : numer0n.rule = ( RuleID ) edit.pos; return TRUE; case PARAM_NAME_FIRST : case PARAM_NAME_SECOND : move = ( MoveID ) ( pid - PARAM_NAME_FIRST ); strcpy( numer0n.players[ move ].name, edit.text ); return TRUE; case PARAM_TYPE_FIRST : case PARAM_TYPE_SECOND : move = ( MoveID ) ( pid - PARAM_TYPE_FIRST ); numer0n.players[ move ].type = ( TypeID ) edit.pos; return TRUE; case PARAM_CARD0_FIRST : case PARAM_CARD1_FIRST : case PARAM_CARD2_FIRST : case PARAM_CARD3_FIRST : case PARAM_CARD4_FIRST : case PARAM_CARD5_FIRST : case PARAM_CARD6_FIRST : case PARAM_CARD7_FIRST : case PARAM_CARD8_FIRST : case PARAM_CARD9_FIRST : case PARAM_CARD0_SECOND : case PARAM_CARD1_SECOND : case PARAM_CARD2_SECOND : case PARAM_CARD3_SECOND : case PARAM_CARD4_SECOND : case PARAM_CARD5_SECOND : case PARAM_CARD6_SECOND : case PARAM_CARD7_SECOND : case PARAM_CARD8_SECOND : case PARAM_CARD9_SECOND : move = ( MoveID ) ( ( pid - PARAM_CARD0_FIRST ) / MAX_DIGIT ); digit = ( DigitID ) ( ( pid - PARAM_CARD0_FIRST ) % MAX_DIGIT ); numer0n.players[ move ].cards[ digit ] = edit.pos; return TRUE; case PARAM_HIGHLOW_FIRST : case PARAM_DOUBLE_FIRST : case PARAM_TARGET_FIRST : case PARAM_SLASH_FIRST : case PARAM_SHUFFLE_FIRST : case PARAM_CHANGE_FIRST : case PARAM_HIGHLOW_SECOND : case PARAM_DOUBLE_SECOND : case PARAM_TARGET_SECOND : case PARAM_SLASH_SECOND : case PARAM_SHUFFLE_SECOND : case PARAM_CHANGE_SECOND : move = ( MoveID ) ( ( pid - PARAM_HIGHLOW_FIRST ) / MAX_ITEM ); item = ( ItemID ) ( ( pid - PARAM_HIGHLOW_FIRST ) % MAX_ITEM ); numer0n.players[ move ].items[ item ] = edit.pos; return TRUE; case PARAM_OPEN_FIRST : case PARAM_OPEN_SECOND : move = ( MoveID ) ( pid - PARAM_OPEN_FIRST ); numer0n.players[ move ].number = ( Number ) edit.pos; numer0n.players[ move ].open = ( Number ) edit.pos; return TRUE; } return FALSE; }
編集パラメータの内容を各編集セルの種類に応じてヌメロンデータにセットしています。
今回の編集処理の実装は以上となります。お疲れさまでした。*3
次回はボタン対応処理です。