在檔案總管、自訂開啟對話框、拖放介面等場景,顯示檔案原生圖示能大幅提升使用者辨識度。.NET 提供兩條路徑:純 Managed Icon.ExtractAssociatedIcon 簡單易用;P/Invoke Shell32 SHGetFileInfo 功能完整(支援大/小圖、資料夾、僅副檔名、Overlay 圖層)。本文完整實作並比較兩者優劣。
using System.Drawing;
public static Icon GetFileIcon(string filePath, bool large = false)
{
// 僅支援 32x32(large=false)或 16x16,無法指定 48x48、256x256 等尺寸
Icon icon = Icon.ExtractAssociatedIcon(filePath);
return large ? new Icon(icon, 32, 32) : new Icon(icon, 16, 16);
}
// 使用範例
Icon small = GetFileIcon(@"C:\Windows\notepad.exe");
Icon large = GetFileIcon(@"C:\Windows\notepad.exe", true);
pictureBox1.Image = large.ToBitmap();
優點:零 P/Invoke、自動處理圖示快取、程式碼極短。
缺點:僅支援系統預設兩種尺寸、需實體檔案存在、無法取得資料夾圖示、無 Overlay(捷徑箭頭、鎖定圖示等)。
定義結構與 P/Invoke 簽名:
using System;
using System.Drawing;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct SHFILEINFO
{
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 static class Shell32
{
[DllImport("Shell32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr SHGetFileInfo(
string pszPath, uint dwFileAttributes,
ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags);
[DllImport("User32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DestroyIcon(IntPtr hIcon);
}
[Flags]
public enum FileInfoFlags : uint
{
SHGFI_ICON = 0x100,
SHGFI_DISPLAYNAME = 0x200,
SHGFI_TYPENAME = 0x400,
SHGFI_ATTRIBUTES = 0x800,
SHGFI_ICONLOCATION = 0x1000,
SHGFI_EXETYPE = 0x2000,
SHGFI_SYSICONINDEX = 0x4000,
SHGFI_LINKOVERLAY = 0x8000,
SHGFI_SELECTED = 0x10000,
SHGFI_ATTR_SPECIFIED = 0x20000,
SHGFI_LARGEICON = 0x0, // 32x32
SHGFI_SMALLICON = 0x1, // 16x16
SHGFI_OPENICON = 0x2, // 打開狀態資料夾
SHGFI_SHELLICONSIZE = 0x4, // 系統圖示大小(通常 32x32)
SHGFI_PIDL = 0x8,
SHGFI_USEFILEATTRIBUTES = 0x10, // pszPath 僅為副檔名,不需實體檔案
SHGFI_ADDOVERLAYS = 0x20, // 疊加捷徑/共用/鎖定 overlay
SHGFI_OVERLAYINDEX = 0x40
}
public static Icon GetIcon(string pathOrExtension, bool large, bool useFileAttributes = false)
{
var shfi = new SHFILEINFO();
uint flags = FileInfoFlags.SHGFI_ICON | FileInfoFlags.SHGFI_SYSICONINDEX;
flags |= large ? FileInfoFlags.SHGFI_LARGEICON : FileInfoFlags.SHGFI_SMALLICON;
if (useFileAttributes) flags |= FileInfoFlags.SHGFI_USEFILEATTRIBUTES;
IntPtr h = Shell32.SHGetFileInfo(pathOrExtension, 0, ref shfi, (uint)Marshal.SizeOf(shfi), flags);
if (h == IntPtr.Zero || shfi.hIcon == IntPtr.Zero) return null;
Icon icon = Icon.FromHandle(shfi.hIcon).Clone() as Icon; // Clone 避免原生控制權問題
Shell32.DestroyIcon(shfi.hIcon); // 釋放非受控資源
return icon;
}
// 1. 實體檔案大圖示(含 overlay)
Icon exeLarge = GetIcon(@"C:\Windows\notepad.exe", true);
// 2. 僅副檔名取圖示(無需實體檔案)
Icon pdfSmall = GetIcon(".pdf", false, useFileAttributes: true);
// 3. 資料夾圖示(大/小、開啟/關閉)
Icon folderLarge = GetFolderIcon(true);
Icon folderOpen = GetFolderIcon(true, open: true);
public static Icon GetFolderIcon(bool large, bool open = false)
{
var shfi = new SHFILEINFO();
uint flags = FileInfoFlags.SHGFI_ICON | FileInfoFlags.SHGFI_USEFILEATTRIBUTES;
flags |= large ? FileInfoFlags.SHGFI_LARGEICON : FileInfoFlags.SHGFI_SMALLICON;
if (open) flags |= FileInfoFlags.SHGFI_OPENICON;
IntPtr h = Shell32.SHGetFileInfo("", 0x10, ref shfi, (uint)Marshal.SizeOf(shfi), flags); // FILE_ATTRIBUTE_DIRECTORY
if (h == IntPtr.Zero || shfi.hIcon == IntPtr.Zero) return null;
Icon icon = Icon.FromHandle(shfi.hIcon).Clone() as Icon;
Shell32.DestroyIcon(shfi.hIcon);
return icon;
}
| 特性 | Icon.ExtractAssociatedIcon | SHGetFileInfo (P/Invoke) |
|---|---|---|
| 程式複雜度 | 極低(1 行) | 中(需結構、P/Invoke、資源釋放) |
| 支援尺寸 | 16x16, 32x32 | 16x16, 32x32, 48x48 (Vista+), 256x256 (Win7+) |
| 需實體檔案 | 是 | 否(SHGFI_USEFILEATTRIBUTES) |
| 資料夾圖示 | 不支援 | 支援(含開啟狀態) |
| Overlay 圖層 | 不支援 | 支援(SHGFI_ADDOVERLAYS) |
| 顯示名稱/類型名 | 不支援 | 支援(SHGFI_DISPLAYNAME/TYPENAME) |
| 效能(大量檔案) | 較慢(每次查詢註冊表) | 較快(Shell 快取),可配合 SHGFI_SYSICONINDEX 配合 ImageList 批次繪製 |
| 跨平台 | 僅 Windows | 僅 Windows |
// 取得系統影像清單控制碼(SHGFI_SYSICONINDEX)
var shfi = new SHFILEINFO();
Shell32.SHGetFileInfo("", 0x10, ref shfi, (uint)Marshal.SizeOf(shfi),
FileInfoFlags.SHGFI_ICON | FileInfoFlags.SHGFI_SYSICONINDEX | FileInfoFlags.SHGFI_SMALLICON);
IntPtr hSmall = shfi.hIcon; // 此時 hIcon 實為系統 ImageList 索引
// 建立 .NET ImageList 並關聯系統控制碼
var imgList = new ImageList();
imgList.Handle = hSmall; // 直接掛載系統 ImageList
listView1.SmallImageList = imgList;
// 新增項目時只需指定 ImageIndex = shfi.iIcon
listView1.Items.Add("Document.pdf", shfi.iIcon);
此法避免重複提取圖示、大幅降低記憶體與 GDI 物件壓力,適合檔案總管類應用。
Icon.FromHandle 取得的 Icon 必須 .Clone() 再 DestroyIcon 原始 hIcon,否則會導致 GDI 物件洩漏。依專案需求選擇:原型驗證、簡易工具用 ExtractAssociatedIcon;正式產品、檔案總管、需 Overlay/資料夾/高解析度圖示者,請投資實作 SHGetFileInfo 封裝庫,長期受益無窮。