SDK Index Previous page Next page

エクスプローラーを作る その2(フォルダの表示)


はじめに

2000/02/17

前回はリストビューにファイルを表示しましたが、今回はツリービューにフォルダを表示します。 前回より増えた機能は、 です。また、変更された関数、構造体は です。

今回のサンプルファイル

1 変更された関数、構造体について

CreateFileView

大きさ、スタイル等を引数から指定するようにしました。

SetFolderIDLToFileView

前回では、アイテムIDリストを解放してはいけない仕様になっていましたが、今回の変更で解放しなければならなくなりました。

FileViewNotify

LPMALLOCを引数にとるようにして、関数内でIMALLOCを取得しないようにしました。
NM_DBLCLK処理を追加しました(詳細は後述)。

EVCREATE構造体

ウインドウの大きさ、サイズなどのメンバが増えています。

EVDATA構造体

ツリービュー関係のメンバが増えています。

その他

そのほかも所々変わってます。説明は面倒なのでしません。

3 フォルダを表示するウインドウ(FolderTree.c)の説明

フォルダはツリービューに表示していきます。フォルダの列挙はファイルの列挙と全く同じ方法です。

ツリービュー全体の情報は、今のところ必要無いのですが今後のためにダミーを入れています。また各アイテムの情報(以降フォルダ情報)としては自分を表すIShellFolder、アイテムIDリスト、デスクトップからのフルアイテムIDリスト、それとそのフォルダの属性を入れています。

フォルダの検索は与えられたアイテムIDリストからアイテムIDを一つずつ取り出して順にIShellFolder:CompareIDsを使って探していきます。また、フォルダのソートにもCompareIDsを使っています。

CreateFolderTree、DestroyFolderTree

ツリービューの作成、破棄をする関数です。リストビューの時とほとんど同じです。
//フォルダツリーの作成

HWND CreateFolderTree(LPFTCREATE lpFTCreate,LPFTDATA lpFTData)
{
    HWND    hWndFT;

    hWndFT = CreateWindowEx(lpFTCreate->exstyle,
                            WC_TREEVIEW, "",
                            lpFTCreate->style,
                            lpFTCreate->rc.left,lpFTCreate->rc.top,
                            lpFTCreate->rc.right-lpFTCreate->rc.left, lpFTCreate->rc.bottom-lpFTCreate->rc.top,
                            lpFTCreate->hParent,
                            (HMENU)IDD_FOLDERTREE, 
                            Instance, 
                            NULL);

    if(!hWndFT) return NULL;

    //データ領域の初期化
    InitFolderTreeData(lpFTData);
    //システムイメージリストを使うように設定する。
    InitFolderTreeImageLists(hWndFT);
    //デスクトップをフォルダツリーに追加する。
    InsertDesktop(hWndFT,lpFTData);

    return hWndFT;
}

//フォルダツリーの破棄

void DestroyFolderTree(HWND hWndFT,LPFTDATA lpFTData)
{
    DestroyWindow(hWndFT);
    ReleaseFolderTreeData(lpFTData);
}

InitFolderTreeData、InitFolderTreeImageLists

ツリービュー情報や、イメージリストの初期化関数ですがリストビューの時と同じようなものなので省略します。

InsertDesktop

ツリービューのルートにデスクトップを追加します。SHGetDesktopFolder及びSHGetSpecialFolderLocationを使ってIShellFolderとアイテムIDリストを取得しています。InsertItemIDLToFolderTreeはツリービューにアイテムIDリストを追加する関数ですが、説明は後でします。
//デスクトップをフォルダツリーに追加

static BOOL InsertDesktop(HWND hWndFT,LPFTDATA lpFTData)
{
    LPMALLOC        lpMalloc;
    LPSHELLFOLDER   lpShellFolder;
    LPITEMIDLIST    lpDesktopIDL;
    HRESULT         hr;

    //IMallocを取得
    //シェルインターフェースの場合のメモリ操作はこれを使う。GlobalAlloc等は使えない。
    hr=SHGetMalloc(&lpMalloc);
    if (FAILED(hr)) return FALSE;

    SHGetDesktopFolder(&lpShellFolder);
    SHGetSpecialFolderLocation(GetMainWindow(hWndFT),CSIDL_DESKTOP,&lpDesktopIDL);

    InsertItemIDLToFolderTree(hWndFT,lpFTData,NULL,lpDesktopIDL,lpShellFolder,TVI_ROOT,TVI_LAST,lpMalloc);

    lpShellFolder->lpVtbl->Release(lpShellFolder);
    lpMalloc->lpVtbl->Free(lpMalloc,lpDesktopIDL);
    lpMalloc->lpVtbl->Release(lpMalloc);

    return  TRUE;
}

SetFolderIDLToFolderTree

フォルダを列挙してツリービューに追加します。基本的にリストビューの時と同じですが、アイテムIDリストをツリービューに追加する部分がデスクトップを追加するときにも必要だったので、別の関数にしました。また、フォルダが1つもなかったときにはアイテムの前につく+印を消します。
//フォルダをフォルダツリーに追加する
//この関数を呼び出した後、IShellFolderはReleaseし、
//lpFolderIDLを解放する。

BOOL SetFolderIDLToFolderTree(HWND hWndFT,LPFTDATA lpFTData,
                        LPITEMIDLIST lpFolderIDL,
                        LPSHELLFOLDER lpShellFolder,HTREEITEM hParent)
{
    int             iCtr;
    HTREEITEM       hPrev=NULL,hNext; 
    HRESULT         hr;
    HWND            hWndParent;
    LPMALLOC        lpMalloc;
    LPITEMIDLIST    lpItem=NULL,lpItemAll=NULL;
    LPENUMIDLIST    lpEnum=NULL;
    LPFTITEMDATA    lpFTItemData=NULL;
    TV_ITEM         tvi;
    ULONG           ulFetched;
    BOOL            bHasFolder;

    //IMallocを取得
    //シェルインターフェースの場合のメモリ操作はこれを使う。GlobalAlloc等は使えない。
    hr=SHGetMalloc(&lpMalloc);
    if (FAILED(hr)) return FALSE;

    if(!ReleaseFolderTreeData(lpFTData)) return FALSE;
    
    hWndParent=GetMainWindow(hWndFT);

    //マウスカーソルを砂時計に
    SetCapture(hWndParent);
    SetCursor(LoadCursor(NULL, IDC_WAIT));

    //ファイル列挙するためのインターフェースを取得
    hr=lpShellFolder->lpVtbl->EnumObjects(lpShellFolder,
                                 hWndParent, 
                                 SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN,
                                 &lpEnum);

    if (SUCCEEDED(hr)){
        iCtr = 0;

        bHasFolder=FALSE;
        //ファイルのアイテムIDリスト(ファイルのハンドルみたいなもの)を取得
        while (S_OK==lpEnum->lpVtbl->Next(lpEnum,1,&lpItem,&ulFetched)){
            hNext=InsertItemIDLToFolderTree(hWndFT,lpFTData,lpFolderIDL,lpItem,lpShellFolder,
                                            hParent,hPrev,lpMalloc);
            if(hNext!=hPrev){
                bHasFolder=TRUE;
                hPrev=hNext;
            }
            lpMalloc->lpVtbl->Free(lpMalloc, lpItem);
        }
        
        //フォルダが1つもなかった場合 +印を消す
        if(!bHasFolder){
            tvi.hItem=hParent;
            tvi.mask=TVIF_CHILDREN;
            tvi.cChildren=0;
            TreeView_SetItem(hWndFT, &tvi);
        }
    }

    //マウスカーソルを元に
    ReleaseCapture();
    SetCursor(LoadCursor(NULL, IDC_ARROW));

    //取得したインターフェースはちゃんと解放する。
    if (lpEnum)  lpEnum->lpVtbl->Release(lpEnum);
    if (lpMalloc) lpMalloc->lpVtbl->Release(lpMalloc);
 
    return TRUE;
}

InsertItemIDLToFolderTree

基本的にリストビューの時と変わらないのですが、ツリービューではLPSTR_TEXTCALLBACK等を使わずにその場でフォルダ名とアイコンを設定しています。理由は、フォルダはそんなに大量にないだろうと考えたからです。アイコンはショートカットや隠し属性とかは必要ないので共有アイコンだけ調べています。また、デスクトップ追加時のためにlpFolderIDLがNULLだったら親フォルダからBindToObjectをして自分自身を得る作業をとばすようにしています。
関数の戻り値は追加したツリービューアイテムのハンドルです。追加しなかった場合は引数で与えられたのをそのまま返します。
//ツリービューにアイテムIDリストを追加

static HTREEITEM InsertItemIDLToFolderTree(HWND hWndFT,LPFTDATA lpFTData,
                        LPITEMIDLIST lpFolderIDL,LPITEMIDLIST lpItem,
                        LPSHELLFOLDER lpShellFolder,HTREEITEM hParent,HTREEITEM hPrev,LPMALLOC lpMalloc)
{
    TV_ITEM         tvi;
    TV_INSERTSTRUCT tvins;
    HRESULT         hr;
    LPITEMIDLIST    lpItemAll=NULL;
    LPFTITEMDATA    lpFTItemData=NULL;
    ULONG           ulAttrs;
    char            szBuff[MAX_PATH];


    ulAttrs = SFGAO_CAPABILITYMASK | SFGAO_HASSUBFOLDER | SFGAO_SHARE | SFGAO_FOLDER | SFGAO_FILESYSTEM;

    //ファイル属性の取得
    lpShellFolder->lpVtbl->GetAttributesOf(lpShellFolder, 1, &lpItem, &ulAttrs);

    //フォルダか?
    if(!(ulAttrs & SFGAO_FOLDER)) return hPrev;

    tvi.mask=TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM;
    
    //その中にフォルダがあるか?
    if(ulAttrs & SFGAO_HASSUBFOLDER){
         tvi.cChildren=1;
         tvi.mask |= TVIF_CHILDREN;
    }

    //フォルダ情報を格納するための領域の確保
    lpFTItemData = (LPFTITEMDATA)lpMalloc->lpVtbl->Alloc(lpMalloc, sizeof(FTITEMDATA));
    if (!lpFTItemData) goto Error;

    if (!GetName(lpShellFolder, lpItem, SHGDN_NORMAL, szBuff)) goto Error;

    lpFTItemData->ulAttribs=ulAttrs;

    tvi.pszText    = szBuff;
    tvi.cchTextMax = MAX_PATH;

    lpItemAll=ConcatPidls(lpFolderIDL, lpItem);

    lpFTItemData->lpItemIDL=CopyITEMID(lpMalloc, lpItem);

    GetNormalAndSelectedIcons(lpItemAll, &tvi);

    //共有ファイルの場合は共有アイコンを重ねる
    if (ulAttrs & SFGAO_SHARE){
        tvi.mask |= LVIF_STATE;
        tvi.stateMask = LVIS_OVERLAYMASK;
        tvi.state = INDEXTOOVERLAYMASK(1);
    }

    if(lpFolderIDL==NULL){
        lpFTItemData->lpShellFolder=lpShellFolder;
        lpShellFolder->lpVtbl->AddRef(lpShellFolder);
        hr=0;
    }else{
        hr=lpShellFolder->lpVtbl->BindToObject( lpShellFolder,
                                                lpItem,0,&IID_IShellFolder,
                                                (LPVOID *)&lpFTItemData->lpShellFolder);
    }

    if(!SUCCEEDED(hr)) goto Error;

    lpFTItemData->lpItemAllIDL=lpItemAll;

    tvi.lParam=(LPARAM)lpFTItemData;

    tvins.item         = tvi;
    tvins.hInsertAfter = hPrev;
    tvins.hParent      = hParent;

    hPrev=TreeView_InsertItem(hWndFT, &tvins);

    lpFTItemData=NULL;

Error:

    if(lpFTItemData){
        lpMalloc->lpVtbl->Free(lpMalloc, lpFTItemData);
        lpFTItemData=NULL;
    }

    return hPrev;
}

TreeViewCompareProc

ソート用の比較関数です。lparamSortには親フォルダのインターフェース、lparam1,lparam2には比較する2つのフォルダ情報のアドレス(LPFTITEMDATA)が入っているので、CompareIDsを使って2つのアイテムIDリストを比較しています。SCODE_CODE(GetScode(hr))とすれば結果が得られるので、それを返値にします。
//比較関数

int CALLBACK TreeViewCompareProc(LPARAM lparam1, 
                                 LPARAM lparam2,
                                 LPARAM lparamSort)
{
    #define lpFTItemData1   ((LPFTITEMDATA)lparam1)
    #define lpFTItemData2   ((LPFTITEMDATA)lparam2)

    HRESULT   hr;

    hr = ((LPSHELLFOLDER)lparamSort)->lpVtbl->CompareIDs((LPSHELLFOLDER)lparamSort,
                                                 0,
                                                 lpFTItemData1->lpItemIDL,
                                                 lpFTItemData2->lpItemIDL);

    if (FAILED(hr)){
        return 0;
    }

    return (short)SCODE_CODE(GetScode(hr));
}

FolderTreeNotify

ツリービューからのメッセージの処理用関数です。

TVN_SELCHANGED

ツリービューのアイテムが選択されたことを示すメッセージです。itemNewに選択されたアイテムが入っているので、ExplorerView_ChangeTreeItemをつかって表示フォルダを変更します。ExplorerView_ChangeTreeItemはExView.cの中にありますが、例によって省略します。

TVN_DELETEITEM

ツリービューからアイテムが削除されたときのメッセージです。itemOldに削除されたアイテムが入っています。そこからフォルダ情報(FTITEMDATA)を取得しIShellFolderとアイテムIDリストを解放します。

TVN_ITEMEXPANDING

ツリービューアイテムの中の子アイテムを表示しようとしたときのメッセージです。itemNewに表示しようとしているアイテムが入っています。このメッセージがきたら、中のフォルダを列挙してツリービューに追加するのですが、TVIS_EXPANDEDONCEフラグがあるときは過去に列挙されたことがあるので、そのときはなにもしません。
SetFolderIDLToFolderTreeを使って列挙した後で、今度はTreeView_SortChildrenCBで列挙したフォルダをソートしておきます。ソート用の比較関数はTreeViewCompareProcです。
//ツリービューからのメッセージを処理する

LRESULT FolderTreeNotify(HWND hWnd,WPARAM wParam,LPARAM lParam,LPFTDATA lpFTData,LPMALLOC lpMalloc)
{
    #define lpNMTV      ((LPNMTREEVIEW)lParam)

    LPFTITEMDATA    lpFTItemData;
    TV_SORTCB       tvscb;

    switch(lpNMTV->hdr.code){
        case TVN_SELCHANGED:
            lpFTItemData=(LPFTITEMDATA)lpNMTV->itemNew.lParam;
            ExplorerView_ChangeTreeItem(lpNMTV->hdr.hwndFrom,lpFTItemData,
                                                        lpNMTV->itemNew.hItem);
            break;

        case TVN_DELETEITEM:

            lpFTItemData=(LPFTITEMDATA)lpNMTV->itemOld.lParam;

            //IShellFolderを解放する
            lpFTItemData->lpShellFolder->lpVtbl->Release(lpFTItemData->lpShellFolder);
            lpMalloc->lpVtbl->Free(lpMalloc, lpFTItemData->lpItemIDL);  
            lpMalloc->lpVtbl->Free(lpMalloc, lpFTItemData->lpItemAllIDL);  
            lpMalloc->lpVtbl->Free(lpMalloc, lpFTItemData);  
            break;

        case TVN_ITEMEXPANDING:

            if ((lpNMTV->itemNew.state & TVIS_EXPANDEDONCE)) break;

            lpFTItemData=(LPFTITEMDATA)lpNMTV->itemNew.lParam;

            SetFolderIDLToFolderTree(lpNMTV->hdr.hwndFrom,lpFTData,lpFTItemData->lpItemAllIDL,
                                    lpFTItemData->lpShellFolder,lpNMTV->itemNew.hItem);

            tvscb.hParent     = lpNMTV->itemNew.hItem;
            tvscb.lpfnCompare = TreeViewCompareProc;
            tvscb.lParam      = (LPARAM)lpFTItemData->lpShellFolder;

            TreeView_SortChildrenCB(lpNMTV->hdr.hwndFrom, &tvscb, 0);

            break;
    }

    lpMalloc->lpVtbl->Release(lpMalloc);

    return  0;
}

FindFolderIDL

アイテムIDリストからツリービューアイテムを探します。検索はアイテムIDを一つずつ取り出しながら、与えられた場所から順に検索していきます。そのため、アイテムIDリストはそこからの相対リストでなければなりません。実際の検索部分はFindChildとして別関数になっています。
//指定されたItemIDLを探す

HTREEITEM FindFolderIDL(HWND hWndFT,HTREEITEM hParent,LPITEMIDLIST lpItemIDL)
{
    LPMALLOC        lpMalloc;
    HRESULT         hr;
    LPITEMIDLIST    lpItemOneID;

    hr=SHGetMalloc(&lpMalloc);
    if (FAILED(hr)) return FALSE;

    if(hParent==TVI_ROOT){
        hParent=TreeView_GetRoot(hWndFT);
    }

    while(hParent){
        if(lpItemIDL->mkid.cb==0) break;
        //アイテムIDを1つだけ取り出す。
        lpItemOneID=CopyITEMID(lpMalloc,lpItemIDL);
        //アイテムの中を探す、あったらそれを親としてさらに探し続ける。
        hParent=FindChild(hWndFT,hParent,lpItemOneID);
        lpMalloc->lpVtbl->Free(lpMalloc,lpItemOneID);
        //次のアイテムIDに移動する。
        lpItemIDL=GetNextIDL(lpItemIDL);
    }

    lpMalloc->lpVtbl->Release(lpMalloc);

    return  hParent;
}

FindChild

与えられたアイテムの中だけを探して(子アイテムの中は探さない)、一致するものを見つける関数です。TreeView_GetChild、TreeView_GetNextSiblingで子アイテムを順次列挙してIShellFolder:CompareIDsで比較しています。
//子アイテムを列挙して、同じものを探す

static HTREEITEM FindChild(HWND hWndFT,HTREEITEM hParent,LPITEMIDLIST lpItemIDL)
{
    HRESULT         hr;
    HTREEITEM       hItem;
    TVITEM          TVItem;
    LPFTITEMDATA    lpFTParentData,lpFTItemData;

    //探すアイテムのフォルダ情報を取得
    TVItem.mask=TVIF_PARAM;
    TVItem.hItem=hParent;
    if(!TreeView_GetItem(hWndFT,&TVItem)) return NULL;
    
    lpFTParentData=(LPFTITEMDATA)TVItem.lParam;

    //探すアイテム内の子アイテムを列挙しておく
    TreeView_Expand(hWndFT,hParent,TVE_EXPAND);

    //1つ目の子アイテムを取得
    hItem=TreeView_GetChild(hWndFT,hParent);
    while(hItem!=NULL){
        //子アイテムのフォルダ情報を取得
        TVItem.mask=TVIF_PARAM;
        TVItem.hItem=hItem;
        if(TreeView_GetItem(hWndFT,&TVItem)){
            lpFTItemData=(LPFTITEMDATA)TVItem.lParam;
            //比較
            hr=lpFTParentData->lpShellFolder->lpVtbl->CompareIDs(lpFTParentData->lpShellFolder,
                                                                 0,
                                                                 lpFTItemData->lpItemIDL,
                                                                 lpItemIDL);
            if(FAILED(hr)) break;

            if(SCODE_CODE(GetScode(hr))==0) break;
        }
        //次の子アイテムを取得
        hItem=TreeView_GetNextSibling(hWndFT,hItem);
    }

    return  hItem;
}

4 ファイルを表示するウインドウの変更点

フォルダやファイルをダブルクリックして、そのフォルダを開いたりファイルを実行したりできるようにしました。変更箇所はFileViewNotifyにNM_DBLCLKの処理を追加しただけです。

FileViewNotify

NM_DBLCLKでは、どのアイテムが選択されたか分からない(IE4以上なら分かるらしい)のでマウスカーソルの位置からアイテムを見つけます。選択アイテムが分かったら、それがファイルかフォルダかを調べそれぞれの処理を行います。
フォルダの変更にはExplorerView_ToChildFolder関数、ファイルの実行にはExecuteIDL関数を使います。これらの関数はそれぞれExView.c,Pidl.cで定義されています。
//リストビューからのメッセージを処理する

LRESULT FileViewNotify(HWND hWnd,WPARAM wParam,LPARAM lParam,LPFVDATA lpFVData,LPMALLOC lpMalloc)
{
    #define lpNMLV      ((LPNMLISTVIEW)lParam)
    #define lpNMLVDISP  ((NMLVDISPINFO*)lParam)

    LV_ITEM         LVItem;
    LPFVITEMDATA    lpFVItemData;
    WIN32_FIND_DATA wfd;
    LV_HITTESTINFO  lvhti;
    POINT           pt;

    switch(lpNMLV->hdr.code){
        case NM_DBLCLK:
            //カーソル位置から選択されたアイテムを探す
            GetCursorPos((LPPOINT)&pt);
            lvhti.pt=pt;
            ScreenToClient(lpNMLV->hdr.hwndFrom, &lvhti.pt);
            ListView_HitTest(lpNMLV->hdr.hwndFrom, &lvhti);
            if (lvhti.flags & LVHT_ONITEM){
                LVItem.mask = LVIF_PARAM;
                LVItem.iItem = lvhti.iItem;
                LVItem.iSubItem = 0;

                if (!ListView_GetItem(lpNMLV->hdr.hwndFrom, &LVItem)) break;

                lpFVItemData=(LPFVITEMDATA)LVItem.lParam;

                //フォルダか
                if (lpFVItemData->ulAttribs & SFGAO_FOLDER ){
                    //フォルダの変更
                    ExplorerView_ToChildFolder(lpNMLV->hdr.hwndFrom,lpFVItemData->lpItemIDL,FALSE);
                }else{
                    //ファイルの実行
                    ExecuteIDL(GetMainWindow(lpNMLV->hdr.hwndFrom),
                        lpFVData->lpItemIDL,lpFVItemData->lpItemIDL);
                }
            }
            break;

以下略

ExecuteIDL

与えられた2つアイテムIDリストを連結したリストで示されるファイルを実行します。ファイルの実行は、ShellExecuteExを使います。ShellExecuteExの解説はどこにでもあるので省きますが、ほとんどの引数はNULLで大丈夫です。ここで少し注意して欲しいのは、SHELLEXECUTEINFOのlpVerbメンバに動作を指定するのですが、これは必ずNULLにしてください、これを"Open" 等にするとうまく動きません(コントロールパネルの中のものが開けない)。ヘルプファイルにNULLの場合は"Open"になると書いてありますが大嘘です。NULLと"Open"は別物です。
BOOL ExecuteIDL(HWND hWnd,LPITEMIDLIST lpi1,LPITEMIDLIST lpi2)
{
    LPMALLOC        lpMalloc;
    HRESULT         hr;
    SHELLEXECUTEINFO sei = {
                            sizeof(SHELLEXECUTEINFO),
                            SEE_MASK_INVOKEIDLIST,
                            NULL,       //NULLにすること
                            NULL,
                            NULL,
                            "",
                            "",
                            SW_SHOWNORMAL,
                            NULL,
                            (LPVOID)NULL,
                            NULL,
                            0,
                            0,
                            NULL
                            };

    sei.hwnd=hWnd;
    sei.hInstApp=Instance;

    hr=SHGetMalloc(&lpMalloc);
    if (FAILED(hr)) return -1;

    sei.lpIDList=ConcatPidls(lpi1,lpi2);
    ShellExecuteEx(&sei);
    GetLastError();
    lpMalloc->lpVtbl->Free(lpMalloc,sei.lpIDList);
    lpMalloc->lpVtbl->Release(lpMalloc);

    return TRUE;
}

おわりに

今回も、長いだけで何の役にも立たないページになってしまいました。どうも説明するのが苦手なので、プログラムが長くなればなるほど説明が手抜きになっていきます。だから、多少分かる人はソースを見てもらった方がいいと思います。


to sdk prev next