217 lines
7.8 KiB
C#
217 lines
7.8 KiB
C#
|
|
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;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|