Files

217 lines
7.8 KiB
C#
Raw Permalink Normal View History

2026-03-01 10:42:42 +08:00
using System.Runtime.InteropServices;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
namespace Melskin.Controls
{
public class TrayIcon : FrameworkElement, IDisposable
{
// 1. 悬浮提示文本
public static readonly DependencyProperty TipProperty =
DependencyProperty.Register(nameof(Tip), typeof(string), typeof(TrayIcon),
new PropertyMetadata(string.Empty, OnTextChanged));
public string Tip
{
get => (string)GetValue(TipProperty);
set => SetValue(TipProperty, value);
}
// 2. 双击命令
public static readonly DependencyProperty DoubleClickCommandProperty =
DependencyProperty.Register(nameof(DoubleClickCommand), typeof(ICommand), typeof(TrayIcon));
public ICommand DoubleClickCommand
{
get => (ICommand)GetValue(DoubleClickCommandProperty);
set => SetValue(DoubleClickCommandProperty, value);
}
public static readonly DependencyProperty ClickCommandProperty =
DependencyProperty.Register(nameof(ClickCommand), typeof(ICommand), typeof(TrayIcon));
public ICommand ClickCommand
{
get => (ICommand)GetValue(ClickCommandProperty);
set => SetValue(ClickCommandProperty, value);
}
// 3. 【新增】图标本地路径 (例如: "app.ico")
public static readonly DependencyProperty IconPathProperty =
DependencyProperty.Register(nameof(IconPath), typeof(string), typeof(TrayIcon),
new PropertyMetadata(string.Empty, OnIconPathChanged));
public string IconPath
{
get => (string)GetValue(IconPathProperty);
set => SetValue(IconPathProperty, value);
}
private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ctrl = (TrayIcon)d;
if (ctrl._isAdded)
{
ctrl._nid.szTip = e.NewValue?.ToString() ?? "";
Shell_NotifyIcon(NIM_MODIFY, ref ctrl._nid);
}
}
private static void OnIconPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ctrl = (TrayIcon)d;
if (ctrl._isAdded)
{
ctrl._nid.hIcon = ctrl.LoadMyIcon(e.NewValue?.ToString());
Shell_NotifyIcon(NIM_MODIFY, ref ctrl._nid);
}
}
// --- Win32 API 声明 ---
private const int NIM_ADD = 0x0000;
private const int NIM_MODIFY = 0x0001;
private const int NIM_DELETE = 0x0002;
private const int NIF_MESSAGE = 0x0001;
private const int NIF_ICON = 0x0002;
private const int NIF_TIP = 0x0004;
private const int WM_LBUTTONUP = 0x0202; // 左键弹起消息
private const int WM_TRAYMOUSEMESSAGE = 0x0400 + 1024;
private const int WM_LBUTTONDBLCLK = 0x0203;
private const int WM_RBUTTONUP = 0x0205; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct NOTIFYICONDATA
{
public int cbSize;
public IntPtr hwnd;
public int uID;
public int uFlags;
public int uCallbackMessage;
public IntPtr hIcon; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string szTip;
public int dwState;
public int dwStateMask; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string szInfo;
public int uTimeoutOrVersion;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string szInfoTitle;
public int dwInfoFlags;
public Guid guidItem;
public IntPtr hBalloonIcon;
}
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern bool Shell_NotifyIcon(int dwMessage, ref NOTIFYICONDATA pnid);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetForegroundWindow(IntPtr hWnd);
// 【新增】纯 Win32 加载本地图标 API
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr LoadImage(IntPtr hInst, string lpszName, uint uType, int cxDesired, int cyDesired, uint fuLoad);
private IntPtr LoadMyIcon(string iconPath)
{
if (string.IsNullOrEmpty(iconPath)) return IntPtr.Zero;
// 1 = IMAGE_ICON, 0x00000010 = LR_LOADFROMFILE
return LoadImage(IntPtr.Zero, iconPath, 1, 0, 0, 0x00000010);
}
// --- 内部状态 ---
private HwndSource _messageSink;
private int _uID = 100;
private bool _isAdded = false;
private NOTIFYICONDATA _nid;
public TrayIcon()
{
Visibility = Visibility.Collapsed;
this.Loaded += OnLoaded;
this.Unloaded += OnUnloaded;
if (Application.Current != null)
Application.Current.Exit += (s, e) => Dispose();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (_isAdded) return;
HwndSourceParameters p = new HwndSourceParameters("TrayIconMessageSink")
{
Width = 0, Height = 0, WindowStyle = 0, ExtendedWindowStyle = 0x00000080
};
_messageSink = new HwndSource(p);
_messageSink.AddHook(WndProc);
_nid = new NOTIFYICONDATA
{
cbSize = Marshal.SizeOf(typeof(NOTIFYICONDATA)),
hwnd = _messageSink.Handle,
uID = _uID,
uFlags = NIF_MESSAGE | NIF_TIP | NIF_ICON,
uCallbackMessage = WM_TRAYMOUSEMESSAGE,
szTip = this.Tip ?? "",
// 加载属性中指定的图标
hIcon = LoadMyIcon(this.IconPath)
};
Shell_NotifyIcon(NIM_ADD, ref _nid);
_isAdded = true;
}
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == WM_TRAYMOUSEMESSAGE)
{
int mouseMsg = lParam.ToInt32();
if (mouseMsg == WM_LBUTTONUP)
{
if (ClickCommand?.CanExecute(null) == true)
{
ClickCommand.Execute(null);
}
handled = true;
}
else if (mouseMsg == WM_LBUTTONDBLCLK)
{
if (DoubleClickCommand?.CanExecute(null) == true)
DoubleClickCommand.Execute(null);
handled = true;
}
else if (mouseMsg == WM_RBUTTONUP)
{
ShowContextMenu();
handled = true;
}
}
return IntPtr.Zero;
}
private void ShowContextMenu()
{
if (this.ContextMenu != null)
{
SetForegroundWindow(_messageSink.Handle);
this.ContextMenu.PlacementTarget = null;
this.ContextMenu.Placement = PlacementMode.MousePoint;
this.ContextMenu.IsOpen = true;
}
}
private void OnUnloaded(object sender, RoutedEventArgs e) => Dispose();
public void Dispose()
{
if (_isAdded)
{
Shell_NotifyIcon(NIM_DELETE, ref _nid);
_isAdded = false;
}
if (_messageSink != null)
{
_messageSink.RemoveHook(WndProc);
_messageSink.Dispose();
_messageSink = null;
}
}
}
}