ホーム | Software Lab | Profile | Favorites | Diary | Links | About This Site | Feedback

 

 

第6回は、 前回のサンプル S2H_Conv のフォルダ表示用の View 「CFolderView」を実装します。

今回のポイント
スマートポインタを使って COM を体験しよう。

Shell プログラミングをしてみよう。
TreeView のソートをしてみよう。

  •  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)として提供しよう。このインタフェースは言語を選ばすに使えるが、その代わりいろいろと 手順や決まりがあるのでそれは守ってくださいという感じでしょうか。

    決まりには次のようなものがあります。

    • インタフェースのメソッドは HRESULT と呼ばれるエラーコードを表す値を返すのが基本

    • インタフェースは IUnknown と呼ばれる純粋仮想関数からなる抽象クラスを継承している必要がある

    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 を取得した時点で cb + abID + NULL(2 Byte)

    • C:\ を取得した時点で cb + abID + NULL(2 Byte)

    となっているわけなので、親である 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回へ

最終更新日 : 2005/04/13