using System.Runtime.InteropServices;
namespace FluidExplorer.Services.Icons;
///
/// Windows 外壳 / GDI 的原生互操作层:只放 P/Invoke、COM 接口、常量与结构体,
/// 不含任何 WinUI 依赖,因此可以被独立控制台探针直接链接复用。
///
///
/// 本文件遵守「只用 Windows 11 原版组件」原则:所有图标都来自 imageres.dll / shell32.dll /
/// 各应用自己的图标资源,经 SHGetFileInfoW、SHGetImageList、IShellItemImageFactory 取回,
/// 不做任何自制或矢量化替代。
///
#if ICONPROBE
// 独立控制台探针(E:\deepseek\_icon_probe)链接本文件时定义 ICONPROBE,
// 以便直接驱动内部的 Win32/COM 步骤做验证;主工程编译时保持 internal。
public static class ShellNative
#else
internal static class ShellNative
#endif
{
// ─────────────────────────────────────────────────────────────
// 常量
// ─────────────────────────────────────────────────────────────
/// SHGetFileInfo 标志:取图标句柄。
internal const uint SHGFI_ICON = 0x000000100;
/// SHGetFileInfo 标志:取大图标(默认,值为 0)。
internal const uint SHGFI_LARGEICON = 0x000000000;
/// SHGetFileInfo 标志:取小图标。
internal const uint SHGFI_SMALLICON = 0x000000001;
/// SHGetFileInfo 标志:取系统图像列表中的索引(配合 SHGetImageList 拿任意尺寸)。
internal const uint SHGFI_SYSICONINDEX = 0x00004000;
/// SHGetFileInfo 标志:不访问磁盘,用 dwFileAttributes 直接推断图标(扩展名通用图标走这条)。
internal const uint SHGFI_USEFILEATTRIBUTES = 0x000000010;
/// SHGetFileInfo 标志:取打开状态的图标(文件夹张开形态)。
internal const uint SHGFI_OPENICON = 0x000000002;
/// SHGetFileInfo 标志:pszPath 传的是 ITEMIDLIST 指针(shell: 解析名走这条)。
internal const uint SHGFI_PIDL = 0x000000008;
///
/// 系统图像列表尺寸标识:16x16(资源管理器列表视图的小图标)。
/// 实测(_icon_probe):kind=1 → ImageList_GetIconSize 返回 16x16。
///
internal const int SHIL_SMALL = 1;
///
/// 系统图像列表尺寸标识:32x32(资源管理器详细信息的标准大图标)。
/// ⚠ 实测值是 0 而不是 1:Windows SDK 里 SHIL_LARGE=0x0、SHIL_SMALL=0x1,
/// kind=0 → 32x32、kind=1 → 16x16。写反会导致 32px 图标实际拿到 16x16 再被放大(发虚)。
///
internal const int SHIL_LARGE = 0;
/// 系统图像列表尺寸标识:48x48(超大图标,实测 kind=2 → 48x48)。
internal const int SHIL_EXTRALARGE = 2;
/// 系统图像列表尺寸标识:16x16(同 SHIL_SMALL,历史别名;实测 kind=3 → 16x16)。
internal const int SHIL_SYSSMALL = 3;
/// 系统图像列表尺寸标识:256x256(Windows 11 高清图标,imageres.dll 里的原版 256px 资源;实测 kind=4)。
internal const int SHIL_JUMBO = 4;
/// IShellItemImageFactory 标志:等比缩放到请求尺寸(值为 0)。
internal const int SIIGBF_RESIZETOFIT = 0x000;
/// IShellItemImageFactory 标志:允许返回比请求更大的位图。
internal const int SIIGBF_BIGGERSIZEOK = 0x001;
/// IShellItemImageFactory 标志:只返回图标,绝不返回缩略图。
internal const int SIIGBF_ICONONLY = 0x004;
/// IShellItemImageFactory 标志:只返回缩略图,没有就失败(不做图标回退)。
internal const int SIIGBF_THUMBNAILONLY = 0x008;
/// IShellItemImageFactory 标志:只在内存/本地缓存里找,不触发网络或慢速解码。
internal const int SIIGBF_MEMORYONLY = 0x100;
/// 扩展名通用图标用的文件属性:普通文件(配合 SHGFI_USEFILEATTRIBUTES 不碰磁盘)。
internal const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
/// 扩展名通用图标用的文件属性:目录。
internal const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
/// E_PENDING:外壳正在后台解码,稍后重试即可(缩略图首次生成时常见)。
internal const int E_PENDING = unchecked((int)0x8000000A);
/// DrawIconEx 标志:按图标自身 alpha / 掩码正常绘制。
internal const uint DI_NORMAL = 0x0003;
/// GetSystemMetrics:标准大图标宽度。
internal const int SM_CXICON = 11;
/// GetSystemMetrics:标准大图标高度。
internal const int SM_CYICON = 12;
/// BITMAPINFOHEADER 的 BI_RGB。
internal const uint BI_RGB = 0;
/// DIB_RGB_COLORS。
internal const uint DIB_RGB_COLORS = 0;
// ─────────────────────────────────────────────────────────────
// 结构体
// ─────────────────────────────────────────────────────────────
/// SHGetFileInfoW 的输出结构(SHFILEINFOW)。
[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;
}
/// 尺寸参数(SIZE / Windows.Graphics.SizeInt32 的原生形态)。
[StructLayout(LayoutKind.Sequential)]
internal struct SIZE
{
public int cx;
public int cy;
public SIZE(int cx, int cy)
{
this.cx = cx;
this.cy = cy;
}
}
/// GDI BITMAP 结构(GetObject 输出,用于确认位图尺寸/位深)。
[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;
}
/// BITMAPINFOHEADER。
[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;
}
/// BITMAPINFO(此处只用 32bpp BI_RGB,无调色板)。
[StructLayout(LayoutKind.Sequential)]
internal struct BITMAPINFO
{
public BITMAPINFOHEADER bmiHeader;
public uint bmiColors;
}
/// GetIconInfo 的输出:HICON 拆出的掩码位图与颜色位图(必须 DeleteObject 释放)。
[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
// ─────────────────────────────────────────────────────────────
///
/// SHGetFileInfoW:按真实文件系统路径取外壳信息(会访问该路径,路径可带个性化图标叠加,如 OneDrive/共享角标)。
/// 这里显式写 EntryPoint,因为下面还有一个同签名的 PIDL 重载,C# 端必须用不同方法名区分。
///
[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);
/// SHGetFileInfoW(PIDL 版):传 ITEMIDLIST,用于 shell: 解析名(回收站、此电脑等)。
[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);
///
/// SHGetImageList:取系统图像列表(小/大/超大/巨幅)。
///
///
/// 【重要实现说明 · 实测踩坑记录】
/// SHGetImageList 返回的接口指针**不能**用 [ComImport] 的 IImageList 走 RCW 调用:
/// 实测(见 _icon_probe 验证)该对象的所谓 "vtable" 槽位里并不是函数指针
/// (读出来是 0x100000004 / 0x20021 这类数据),因此按槽位序号调用会直接
/// AccessViolationException,或在 GetImageCount 上返回垃圾值(16777215)。
/// Shell 文档保证该返回值同时就是一个 HIMAGELIST,所以这里按 HIMAGELIST 处理,
/// 用 comctl32 的 ImageList_GetIcon / ImageList_GetIconSize 取值——实测完全正常。
/// 注意:C# 方法名与导出名不同,必须显式写 EntryPoint,否则会 EntryPointNotFoundException。
///
[DllImport("shell32.dll", EntryPoint = "SHGetImageList", PreserveSig = true)]
internal static extern int SHGetImageListRaw(int iImageList, ref Guid riid, out IntPtr ppvObj);
/// comctl32 图像列表函数:按索引取 HICON(调用方负责 DestroyIcon)。ILD_TRANSPARENT = 1。
[DllImport("comctl32.dll", SetLastError = true)]
internal static extern IntPtr ImageList_GetIcon(IntPtr himl, int i, uint flags);
/// comctl32 图像列表函数:取图像列表的图标尺寸。
[DllImport("comctl32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool ImageList_GetIconSize(IntPtr himl, ref int cx, ref int cy);
/// comctl32 图像列表函数:取图像数量(用于诊断/兜底校验索引是否越界)。
[DllImport("comctl32.dll", SetLastError = true)]
internal static extern int ImageList_GetImageCount(IntPtr himl);
/// SHParseDisplayName:把 "shell:RecycleBinFolder"、"{CLSID}" 之类解析成 PIDL(调用方负责 CoTaskMemFree)。
[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);
/// SHCreateItemFromParsingName:按路径创建 IShellItem(缩略图路径的入口)。
[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);
/// DestroyIcon:释放 SHGetFileInfo / GetIcon 返回的 HICON。
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DestroyIcon(IntPtr hIcon);
/// GetIconInfo:HICON → 掩码位图 + 颜色位图(两个 HBITMAP 都要 DeleteObject)。
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO piconinfo);
/// DrawIconEx:把 HICON 画进 DC(用于把图标栅格化成 32bpp BGRA)。
[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);
/// GetSystemMetrics:取标准图标尺寸等系统度量。
[DllImport("user32.dll")]
internal static extern int GetSystemMetrics(int nIndex);
/// CreateCompatibleDC:创建内存 DC。
[DllImport("gdi32.dll", SetLastError = true)]
internal static extern IntPtr CreateCompatibleDC(IntPtr hdc);
/// DeleteDC:释放内存 DC。
[DllImport("gdi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteDC(IntPtr hdc);
/// CreateDIBSection:创建 32bpp 自上而下 DIB,直接拿到 BGRA 像素指针。
[DllImport("gdi32.dll", SetLastError = true)]
internal static extern IntPtr CreateDIBSection(
IntPtr hdc,
ref BITMAPINFO pbmi,
uint usage,
out IntPtr ppvBits,
IntPtr hSection,
uint offset);
/// CreateCompatibleBitmap:创建与 DC 兼容的位图(缩略图 HBITMAP 转像素时用)。
[DllImport("gdi32.dll", SetLastError = true)]
internal static extern IntPtr CreateCompatibleBitmap(IntPtr hdc, int width, int height);
/// SelectObject:把 GDI 对象选入 DC,返回旧对象(必须还原)。
[DllImport("gdi32.dll", SetLastError = true)]
internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr hObject);
/// DeleteObject:释放 HBITMAP / DIB 等 GDI 对象。
[DllImport("gdi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteObject(IntPtr hObject);
/// GetObjectW:读取 HBITMAP 的尺寸与位深。
[DllImport("gdi32.dll", EntryPoint = "GetObjectW", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern int GetObjectBitmap(IntPtr hgdiobj, int cbBuffer, ref BITMAP lpvObject);
/// GetDIBits:把 HBITMAP 的像素读成指定格式的 DIB(这里固定 32bpp BGRA)。
[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);
/// CoTaskMemFree:释放 SHParseDisplayName 返回的 PIDL。
[DllImport("ole32.dll")]
internal static extern void CoTaskMemFree(IntPtr pv);
// ─────────────────────────────────────────────────────────────
// COM:图像列表与外壳条目
// ─────────────────────────────────────────────────────────────
///
/// IImageList:完整的 vtable 声明,作为外壳图像列表接口的参考定义保留。
///
///
/// ⚠ 不要用这个接口去调用 SHGetImageList 返回的对象(RCW 调用会 AccessViolation)。
/// 原因与正确做法见 的注释。
/// 这里保留完整声明是为了:1) 提供 IID(SHGetImageListRaw 需要传 REFIID);
/// 2) 当外层需要 IImageList 的其它能力(如 GetImageInfo)时,有准确的槽位定义可查。
/// 由于使用 [ComImport] 映射 vtable,中间槽位必须按顺序原样占位,否则偏移错位会取到错误的函数。
///
[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);
}
/// IMAGEINFO(IImageList.GetImageInfo 输出)。
[StructLayout(LayoutKind.Sequential)]
internal struct IMAGEINFO
{
public IntPtr hbmImage;
public IntPtr hbmMask;
public int Unused1;
public int Unused2;
public RECT rcImage;
}
/// RECT。
[StructLayout(LayoutKind.Sequential)]
internal struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
/// POINT。
[StructLayout(LayoutKind.Sequential)]
internal struct POINT
{
public int x;
public int y;
}
///
/// IShellItemImageFactory:外壳缩略图/高清图标工厂,唯一能拿到「资源管理器同款」缩略图的接口。
///
[ComImport]
[Guid("bcc18b79-ba16-442f-80c4-8a59c30c463b")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IShellItemImageFactory
{
/// 取该条目的位图。返回 S_OK 时 phbm 为 HBITMAP(调用方 DeleteObject)。
[PreserveSig]
int GetImage(SIZE size, int flags, out IntPtr phbm);
/// 不参与本工程,仅用于占位以保持 vtable 顺序正确。
[PreserveSig]
int GetImageEx(SIZE size, int flags, IntPtr pColor, out IntPtr phbm);
}
// ─────────────────────────────────────────────────────────────
// 高层封装:HICON / HBITMAP → 32bpp BGRA 像素
// ─────────────────────────────────────────────────────────────
/// 32bpp BGRA 像素缓冲(自上而下,行距 = width * 4)。
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; }
/// Alpha 通道是否全 0(说明位图本身不带 alpha,需要靠亮度重建)。
public bool HasAlphaChannel()
{
for (int i = 3; i < Pixels.Length; i += 4)
{
if (Pixels[i] != 0) return true;
}
return false;
}
}
/// 解析名(shell: 语法 / CLSID)→ PIDL;失败返回 IntPtr.Zero。
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;
}
}
/// 按真实路径取系统图标索引;失败返回 -1。目录/驱动器/文件都走这条。
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(), flags);
return result == IntPtr.Zero ? -1 : info.iIcon;
}
/// 按 PIDL 取系统图标索引(shell: 解析名);失败返回 -1。
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(), flags);
return result == IntPtr.Zero ? -1 : info.iIcon;
}
///
/// 按真实路径直接取 HICON(SHGetFileInfoW + SHGFI_ICON)。
/// 小尺寸最贴近资源管理器列表视图;调用方必须在 finally 里 DestroyIcon。
///
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(), flags);
return result == IntPtr.Zero ? IntPtr.Zero : info.hIcon;
}
/// 按 PIDL 直接取 HICON(shell: 解析名);调用方负责 DestroyIcon。
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(), flags);
return result == IntPtr.Zero ? IntPtr.Zero : info.hIcon;
}
///
/// 从系统图像列表按索引取指定尺寸的 HICON(256/48/32/16);调用方负责 DestroyIcon。
/// 返回的 HICON 是外壳原版图标资源在该尺寸下的真实副本(不是放大后的位图)。
///
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);
}
}
/// 读系统图像列表在指定尺寸下的图标边长(用于诊断与兜底,不用它决定渲染尺寸)。
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);
}
}
///
/// HICON → 32bpp BGRA 像素(自上而下)。
/// 主路径:GetIconInfo 拿尺寸 → 32bpp DIB → DrawIconEx → GetDIBits 读回。
/// 若 GDI 绘制把 alpha 通道抹成 0(常见于只带掩码的老式图标),
/// 再用「黑底 vs 白底两次绘制」重建 alpha:像素在两次绘制中完全一致即为透明区。
///
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);
}
}
}
///
/// 把 HICON 画到指定纯色背景上,返回 32bpp BGRA 像素(背景不透明)。
/// 与主流程共用同一套 DIB + DrawIconEx + GetDIBits 逻辑,避免两处实现不一致。
///
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);
}
}
/// 用 GetDIBits 把已选入 DC 的 32bpp DIB 读成自上而下的 BGRA 缓冲。
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;
}
/// 构造 32bpp 自上而下 DIB 的 BITMAPINFO。
private static BITMAPINFO CreateBgraBitmapInfo(int width, int height) => new()
{
bmiHeader = new BITMAPINFOHEADER
{
biSize = (uint)Marshal.SizeOf(),
biWidth = width,
// 负高度 = top-down,行序与 BGRA 缓冲一致
biHeight = -height,
biPlanes = 1,
biBitCount = 32,
biCompression = BI_RGB,
},
};
///
/// 判断一个 HBITMAP 的颜色位图是否自带真实 alpha 通道。
/// 方法:把它读成 32bpp 后看 alpha 字节;全 0 说明它只有 1bpp AND 掩码(老式图标)。
///
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(), 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);
}
}
///
/// HBITMAP(通常来自 IShellItemImageFactory)→ 32bpp BGRA 像素。
/// 该路径返回的是 DDB,必须借助内存 DC 用 GetDIBits 转换。
///
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(), 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(),
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);
}
}
/// 取 HICON 的真实像素尺寸(GetIconInfo 的颜色/掩码位图尺寸),失败则回退到请求尺寸。
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(), 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;
}
///
/// 把左上角的图标图形搬到画布正中。
/// DrawIconEx 总是从 (0,0) 开始画,当请求画布大于图标自身尺寸时(高 DPI 下必然发生,
/// 例如逻辑 32px × 缩放 2.0 = 64px 画布 + 32px 图标),不居中就会出现"图标缩在左上角"。
///
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);
}
///
/// 由「黑底 / 白底」两次绘制结果解出 alpha 与颜色,就地写回 。
///
///
/// 原理:像素 = 原色 × 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 也接近白,
/// 会与"黑底背景"混淆。这里用邻居像素的判定结果纠正:图标内部被不透明像素包围的像素
/// 不可能是透明的。图标图案很细,单轮邻居传播已经足够。
///
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;
}
}
/// 按 BGRA 颜色填充像素缓冲(byteCount 必须是 4 的倍数)。
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;
}
}
}
/// 清零像素缓冲(全透明黑)。
private static void ClearBits(IntPtr bits, int byteCount) => FillBits(bits, byteCount, 0, 0, 0, 0);
}