Files
fluid-explorer/Services/Icons/ShellNative.cs
T

1152 lines
46 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.InteropServices;
namespace FluidExplorer.Services.Icons;
/// <summary>
/// Windows 外壳 / GDI 的原生互操作层:只放 P/Invoke、COM 接口、常量与结构体,
/// 不含任何 WinUI 依赖,因此可以被独立控制台探针直接链接复用。
/// </summary>
/// <remarks>
/// 本文件遵守「只用 Windows 11 原版组件」原则:所有图标都来自 imageres.dll / shell32.dll /
/// 各应用自己的图标资源,经 SHGetFileInfoW、SHGetImageList、IShellItemImageFactory 取回,
/// 不做任何自制或矢量化替代。
/// </remarks>
#if ICONPROBE
// 独立控制台探针(E:\deepseek\_icon_probe)链接本文件时定义 ICONPROBE,
// 以便直接驱动内部的 Win32/COM 步骤做验证;主工程编译时保持 internal。
public static class ShellNative
#else
internal static class ShellNative
#endif
{
// ─────────────────────────────────────────────────────────────
// 常量
// ─────────────────────────────────────────────────────────────
/// <summary>SHGetFileInfo 标志:取图标句柄。</summary>
internal const uint SHGFI_ICON = 0x000000100;
/// <summary>SHGetFileInfo 标志:取大图标(默认,值为 0)。</summary>
internal const uint SHGFI_LARGEICON = 0x000000000;
/// <summary>SHGetFileInfo 标志:取小图标。</summary>
internal const uint SHGFI_SMALLICON = 0x000000001;
/// <summary>SHGetFileInfo 标志:取系统图像列表中的索引(配合 SHGetImageList 拿任意尺寸)。</summary>
internal const uint SHGFI_SYSICONINDEX = 0x00004000;
/// <summary>SHGetFileInfo 标志:不访问磁盘,用 dwFileAttributes 直接推断图标(扩展名通用图标走这条)。</summary>
internal const uint SHGFI_USEFILEATTRIBUTES = 0x000000010;
/// <summary>SHGetFileInfo 标志:取打开状态的图标(文件夹张开形态)。</summary>
internal const uint SHGFI_OPENICON = 0x000000002;
/// <summary>SHGetFileInfo 标志:pszPath 传的是 ITEMIDLIST 指针(shell: 解析名走这条)。</summary>
internal const uint SHGFI_PIDL = 0x000000008;
/// <summary>
/// 系统图像列表尺寸标识:16x16(资源管理器列表视图的小图标)。
/// 实测(_icon_probe):kind=1 → ImageList_GetIconSize 返回 16x16。
/// </summary>
internal const int SHIL_SMALL = 1;
/// <summary>
/// 系统图像列表尺寸标识:32x32(资源管理器详细信息的标准大图标)。
/// ⚠ 实测值是 0 而不是 1:Windows SDK 里 SHIL_LARGE=0x0、SHIL_SMALL=0x1,
/// kind=0 → 32x32、kind=1 → 16x16。写反会导致 32px 图标实际拿到 16x16 再被放大(发虚)。
/// </summary>
internal const int SHIL_LARGE = 0;
/// <summary>系统图像列表尺寸标识:48x48(超大图标,实测 kind=2 → 48x48)。</summary>
internal const int SHIL_EXTRALARGE = 2;
/// <summary>系统图像列表尺寸标识:16x16(同 SHIL_SMALL,历史别名;实测 kind=3 → 16x16)。</summary>
internal const int SHIL_SYSSMALL = 3;
/// <summary>系统图像列表尺寸标识:256x256(Windows 11 高清图标,imageres.dll 里的原版 256px 资源;实测 kind=4)。</summary>
internal const int SHIL_JUMBO = 4;
/// <summary>IShellItemImageFactory 标志:等比缩放到请求尺寸(值为 0)。</summary>
internal const int SIIGBF_RESIZETOFIT = 0x000;
/// <summary>IShellItemImageFactory 标志:允许返回比请求更大的位图。</summary>
internal const int SIIGBF_BIGGERSIZEOK = 0x001;
/// <summary>IShellItemImageFactory 标志:只返回图标,绝不返回缩略图。</summary>
internal const int SIIGBF_ICONONLY = 0x004;
/// <summary>IShellItemImageFactory 标志:只返回缩略图,没有就失败(不做图标回退)。</summary>
internal const int SIIGBF_THUMBNAILONLY = 0x008;
/// <summary>IShellItemImageFactory 标志:只在内存/本地缓存里找,不触发网络或慢速解码。</summary>
internal const int SIIGBF_MEMORYONLY = 0x100;
/// <summary>扩展名通用图标用的文件属性:普通文件(配合 SHGFI_USEFILEATTRIBUTES 不碰磁盘)。</summary>
internal const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
/// <summary>扩展名通用图标用的文件属性:目录。</summary>
internal const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
/// <summary>E_PENDING:外壳正在后台解码,稍后重试即可(缩略图首次生成时常见)。</summary>
internal const int E_PENDING = unchecked((int)0x8000000A);
/// <summary>DrawIconEx 标志:按图标自身 alpha / 掩码正常绘制。</summary>
internal const uint DI_NORMAL = 0x0003;
/// <summary>GetSystemMetrics:标准大图标宽度。</summary>
internal const int SM_CXICON = 11;
/// <summary>GetSystemMetrics:标准大图标高度。</summary>
internal const int SM_CYICON = 12;
/// <summary>BITMAPINFOHEADER 的 BI_RGB。</summary>
internal const uint BI_RGB = 0;
/// <summary>DIB_RGB_COLORS。</summary>
internal const uint DIB_RGB_COLORS = 0;
// ─────────────────────────────────────────────────────────────
// 结构体
// ─────────────────────────────────────────────────────────────
/// <summary>SHGetFileInfoW 的输出结构(SHFILEINFOW)。</summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal 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;
}
/// <summary>尺寸参数(SIZE / Windows.Graphics.SizeInt32 的原生形态)。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct SIZE
{
public int cx;
public int cy;
public SIZE(int cx, int cy)
{
this.cx = cx;
this.cy = cy;
}
}
/// <summary>GDI BITMAP 结构(GetObject 输出,用于确认位图尺寸/位深)。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct BITMAP
{
public int bmType;
public int bmWidth;
public int bmHeight;
public int bmWidthBytes;
public ushort bmPlanes;
public ushort bmBitsPixel;
public IntPtr bmBits;
}
/// <summary>BITMAPINFOHEADER。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct BITMAPINFOHEADER
{
public uint biSize;
public int biWidth;
public int biHeight;
public ushort biPlanes;
public ushort biBitCount;
public uint biCompression;
public uint biSizeImage;
public int biXPelsPerMeter;
public int biYPelsPerMeter;
public uint biClrUsed;
public uint biClrImportant;
}
/// <summary>BITMAPINFO(此处只用 32bpp BI_RGB,无调色板)。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct BITMAPINFO
{
public BITMAPINFOHEADER bmiHeader;
public uint bmiColors;
}
/// <summary>GetIconInfo 的输出:HICON 拆出的掩码位图与颜色位图(必须 DeleteObject 释放)。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct ICONINFO
{
[MarshalAs(UnmanagedType.Bool)]
public bool fIcon;
public int xHotspot;
public int yHotspot;
public IntPtr hbmMask;
public IntPtr hbmColor;
}
// ─────────────────────────────────────────────────────────────
// P/Invoke
// ─────────────────────────────────────────────────────────────
/// <summary>
/// SHGetFileInfoW:按真实文件系统路径取外壳信息(会访问该路径,路径可带个性化图标叠加,如 OneDrive/共享角标)。
/// 这里显式写 EntryPoint,因为下面还有一个同签名的 PIDL 重载,C# 端必须用不同方法名区分。
/// </summary>
[DllImport("shell32.dll", EntryPoint = "SHGetFileInfoW", CharSet = CharSet.Unicode, SetLastError = false)]
internal static extern IntPtr SHGetFileInfoByPath(
string pszPath,
uint dwFileAttributes,
ref SHFILEINFOW psfi,
uint cbFileInfo,
uint uFlags);
/// <summary>SHGetFileInfoW(PIDL 版):传 ITEMIDLIST,用于 shell: 解析名(回收站、此电脑等)。</summary>
[DllImport("shell32.dll", EntryPoint = "SHGetFileInfoW", CharSet = CharSet.Unicode, SetLastError = false)]
internal static extern IntPtr SHGetFileInfoByPidl(
IntPtr pidl,
uint dwFileAttributes,
ref SHFILEINFOW psfi,
uint cbFileInfo,
uint uFlags);
/// <summary>
/// SHGetImageList:取系统图像列表(小/大/超大/巨幅)。
/// </summary>
/// <remarks>
/// 【重要实现说明 · 实测踩坑记录】
/// SHGetImageList 返回的接口指针**不能**用 [ComImport] 的 IImageList 走 RCW 调用:
/// 实测(见 _icon_probe 验证)该对象的所谓 "vtable" 槽位里并不是函数指针
/// (读出来是 0x100000004 / 0x20021 这类数据),因此按槽位序号调用会直接
/// AccessViolationException,或在 GetImageCount 上返回垃圾值(16777215)。
/// Shell 文档保证该返回值同时就是一个 HIMAGELIST,所以这里按 HIMAGELIST 处理,
/// 用 comctl32 的 ImageList_GetIcon / ImageList_GetIconSize 取值——实测完全正常。
/// 注意:C# 方法名与导出名不同,必须显式写 EntryPoint,否则会 EntryPointNotFoundException。
/// </remarks>
[DllImport("shell32.dll", EntryPoint = "SHGetImageList", PreserveSig = true)]
internal static extern int SHGetImageListRaw(int iImageList, ref Guid riid, out IntPtr ppvObj);
/// <summary>comctl32 图像列表函数:按索引取 HICON(调用方负责 DestroyIcon)。ILD_TRANSPARENT = 1。</summary>
[DllImport("comctl32.dll", SetLastError = true)]
internal static extern IntPtr ImageList_GetIcon(IntPtr himl, int i, uint flags);
/// <summary>comctl32 图像列表函数:取图像列表的图标尺寸。</summary>
[DllImport("comctl32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool ImageList_GetIconSize(IntPtr himl, ref int cx, ref int cy);
/// <summary>comctl32 图像列表函数:取图像数量(用于诊断/兜底校验索引是否越界)。</summary>
[DllImport("comctl32.dll", SetLastError = true)]
internal static extern int ImageList_GetImageCount(IntPtr himl);
/// <summary>SHParseDisplayName:把 "shell:RecycleBinFolder"、"{CLSID}" 之类解析成 PIDL(调用方负责 CoTaskMemFree)。</summary>
[DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = true)]
internal static extern int SHParseDisplayName(
string pszName,
IntPtr pbc,
out IntPtr ppidl,
uint sfgaoIn,
out uint psfgaoOut);
/// <summary>SHCreateItemFromParsingName:按路径创建 IShellItem(缩略图路径的入口)。</summary>
[DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = true)]
internal static extern int SHCreateItemFromParsingName(
string pszPath,
IntPtr pbc,
ref Guid riid,
[MarshalAs(UnmanagedType.Interface)] out IShellItemImageFactory ppv);
/// <summary>DestroyIcon:释放 SHGetFileInfo / GetIcon 返回的 HICON。</summary>
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DestroyIcon(IntPtr hIcon);
/// <summary>GetIconInfo:HICON → 掩码位图 + 颜色位图(两个 HBITMAP 都要 DeleteObject)。</summary>
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO piconinfo);
/// <summary>DrawIconEx:把 HICON 画进 DC(用于把图标栅格化成 32bpp BGRA)。</summary>
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DrawIconEx(
IntPtr hdc,
int xLeft,
int yTop,
IntPtr hIcon,
int cxWidth,
int cyWidth,
uint istepIfAniCur,
IntPtr hbrFlickerFreeDraw,
uint diFlags);
/// <summary>GetSystemMetrics:取标准图标尺寸等系统度量。</summary>
[DllImport("user32.dll")]
internal static extern int GetSystemMetrics(int nIndex);
/// <summary>CreateCompatibleDC:创建内存 DC。</summary>
[DllImport("gdi32.dll", SetLastError = true)]
internal static extern IntPtr CreateCompatibleDC(IntPtr hdc);
/// <summary>DeleteDC:释放内存 DC。</summary>
[DllImport("gdi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteDC(IntPtr hdc);
/// <summary>CreateDIBSection:创建 32bpp 自上而下 DIB,直接拿到 BGRA 像素指针。</summary>
[DllImport("gdi32.dll", SetLastError = true)]
internal static extern IntPtr CreateDIBSection(
IntPtr hdc,
ref BITMAPINFO pbmi,
uint usage,
out IntPtr ppvBits,
IntPtr hSection,
uint offset);
/// <summary>CreateCompatibleBitmap:创建与 DC 兼容的位图(缩略图 HBITMAP 转像素时用)。</summary>
[DllImport("gdi32.dll", SetLastError = true)]
internal static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int width, int height);
/// <summary>SelectObject:把 GDI 对象选入 DC,返回旧对象(必须还原)。</summary>
[DllImport("gdi32.dll", SetLastError = true)]
internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr hObject);
/// <summary>DeleteObject:释放 HBITMAP / DIB 等 GDI 对象。</summary>
[DllImport("gdi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteObject(IntPtr hObject);
/// <summary>GetObjectW:读取 HBITMAP 的尺寸与位深。</summary>
[DllImport("gdi32.dll", EntryPoint = "GetObjectW", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern int GetObjectBitmap(IntPtr hgdiobj, int cbBuffer, ref BITMAP lpvObject);
/// <summary>GetDIBits:把 HBITMAP 的像素读成指定格式的 DIB(这里固定 32bpp BGRA)。</summary>
[DllImport("gdi32.dll", SetLastError = true)]
internal static extern int GetDIBits(
IntPtr hdc,
IntPtr hbm,
uint start,
uint cLines,
IntPtr lpvBits,
ref BITMAPINFO lpbmi,
uint usage);
/// <summary>CoTaskMemFree:释放 SHParseDisplayName 返回的 PIDL。</summary>
[DllImport("ole32.dll")]
internal static extern void CoTaskMemFree(IntPtr pv);
// ─────────────────────────────────────────────────────────────
// COM:图像列表与外壳条目
// ─────────────────────────────────────────────────────────────
/// <summary>
/// IImageList:完整的 vtable 声明,作为外壳图像列表接口的参考定义保留。
/// </summary>
/// <remarks>
/// ⚠ 不要用这个接口去调用 SHGetImageList 返回的对象(RCW 调用会 AccessViolation)。
/// 原因与正确做法见 <see cref="SHGetImageListRaw"/> 的注释。
/// 这里保留完整声明是为了:1) 提供 IID(SHGetImageListRaw 需要传 REFIID);
/// 2) 当外层需要 IImageList 的其它能力(如 GetImageInfo)时,有准确的槽位定义可查。
/// 由于使用 [ComImport] 映射 vtable,中间槽位必须按顺序原样占位,否则偏移错位会取到错误的函数。
/// </remarks>
[ComImport]
[Guid("46EB5926-582E-4017-9FDF-E8998DAA0950")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IImageList
{
// ── IUnknown 占位(3 槽) ──
[PreserveSig]
int QueryInterface(ref Guid riid, out IntPtr ppvObject);
[PreserveSig]
IntPtr AddRef();
[PreserveSig]
IntPtr Release();
// ── vtable 槽位 3.. ──
[PreserveSig]
int Add(IntPtr hbmImage, IntPtr hbmMask, out int pi);
[PreserveSig]
int ReplaceIcon(int i, IntPtr hicon, out int pi);
[PreserveSig]
int SetOverlayImage(int iImage, int iOverlay);
[PreserveSig]
int Replace(int i, IntPtr hbmImage, IntPtr hbmMask);
[PreserveSig]
int AddMasked(IntPtr hbmImage, int crMask, out int pi);
[PreserveSig]
int Draw(IntPtr pimldp);
[PreserveSig]
int Remove(int i);
[PreserveSig]
int GetIcon(int i, int flags, out IntPtr picon);
[PreserveSig]
int GetImageInfo(int i, out IMAGEINFO pImageInfo);
[PreserveSig]
int Copy(int iDst, IImageList punkSrc, int iSrc, int uFlags);
[PreserveSig]
int Merge(int i1, IImageList punk2, int i2, int dx, int dy, ref Guid riid, out IntPtr ppv);
[PreserveSig]
int Clone(ref Guid riid, out IntPtr ppv);
[PreserveSig]
int GetImageRect(int i, out RECT prc);
[PreserveSig]
int GetIconSize(out int cx, out int cy);
[PreserveSig]
int SetIconSize(int cx, int cy);
[PreserveSig]
int GetImageCount(out int pi);
[PreserveSig]
int SetImageCount(int uNewCount);
[PreserveSig]
int SetBkColor(int clrBk, out int pclr);
[PreserveSig]
int GetBkColor(out int pclr);
[PreserveSig]
int BeginDrag(int iTrack, int dxHotspot, int dyHotspot);
[PreserveSig]
int EndDrag();
[PreserveSig]
int DragEnter(IntPtr hwndLock, int x, int y);
[PreserveSig]
int DragLeave(IntPtr hwndLock);
[PreserveSig]
int DragMove(int x, int y);
[PreserveSig]
int SetDragCursorImage(IImageList punk, int iDrag, int dxHotspot, int dyHotspot);
[PreserveSig]
int DragShowNolock([MarshalAs(UnmanagedType.Bool)] bool fShow);
[PreserveSig]
int GetDragImage(out POINT ppt, out POINT pptHotspot, ref Guid riid, out IntPtr ppv);
[PreserveSig]
int GetItemFlags(int i, out int dwFlags);
[PreserveSig]
int GetOverlayImage(int iOverlay, out int piIndex);
}
/// <summary>IMAGEINFO(IImageList.GetImageInfo 输出)。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct IMAGEINFO
{
public IntPtr hbmImage;
public IntPtr hbmMask;
public int Unused1;
public int Unused2;
public RECT rcImage;
}
/// <summary>RECT。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
/// <summary>POINT。</summary>
[StructLayout(LayoutKind.Sequential)]
internal struct POINT
{
public int x;
public int y;
}
/// <summary>
/// IShellItemImageFactory:外壳缩略图/高清图标工厂,唯一能拿到「资源管理器同款」缩略图的接口。
/// </summary>
[ComImport]
[Guid("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IShellItemImageFactory
{
/// <summary>取该条目的位图。返回 S_OK 时 phbm 为 HBITMAP(调用方 DeleteObject)。</summary>
[PreserveSig]
int GetImage(SIZE size, int flags, out IntPtr phbm);
/// <summary>不参与本工程,仅用于占位以保持 vtable 顺序正确。</summary>
[PreserveSig]
int GetImageEx(SIZE size, int flags, IntPtr pColor, out IntPtr phbm);
}
// ─────────────────────────────────────────────────────────────
// 高层封装:HICON / HBITMAP → 32bpp BGRA 像素
// ─────────────────────────────────────────────────────────────
/// <summary>32bpp BGRA 像素缓冲(自上而下,行距 = width * 4)。</summary>
public sealed class BgraBuffer
{
internal BgraBuffer(int width, int height, byte[] pixels)
{
Width = width;
Height = height;
Pixels = pixels;
}
public int Width { get; }
public int Height { get; }
public byte[] Pixels { get; }
/// <summary>Alpha 通道是否全 0(说明位图本身不带 alpha,需要靠亮度重建)。</summary>
public bool HasAlphaChannel()
{
for (int i = 3; i < Pixels.Length; i += 4)
{
if (Pixels[i] != 0) return true;
}
return false;
}
}
/// <summary>解析名(shell: 语法 / CLSID)→ PIDL;失败返回 IntPtr.Zero。</summary>
internal static IntPtr ParseDisplayNameToPidl(string parsingName)
{
IntPtr pidl = IntPtr.Zero;
try
{
// SHParseDisplayName 对 "shell:RecycleBinFolder" 与 "::{CLSID}" 都有效
int hr = SHParseDisplayName(parsingName, IntPtr.Zero, out pidl, 0, out _);
if (hr < 0)
{
if (pidl != IntPtr.Zero)
{
CoTaskMemFree(pidl);
}
return IntPtr.Zero;
}
return pidl;
}
catch (DllNotFoundException)
{
return IntPtr.Zero;
}
catch (EntryPointNotFoundException)
{
return IntPtr.Zero;
}
}
/// <summary>按真实路径取系统图标索引;失败返回 -1。目录/驱动器/文件都走这条。</summary>
internal static int GetSystemIconIndexByPath(string path, uint attributes, uint extraFlags)
{
var info = new SHFILEINFOW { szDisplayName = string.Empty, szTypeName = string.Empty };
uint flags = SHGFI_SYSICONINDEX | extraFlags;
IntPtr result = SHGetFileInfoByPath(path, attributes, ref info, (uint)Marshal.SizeOf<SHFILEINFOW>(), flags);
return result == IntPtr.Zero ? -1 : info.iIcon;
}
/// <summary>按 PIDL 取系统图标索引(shell: 解析名);失败返回 -1。</summary>
internal static int GetSystemIconIndexByPidl(IntPtr pidl, uint extraFlags)
{
if (pidl == IntPtr.Zero) return -1;
var info = new SHFILEINFOW { szDisplayName = string.Empty, szTypeName = string.Empty };
uint flags = SHGFI_SYSICONINDEX | SHGFI_PIDL | extraFlags;
IntPtr result = SHGetFileInfoByPidl(pidl, 0, ref info, (uint)Marshal.SizeOf<SHFILEINFOW>(), flags);
return result == IntPtr.Zero ? -1 : info.iIcon;
}
/// <summary>
/// 按真实路径直接取 HICON(SHGetFileInfoW + SHGFI_ICON)。
/// 小尺寸最贴近资源管理器列表视图;调用方必须在 finally 里 DestroyIcon。
/// </summary>
internal static IntPtr GetHIconByPath(string path, uint attributes, uint extraFlags)
{
var info = new SHFILEINFOW { szDisplayName = string.Empty, szTypeName = string.Empty };
uint flags = SHGFI_ICON | extraFlags;
IntPtr result = SHGetFileInfoByPath(path, attributes, ref info, (uint)Marshal.SizeOf<SHFILEINFOW>(), flags);
return result == IntPtr.Zero ? IntPtr.Zero : info.hIcon;
}
/// <summary>按 PIDL 直接取 HICON(shell: 解析名);调用方负责 DestroyIcon。</summary>
internal static IntPtr GetHIconByPidl(IntPtr pidl, uint extraFlags)
{
if (pidl == IntPtr.Zero) return IntPtr.Zero;
var info = new SHFILEINFOW { szDisplayName = string.Empty, szTypeName = string.Empty };
uint flags = SHGFI_ICON | SHGFI_PIDL | extraFlags;
IntPtr result = SHGetFileInfoByPidl(pidl, 0, ref info, (uint)Marshal.SizeOf<SHFILEINFOW>(), flags);
return result == IntPtr.Zero ? IntPtr.Zero : info.hIcon;
}
/// <summary>
/// 从系统图像列表按索引取指定尺寸的 HICON(256/48/32/16);调用方负责 DestroyIcon。
/// 返回的 HICON 是外壳原版图标资源在该尺寸下的真实副本(不是放大后的位图)。
/// </summary>
internal static IntPtr GetHIconFromSystemImageList(int imageListKind, int iconIndex)
{
if (iconIndex < 0) return IntPtr.Zero;
IntPtr hImageList = IntPtr.Zero;
try
{
Guid iid = typeof(IImageList).GUID;
if (SHGetImageListRaw(imageListKind, ref iid, out hImageList) < 0 || hImageList == IntPtr.Zero)
{
return IntPtr.Zero;
}
// 索引越界时 comctl32 会返回 NULL,这里先挡一道,省得白跑一次
if (ImageList_GetImageCount(hImageList) <= iconIndex) return IntPtr.Zero;
// ILD_TRANSPARENT = 1:保留图标自身的透明与 alpha
return ImageList_GetIcon(hImageList, iconIndex, 1);
}
catch (EntryPointNotFoundException)
{
return IntPtr.Zero;
}
finally
{
// SHGetImageListRaw 已经 AddRef,用完后必须 Release
if (hImageList != IntPtr.Zero) Marshal.Release(hImageList);
}
}
/// <summary>读系统图像列表在指定尺寸下的图标边长(用于诊断与兜底,不用它决定渲染尺寸)。</summary>
internal static int GetSystemImageListIconSize(int imageListKind)
{
IntPtr hImageList = IntPtr.Zero;
try
{
Guid iid = typeof(IImageList).GUID;
if (SHGetImageListRaw(imageListKind, ref iid, out hImageList) < 0 || hImageList == IntPtr.Zero)
{
return 0;
}
int cx = 0, cy = 0;
return ImageList_GetIconSize(hImageList, ref cx, ref cy) ? cx : 0;
}
catch (EntryPointNotFoundException)
{
return 0;
}
finally
{
if (hImageList != IntPtr.Zero) Marshal.Release(hImageList);
}
}
/// <summary>
/// HICON → 32bpp BGRA 像素(自上而下)。
/// 主路径:GetIconInfo 拿尺寸 → 32bpp DIB → DrawIconEx → GetDIBits 读回。
/// 若 GDI 绘制把 alpha 通道抹成 0(常见于只带掩码的老式图标),
/// 再用「黑底 vs 白底两次绘制」重建 alpha:像素在两次绘制中完全一致即为透明区。
/// </summary>
internal static BgraBuffer? IconToBgra(IntPtr hIcon, int width, int height)
{
if (hIcon == IntPtr.Zero || width <= 0 || height <= 0) return null;
IntPtr hdc = IntPtr.Zero;
IntPtr hDib = IntPtr.Zero;
IntPtr hOldBitmap = IntPtr.Zero;
IntPtr dibBits = IntPtr.Zero;
bool iconInfoValid = false;
ICONINFO iconInfo = default;
try
{
iconInfoValid = GetIconInfo(hIcon, out iconInfo);
// 1) 建 32bpp 自上而下 DIB(biHeight 取负 = top-down,行序与 BGRA 缓冲一致)
hdc = CreateCompatibleDC(IntPtr.Zero);
if (hdc == IntPtr.Zero) return null;
var bmi = CreateBgraBitmapInfo(width, height);
hDib = CreateDIBSection(hdc, ref bmi, DIB_RGB_COLORS, out dibBits, IntPtr.Zero, 0);
if (hDib == IntPtr.Zero || dibBits == IntPtr.Zero) return null;
hOldBitmap = SelectObject(hdc, hDib);
// 2) 判断这个 HICON 是不是"真 alpha"图标(颜色位图自带 alpha 通道)。
// 这决定了能不能直接信任 GDI 写的 alpha:
// · 真 alpha 图标 → DrawIconEx 会写正确的 alpha,直接采信即可;
// · 只有 1bpp AND 掩码的老式图标 → 必须靠掩码/双背景对比重建透明度。
bool hasRealAlpha = BitmapHasAlphaChannel(iconInfoValid ? iconInfo.hbmColor : IntPtr.Zero);
GetIconSize(hIcon, ref iconInfo, iconInfoValid, width, height, out int drawW, out int drawH);
// 清屏用「全透明黑」:
// - 真 alpha 图标:0 是正确初值,GDI 会补上被覆盖像素的 alpha;
// - 掩码图标:GDI 不写 alpha,留下 0 正好代表"未覆盖 = 透明",颜色保持黑。
// ⚠ 切记不要填成不透明黑(alpha=0xFF):那样未覆盖像素会变成"不透明黑",
// 不但无法识别透明,还会误判为 alpha 有效而跳过重建(此坑已被探针实测捕获)。
FillBits(dibBits, width * height * 4, 0x00, 0x00, 0x00, 0x00);
if (!DrawIconEx(hdc, 0, 0, hIcon, drawW, drawH, 0, IntPtr.Zero, DI_NORMAL))
{
return null;
}
var pixels = ReadDibSectionPixels(hdc, hDib, width, height);
if (pixels is null) return null;
// 3) 真 alpha 图标:验证 GDI 确实写了 alpha,成立就直接用,精度最高
if (hasRealAlpha)
{
var direct = new BgraBuffer(width, height, pixels);
bool usable = direct.HasAlphaChannel();
if (usable)
{
// 画布比图标大时(高 DPI 缩放),DrawIconEx 会把它画在左上角,这里搬回正中
CenterGlyph(pixels, width, height, drawW, drawH);
return new BgraBuffer(width, height, pixels);
}
}
// 4) 否则做双背景对比重建:白底再画一次,用两次结果解出 alpha 与原色
byte[]? light = RenderIconOnBackground(hIcon, width, height, drawW, drawH, 0xFF);
if (light is null)
{
// 白底渲染失败:退回黑底结果(黑底结果本身是 premultiplied,至少颜色正确)
CenterGlyph(pixels, width, height, drawW, drawH);
return new BgraBuffer(width, height, pixels);
}
RebuildAlphaViaContrast(pixels, light);
// 重建完 alpha 后再居中:居中只是搬运像素,放在 alpha 处理之后不会影响对比判定
CenterGlyph(pixels, width, height, drawW, drawH);
return new BgraBuffer(width, height, pixels);
}
catch (SEHException)
{
return null;
}
finally
{
// 严格释放,顺序:先还原 DC 选中的对象,再删对象,最后删 DC
if (hdc != IntPtr.Zero && hOldBitmap != IntPtr.Zero) SelectObject(hdc, hOldBitmap);
if (hDib != IntPtr.Zero) DeleteObject(hDib);
if (hdc != IntPtr.Zero) DeleteDC(hdc);
// GetIconInfo 拆出的两个位图必须释放,否则每次取图标泄漏两个 GDI 对象
if (iconInfoValid)
{
if (iconInfo.hbmMask != IntPtr.Zero) DeleteObject(iconInfo.hbmMask);
if (iconInfo.hbmColor != IntPtr.Zero) DeleteObject(iconInfo.hbmColor);
}
}
}
/// <summary>
/// 把 HICON 画到指定纯色背景上,返回 32bpp BGRA 像素(背景不透明)。
/// 与主流程共用同一套 DIB + DrawIconEx + GetDIBits 逻辑,避免两处实现不一致。
/// </summary>
private static byte[]? RenderIconOnBackground(IntPtr hIcon, int width, int height, int drawW, int drawH, byte background)
{
IntPtr hdc = IntPtr.Zero;
IntPtr hDib = IntPtr.Zero;
IntPtr hOldBitmap = IntPtr.Zero;
try
{
hdc = CreateCompatibleDC(IntPtr.Zero);
if (hdc == IntPtr.Zero) return null;
var bmi = CreateBgraBitmapInfo(width, height);
hDib = CreateDIBSection(hdc, ref bmi, DIB_RGB_COLORS, out IntPtr dibBits, IntPtr.Zero, 0);
if (hDib == IntPtr.Zero || dibBits == IntPtr.Zero) return null;
hOldBitmap = SelectObject(hdc, hDib);
// 背景必须是不透明的(alpha=0xFF):这样 alpha 合成才有确定的结果
FillBits(dibBits, width * height * 4, background, background, background, 0xFF);
if (!DrawIconEx(hdc, 0, 0, hIcon, drawW, drawH, 0, IntPtr.Zero, DI_NORMAL)) return null;
return ReadDibSectionPixels(hdc, hDib, width, height);
}
catch (SEHException)
{
return null;
}
finally
{
if (hdc != IntPtr.Zero && hOldBitmap != IntPtr.Zero) SelectObject(hdc, hOldBitmap);
if (hDib != IntPtr.Zero) DeleteObject(hDib);
if (hdc != IntPtr.Zero) DeleteDC(hdc);
}
}
/// <summary>用 GetDIBits 把已选入 DC 的 32bpp DIB 读成自上而下的 BGRA 缓冲。</summary>
private static byte[]? ReadDibSectionPixels(IntPtr hdc, IntPtr hDib, int width, int height)
{
var readBmi = CreateBgraBitmapInfo(width, height);
var pixels = new byte[width * height * 4];
unsafe
{
fixed (byte* p = pixels)
{
int lines = GetDIBits(hdc, hDib, 0, (uint)height, (IntPtr)p, ref readBmi, DIB_RGB_COLORS);
if (lines == 0) return null;
}
}
return pixels;
}
/// <summary>构造 32bpp 自上而下 DIB 的 BITMAPINFO。</summary>
private static BITMAPINFO CreateBgraBitmapInfo(int width, int height) => new()
{
bmiHeader = new BITMAPINFOHEADER
{
biSize = (uint)Marshal.SizeOf<BITMAPINFOHEADER>(),
biWidth = width,
// 负高度 = top-down,行序与 BGRA 缓冲一致
biHeight = -height,
biPlanes = 1,
biBitCount = 32,
biCompression = BI_RGB,
},
};
/// <summary>
/// 判断一个 HBITMAP 的颜色位图是否自带真实 alpha 通道。
/// 方法:把它读成 32bpp 后看 alpha 字节;全 0 说明它只有 1bpp AND 掩码(老式图标)。
/// </summary>
private static bool BitmapHasAlphaChannel(IntPtr hbmColor)
{
if (hbmColor == IntPtr.Zero) return false;
IntPtr hdc = IntPtr.Zero;
IntPtr hDib = IntPtr.Zero;
IntPtr hOldBitmap = IntPtr.Zero;
try
{
var native = new BITMAP();
if (GetObjectBitmap(hbmColor, Marshal.SizeOf<BITMAP>(), ref native) <= 0) return false;
if (native.bmWidth <= 0 || native.bmHeight <= 0 || native.bmBitsPixel < 32) return false;
int width = native.bmWidth;
int height = native.bmHeight;
int byteCount = width * height * 4;
hdc = CreateCompatibleDC(IntPtr.Zero);
if (hdc == IntPtr.Zero) return false;
var bmi = CreateBgraBitmapInfo(width, height);
hDib = CreateDIBSection(hdc, ref bmi, DIB_RGB_COLORS, out IntPtr dibBits, IntPtr.Zero, 0);
if (hDib == IntPtr.Zero || dibBits == IntPtr.Zero) return false;
hOldBitmap = SelectObject(hdc, hDib);
FillBits(dibBits, byteCount, 0, 0, 0, 0);
var pixels = new byte[byteCount];
unsafe
{
fixed (byte* p = pixels)
{
if (GetDIBits(hdc, hbmColor, 0, (uint)height, (IntPtr)p, ref bmi, DIB_RGB_COLORS) == 0) return false;
}
}
for (int i = 3; i < byteCount; i += 4)
{
if (pixels[i] != 0) return true;
}
return false;
}
catch (SEHException)
{
return false;
}
finally
{
if (hdc != IntPtr.Zero && hOldBitmap != IntPtr.Zero) SelectObject(hdc, hOldBitmap);
if (hDib != IntPtr.Zero) DeleteObject(hDib);
if (hdc != IntPtr.Zero) DeleteDC(hdc);
}
}
/// <summary>
/// HBITMAP(通常来自 IShellItemImageFactory)→ 32bpp BGRA 像素。
/// 该路径返回的是 DDB,必须借助内存 DC 用 GetDIBits 转换。
/// </summary>
internal static BgraBuffer? BitmapToBgra(IntPtr hBitmap, int requestW, int requestH)
{
if (hBitmap == IntPtr.Zero) return null;
IntPtr hdc = IntPtr.Zero;
IntPtr hDib = IntPtr.Zero;
IntPtr hOldBitmap = IntPtr.Zero;
try
{
// 1) 先问 GDI 这张位图的真实尺寸
int width = requestW;
int height = requestH;
var native = new BITMAP();
if (GetObjectBitmap(hBitmap, Marshal.SizeOf<BITMAP>(), ref native) > 0 && native.bmWidth > 0 && native.bmHeight > 0)
{
width = native.bmWidth;
height = native.bmHeight;
}
if (width <= 0 || height <= 0) return null;
hdc = CreateCompatibleDC(IntPtr.Zero);
if (hdc == IntPtr.Zero) return null;
// 2) 建 32bpp top-down DIB,选入 DC 后对其调用 GetDIBits
var bmi = new BITMAPINFO
{
bmiHeader = new BITMAPINFOHEADER
{
biSize = (uint)Marshal.SizeOf<BITMAPINFOHEADER>(),
biWidth = width,
biHeight = -height,
biPlanes = 1,
biBitCount = 32,
biCompression = BI_RGB,
},
};
hDib = CreateDIBSection(hdc, ref bmi, DIB_RGB_COLORS, out IntPtr dibBits, IntPtr.Zero, 0);
if (hDib == IntPtr.Zero || dibBits == IntPtr.Zero) return null;
hOldBitmap = SelectObject(hdc, hDib);
ClearBits(dibBits, width * height * 4);
var pixels = new byte[width * height * 4];
unsafe
{
fixed (byte* p = pixels)
{
int lines = GetDIBits(hdc, hBitmap, 0, (uint)height, (IntPtr)p, ref bmi, DIB_RGB_COLORS);
if (lines == 0) return null;
}
}
return new BgraBuffer(width, height, pixels);
}
catch (SEHException)
{
return null;
}
finally
{
if (hdc != IntPtr.Zero && hOldBitmap != IntPtr.Zero) SelectObject(hdc, hOldBitmap);
if (hDib != IntPtr.Zero) DeleteObject(hDib);
if (hdc != IntPtr.Zero) DeleteDC(hdc);
}
}
/// <summary>取 HICON 的真实像素尺寸(GetIconInfo 的颜色/掩码位图尺寸),失败则回退到请求尺寸。</summary>
private static void GetIconSize(IntPtr hIcon, ref ICONINFO iconInfo, bool iconInfoValid, int fallbackW, int fallbackH, out int width, out int height)
{
width = fallbackW;
height = fallbackH;
if (!iconInfoValid) return;
IntPtr probe = iconInfo.hbmColor != IntPtr.Zero ? iconInfo.hbmColor : iconInfo.hbmMask;
if (probe == IntPtr.Zero) return;
var bm = new BITMAP();
if (GetObjectBitmap(probe, Marshal.SizeOf<BITMAP>(), ref bm) <= 0) return;
// 只有掩码位图时高度是「颜色 + 掩码」两倍,取一半
int h = bm.bmHeight;
if (iconInfo.hbmColor == IntPtr.Zero) h /= 2;
if (bm.bmWidth > 0) width = bm.bmWidth;
if (h > 0) height = h;
}
/// <summary>
/// 把左上角的图标图形搬到画布正中。
/// DrawIconEx 总是从 (0,0) 开始画,当请求画布大于图标自身尺寸时(高 DPI 下必然发生,
/// 例如逻辑 32px × 缩放 2.0 = 64px 画布 + 32px 图标),不居中就会出现"图标缩在左上角"。
/// </summary>
private static void CenterGlyph(byte[] pixels, int width, int height, int glyphWidth, int glyphHeight)
{
if (glyphWidth <= 0 || glyphHeight <= 0) return;
if (glyphWidth >= width && glyphHeight >= height) return;
int offsetX = (width - glyphWidth) / 2;
int offsetY = (height - glyphHeight) / 2;
if (offsetX == 0 && offsetY == 0) return;
var moved = new byte[pixels.Length];
for (int y = 0; y < glyphHeight; y++)
{
int destY = y + offsetY;
if (destY < 0 || destY >= height) continue;
Buffer.BlockCopy(
pixels,
(y * width) * 4,
moved,
(destY * width + offsetX) * 4,
glyphWidth * 4);
}
Buffer.BlockCopy(moved, 0, pixels, 0, pixels.Length);
}
/// <summary>
/// 由「黑底 / 白底」两次绘制结果解出 alpha 与颜色,就地写回 <paramref name="dark"/>。
/// </summary>
/// <remarks>
/// 原理:像素 = 原色 × a + 底色 × (1 - a)
/// · 黑底 C_b = 原色 × a (即 premultiplied 值,alpha 与原色成对出现)
/// · 白底 C_w = 原色 × a + 255 × (1 - a)
/// 于是 delta = C_w - C_b = 255 × (1 - a) ⇒ a = 255 - delta,原色 = C_b / a。
///
/// 两个必须踩过的坑(均由 _icon_probe 实测捕获):
/// 1) 完全透明区:两次绘制结果**逐字节相同**(黑底与白底都原样保留背景色)。判据是"相等",
/// 不能只看黑底是不是黑色——否则白色/接近白色的像素会被误判成透明(白色图标会整体消失)。
/// 2) 二次判定:对 a=255 的像素,C_b 就是原色;但若原色本身接近白色,C_b 也接近白,
/// 会与"黑底背景"混淆。这里用邻居像素的判定结果纠正:图标内部被不透明像素包围的像素
/// 不可能是透明的。图标图案很细,单轮邻居传播已经足够。
/// </remarks>
private static void RebuildAlphaViaContrast(byte[] dark, byte[] light)
{
int pixelCount = dark.Length / 4;
var transparent = new bool[pixelCount];
// 第一轮:严格相等判据
for (int i = 0, p = 0; i < dark.Length; i += 4, p++)
{
transparent[p] = dark[i] == light[i]
&& dark[i + 1] == light[i + 1]
&& dark[i + 2] == light[i + 2];
}
// 第二轮:邻居纠正(上下左右四邻,只做一轮,避免误差扩散)
int width = 0;
int height = 0;
// 由长度反推宽度:本工程里图标总是正方形,这里按正方形处理
int edge = (int)Math.Sqrt(pixelCount);
if (edge > 0 && edge * edge == pixelCount)
{
width = edge;
height = edge;
}
if (width > 0 && height > 0)
{
var corrected = new bool[pixelCount];
Array.Copy(transparent, corrected, pixelCount);
for (int y = 1; y < height - 1; y++)
{
for (int x = 1; x < width - 1; x++)
{
int p = y * width + x;
if (!transparent[p]) continue;
// 四邻里有 3 个及以上是不透明像素 ⇒ 自己是被图案包围的内部像素,改判为不透明
int opaqueNeighbors = 0;
if (!transparent[p - 1]) opaqueNeighbors++;
if (!transparent[p + 1]) opaqueNeighbors++;
if (!transparent[p - width]) opaqueNeighbors++;
if (!transparent[p + width]) opaqueNeighbors++;
if (opaqueNeighbors >= 3) corrected[p] = false;
}
}
transparent = corrected;
}
// 第三轮:按 alpha 反解原色并重新预乘(SoftwareBitmap 需要 Premultiplied)
for (int i = 0, p = 0; i < dark.Length; i += 4, p++)
{
if (transparent[p])
{
dark[i] = 0;
dark[i + 1] = 0;
dark[i + 2] = 0;
dark[i + 3] = 0;
continue;
}
int bD = dark[i];
int gD = dark[i + 1];
int rD = dark[i + 2];
int delta = ((light[i] - bD) + (light[i + 1] - gD) + (light[i + 2] - rD)) / 3;
if (delta <= 0)
{
// C_w == C_b 但第一轮判定不是"完全相等"(差在其它通道),按完全不透明处理
dark[i + 3] = 255;
continue;
}
int a = 255 - delta;
if (a <= 0)
{
dark[i] = 0;
dark[i + 1] = 0;
dark[i + 2] = 0;
dark[i + 3] = 0;
continue;
}
// 原色 = C_b × 255 / a,再按 Premultiplied 重新折算回 C_b(此处即保持 C_b 不变)
dark[i + 3] = (byte)a;
}
}
/// <summary>按 BGRA 颜色填充像素缓冲(byteCount 必须是 4 的倍数)。</summary>
private static void FillBits(IntPtr bits, int byteCount, byte b, byte g, byte r, byte a)
{
unsafe
{
var p = (byte*)bits;
for (int i = 0; i < byteCount; i += 4)
{
p[i] = b;
p[i + 1] = g;
p[i + 2] = r;
p[i + 3] = a;
}
}
}
/// <summary>清零像素缓冲(全透明黑)。</summary>
private static void ClearBits(IntPtr bits, int byteCount) => FillBits(bits, byteCount, 0, 0, 0, 0);
}