悬停托盘图标触发事件 在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;//同时满足 是否有悬停消息缓存 如果没有就不显示