« システムイメージリストを使用したアイコン表示(1) | トップページ | ファイルに関連づけされたアイコンをImageクラスで取得 »

2010年11月25日 (木)

システムイメージリストを使用したアイコン表示(2)

※2011/03/27 kurimu さんからの御指摘内容を本記事に反映させました。

さて前回、システムイメージリストのハンドルを取得して、ListView への登録を行なうところまで実装しました。今回は実際にアイコンを表示させます。

1.とりあえずアイコンを表示させてみる

既にシステムイメージリストと ListView の関連付けを行なっているので、実はアイコンインデックスを指定するだけでアイコンが表示されます。
例えば前回最終時点のソースに、1行加えて、ImageIndex を指定してみます。

/// <summary>
/// 指定されたファイル名の文字列配列をListViewに登録する
/// </summary>
/// <param name="fileNames">表示対象のファイル名を格納した配列</param>
private void PutFileListView(string[] fileNames)
{
    foreach (string file in fileNames)
    {
        ListViewItem lvi = new ListViewItem(Path.GetFileName(file));
        lvi.ImageIndex = 4;
        lsvFileList.Items.Add(lvi);
    }
}

これで実行すると、アイコンが表示されます。右図は、私の環境での実行直後の状態です。

皆さんの大多数の環境では、これと違ったアイコンが表示されると思いますが、アイコンは表示される筈です。アイコンインデックス値の特定値がどのアイコンを意味するかは、それぞれの環境に依存します。つまり、正しいアイコンインデックスを取得出来れば、それだけで表示出来るように既になっているのです。

では、正しいアイコンインデックスをどうやって取得するかというと、またまた Win32API の SHGetFileInfo を使用します。指定するフラグを、SHGFI_SYSICONINDEX に変えて使うだけです。

/// <summary>
/// 指定されたファイル名の文字列配列をListViewに登録する
/// </summary>
/// <param name="fileNames">表示対象のファイル名を格納した配列</param>
private void PutFileListView(string[] fileNames)
{
    SHFILEINFO shFileInfo = new SHFILEINFO();
    foreach (string file in fileNames)
    {
        ListViewItem lvi = new ListViewItem(Path.GetFileName(file));
        NativeMethods.SHGetFileInfo(file, 0, out shFileInfo,
            (uint)Marshal.SizeOf(shFileInfo),
            NativeMethods.SHGFI_SYSICONINDEX);
        int iconIndex = shFileInfo.iIcon;
        lvi.ImageIndex = iconIndex;

        lsvFileList.Items.Add(lvi);
    }
}

これで、正しいアイコンが表示されるようになる筈です。

これで完成?...いえ、まだです。確かにアイコンが表示されるようになりました。しかし例えば、デスクトップのフォルダ等、ショートカットファイルがあるフォルダを指定して、この一覧を見て下さい。

右図は私のデスクトップを指定した時の画面です(本当は市販アプリ等のショートカットファイルもあるのですが、編集して削除してます)。

この様に、アイコンは表示されていますが、ショートカットの印が表示されていない筈です。実は、この指定はアイコンインデックスの指定だけでは表示されません。

以前の関連記事「ListView/TreeViewにオーバーレイアイコンを表示する」とオーバーラップするのですが、アイコンのオーバーレイ表示が必要になって来ます。またその為の、オーバーレイ用インデックス値の取得も必要になります。

※補足情報
このサンプルでは、実在するファイルを指定するので必要ありませんが、実在しないファイルに対して、アイコンを表示したい場合もあるかと思います。例えば、書庫の中のメンバ一覧を表示する場合は、書庫を解凍しない限りファイルが実在しません。こういった場合に、上記指定のままですとアイコンインデックスが取得出来ません。しかし、フラグに NativeMethods.SHGFI_USEFILEATTRIBUTES を追加指定して、

        NativeMethods.SHGetFileInfo(file, 0, out shFileInfo,
            (uint)Marshal.SizeOf(shFileInfo),
            NativeMethods.SHGFI_SYSICONINDEX | NativeMethods.SHGFI_USEFILEATTRIBUTES);

といった指定にすれば、アイコンインデックスが取得出来ます。

2.オーバーレイも含めて正しいシステムアイコンを表示する

まず、オーバーレイ用のインデックス値の取得ですが、これは SHGetFileInfo のフラグ指定を追加するだけで取得出来ます。但し、通常のインデックス値が格納される構造体 SHFILEINFO の iIcon メンバに一緒に格納される様になります。具体的に説明すると、今まで未使用であった iIcon メンバの最上位部8ビットにオーバーレイアイコンのインデックス値が格納される様になります。

従って、ListViewItem の ImageIndex には、最上位部8ビットを除去(この部分のビットを全て0にする)した数値を格納する様にします。具体的には、以下の様に記述します。

/// <summary>
/// 指定されたファイル名の文字列配列をListViewに登録する
/// </summary>
/// <param name="fileNames">表示対象のファイル名を格納した配列</param>
private void PutFileListView(string[] fileNames)
{
    SHFILEINFO shFileInfo = new SHFILEINFO();
    foreach (string file in fileNames)
    {
        ListViewItem lvi = new ListViewItem(Path.GetFileName(file));
        NativeMethods.SHGetFileInfo(file, 0, out shFileInfo,
            (uint)Marshal.SizeOf(shFileInfo), NativeMethods.SHGFI_ICON |
            NativeMethods.SHGFI_SYSICONINDEX | NativeMethods.SHGFI_OVERLAYINDEX);
        // 最上位8ビットを除いた値をアイコンインデックス値とする
        int iconIndex = (shFileInfo.iIcon & 0xFFFFFF);
        lvi.ImageIndex = iconIndex;
        lsvFileList.Items.Add(lvi);
    }
}

ここで、SHGetFileInfo の第5引数に NativeMethods.SHGFI_ICON の指定を追加しています。理由はよく判りませんが、これを指定しないとオーバーレイ用のインデックス値が格納されません。
もちろんこれを指定するだけで済む事なら特に問題はないのですが、実はこの指定により、アイコンハンドルを取得する様になってしまいます。実際、NativeMethods.SHGFI_ICON を指定する事で、構造体 SHFILEINFO の hIcon メンバにも値が入る様になります。つまり、アイコンハンドル値が格納される様になります。
すると、アイコンハンドルが不要になった時点で、DestroyIcon を実行してやらないとリソースリークを招いてしまいます。ここでは、アイコンハンドルは使用しないので、すぐに DestroyIcon を実行出来ます。


private void PutFileListView(string[] fileNames)
{
    SHFILEINFO shFileInfo = new SHFILEINFO();
    foreach (string file in fileNames)
    {
        ListViewItem lvi = new ListViewItem(Path.GetFileName(file));
        NativeMethods.SHGetFileInfo(file, 0, out shFileInfo,
            (uint)Marshal.SizeOf(shFileInfo), NativeMethods.SHGFI_ICON |
            NativeMethods.SHGFI_SYSICONINDEX | NativeMethods.SHGFI_OVERLAYINDEX);
        // 最上位8ビットを除いた値をアイコンインデックス値とする
        int iconIndex = (shFileInfo.iIcon & 0xFFFFFF);
        lvi.ImageIndex = iconIndex;
        lsvFileList.Items.Add(lvi);
        if (shFileInfo.hIcon != IntPtr.Zero)
            NativeMethods.DestroyIcon(shFileInfo.hIcon);

    }
}

また、DestroyIcon が使える様に、NativeMethods クラスにも、DestroyIcon の定義を追加します。


/// <summary>
/// NativeMethods の概要の説明です。
/// </summary>
public class NativeMethods
{
    ...
    [DllImport("user32.dll", SetLastError=true)]
    public static extern bool DestroyIcon(IntPtr hIcon);

    ...
}

さて、話を元に戻して、残りのオーバーレイ表示の実装を行ないます。ここの詳細な説明は、前述の記事「ListView/TreeViewにオーバーレイアイコンを表示する」を参照して下さい。ここでは、実際に表示する様にしたコードを示します。

/// <summary>
/// 指定されたファイル名の文字列配列をListViewに登録する
/// </summary>
/// <param name="fileNames">表示対象のファイル名を格納した配列</param>
private void PutFileListView(string[] fileNames)
{
    SHFILEINFO shFileInfo = new SHFILEINFO();
    foreach (string file in fileNames)
    {
        ListViewItem lvi = new ListViewItem(Path.GetFileName(file));
        NativeMethods.SHGetFileInfo(file, 0, out shFileInfo,
            (uint)Marshal.SizeOf(shFileInfo), NativeMethods.SHGFI_ICON |
            NativeMethods.SHGFI_SYSICONINDEX | NativeMethods.SHGFI_OVERLAYINDEX);
        // 最上位8ビットを除いた値をアイコンインデックス値とする
        int iconIndex = (shFileInfo.iIcon & 0xFFFFFF);
        lvi.ImageIndex = iconIndex;
        lsvFileList.Items.Add(lvi);
        LVITEM lvitem = new LVITEM();
        lvitem.stateMask = NativeMethods.LVIS_OVERLAYMASK;
        // 最上位8ビットの値を、8-11ビット目に格納、それ以外のビットは 0 にする
        lvitem.state = ((shFileInfo.iIcon >> 16) & 0x0F00);
        NativeMethods.SendMessage(lsvFileList.Handle,
            NativeMethods.LVM_SETITEMSTATE, lvi.Index, ref lvitem);

        if (shFileInfo.hIcon != IntPtr.Zero)
            NativeMethods.DestroyIcon(shFileInfo.hIcon);

    }
}

また、API関連の定義ファイル NativeMethods.cs の方にも、LVITEM 構造体の定義と、SendMessage をオーバーロードして、別の型指定ができる様にした定義を追加します。

using System;
using System.Runtime.InteropServices;

namespace Acha_ya.SampleApplication
{
    ...
    [ StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto) ]
    public struct LVITEM
    {
        public uint mask;
        public int  iItem;
        public int  iSubItem;
        public int  state;
        public int  stateMask;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
        public string pszText;
        public int  cchTextMax;
        public int  iImage;
        public uint lParam;
        public int  iIndent;
    }
    /// <summary>
    /// NativeMethods の概要の説明です。
    /// </summary>
    public class NativeMethods
    {
        ...
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessage(IntPtr hWnd, uint Msg,
            int wParam, ref LVITEM lParam);
        ...
    }
}

これらを実装してから実行すると、右図のようにショートカットの印の付いたアイコンが表示される様になります。

注意点は、必ず lsvFileList.Items.Add(lvi); の実行後に実装することです。これより前で実行しても、ListView に該当する ListViewItem がないので、正しく表示されません。また、shFileInfo.iIcon の最上位8ビット部分だけの値を、LVITEM 構造体の state メンバの 8-11ビット目に格納する様にします。state メンバが 8ビット長ではないのはおかしいって? 確かにそうなのですが、実際にオーバーレイに使用出来るアイコン数はかなり小さいので、実質4ビット長で問題ないのです。実際の制限数は、ComCtl32.dllのバージョンが4.70以前は4個まで、4.71以降で15個までだそうです。とはいえ、もうぎりぎりですね。これ以上の制限数緩和はありえないって事でしょうか。

ちょっと話がそれてしまいましたが、とにかくこれで、オーバーレイも含めてシステムアイコンを正しく表示する事が出来ます。これでようやく完成です。
では、最後に最終的なソース一式をまたCAB形式にしてここに置いておきます。
「SystemIcon3a.cab」をダウンロード


« システムイメージリストを使用したアイコン表示(1) | トップページ | ファイルに関連づけされたアイコンをImageクラスで取得 »

C# Tips」カテゴリの記事

コメント

当方 趣味で win7 vcs2010 環境でwinAPを開発しているのですが
ちょうど探していたサンプルをこちらで見つけたので
「SystemIcon3.cab」をダウンロードしてテストしたところ、リークが発生していました。
そこで下記のようにメモリ解放を追加したところうまくいきました。
今後に役立ててもらえれば幸いです。

クラス内で追加-> [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = CharSet.Auto)]
クラス内で追加-> extern static bool DestroyIcon(IntPtr handle);

     foreach (string file in fileNames)
{
移動-> SHFILEINFO shFileInfo = new SHFILEINFO();

ListViewItem lvi = new ListViewItem(Path.GetFileName(file));
NativeMethods.SHGetFileInfo(file, 0, out shFileInfo,
(uint)Marshal.SizeOf(shFileInfo), NativeMethods.SHGFI_ICON |
NativeMethods.SHGFI_SYSICONINDEX | NativeMethods.SHGFI_OVERLAYINDEX);


// 最上位8ビットを除いた値をアイコンインデックス値とする
int iconIndex = (shFileInfo.iIcon & 0xFFFFFF);
lvi.ImageIndex = iconIndex;
listView1.Items.Add(lvi);

LVITEM lvitem = new LVITEM();
lvitem.stateMask = NativeMethods.LVIS_OVERLAYMASK;

// 最上位8ビットの値を、8-11ビット目に格納、それ以外のビットは 0 にする
lvitem.state = ((shFileInfo.iIcon >> 16) & 0x0F00);
NativeMethods.SendMessage(listView1.Handle,
NativeMethods.LVM_SETITEMSTATE, lvi.Index, ref lvitem);

追加-> if (shFileInfo.hIcon != IntPtr.Zero)
追加-> DestroyIcon(shFileInfo.hIcon);
}

--------------------------------------------------------------------------------

kurimuさん

御指摘ありがとうございます。

オーバーレイ機能を使用する場合、SHGetFileInfo 関数において SHGFI_ICON の指定が必要となるみたいですが、それに伴いアイコンハンドルもとられます。ご指摘のとおり、アイコンハンドルを取得した場合は DestroyIcon 関数を呼び出す必要があります。

最近、ブログ更新する時間がとれなくて半ば放置状態なのですが、何とか記事自体とサンプルソースを修正してみます。

以前 コメントを寄せたkurimuです。

システムイメージリストからアイコンインデックスを使用してimageListに表示させるのはこのサイトでできるようになりましたが imageListを使用せずに アイコンインデックスを使用してpicturebox等に直接表示する方法がわかりません。

もしわかるようなら、ヒントでも良いので教えてもらえませんでしょうか?

kurimu さん、こんにちは。返信遅くなりましたm(__)m

さて、質問の件ですが、以前自分のアプリでやっていた方法は、PictureboxのPaintイベント処理内で、
1.SHGetFileInfo(WinAPI関数)を使用して、アイコンハンドルを取得
2.取得したアイコンハンドルから Icon.FromHandle を使用して、Icon を取得
3.e.Graphics.DrawIcon を使用して、アイコンを描画
です。
多忙の為、ロジックをしっかり確認しきれてないですが。。。多分これでいけると思います。

この記事へのコメントは終了しました。

トラックバック


この記事へのトラックバック一覧です: システムイメージリストを使用したアイコン表示(2):

« システムイメージリストを使用したアイコン表示(1) | トップページ | ファイルに関連づけされたアイコンをImageクラスで取得 »