5ce2c8a982
为 Claude Code 提供原生 Windows toast 通知:点击跳回原窗口、切回 Windows Terminal 标签、跨虚拟桌面、调用方图标、非阻塞投递;NativeAOT 单文件分发。
203 lines
5.4 KiB
C#
203 lines
5.4 KiB
C#
using System;
|
|
using System.Runtime.InteropServices;
|
|
using Avalonia;
|
|
using Avalonia.Media.Imaging;
|
|
using Avalonia.Platform;
|
|
|
|
namespace Notify.Interop;
|
|
|
|
/// <summary>
|
|
/// 从 exe 提取图标并转成 Avalonia 位图
|
|
///
|
|
/// ExtractIconEx 拿 HICON,再用 GDI 读出 BGRA 像素构造 Bitmap;不依赖
|
|
/// System.Drawing(其 AOT 不友好)
|
|
/// </summary>
|
|
internal static partial class AppIcon
|
|
{
|
|
public static Bitmap? Extract(string exePath)
|
|
{
|
|
if (string.IsNullOrEmpty(exePath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var hIcon = IntPtr.Zero;
|
|
try
|
|
{
|
|
if (ExtractIconExW(exePath, 0, out hIcon, out _, 1) == 0 || hIcon == IntPtr.Zero)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return IconToBitmap(hIcon);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
finally
|
|
{
|
|
if (hIcon != IntPtr.Zero)
|
|
{
|
|
DestroyIcon(hIcon);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static Bitmap? IconToBitmap(IntPtr hIcon)
|
|
{
|
|
if (!GetIconInfo(hIcon, out var ii))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
var bm = default(BITMAP);
|
|
if (GetObjectW(ii.hbmColor, Marshal.SizeOf<BITMAP>(), ref bm) == 0 || bm.bmWidth <= 0 || bm.bmHeight <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var w = bm.bmWidth;
|
|
var h = bm.bmHeight;
|
|
var buffer = new byte[w * h * 4];
|
|
|
|
var bmi = new BITMAPINFOHEADER
|
|
{
|
|
biSize = (uint)Marshal.SizeOf<BITMAPINFOHEADER>(),
|
|
biWidth = w,
|
|
biHeight = -h, // 负数 = 自上而下,行序正常
|
|
biPlanes = 1,
|
|
biBitCount = 32,
|
|
biCompression = 0,
|
|
};
|
|
|
|
var hdc = GetDC(IntPtr.Zero);
|
|
try
|
|
{
|
|
if (GetDIBits(hdc, ii.hbmColor, 0, (uint)h, buffer, ref bmi, 0) == 0)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
ReleaseDC(IntPtr.Zero, hdc);
|
|
}
|
|
|
|
// 某些老图标无 alpha 通道(全 0),那样会整块透明,补成不透明
|
|
var anyAlpha = false;
|
|
for (var i = 3; i < buffer.Length; i += 4)
|
|
{
|
|
if (buffer[i] != 0)
|
|
{
|
|
anyAlpha = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!anyAlpha)
|
|
{
|
|
for (var i = 3; i < buffer.Length; i += 4)
|
|
{
|
|
buffer[i] = 255;
|
|
}
|
|
}
|
|
|
|
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
|
try
|
|
{
|
|
return new Bitmap(
|
|
PixelFormat.Bgra8888,
|
|
AlphaFormat.Unpremul,
|
|
handle.AddrOfPinnedObject(),
|
|
new PixelSize(w, h),
|
|
new Vector(96, 96),
|
|
w * 4);
|
|
}
|
|
finally
|
|
{
|
|
handle.Free();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (ii.hbmColor != IntPtr.Zero)
|
|
{
|
|
DeleteObject(ii.hbmColor);
|
|
}
|
|
|
|
if (ii.hbmMask != IntPtr.Zero)
|
|
{
|
|
DeleteObject(ii.hbmMask);
|
|
}
|
|
}
|
|
}
|
|
|
|
[LibraryImport("shell32.dll", EntryPoint = "ExtractIconExW", StringMarshalling = StringMarshalling.Utf16)]
|
|
private static partial uint ExtractIconExW(string lpszFile, int nIconIndex, out IntPtr phiconLarge, out IntPtr phiconSmall, uint nIcons);
|
|
|
|
[LibraryImport("user32.dll")]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static partial bool GetIconInfo(IntPtr hIcon, out ICONINFO piconinfo);
|
|
|
|
[LibraryImport("user32.dll")]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static partial bool DestroyIcon(IntPtr hIcon);
|
|
|
|
[LibraryImport("gdi32.dll", EntryPoint = "GetObjectW")]
|
|
private static partial int GetObjectW(IntPtr hgdiobj, int cbBuffer, ref BITMAP lpvObject);
|
|
|
|
[LibraryImport("gdi32.dll")]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static partial bool DeleteObject(IntPtr hObject);
|
|
|
|
[LibraryImport("gdi32.dll")]
|
|
private static partial int GetDIBits(IntPtr hdc, IntPtr hbmp, uint uStartScan, uint cScanLines, [Out] byte[] lpvBits, ref BITMAPINFOHEADER lpbi, uint uUsage);
|
|
|
|
[LibraryImport("user32.dll")]
|
|
private static partial IntPtr GetDC(IntPtr hWnd);
|
|
|
|
[LibraryImport("user32.dll")]
|
|
private static partial int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
internal struct ICONINFO
|
|
{
|
|
public int fIcon;
|
|
public uint xHotspot;
|
|
public uint yHotspot;
|
|
public IntPtr hbmMask;
|
|
public IntPtr hbmColor;
|
|
}
|
|
|
|
[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;
|
|
}
|
|
|
|
[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;
|
|
}
|