悬停托盘图标触发事件 在maui中实现托盘图标悬停事件
在maui中实现托盘图标悬停事件 在maui中实现托盘图标显示闪烁
系统托盘图标悬停触发事件
怎么实现,悬停托盘图标触发事件
托盘图标悬停后触发事件
解决了鼠标移入 移出事件, 优化了系统自动触发鼠标移出事件 是否在托盘图标上悬停的判断
NotifyIcon 收不到 WM_MOUSELEAVE。
用 NIN_POPUPOPEN / NIN_POPUPCLOSE(Win7+,版本 4)即可拿到 悬停 / 离开 事件,无需 TrackMouseEvent。
从 Windows 7 开始,当用户 将鼠标悬停在托盘图标上 时,Shell 会发送:
NIN_POPUPOPEN(0x0406) → 鼠标 进入
NIN_POPUPCLOSE(0x0407) → 鼠标 离开
这两个消息 仅当 NotifyIcon 版本 ≥ 4 且 设置了 NIF_SHOWTIP 才会发出。
为什么WM_MOUSELEAVE 不触发 WM_MOUSELEAVE 只会发给「普通窗口」;托盘图标位于系统壳层(Shell_TrayWnd)内部,根本不是你进程里的窗口,所以永远不会收到这条消息。
iconData.VersionOrTimeout = (uint)0x4;
bool status = WinApi.Shell_NotifyIcon(NotifyCommand.SetVersion, ref iconData);
已经返回true了,为什么 case WindowsMessages.NIN_POPUPOPEN:
ChangeToolTipStateRequest?.Invoke(true);
break;
case WindowsMessages.NIN_POPUPCLOSE:
ChangeToolTipStateRequest?.Invoke(false);
break; 为什么没有触发
在函数下加一个ToolTipText就触发了
private void CreateTaskbarIcon()
{
lock (lockObject)
{
if (IsTaskbarIconCreated)
{
return;
}
const IconDataMembers members = IconDataMembers.Message
| IconDataMembers.Icon
| IconDataMembers.Tip;
iconData.ToolTipText = "task";
windowstrayicon.cs
private void MessageSink_MouseEventReceived(MouseEvent obj)
{
if (obj == MouseEvent.IconLeftMouseUp)
{
LeftClick?.Invoke();
} else if (obj == MouseEvent.IconRightMouseUp)
{
RightClick?.Invoke();
}
else if (obj == MouseEvent.MouseHover)
{
HoverClick?.Invoke();
}
else if (obj == MouseEvent.MouseLeave)
{
LeaveClick?.Invoke();
}
}
TrayService.cs
tray.HoverClick = () => {
HoverHandler?.Invoke();
};
tray.LeaveClick = () => {
LeaveHandler?.Invoke();
};
App.xaml
trayService.HoverHandler = () =>
{
trayService.ShowTrayNotification("Enter", "Enter icon", 3);
};
trayService.LeaveHandler = () =>
{
trayService.ShowTrayNotification("Leave", "Leave icon", 3);
};
//鼠标离开托盘图标 关闭消息气泡 但是如果进入TrayNotificationWindow(消息气泡)的窗体,就继续显示
// 如果鼠标不在 消息气泡上,才关闭通知 还需要考虑一点,鼠标不在消息气泡上,但是还在托盘图标上,也不能关闭通知
鼠标不在窗口上 但是在图标上 也应该不启动定时器 防止被关闭 只要鼠标还在托盘相关区域(图标或通知窗口),通知就会一直显示。
鼠标一直悬停在图标icon上,是什么原因触发了 tray.LeaveClick 鼠标移出事件
通过调用堆栈我知道是窗口触发的鼠标移出事件,但是明明我鼠标一直都是停留在icon图标上面的,为什么会触发NIN_POPUPCLOSE事件 case WindowsMessages.NIN_POPUPCLOSE:
MouseEventReceived?.Invoke(MouseEvent.MouseLeave);
ChangeToolTipStateRequest?.Invoke(false);
break;
首先尝试修改 ShowNotification - 移除 Activate() 调用
避免使用 Activate(),改用更温和的显示方式: 使用 Win32 API 检查实际鼠标位置 case WindowsMessages.NIN_POPUPCLOSE:
// 检查鼠标实际位置,而不是立即触发 MouseLeave
最重要的是
解决方案
为了防止 NIN_POPUPCLOSE 事件影响你的托盘图标悬停事件(即使鼠标仍然停留在托盘图标上),你可以在处理 NIN_POPUPCLOSE 事件时,加入一些额外的条件判断,确保该事件不会误触发你的鼠标移出逻辑。
1. 添加条件检查:
你可以检查是否在气泡通知窗口关闭之前鼠标已移开托盘图标,如果是,则不再触发 MouseLeave 事件case WindowsMessages.NIN_POPUPCLOSE:
// 检查是否鼠标仍然悬停在托盘图标上
if (_isMouseOverTray)
{
// 鼠标仍然悬停在托盘图标上,不触发 MouseLeave
break;
}
// 如果气泡窗口关闭并且鼠标已经离开,则执行正常的操作
MouseEventReceived?.Invoke(MouseEvent.MouseLeave);
ChangeToolTipStateRequest?.Invoke(false);
break;
// 使用实际鼠标位置检测,而不是依赖状态标志
需要延迟检测,基本上满足大部分情况了
但是有时候 有可能出现鼠标移出太快或者太慢,导致无法正确判断鼠标位置导致不能正常更新位置和状态,那么应该在鼠标真正移出离开的时候(标志位是真正关闭消息气泡通知窗口的时候)做一个循环检测标志位 检测如果鼠标位置还是在图标上没有移出,就不动,移出了就要触发移出事件
左键或者右键点击click事件,必须退出气泡显示,这个时候会冒泡
如何“找到”托盘图标对应的系统控件
获取托盘图标在屏幕上的精确位置
// 1. 先找到系统托盘窗口
IntPtr trayWnd = FindWindow("Shell_TrayWnd", null);
IntPtr trayNotify = FindWindowEx(trayWnd, IntPtr.Zero, "TrayNotifyWnd", null);
IntPtr sysPager = FindWindowEx(trayNotify, IntPtr.Zero, "SysPager", null);
IntPtr toolbar = FindWindowEx(sysPager, IntPtr.Zero, "ToolbarWindow32", null);
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
public static class TrayIconEnumerator
{
private const int TB_BUTTONCOUNT = 0x0418;
private const int TB_GETBUTTON = 0x0417;
private const int TB_GETITEMRECT = 0x041A;
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left, Top, Right, Bottom;
public override string ToString() => $"({Left},{Top})-({Right},{Bottom})";
}
[StructLayout(LayoutKind.Sequential)]
private struct TBBUTTON
{
public int iBitmap;
public int idCommand;
public byte fsState;
public byte fsStyle;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public byte[] bReserved;
public IntPtr dwData; // 指向 TRAYDATA
public IntPtr iString;
}
[StructLayout(LayoutKind.Sequential)]
private struct TRAYDATA
{
public IntPtr hwnd;
public uint uID;
public uint uCallbackMessage;
public uint Reserved0;
public IntPtr hIcon;
}
public record TrayIconInfo(IntPtr Hwnd, RECT Rect, string Tip, uint Pid);
public static IReadOnlyList<TrayIconInfo> Enumerate()
{
var list = new List<TrayIconInfo>();
EnumerateBar(FindTrayToolbar(), list); // 可见区
EnumerateBar(FindOverflowToolbar(), list); // 溢出区
return list;
}
/* ========================== 私有实现 ========================== */
private static IntPtr FindTrayToolbar()
{
IntPtr tray = FindWindow("Shell_TrayWnd", null);
tray = FindWindowEx(tray, IntPtr.Zero, "TrayNotifyWnd", null);
tray = FindWindowEx(tray, IntPtr.Zero, "SysPager", null);
return FindWindowEx(tray, IntPtr.Zero, "ToolbarWindow32", null);
}
private static IntPtr FindOverflowToolbar()
{
IntPtr ov = FindWindow("NotifyIconOverflowWindow", null);
return FindWindowEx(ov, IntPtr.Zero, "ToolbarWindow32", null);
}
private static void EnumerateBar(IntPtr hToolbar, List<TrayIconInfo> list)
{
if (hToolbar == IntPtr.Zero) return;
uint pid;
GetWindowThreadProcessId(hToolbar, out pid);
var hProc = OpenProcess(0x1F0FFF, false, pid);
if (hProc == IntPtr.Zero) return;
int cnt = SendMessage(hToolbar, TB_BUTTONCOUNT, 0, 0);
IntPtr pBtn = VirtualAllocEx(hProc, IntPtr.Zero, 0x1000, 0x1000 | 0x2000, 0x40);
IntPtr pRect = VirtualAllocEx(hProc, IntPtr.Zero, Marshal.SizeOf<RECT>(), 0x1000 | 0x2000, 0x40);
for (int i = 0; i < cnt; i++)
{
// 1. 读 TBBUTTON
SendMessage(hToolbar, TB_GETBUTTON, i, pBtn);
TBBUTTON btn = Read<TBBUTTON>(hProc, pBtn);
// 2. 读 TRAYDATA
TRAYDATA tray = Read<TRAYDATA>(hProc, btn.dwData);
// 3. 读提示文本(TRAYDATA 后紧跟 wchar_t[128])
string tip = ReadString(hProc, btn.dwData + Marshal.SizeOf<TRAYDATA>(), 128);
// 4. 读图标矩形
SendMessage(hToolbar, TB_GETITEMRECT, i, pRect);
RECT rc = Read<RECT>(hProc, pRect);
// 5. 转成屏幕坐标
ClientToScreen(hToolbar, ref rc);
list.Add(new TrayIconInfo(tray.hwnd, rc, tip, pid));
}
VirtualFreeEx(hProc, pBtn, 0, 0x8000);
VirtualFreeEx(hProc, pRect, 0, 0x8000);
CloseHandle(hProc);
}
private static T Read<T>(IntPtr hProc, IntPtr addr)
{
int size = Marshal.SizeOf<T>();
byte[] buf = new byte[size];
ReadProcessMemory(hProc, addr, buf, size, out _);
GCHandle h = GCHandle.Alloc(buf, GCHandleType.Pinned);
try { return Marshal.PtrToStructure<T>(h.AddrOfPinnedObject()); }
finally { h.Free(); }
}
private static string ReadString(IntPtr hProc, IntPtr addr, int len)
{
byte[] buf = new byte[len * 2];
ReadProcessMemory(hProc, addr, buf, buf.Length, out _);
return Encoding.Unicode.GetString(buf).TrimEnd('\0');
}
/* ========================== Win32 P/Invoke ========================== */
[DllImport("user32.dll")] static extern IntPtr FindWindow(string cls, string wnd);
[DllImport("user32.dll")] static extern IntPtr FindWindowEx(IntPtr hParent, IntPtr hChild, string cls, string wnd);
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("kernel32.dll")] static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h);
[DllImport("kernel32.dll")] static extern IntPtr VirtualAllocEx(IntPtr h, IntPtr lp, int size, int type, int prot);
[DllImport("kernel32.dll")] static extern bool VirtualFreeEx(IntPtr h, IntPtr lp, int size, int freeType);
[DllImport("kernel32.dll")] static extern bool ReadProcessMemory(IntPtr h, IntPtr baseAddr, byte[] buffer, int size, out IntPtr read);
[DllImport("user32.dll")] static extern int SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam);
[DllImport("user32.dll")] static extern bool ClientToScreen(IntPtr hWnd, ref RECT rc);
}
使用方法
foreach (var icon in TrayIconEnumerator.Enumerate())
{
Console.WriteLine($"{icon.Rect} PID={icon.Pid} Tip={icon.Tip}");
}
退出应用程序的时候,图标还短暂停留在托盘区域,鼠标放上去才消失,可以主动退出图标吗优化
托盘图标的生命周期跟你的进程并不严格绑定:
只要 Explorer 没收到你主动删除(或 Explorer 自己重启),它就会把图标缓存到下次刷新。
因此必须在退出前显式调用 Shell_NotifyIcon(NIM_DELETE) 把图标摘掉
//_hoverCache.Clear(); // 显示完即不立即清空 只在点击托盘图标或者点击消息气泡的时候跳转到主页 然后再清空
show = (tray.IsMouseOverTrayIcon()|| _notificationWindow.IsMouseOver) && _hoverCache.Count>0;//同时满足 是否有悬停消息缓存 如果没有就不显示