Files
chuan 5ce2c8a982 feat: Claude Code 原生 Windows 通知(C# / .NET 10 + Avalonia 12)
为 Claude Code 提供原生 Windows toast 通知:点击跳回原窗口、切回 Windows
Terminal 标签、跨虚拟桌面、调用方图标、非阻塞投递;NativeAOT 单文件分发。
2026-06-22 18:05:15 +08:00

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