Mesoscopic Programming

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

ウィンドウズプログラミング講座第11回:ツリービューコントロール

概要

ツリービュークラスにツリービューコントロールを実装します。
名前がいっしょなのでややこしいけど、ツリービュークラスはツリービューコントロールの親ウィンドウです。
ツリービューコントロールは自分でスクロールしてくれるんで、本当はビュークラスで管理する必要もないんだけど、何でもかんでもアプリケーションクラスに入れちゃうとソースが複雑になるので分けました。


ツリービュークラス

変数

class TreeView : public View
{
    HWND    hTree;

ツリービューコントロールハンドルを追加しますた。

追加関数

    VOID    CreateTree();
    INT     CreateChild( HTREEITEM hParent );
    VOID    MakeFullPath( LPTSTR path, HTREEITEM hItem, BOOL first = TRUE );
    LRESULT OnTvnGetDispInfo();
};

CreateTree() 関数でツリーを作成します。
CreateChild() 関数でサブディレクトリ内のツリーを作成します。
MakeFullPath() 関数で指定項目のフルパス文字列を作成します。
OnTvnGetDispInfo() はまだ表示されてない項目を作成しろというツリービューからの通知対応処理です。

ツリー作成

クラス初期化時にツリービューコントロールと最初のツリー項目を作成します。

BOOL TreeView :: Init( Window * parent, UINT id )
{
    dwStyle = WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_SHOWSELALWAYS;
    hTree   = CreateWindow( WC_TREEVIEW, NULL, dwStyle, 0, 0, 0, 0, hwnd, ( HMENU )( UINT_PTR ) idTreeCtrl, GetModuleHandle( NULL ), NULL );

    CreateTree();
}

VOID TreeView :: CreateTree()
{
    TVINSERTSTRUCT tvIns;
    TCHAR          drives[ _MAX_PATH ];
    TCHAR          fname[ _MAX_PATH ];

    TreeView_DeleteAllItems( hTree );

    ZeroMemory( & tvIns, sizeof tvIns );

    tvIns.hParent      = TVI_ROOT;
    tvIns.hInsertAfter = TVI_LAST;
    tvIns.item.mask    = TVIF_TEXT | TVIF_STATE;
    tvIns.item.pszText = "Root";
    tvIns.item.state   = tvIns.item.stateMask = TVIS_BOLD | TVIS_EXPANDED;
    tvIns.hParent      = TreeView_InsertItem( hTree, & tvIns );
    tvIns.item.mask    = TVIF_TEXT | TVIF_PARAM;
    tvIns.item.pszText = fname;
    tvIns.item.lParam  = FILE_ATTRIBUTE_DIRECTORY;

    GetLogicalDriveStrings( sizeof ( drives ), drives );

    for ( LPTSTR s = drives; ( * s ) != '\0'; s += strlen( s ) + 1 )
    {
        strcpy( fname, s );

        fname[ strlen( fname ) - 1 ] = '\0';

        CreateChild( TreeView_InsertItem( hTree, & tvIns ) );
    }
}

初期のツリーで表示されるのはドライブ名のみです。
CreateChild() 関数でドライブ内の項目も作成していますが、これを表示するとさらにサブフォルダ下の項目も作成しなければならないので非表示にしました。
ドライブを展開するとイベントが発生するのでそのとき作られます。

子アイテム作成

hParent で親となるアイテム(=フォルダ)を指定するとその中身のアイテムツリーを作成します。

INT TreeView :: CreateChild( HTREEITEM hParent )
{
    TCHAR           path[ _MAX_PATH ];
    WIN32_FIND_DATA ffd;
    HANDLE          hFind;
    TVINSERTSTRUCT  tvIns;
    INT             count;

    MakeFullPath( path, hParent );

    ZeroMemory( & ffd, sizeof ffd );

    strcat( path, "\\*.*" );

    hFind = FindFirstFile( path, & ffd );
    count = 0;

    if ( hFind != INVALID_HANDLE_VALUE )
    {
        ZeroMemory( & tvIns, sizeof tvIns );

        tvIns.hParent      = hParent;
        tvIns.hInsertAfter = TVI_LAST;
        tvIns.item.mask    = TVIF_TEXT | TVIF_PARAM | TVIF_CHILDREN;

        for ( ;; )
        {
            if ( strcmp( ffd.cFileName, "." ) != 0 && strcmp( ffd.cFileName, ".." ) != 0 )
            {
                tvIns.item.pszText = ffd.cFileName;
                tvIns.item.lParam  = ffd.dwFileAttributes;

                if ( ( ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) != 0 )
                {
                    tvIns.item.cChildren = I_CHILDRENCALLBACK;
                }

                TreeView_InsertItem( hTree, & tvIns );

                count++;
            }

            if ( ! FindNextFile( hFind, & ffd ) )
            {
                break;
            }
        }

        FindClose( hFind );
    }

    return count;
}

最初に呼ばれるのはドライブ名です。ドライブ名もフォルダですから。
もしディレクトリを見つけた場合は tvIns.item.cChildren = I_CHILDRENCALLBACK をしてるので表示が必要になったときイベントが発生して作成されます。
そのときまたここが呼ばれます。

フルパス文字列作成

指定された項目のフルパス名を作成します。

VOID TreeView :: MakeFullPath( LPTSTR path, HTREEITEM hItem, BOOL first )
{
    HTREEITEM hParent = TreeView_GetParent( hTree, hItem );
    TCHAR     fname[ _MAX_FNAME ];
    TVITEMEX  tvItem;

    if ( first )
    {
        ZeroMemory( path, sizeof path );
    }

    if ( hParent != NULL )
    {
        ZeroMemory( & tvItem, sizeof tvItem );

        tvItem.hItem      = hItem;
        tvItem.pszText    = fname;
        tvItem.cchTextMax = sizeof fname;
        tvItem.mask       = TVIF_HANDLE | TVIF_TEXT;

        TreeView_GetItem( hTree, & tvItem );

        if ( first )
        {
            if ( fname[ strlen( fname ) - 1 ] == '\\' )
            {
                fname[ strlen( fname ) - 1 ] = '\0';
            }
        }
        else
        {
            strcat( fname, "\\" );
        }

        MoveMemory( path + strlen( fname ), path, strlen( path ) + 1 );
        CopyMemory( path, fname, strlen( fname ) );
        MakeFullPath( path, hParent, FALSE );
    }
}

親をたどれば親フォルダ名が分かるのでルートに達するまで親のアイテム名を先頭に足し続ければ完成です。

TVN_GETDISPINFO 通知処理

まだ作成されていないフォルダの中身を作成します。

LRESULT TreeView :: OnTvnGetDispInfo()
{
    TVITEM tvItem;

    ZeroMemory( & tvItem, sizeof tvItem );

    tvItem.mask  = TVIF_HANDLE | TVIF_CHILDREN;
    tvItem.hItem = lpnmtvdi->item.hItem;

    if ( CreateChild( lpnmtvdi->item.hItem ) > 0 )
    {
        tvItem.cChildren = 1;
    }

    TreeView_SetItem( hTree, & tvItem );

    InvalidateRect( hTree, NULL, FALSE );

    return 0;
}

CreateChild() 関数でフォルダの中身を作成します。
tvItem.cChildren = 1; で作成済みであることを設定します。これが 0 なら空のフォルダです。


実行画面

実行ファイル
  1. Windows.zip

以上です。