L'Isola di Niente
L'Isola di Niente » .NET Tips » C# IronPython C++/CLI で SHGetFileInfo

C# IronPython C++/CLI で SHGetFileInfo

C# は P/Invoke を利用して WPF から使う方法。
IronPython は ctypes モジュールから WindowsAPI を呼ぶ方法。
C++/CLI は直接 SHGetFileInfo 関数を使う方法。

で書いていきます、コードはすべてドラッグ&ドロップで表示させる方法に書き換えております。

C# (UNICODE 関数及び x64 Windows 対応コード)

UNICODE 関数を使う為に SHFILEINFOW 構造体及び SHGetFileInfoW 関数を利用します。
SHFILEINFOW 構造体の第二引数は int です、IntPtr では x64 環境では構造体サイズの違いでコケます。
LLP64 である x64 Windows はポインタが 64bit、int は 32bit のままです。

using System;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Runtime.InteropServices;
using System.Windows.Interop;

namespace icon_cs
{
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct SHFILEINFOW
    {
        public IntPtr hIcon;
        public int iIcon;
        public uint dwAttributes;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        public string szDisplayName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
        public string szTypeName;
    }

    public enum ShellFileInfoFlags : uint
    {
        SHGFI_ICON = 0x000000100,
        SHGFI_DISPLAYNAME = 0x000000200,
        SHGFI_TYPENAME = 0x000000400,
        SHGFI_ATTRIBUTES = 0x000000800,
        SHGFI_ICONLOCATION = 0x000001000,
        SHGFI_EXETYPE = 0x000002000,
        SHGFI_SYSICONINDEX = 0x000004000,
        SHGFI_LINKOVERLAY = 0x000008000,
        SHGFI_SELECTED = 0x000010000,
        SHGFI_ATTR_SPECIFIED = 0x000020000,
        SHGFI_LARGEICON = 0x000000000,
        SHGFI_SMALLICON = 0x000000001,
        SHGFI_OPENICON = 0x000000002,
        SHGFI_SHELLICONSIZE = 0x000000004,
        SHGFI_PIDL = 0x000000008,
        SHGFI_USEFILEATTRIBUTES = 0x000000010
    }

    public enum FileAttributesFlags : uint
    {
        FILE_ATTRIBUTE_ARCHIVE = 0x00000020,
        FILE_ATTRIBUTE_ENCRYPTED = 0x00004000,
        FILE_ATTRIBUTE_HIDDEN = 0x00000002,
        FILE_ATTRIBUTE_NORMAL = 0x00000080,
        FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000,
        FILE_ATTRIBUTE_OFFLINE = 0x00001000,
        FILE_ATTRIBUTE_READONLY = 0x00000001,
        FILE_ATTRIBUTE_SYSTEM = 0x00000004,
        FILE_ATTRIBUTE_TEMPORARY = 0x00000100
    }

    public class Win32
    {
        //
        [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
        public static extern IntPtr SHGetFileInfoW(
            string pszPath,
            FileAttributesFlags dwFileAttributes,
            ref SHFILEINFOW psfi,
            uint cbSizeFileInfo,
            ShellFileInfoFlags uFlags);
    }

    public class Win32FileInfo
    {
        public static bool GetFileInfo(string fileName, ref CItem item)
        {
            if (!System.IO.File.Exists(fileName))
                return false;
            SHFILEINFOW shinfo = new SHFILEINFOW();
            Win32.SHGetFileInfoW(
                    fileName,
                    FileAttributesFlags.FILE_ATTRIBUTE_NORMAL,
                    ref shinfo,
                    (uint)Marshal.SizeOf(shinfo),
                    ShellFileInfoFlags.SHGFI_ICON |
                    ShellFileInfoFlags.SHGFI_LARGEICON |
                    ShellFileInfoFlags.SHGFI_TYPENAME |
                    ShellFileInfoFlags.SHGFI_DISPLAYNAME
                );
            item.image = Imaging.CreateBitmapSourceFromHIcon(
                    shinfo.hIcon,
                    new Int32Rect(0, 0, 32, 32),
                    BitmapSizeOptions.FromEmptyOptions()
                );
            item.type = shinfo.szTypeName;
            item.name = shinfo.szDisplayName;
            return true;
        }
    }

	public class CItem
	{
		public BitmapSource image { get; set; }
		public string name { get; set; }
		public string type { get; set; }
	}
}

shell32.dll が気になる人もいるかもしれませんが x64 でもこの名前は同じです。
WOW64 では WOW64 用の 32bit shell32.dll が別に存在します。

関数名指定や SHGetFileInfoW を利用するには CharSet = CharSet.Unicode 指定が必要です。
StructLayout, DllImport だけでは Ansi 関数を呼び出したり文字列を Ansi 化してしまいます。

msdn: 文字セットの指定
msdn: 文字列に対する既定のマーシャリング

このコードを使って WPF ウインドウに表示させるコード。
Visual Studio を使わず Build できるようにコードのみで実装してみました。
というか WPF ってこんなに簡単かつ便利なのに何故イマイチ普及しないのか?

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using icon_cs;

namespace iicon
{
	public class FileInfoWindow : Window
	{
		Image _image;
		TextBlock[] _texts;

		public FileInfoWindow()
		{
			this.Title = "Get FileInfo";
			this.Drop += new DragEventHandler(OnDrop);
			this.AllowDrop = true;
			this.SizeToContent = SizeToContent.WidthAndHeight;
			this.ResizeMode = ResizeMode.NoResize;
			// Controls
			var panel = new StackPanel();
			this.Content = panel;
			var b = new TextBlock(new Bold(new Run("Get FileInfo as Droped File")));
			panel.Children.Add(b);
			_image = new Image();
			_image.Height = _image.Width = 32;
			panel.Children.Add(_image);
			// Create TextBlock List
			_texts = new TextBlock[4];
			for (int i=0; i<4; i++)
			{
				TextBlock tb = new TextBlock();
				panel.Children.Add(tb);
				_texts.SetValue(tb, i);
			}
		}

		void OnDrop(object sender, DragEventArgs e)
		{
			var filenames = (string[])e.Data.GetData(DataFormats.FileDrop);
			var item = new CItem();
			if (Win32FileInfo.GetFileInfo(filenames[0], ref item))
			{
				// GetSystem.IO.FileInfo
				var ioinfo = new System.IO.FileInfo(filenames[0]);
				// icon
				_image.Source = item.image;
				// FileName
				_texts[0].Text = item.name;
				// Updated
				_texts[1].Text = ioinfo.LastWriteTime.ToString("yyyy/MM/dd HH:mm");
				// Type Name
				_texts[2].Text = item.type;
				// File Size
				_texts[3].Text = Math.Ceiling(ioinfo.Length / 1024.0).ToString("#,##0 KB");
			}
		}

		[STAThread]
		public static void Main()
		{
			var app = new Application();
			app.Run(new FileInfoWindow());
		}
	}
}

まとめた ZIP も置いておきます。
build.bat を W クリックして作成された exe を起動しウインドウにファイルをドロップしてみましょう。

icon_cs.zip

img/get_fileinfo.png

IronPython

ctypes から WindowsAPI を呼ぶには実際の関数名や構造体名で呼ぶ必要があります。
現状では UNICODE 関数を利用したほうが賢明でしょう。
x64 対応は ipyw.exe, ipyw64.exe のどちらを使うかでどちらの shell32.dll かが決まるはず。

文字列は string 型、 Python の str 型、 ctypes の c_wchar が全部自動で相互変換される。
少しの手間で SHGetFileInfoW 関数がそのまま使えるのでわりと簡単。

# -*- coding: UTF-8 -*-

"""
    get_file_detailst.py
    System.IO.FileInfo and SHGetFileInfoW Windows API use
    both the x86 and x64 editions
"""

import clr

clr.AddReferenceByPartialName("PresentationCore")
clr.AddReferenceByPartialName("PresentationFramework")
clr.AddReferenceByPartialName("WindowsBase")

from System import *
from System.IO import *
from System.Windows import *
from System.Windows.Controls import *
from System.Windows.Documents import *
from System.Windows.Media.Imaging import *
from ctypes import *

MAX_PATH = 260
HICON = c_void_p

# ShellFileInfoFlags
SHGFI_ICON = 0x000000100
SHGFI_DISPLAYNAME = 0x000000200
SHGFI_TYPENAME = 0x000000400
SHGFI_ATTRIBUTES = 0x000000800
SHGFI_ICONLOCATION = 0x000001000
SHGFI_EXETYPE = 0x000002000
SHGFI_SYSICONINDEX = 0x000004000
SHGFI_LINKOVERLAY = 0x000008000
SHGFI_SELECTED = 0x000010000
SHGFI_ATTR_SPECIFIED = 0x000020000
SHGFI_LARGEICON = 0x000000000
SHGFI_SMALLICON = 0x000000001
SHGFI_OPENICON = 0x000000002
SHGFI_SHELLICONSIZE = 0x000000004
SHGFI_PIDL = 0x000000008
SHGFI_USEFILEATTRIBUTES = 0x000000010

# FileAttributesFlags
FILE_ATTRIBUTE_ARCHIVE = 0x00000020,
FILE_ATTRIBUTE_ENCRYPTED = 0x00004000,
FILE_ATTRIBUTE_HIDDEN = 0x00000002,
FILE_ATTRIBUTE_NORMAL = 0x00000080,
FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000,
FILE_ATTRIBUTE_OFFLINE = 0x00001000,
FILE_ATTRIBUTE_READONLY = 0x00000001,
FILE_ATTRIBUTE_SYSTEM = 0x00000004,
FILE_ATTRIBUTE_TEMPORARY = 0x00000100

class SHFILEINFOW(Structure):
    """
        typedef struct
    """
    _fields_ = [("hIcon", HICON),
                ("iIcon", c_int),
                ("dwAttributes", c_uint),
                ("szDisplayName", c_wchar * MAX_PATH),
                ("szTypeName", c_wchar * 80)]

class GetFileInfoWindow(Window):
    """
        Get FileInfo as Droped File
    """
    def __init__(self):
        """
            Window.__init__(self) is No
            Constructor has been
        """
        self.Title = "Get FileInfo"
        self.Drop += self.on_drop
        self.AllowDrop = True
        self.SizeToContent = SizeToContent.WidthAndHeight
        self.ResizeMode = ResizeMode.NoResize
        # Controls
        panel = StackPanel()
        self.Content = panel
        b = TextBlock()
        b.Inlines.Add(Bold(Run("Get FileInfo as Droped File")))
        panel.Children.Add(b)
        self.image = Image()
        self.image.Height = 32
        self.image.Width = 32
        panel.Children.Add(self.image)
        # Create TextBlock List
        self.texts = []
        for i in range(4):
            t = TextBlock()
            panel.Children.Add(t)
            self.texts.append(t)

    def on_drop(self, sender, e):
        """
            Get Dropped File Details
        """
        filename = e.Data.GetData(DataFormats.FileDrop)[0].ToString()
        shinfo = SHFILEINFOW()
        r = windll.shell32.SHGetFileInfoW(
                filename,
                FILE_ATTRIBUTE_NORMAL,
                byref(shinfo),
                sizeof(shinfo),
                SHGFI_ICON |
                SHGFI_LARGEICON |
                SHGFI_TYPENAME |
                SHGFI_DISPLAYNAME
            )
        if r == 0:
            self.texts[0].Text = "error"
        else:
            # Get System.IO.FileInfo
            ioinfo = IO.FileInfo(filename)
            # Icon
            bms = Interop.Imaging.CreateBitmapSourceFromHIcon(
                    IntPtr(shinfo.hIcon),
                    Int32Rect(0, 0, 32, 32),
                    BitmapSizeOptions.FromEmptyOptions()
                )
            self.image.Source = bms
            # File Name
            self.texts[0].Text = shinfo.szDisplayName
            # Updated
            self.texts[1].Text = ioinfo.LastWriteTime.ToString("yyyy/MM/dd HH:mm")
            # Type Name
            self.texts[2].Text = shinfo.szTypeName
            # File Size
            self.texts[3].Text = Math.Ceiling(ioinfo.Length / 1024.0).ToString("#,##0 KB")

if __name__ == "__main__":
    Application().Run(GetFileInfoWindow())

UTF-8 で保存するのを忘れずに。
これで上記 C# で作成したものと同じウインドウになるはずです、初期化が遅いけど...

C++/CLI

C++/CLI ならそのまんま WindowsAPI が使えるので簡単だと思ったら大間違いだった。
*wchar_t と System::String を変換しないといけない、marshal_as か marshal_context クラスにて相互変換する。

C++ におけるマーシャリングの概要
marshal_as
marshal_context クラス

C++/CLI は Visual Studio が無いとお手上げなので素直に Visual Studio を利用する。
更に WPF が使えないのでしかたなく WindowsForm という MS がバカの為に残しているゴミを使う。
適当にプロジェクトを作って stdafx.h に以下を追記する。
#pragma once

#pragma comment(lib, "shell32.lib")
#pragma comment(lib, "user32.lib")
// TODO: プログラムに必要な追加ヘッダーをここで参照してください。
#using <mscorlib.dll>
#include <windows.h>
#include <shellapi.h>
#include <tchar.h>
#include <msclr\marshal.h>

SHGetFileInfo で shell32.dll、HICON の破棄で user32.dll への参照が必要。
SHGetFileInfo はビルド方法によって SHGetFileInfoA か SHGetFileInfoW に振り分けされる。
のは SDK プログラミングの時と同じようだ。

そして Form に PictureBox と ListBox を貼り付ける。
次に Form のプロパティで AllowDrop を true に。
後はイベントハンドラで
// 以下を忘れずに
using namespace msclr::interop;

private: System::Void Form1_DragDrop(System::Object^  sender, System::Windows::Forms::DragEventArgs^  e) {
			// C++/CLI の配列はこう宣言するらしい
			array<System::String ^> ^ filenames;
			// ドロップファイル名取得
			filenames = (array<System::String ^> ^)e->Data->GetData(DataFormats::FileDrop);
			// 初期化
			this->listBox1->Items->Clear();
			wchar_t szFileName[1024];
			marshal_context ^ context = gcnew marshal_context();
			// 変換して szFileName に取り込む
			// const にする必要があるので ToString() している
			swprintf_s(szFileName, 1024, context->marshal_as<const wchar_t*>(filenames[0]->ToString()));
			// もういらないからとっとと削除
			delete context;
			// マネージド
			System::IO::FileInfo ^ ioInfo = gcnew System::IO::FileInfo(filenames[0]);
			// アンマネージド
			::SHFILEINFO fi;
			::SHGetFileInfo(szFileName, FILE_ATTRIBUTE_NORMAL, &fi, sizeof(fi),
				SHGFI_TYPENAME | SHGFI_ICON | SHGFI_LARGEICON | SHGFI_USEFILEATTRIBUTES);
			// アイコン
			System::Drawing::Icon ^ ico = Icon->FromHandle((IntPtr)fi.hIcon);
			this->pictureBox1->Image = ico->ToBitmap();
			// HICON の破棄はコレでいいのかな?
			// ico->Handle だけじゃ error なんだが
			// http://msdn.microsoft.com/ja-jp/library/system.drawing.icon.fromhandle.aspx
			DestroyIcon((HICON)ico->Handle.ToPointer());
			// FileName
			this->listBox1->Items->Add(System::IO::Path::GetFileName(filenames[0]));
			// Updated
			this->listBox1->Items->Add(ioInfo->LastWriteTime.ToString("yyyy/MM/dd HH:mm"));
			// Type Name
			this->listBox1->Items->Add(marshal_as<System::String ^>(fi.szTypeName));
			// File Size
			System::Int32 nSize = static_cast<System::Int32>(System::Math::Ceiling(ioInfo->Length / 1024.0));
			this->listBox1->Items->Add(nSize.ToString("#,##0") + " kb");
			 
		 }
private: System::Void Form1_DragEnter(System::Object^  sender, System::Windows::Forms::DragEventArgs^  e) {
			if( e->Data->GetDataPresent(DataFormats::FileDrop))
			{
				e->Effect = DragDropEffects::All;
			}
			else
			{
				e->Effect = DragDropEffects::None;
			}
		 }

Windows SDK で書くより面倒くさい...
Copyright(C) sasakima-nao All rights reserved 2002 --- 2017.