新增托盘图标

This commit is contained in:
2026-03-01 10:42:42 +08:00
parent 0ba966cef2
commit e03e1b9766
26 changed files with 582 additions and 72 deletions

View File

@@ -235,7 +235,7 @@ namespace Melskin.Controls
}
view.Refresh();
// !string.IsNullOrEmpty(Text) 的判断
// !string.IsNullOrEmpty(Tip) 的判断
if (!string.IsNullOrEmpty(Text) && !view.IsEmpty && textBox is { IsKeyboardFocused: true })
{
IsDropDownOpen = true;

View File

@@ -17,7 +17,7 @@ namespace Melskin.Controls.Decorations;
/// CornerRadius="10"
/// Background="#40FF0000">
/// <!-- 半透明红色背景 -->
/// <TextBlock Text = "Mini Card" Foreground="White" FontSize="16"/>
/// <TextBlock Tip = "Mini Card" Foreground="White" FontSize="16"/>
///</GlassChromeDecorator>
/// </code>
/// <![CDATA[]]>

View File

@@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices;
using System.Windows.Input;
namespace Melskin.Controls;
@@ -193,7 +192,7 @@ public class MelWindow : Window
if (maximizeRestoreButton is Visual maximizeButtonVisual)
{
var bounds = VisualTreeHelper.GetDescendantBounds(maximizeButtonVisual);
Debug.WriteLine(bounds.ToString());
//Debug.WriteLine(bounds.ToString());
var buttonTransform = maximizeButtonVisual.TransformToAncestor(this);
var buttonRect = buttonTransform.TransformBounds(bounds);
@@ -228,7 +227,7 @@ public class MelWindow : Window
//VisualStateManager.GoToState(maximizeRestoreButton, "MouseOver", true);
new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1A000000")),
"Normal" => Brushes.Transparent,
//VisualStateManager.GoToState(maximizeRestoreButton, "Normal", true);
//VisualStateManager.GoToState(maximizeRestoreButton, "Normal", true);
_ => maximizeRestoreButton?.Background
};
}
@@ -322,7 +321,7 @@ public class MelWindow : Window
/// <inheritdoc />
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
base.OnApplyTemplate();
minimizeButton?.Click -= MinimizeButtonClickHandler;
minimizeButton = GetTemplateChild(VbMinimizeButtonName) as Button;

View File

@@ -83,7 +83,7 @@ public partial class ToastControl
// foreground = new SolidColorBrush(Color.FromRgb(245, 108, 108));
// break;
// }
// control.IconTextBlock.Text = icon;
// control.IconTextBlock.Tip = icon;
// control.IconTextBlock.Foreground = foreground;
// control.RootBorder.Background = background;
// control.RootBorder.BorderBrush = background;

View File

@@ -0,0 +1,216 @@
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;
}
}
}
}