Mesoscopic Programming

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

ヌメロン製作講座第9回:編集処理機能の作成

春はあけぼの、秋はのぼけあ。皆様いかがおすごしでしょうか?
それでは本日のメニューをご紹介いたします。

本日の修正ソースファイル
  1. Numer0n.h
  2. main.h
  3. main.cpp

今回は設定データの編集機能を実装いたします。
まずはアプリケーションクラスの変更点です。


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

変数の内容は以下の通りです。

  1. hctrl
    • コントロールハンドル
  2. hctrl2
    • 第2のコントロールハンドル
  3. edit
    • 編集パラメータ

メソッドの内容は以下の通りです。

  1. OnNmKeyDown()
    • NM_KEYDOWN メッセージ処理
  2. OnEditBegin()
    • 編集開始処理
  3. OnEditBreak()
    • 編集中止処理
  4. 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 ];
};

各変数の内容は以下の通りです。

  1. cid
    • セルの編集タイプです。GetEditParam() 中で定義データと異なる値を返すこともできます。
  2. left、top、width、height
    • コントロールの位置とサイズです。
  3. min
    • 数字編集の場合の最小値です。
  4. max、pos
    • 数字編集の場合は最大値と初期値です。コンボボックスの場合はリスト数と初期選択番号です。
  5. text
    • 文字列編集の初期文字列です。
  6. 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

次回はボタン対応処理です。

*1:メンバ関数

*2:ちなみにこの部分ははてな記法だと色が付けられないので直接 HTML で書きました。Markdown 記法じゃなくても HTML が書けるんですね?やっぱはてなの脚注機能は一度使うとやめられないっすわ

*3:あ~腹減った飯にしよう