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); }