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