-
Folder View の概要
では、前回作った三つのペインの左側にあたら Folder View
の実装に入ります。ずいぶん時間が空いてしまったのには理由があって(単純に時間が取れないということもありますが)この Folder
View を作るにあたっていろいろ調べたりしていたのです(私もこれを作るのは初めてなので)。
今回の内容は盛りだくさんでさらに新しい概念がたくさん出てくるので、頑張って覚えましょう。

これは Explorer
まず Folder View の構成ですが、早い話が Explorer
の左側に表示されているフォルダツリーを作成することになります。作り方はいろいろあると思いますが、以下の操作を行うことになります。
だいたい上記の操作をファイルのディレクトリツリーに従って再帰的に行うことで、目的のツリーを含んだ View
が構築できることは想像できると思います。問題は基準となる場所の取得方法と、ツリーに追加する対象の情報を取得する方法ということになりますが、力技で API
を使ってやる方法も考えられるかもしれません。
しかし、たいていは OS
が提供すべき情報にアクセスする手段が用意されているはずです。Windows ではこのような OS
の仕組みにアクセスするプログラムのカテゴリを「シェルプログラミング」と呼んでいます。じゃあ、シェルプログラミングって何?ってことで、MSDN
を調べてみると、「Windows Shell and
Controls」というカテゴリにいろいろな便利な関数やオブジェクトがあります。公式な文章の日本語版を見つけることができなかったので、ここは
頑張って英語版を読んでみてください。
MSDN Home >
MSDN Library >
User Interface Design and Development >
Windows Shell
では早速はじめましょう!と言いたいところなのですが、これらのシェルインターフェース群は COM で提供されているのです。WTL はその前身となった ATL のもつ COM との親和性を引き継いでいるのでほとんど違和感なく COM
を使えるのが利点の一つに挙げられると思うのですが、COM と聞くと「聞いたことがあるけどよくわからない」とか「結局 COM
ってなに?」と疑問の声が聞こえてきそうです。
まあ、私には COM
を語るにスキルがありませんので、詳しい説明は専門書に譲るとして以下の書籍を紹介するにとどめます。
・ATL
COM プログラミング
・ATL
プログラミング
上記の2冊は実際に私が持っている本なんですが、結構難しいかも知れません。少なくとも私が完全に理解していない(汗)で使ってますので、皆さんにも
COM を使いながら覚えていって興味や疑問が出たらこれらの書籍で調べてみるというスタイルをお勧めしておきます。
買ったのは初版だけど実際に意味がわかるようになったのは3年後だったり
しかしこれだけだとあまりにアレなので、簡単に説明しておきましょう。
-
COM とスマートポインタ
COM とは
Microsoft が提唱するコンポーネントオブジェクトモデルの略で・・・なんて言い出すと WTL 講座ではなく COM
講座になってしまいそうなので、これだけ覚えておけばなんとか使えるという要点だけ。
ということです。ずいぶんあっさり書いたものですが(汗)今回の内容に関連していることで言えば、たとえば特定のフォルダに関する情報(メンバ)や操作(メソッド)を特定のインタフェース(IShellFolder)として提供しよう。このインタフェースは言語を選ばすに使えるが、その代わりいろいろと
手順や決まりがあるのでそれは守ってくださいという感じでしょうか。
決まりには次のようなものがあります。
COM
インタフェースは必ず IUnknown を継承しています。この IUnknown はQueryInterface、AddRef、Release
の3つの純粋仮想関数を持っています。QueryInterface
という関数は「こんなインタフェースを持ってますか?」と問い詰める(笑)ことができます。また、COM
はその特定のインタフェースを使っているかどうかを参照カウントという概念で管理しています。
つまり、自分が使うときにそのカウントを一つ増やし(AddRef)、使わなくなったら一つ減らし(Release)ます。実際にこの処理を書いてみるとわかるのですが、C++
の new と delete
と同様に対にして管理できるうちはいいのですが、複雑なことをはじめるとすぐに管理できなくなってしまいます。そこで COM
では「スマートポインタ」という概念を使ってこれらを自動化することができます(コンストラクタで AddRef() してデストラクタで
Release() するポインタだと思ってください)
Microsoft は
ATL が COM を楽に扱えるように CComPtr
と呼ばれるスマートポインタを用意しました。これによってインタフェースの参照カウントの管理は自動化し、スマートポインタからラップされたインタフェースへの関数をメンバのように呼ぶことができます。ここでは
vtable と呼ばれる仕組みが関わっていますが、興味のある人は調べてみてください。
というわけで、
ぼろが出る前に(汗)実際の
COM インタフェースに焦点を当てていきましょう。
-
始まりは IShellFolder
フォルダツリーを自作するのは私も今回が初めてなので、とりあえず MSDN でいろいろと調べて見ました。以前 VB
でプログラムしたときには、SHBrowserForFolder
という関数を使ったことがある程度でなにやらちんぷんかんぷんです。いろいろ調べた結果、以下のようなことがわかりました。
-
基準となる場所はデスクトップが楽そう(Explorer もそうだし)
-
基本的にシェル系のインタフェースを取得する関数は SH で始まっている
-
シェルには名前空間という概念があり、ITEMIDLIST (SHITEMID のリスト:後述)と呼ばれる構造で表される
-
フォルダやファイルなどのシェルオブジェクトには一意の ID が割り当てられており、それが ITEMIDLIST で管理されている
-
ツリービューに追加するアイコンのイメージはシステムイメージリストというオブジェクトが使えるようだ
だいたいこのあたりを念頭において、実装に入っていきたいと思います。ここで、ITEMIDLIST
ってやつが出てくるんですが、単純な構造のくせにメモリの取得に IMalloc を使えと書いてあったりして面倒だったりします。ITEMIDLIST
って何ぞや?という方は、MSDN の以下の説明に目を通して下さい。
MSDN Home >
MSDN Library >
User Interface Design and Development >
Windows Shell >
Shell Programmer's Guide >
Shell Basics >The Shell Namespace
ツリーのそれぞれの
Item には、フォルダやファイルを表す ITEMIDLIST
が対応するのですが、フルパスで保存すると深い階層になればなるほど TreeView に格納するメモリを使うので、今回は相対的な SHITEMID の内容を、TreeItem
の lParam に格納し、情報を取得する際には、それを親までたどってフルパスの ITEMIDLIST
を作成して利用する、という方針で行きます。
-
CIDL クラスの作成
IShellFolder
インタフェースを使ってフォルダやファイルを扱っていくわけですが、その前に ITEMIDLIST を管理するクラスを作成しましょう。名前は
ITEMIDLIST を短縮して IDL としました。この ITEMIDLIST の構成なんですが、詳しい説明をするよりは、MSDN
とソースを読んで動かしながら理解してもらったほうが早いでしょう。
一応解説しておくと
typedef struct _SHITEMID {
USHORT cb;
BYTE abID[1];
} SHITEMID, * LPSHITEMID;
という構造が、一つのフォルダまたはファイルの階層を表していて、それがフルパスの分リストとして連なっているという構造です。

上の図は MSDN
のパクリなんですが(著作権的にやばい?)こんな構成をしております。つまり「¥」で区切る単位ごとに SHITEMID
があって、その中に、サイズ(cb)と特定の識別子(adID) が入っています。識別子は可変長なのでその長さは cb
は全部ばらばらです。上の例で言うと My Computer を表す一つ目の cb が格納されてるアドレスに cb
サイズ加えることによって次の c:\ を示す識別子を取得することができる、という構造です。
そして一番最後は2バイトの NULL になっています(おそらく文字列を UNICODE
で管理しているからでしょう)。たとえば、この全体の長さを知るには、それぞれの cb
の和を求める必要があります。ここで一つ問題になるのが、最初に My Compurter を取得して、次にその下の C:\
を取得し、MyComputer\C:\ という位置を表したいとします。
C:\
のフルパスに相当する ITEMIDLIST を取得したい場合は、
となっているわけなので、親である My Computer の最後の NULL を取り外してその後ろに c:\ の ITEMIDLIST
をくっつけます。この作業がバイト単位のコピーで泥臭い感じがしますが、この辺もクラスにまとめてしまいましょう。で、問題ってなんだったかというと、このときに親にくっつける分のメモリは
IMalloc というインタフェースから取得する必要があり、使い終わったら開放しなければならないということです。
たぶん Shell が管理している関係上だと思うんですけど面倒臭いですね。ということで自信が無いのですが(メモリリークしてるかも。してたら教えてください)クラス化しました。最初は
CIDLMgr という管理クラスに ITEMIDLIST を渡して CIDLMgr::Append メソッドを呼び、使い終わったメモリは
CIDLMgr::Free() で開放。CIDLMgr のコンストラクタとデストラクタで IMalloc
インタフェースを生成して開放する見たいなものをつくりました。
これはこれで
IMalloc を一つしか作らないという点でうまく行ったのですが ITEMIDLIST を直接扱う必要がありました。そこで
ITEMIDLIST を CIDL というクラスにすることにしますが、ここで ITEMIDLIST で使い終わった
メモリをできるだけ自然に(デストラクタなどで)開放する手段が欲しいところです。
たとえば、Append
する際や開放する際には IMalloc のインタフェースを利用する必要があるので、CIDL のメンバとして
IMallocPtr を持つというアイディアはいいと思うのですが、単純にこうしてしまうと CIDL ごとに IMalloc
をインスタンス化する必要があります。そこで IMalloc
へのポインタをクラスのインスタンスに持たせるけど実体は1つでいいということで、シングルトンを採用しました。
シングルトンがわからない人はデザインパターンとかで検索してみてください。より詳しい説明が得られると思います。
-
TreeView
の動的作成
さて、そろそろ
WTL
と関係ない話題にも飽きてきたころだと思うので、実装に入りましょう。今回はダイアログベースではないので、ダイアログエディタでコントロールを並べて・・・といったようなことができません。そこで、これらは動的に作成することになります。
まず、CFolderView のメンバ変数として CTreeViewCtrl のポインタを宣言します。
/// 宣言(CFolderView.h)
CTreeViewCtrl* m_pcTreeViewCtrl;
CTreeViewCtrl の寿命を管理するために、ここでは OnCreate で生成し、デストラクタで破棄するようにしました。
/// ツリーの作成
m_pcTreeViewCtrl = new CTreeViewCtrl;
m_pcTreeViewCtrl->Create(m_hWnd, rcDefault, _T("FolderView"),
WS_CHILD | WS_VISIBLE | TVS_HASBUTTONS | TVS_HASLINES,
NULL, IDC_FOLDER_TREE);
今回作成する
TreeView
にはツリーの線とそれを開閉するボタンが必要なので、スタイルとして指定します。また、各ノードを展開したときにその中身を取得しますので、展開したことを知る必要があります。そのため通知イベントを利用するのですが、TreeView
を識別する ID が必要になりますので、最後の引数として IDC_FOLDER_TREE を指定しました。この ID
はリソースとして作成する必要があるので注意してください。
破棄はデストラクタで行います。今回例外処理などは書いていませんが、万が一 new
に失敗して確保されなかったときのために、このように書くようにしています( NULL ポインタを delete
しても問題なかったと思いますが)。
CFolderView::~CFolderView()
{
if ( m_pcTreeViewCtrl ) {
delete m_pcTreeViewCtrl;
}
}
CFolderView を子に持つフレームのサイズが変更されたときには、CFolderVIew
のサイズもそれに合わせて変更する必要があります。これに対応するには WM_SIZE
イベントで親のクライアント領域に合わせることで対処します。
LRESULT CFolderView::OnSize(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
CRect cRect;
GetClientRect(cRect);
m_pcTreeViewCtrl->MoveWindow(cRect);
return 0;
}
基本的にはこのような流れで動的にコントロールを作成することができます。
フォルダツリーを構築する作業は、特定の位置のファイル情報を取得して、その名前とアイコンと、ITEMIDLIST(これは後々使う)をツリーに格納することを繰り返すことになります。この情報を取得するためには、SHGetFileInfo
という関数を使います。この関数はパス、または ITEMIDLISTを受け取って、そのファイル、フォルダ、ショートカット、ドライブなどの情報を
SHFILEINFO 構造体として返します。
つまり、
1.ルートから現在注目するフォルダまでのフルパスに相当する ITEMIDLIST を構築する。
2.その一つ下の階層に位置するオブジェクトの一覧を取得(IShellFolder::EnumObjectsを利用)
3.1.で作成した ITEMIDLIST に2.で列挙された ITEMIDLIST を連結したものを SHGetFileInfo
関数に渡して SHFILEINFO 構造体を取得
4.その情報を元に TreeView に格納する情報を取得して格納する
と言うことになります。イメージはつかめたでしょうか?
最初のデスクトップは特殊なので、SHGetDesktopFolder という関数を使って ITEMIDLIST
を取得しました。また、デスクトップを表す IShellFolder はよく利用するので CFolderView
クラスのメンバ変数として宣言します。IShellFolder*
としたいところですが、参照カウントの管理を簡単にしたいのでスマートポインタとして宣言します。スマートポインタとして宣言するには、
IShellFolderPtr m_pIShellFolder;
というように書きます。このように書くためは ComDef.h というヘッダファイルをインクルードする必要があります。今回のサンプルでは、stdafx.h
でインクルードしています。
それから一点気をつけることは、Tree
に追加する情報ではアイコンのハンドルではなく、アイコンのインデックスを指定するということです。どういうことかというと、SHGetFileInfo
関数はその戻り値にシステムイメージリストと呼ばれる特殊なイメージリストのハンドルを返すからです。
Shell
にアクセスするプログラムがいちいちアイコンを作っていたのではリソース的にもよろしくないということなのでしょう。ですから、TreeView
のイメージリストには、SHGetFileInfo の戻り値のシステムイメージリストを設定し、各 TreeItem
ではそのインデックスを指定するということになります。こうすることによって取得するのが面倒くさいネットワークのアイコンやドライブのアイコンも気にすることなく同じコードを利用できます。
ここまででおそらくデスクトップの内容がツリーに表示されたことと思います。しかし、Explorer
などと比較するとファイルの並び順が違ったり、下の階層を持つフォルダに展開用のボタン(「田」みたいなやつ)がありません。このボタンをつける条件は、サブフォルダまたは子に何らかのファイルを持つものということになります。今回は、SHGetFileInfo
で取得した SHFILEINFO 構造体の情報と、WTL の CFindFile クラスを利用しました。
また、上記の手順で追加されたボタンを展開したときのイベントは TreeView の通知メッセージ TVN_ITEMEXPANDING
として通知されますので、以下の用にハンドラを作成しました。
BEGIN_MSG_MAP(CFolderView)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_SIZE, OnSize)
NOTIFY_HANDLER(IDC_FOLDER_TREE, TVN_ITEMEXPANDING, OnItemExpanding)
END_MSG_MAP()
ボタンを展開したとき(OnItemExpanding)は、展開されたフォルダの
hTreeItem を取得してそれを基準とする情報を取得します。
こうして追加された
TreeView のアイテムは名前順とかで並んでいると思うのですが、これを Explorer と同じ順番で並べたいと思います。これは
ITEMIDLIST で並べたものなので、ITEMIDLIST 同士の比較方法と、TreeView
のアイテムを独自の評価方法でソートする方法を調べました。
ITEMIDLIST
の比較方法には IShellFolder::CompareIDs が利用できます。また TreeView
で独自の評価関数をソートに適用するためには、コールバック関数を登録する必要がありました。TVSORTCB という構造体に親となる
HTREEITEM と比較関数のアドレスと比較に利用するパラメータを渡します。
比較には
IShellFolder を利用するのでこれを lParam に渡すといいでしょう。上記の手順によって無事 Explorer
と同じ並びになりました。
さて、今回の
FolderView
には問題があります。そう、最新の状態を反映していないということです。ここは今回は見送りますので読者の皆さんがチャレンジしてみてください。
今回プログラムは割と早くできたんですが、その内容をうまく説明するのが難しかったです。そのためこの説明でわかっていただけたか不安ですが(コメントはできるだけつけたつもりです)、わからない点は掲示板などにどんどん書き込んでください。ただし何分複雑な内容なので一度は自分でプログラムを組んでみてください。
ここまでの内容はこちらからダウンロードできます(85.7
KB)
次回は
FolderView にコンテキストメニューをつけて見ます。
第7回へ