ヌメロン製作講座第6回:グリッドクラスの作成(その2)
今回はグリッドクラスとセル構造体のメソッド*1について説明します。
class Grid
まずは初期化関数から。
VOID Grid :: Init()
VOID Grid :: Init( HWND hWnd ) { hwnd = hWnd; offset.x = 4; offset.y = 8; MakeGrid(); }
これは初期化です。アプリケーションの初期化の際に1度呼び出せば十分です。その際メインウィンドウのハンドルを渡して下さい。グリッドとセルが計算や描画で使用しますもんで。
MakeGrid() はグリッドの各セルの表示位置とサイズ、グリッド全体のサイズなどを計算します。
VOID Grid :: MakeGrid()
VOID Grid :: MakeGrid() { HDC hdc = GetDC( hwnd ); Cell * cells = GetCells(); LONG left = 0; LONG top = 0; LONG width = 0; LONG height = 0; RECT rect0 = rect; TCHAR text[ MAX_STRING ]; LPCTSTR s; LPCTSTR s2; int len; SIZE size; SIZE size2; for ( maxCell = 0; cells[ maxCell ].cid != CELL_NULL; maxCell++ ); rect.left = 0; rect.top = 0; rect.right = 0; rect.bottom = 0; SelectObject( hdc, GetStockObject( DEFAULT_GUI_FONT ) ); for ( int n = 0;; n++ ) { Cell & cell = cells[ n ]; if ( cell.cid == CELL_NULL ) { break; } else if ( cell.cid == CELL_NEWLINE || cell.cid == CELL_NEWLINE2 ) { cell.left = left; cell.width = 0; if ( cell.top == 0 ) { cell.top = top; } if ( cell.height == 0 ) { cell.height = height; } else if ( cell.height > height ) { height = cell.height; } left = 0; top += height; width = 0; height = 0; if ( cell.cid == CELL_NEWLINE ) { top += 4; } } else if ( cell.cid == CELL_SEPARATOR || cell.cid == CELL_SEPARATOR2 ) { cell.left = 0; cell.width = 0; if ( cell.top == 0 ) { cell.top = top; if ( n > 0 ) { cell.top += height + 4; } } if ( cell.height == 0 ) { cell.height = 2; } left = 0; top = cell.top + cell.height; width = 0; height = 0; if ( cell.cid == CELL_SEPARATOR ) { top += 4; } } else { size.cx = 0; size.cy = 0; if ( cell.GetText( text ) ) { for ( s = text; * s != '\0'; s = s2 ) { s2 = strchr( s, '\n' ); if ( s2 != NULL ) { len = ( int ) ( s2 - text ); s2++; } else { len = ( int ) strlen( s ); s2 = s + len; } if ( GetTextExtentPoint32( hdc, s, len, & size2 ) ) { if ( size2.cx > size.cx ) { size.cx = size2.cx; } size.cy += size2.cy; } } size.cy += 7; if ( CanSelectCell( cell ) ) { size.cx += 12; if ( cell.cid == CELL_BUTTON ) { size.cy += 3; } } } if ( cell.left == 0 ) { cell.left = left; } else { left = cell.left; } if ( cell.top < 0 ) { cell.top += top; } else if ( cell.top == 0 ) { cell.top = top; } else { top = cell.top; } if ( cell.width == 0 ) { if ( size.cx == 0 ) { cell.width = width; } else { cell.width = size.cx; } } width = cell.width; if ( cell.height == 0 ) { if ( size.cy == 0 ) { cell.height = height; } else { cell.height = size.cy; } } if ( cell.height > height ) { height = cell.height; } left += width; if ( cell.cid >= CELL_RIGHT ) { left += 2; if ( CanSelectCell( cell ) ) { left += 4; } } } if ( cell.left < rect.left ) { rect.left = cell.left; } if ( cell.top < rect.top ) { rect.top = cell.top; } if ( cell.left + cell.width > rect.right ) { rect.right = cell.left + cell.width; } if ( cell.top + cell.height > rect.bottom ) { rect.bottom = cell.top + cell.height; } } maxCell = n; ReleaseDC( hwnd, hdc ); }
MakeGrid() はグリッド内のセルの表示位置とサイズを計算しますが、もしセル定義データの該当部分が 0 以外の値であれば計算せずにその値を使用します。
セルの種類は以下の通りです。
- CELL_NEWLINE
- 改行
- CELL_NEWLINE2
- CELL_NEWLINE よりも改行幅の小さい改行
- CELL_SEPARATOR
- 改行付きの水平セパレータ描画
- CELL_SEPARATOR2
- CELL_SEPARATOR よりも改行幅の小さいセパレータ
- CELL_LABEL
- 左寄せラベル
- CELL_RIGHT
- 右寄せラベル
- CELL_BUTTON
- ボタン
- CELL_EDIT
- 文字列編集用のエディット
- CELL_NUMBER
- 数字のみ入力可能なエディット
- CELL_UPDOWN
- アップダウンコントロール付きの数字エディット
- CELL_COMBO
- ドロップダウンコンボボックス
セルの幅と高さの計算は cell.GetText() で表示文字列を取得して GetTextExtentPoint32() で表示サイズを取得して計算します。改行にも対応してます。
セルデータの種類として CELL_NULL を発見すると定義データ終了とみなし、それまでカウントしたセルの個数をメンバ変数 maxCell に代入します。
BOOL Grid :: CanSelectCell()
BOOL Grid :: CanSelectCell( Cell & cell ) { if ( cell.cid >= CELL_BUTTON ) { return TRUE; } return FALSE; }
セルの種類によって、そのセルが選択可能かどうか判定します。ボタンとエディット系は選択可能としラベルやセパレータは選択できません。
VOID Grid :: Draw()
VOID Grid :: Draw( HDC hdc ) { Cell * cells = GetCells(); for ( int n = 0; n < maxCell; n++ ) { cells[ n ].Draw( hdc, offset ); } }
グリッドを描画します。MakeGrid() で各セルの表示位置を計算済みなので各セルの描画ルーチンを呼ぶだけです。ただしグリッド全体の表示オフセットも同時に指定します。
今のところグリッドの機能は以上です。まだ入力機能を実装してないので超簡単です。次にセルのメソッドについて説明します。
struct Cell
前述したようにセルは静的データ定義に対応可能とするため、クラスではなく昔ながらの構造体 ( struct ) で定義してありますが、C++ なのでメソッド*2を持たせてあります。だって便利ですから。
まず描画メソッドから説明します。ていうか今のところ描画機能しかないケロ。
VOID Cell :: Draw()
VOID Cell :: Draw( HDC hdc, POINT offset ) { RECT rc; GetRect( rc, offset ); if ( cid == CELL_SEPARATOR || cid == CELL_SEPARATOR2 ) { app.DrawSeparator( top + offset.y ); } else if ( cid >= CELL_LABEL ) { DrawFrame( hdc, rc ); DrawContent( hdc, rc ); } }
ウィンドウのデバイスコンテキストと表示オフセット座標をもらって、GetRect() で表示相対座標とセルサイズを取得してセパレータならセパレータ描画関数を呼び出し、何か書く必要があれば DrawFrame() で枠を描いて DrawContent() で枠の中身を描くということです。シンプルだろぉ~?
DrawSeparator() はアプリケーションクラスのを使いまわしなのでグローバル変数の app. が付いてます。
BOOL Cell :: DrawFrame()
BOOL Cell :: DrawFrame( HDC hdc, RECT rc ) { if ( cid >= CELL_LABEL ) { if ( cid == CELL_BUTTON ) { FrameRect( hdc, & rc, app.hbrBlack ); rc.right--; rc.bottom--; FrameRect( hdc, & rc, app.hbrWhite ); rc.left++; rc.top++; FrameRect( hdc, & rc, app.hbrDarkGray ); rc.right--; rc.bottom--; FillRect( hdc, & rc, app.hbrLightGray ); } else if ( cid >= CELL_EDIT ) { FrameRect( hdc, & rc, app.hbrWhite ); rc.right--; rc.bottom--; FrameRect( hdc, & rc, app.hbrDarkGray ); rc.left++; rc.top++; FrameRect( hdc, & rc, app.hbrLightGray ); rc.right--; rc.bottom--; FrameRect( hdc, & rc, app.hbrDarkGray2 ); rc.left++; rc.top++; FillRect( hdc, & rc, app.hbrWhite ); } else { FillRect( hdc, & rc, app.hbrLightGray ); } return TRUE; } return FALSE; }
枠を描くやつです。まあ見ての通り枠を描いて中を塗りつぶすといった感じです。戻り値を返している理由は、将来的にもし非対応だったセルのとき呼び出し側が何か対応できるようにしてあるだけで今のところ意味はありません。
BOOL Cell :: DrawContent()
BOOL Cell :: DrawContent( HDC hdc, RECT rc ) { COLORREF color = black; TCHAR text[ MAX_STRING ]; UINT format; LPCTSTR s; LPCTSTR s2; int len; SIZE size; SIZE size2; if ( cid >= CELL_LABEL ) { if ( GetText( text ) ) { SelectObject( hdc, GetStockObject( DEFAULT_GUI_FONT ) ); SetBkMode( hdc, TRANSPARENT ); SetTextColor( hdc, color ); size.cx = 0; size.cy = 0; for ( s = text; * s != '\0'; s = s2 ) { s2 = strchr( s, '\n' ); if ( s2 != NULL ) { len = ( int ) ( s2 - text ); s2++; } else { len = ( int ) strlen( s ); s2 = s + len; } if ( GetTextExtentPoint32( hdc, s, len, & size2 ) ) { if ( size2.cx > size.cx ) { size.cx = size2.cx; } size.cy += size2.cy; } } rc.top += ( ( rc.bottom - rc.top ) - size.cy ) / 2; if ( cid >= CELL_EDIT ) { rc.left += 4; } format = DT_NOPREFIX; if ( cid == CELL_RIGHT ) { format |= DT_RIGHT; } else if ( cid == CELL_BUTTON ) { format |= DT_CENTER; } DrawText( hdc, text, -1, & rc, format ); return TRUE; } } return FALSE; }
セルの中身は何じゃろな?中身を描きます。
GetStockObject( DEFAULT_GUI_FONT ) で一番小さいフォントを選択しました。目が悪いと読みづらいけど情報量を増やしたいのでいたしかたありません。
SetBkMode( hdc, TRANSPARENT ) で文字の背景を透明にしました。DrawFrame() でせっかく背景塗りつぶしたんだから当然です。
SetTextColor( hdc, color ) で文字色を変数で指定してますが、今のところ黒しか対応してません。将来的には文字列中にカラー制御コードを入れたいななんて考え中です。。。
文字列として複数行に対応してるので、グリッドクラスの MakeGrid() と同様に GetTextExtentPoint32() で表示文字列のサイズを計算しつつ改行幅を計算して描画してます。
あと右寄せラベルなら右寄せに、ボタンなら中央寄せにして DrawText() を呼んでます。ただし DrawText() は複数行の縦中央寄せには対応してないので、
rc.top += ( ( rc.bottom - rc.top ) - size.cy ) / 2;
で、表示座標をいじって縦の中央寄せを実現しまんた。
VOID Cell :: GetRect()
VOID Cell :: GetRect( RECT & rc, POINT offset ) { rc.left = left + offset.x; rc.top = top + offset.y; rc.right = rc.left + width; rc.bottom = rc.top + height; }
セルの元々の表示座標にオフセットを適用した表示位置を返します。ClientToScreen() みたいなものですね。
BOOL Cell :: GetText()
BOOL Cell :: GetText( PTCHAR text ) { ParamID pid = GetParamID(); if ( pid == PARAM_NULL ) { return FALSE; } else if ( pid >= MAX_PARAM ) { strcpy( text, ( LPCTSTR ) param ); return TRUE; } else switch ( pid ) { case PARAM_TITLE : strcpy( text, _T( "ここにタイトルを表示します" ) ); return TRUE; case PARAM_COLUMN : sprintf( text, "%d", 3 ); return TRUE; case PARAM_RULE : strcpy( text, _T( "ここにルール名を表示します" ) ); return TRUE; case PARAM_NAME_FIRST : case PARAM_NAME_SECOND : strcpy( text, _T( "ここにプレイヤー名を表示します" ) ); return TRUE; case PARAM_TYPE_FIRST : case PARAM_TYPE_SECOND : strcpy( text, _T( "ここにプレイヤータイプを表示します" ) ); 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 : sprintf( text, "%d", 1 ); 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 : sprintf( text, "%d", 1 ); return TRUE; case PARAM_OPEN_FIRST : case PARAM_OPEN_SECOND : sprintf( text, "%0*d", 3, 123 ); return TRUE; default : for ( int i = 0; paramNames[ i ].pid != PARAM_NULL; i++ ) { if ( pid == paramNames[ i ].pid ) { strcpy( text, paramNames[ i ].name ); return TRUE; } } break; } return FALSE; }
セルの表示文字列を取得します。ラベルやボタン以外のほとんどの表示内容は動的に変化するので、メンバ変数 param には文字列のアドレス以外に表示内容の種別を示す整数が指定可能です。これが ParamID 型です。内容は以下の通り。
- PARAM_START_BUTTON
- PARAM_END_BUTTON
- PARAM_NEW_BUTTON
- PARAM_LOAD_BUTTON
- PARAM_SAVE_BUTTON
- PARAM_CLEAR_BUTTON
- ボタン関係
- PARAM_TITLE
- タイトル
- PARAM_COLUMN
- 設定桁数
- PARAM_RULE
- ルール種別
- PARAM_NAME_FIRST
- PARAM_NAME_SECOND
- プレイヤー名
- PARAM_TYPE_FIRST
- PARAM_TYPE_SECOND
- プレイヤータイプ(人間かコンピュータ)
- PARAM_CARD0_FIRST
- PARAM_CARD1_FIRST
- PARAM_CARD2_FIRST
- PARAM_CARD3_FIRST
- PARAM_CARD4_FIRST
- PARAM_CARD5_FIRST
- PARAM_CARD6_FIRST
- PARAM_CARD7_FIRST
- PARAM_CARD8_FIRST
- PARAM_CARD9_FIRST
- 先攻プレイヤーの所持カード枚数
- PARAM_CARD0_SECOND
- PARAM_CARD1_SECOND
- PARAM_CARD2_SECOND
- PARAM_CARD3_SECOND
- PARAM_CARD4_SECOND
- PARAM_CARD5_SECOND
- PARAM_CARD6_SECOND
- PARAM_CARD7_SECOND
- PARAM_CARD8_SECOND
- PARAM_CARD9_SECOND
- 後攻プレイヤーの所持カード枚数
- PARAM_HIGHLOW_FIRST
- PARAM_DOUBLE_FIRST
- PARAM_TARGET_FIRST
- PARAM_SLASH_FIRST
- PARAM_SHUFFLE_FIRST
- PARAM_CHANGE_FIRST
- 先攻プレイヤーの所持アイテム数
- PARAM_HIGHLOW_SECOND
- PARAM_DOUBLE_SECOND
- PARAM_TARGET_SECOND
- PARAM_SLASH_SECOND
- PARAM_SHUFFLE_SECOND
- PARAM_CHANGE_SECOND
- 後攻プレイヤーの所持アイテム数
- PARAM_OPEN_FIRST
- PARAM_OPEN_SECOND
- 設定ナンバー
ボタンの文字列は動的に変化しないとお思いでしょうが、案の定変化しません。ではなぜ param メンバに文字列アドレスではなく種別識別子をあてがっているのかといえば、あとあとこのボタンを直接指定する必要が出てくるからなんです。グリッド内の特定のセルを指定するにはゼロから始まるセルナンバーでも指定できますが、どのセルが何番かなんて知らない人がほとんどです。だってセル定義データの順番変わったら変わっちゃうし。そこで ParamID で指定できるようにわざわざ文字列データをほかにおいて対応したというわけなんです。あしからず。
以上です。
次回はヌメロンクラスを作ってみませう。