c#的任务栏托盘图标控件NotifyIcon只有MouseMove事件,MouseMove事件刷新很快,很不好用,而且我们有时需要鼠标进入和离开的事件,但是不知道c#怎么回事,没有提供,那么就只能自己来处理了。
解决鼠标进入和离开的思路是:
1.通过MouseMove事件确定当前鼠标已经进入托盘图标的范围
2.进入后启动检测timer
3.定时检测托盘图标的位置和当前鼠标的位置,判断鼠标是否在托盘图标的范围内
主要难点:获取当前托盘图标的位置
获取托盘图标位置的思路:
1.查找到托盘图标所在的窗口
private IntPtr FindTrayToolbarWindow() { IntPtr hWnd = FindWindow("Shell_TrayWnd", null); if (hWnd != IntPtr.Zero) { hWnd = FindWindowEx(hWnd, IntPtr.Zero, "TrayNotifyWnd", null); if (hWnd != IntPtr.Zero) { hWnd = FindWindowEx(hWnd, IntPtr.Zero, "SysPager", null); if (hWnd != IntPtr.Zero) { hWnd = FindWindowEx(hWnd, IntPtr.Zero, "ToolbarWindow32", null); } } } return hWnd; }
2.遍历窗口内的托盘图标
3.获取当前托盘图标的句柄,通过句柄得到这个托盘图标所关联的进程id
4.通过进程id比较获取到当前程序的托盘图标
5.拖过api获取当前托盘图标相对于它所在窗口的位置
6.获取窗口在整个屏幕中的位置,在计算出托盘图标相对于屏幕的位置
2-6代码:
private bool FindNotifyIcon(IntPtr hTrayWnd, ref Rect rectNotify) { UInt32 trayPid = 0; Rect rectTray = new Rect(); GetWindowRect(hTrayWnd, out rectTray); int count = (int) SendMessage(hTrayWnd, TB_BUTTONCOUNT, 0, IntPtr.Zero); //给托盘窗口发消息,得到托盘里图标 bool isFind = false; if (count > 0) { GetWindowThreadProcessId(hTrayWnd, out trayPid); //取得托盘窗口对应的进程id //获取托盘图标的位置 IntPtr hProcess = OpenProcess(ProcessAccess.VMOperation | ProcessAccess.VMRead | ProcessAccess.VMWrite, false, trayPid); //打开进程,取得进程句柄 IntPtr address = VirtualAllocEx(hProcess, //在目标进程中申请一块内存,放TBBUTTON信息 IntPtr.Zero, 1024, AllocationType.Commit, MemoryProtection.ReadWrite); TBBUTTON btnData = new TBBUTTON(); TRAYDATA trayData = new TRAYDATA(); // Console.WriteLine("Count:"+count); var handel = Process.GetCurrentProcess().Id; // Console.WriteLine("curHandel:" + handel); for (uint j = 0; j < count; j++) { // Console.WriteLine("j:"+j); var i = j; SendMessage(hTrayWnd, TB_GETBUTTON, i, address); //取得TBBUTTON结构到本地 int iTmp = 0; var isTrue = ReadProcessMemory(hProcess, address, out btnData, Marshal.SizeOf(btnData), out iTmp); if (isTrue == false) continue; //这一步至关重要,不能省略 //主要解决64位系统电脑运行的是x86的程序 if (btnData.dwData == IntPtr.Zero) { btnData.dwData = btnData.iString; } ReadProcessMemory(hProcess, //从目标进程address处存放的是TBBUTTON btnData.dwData, //取dwData字段指向的TRAYDATA结构 out trayData, Marshal.SizeOf(trayData), out iTmp); UInt32 dwProcessId = 0; GetWindowThreadProcessId(trayData.hwnd, //通过TRAYDATA里的hwnd字段取得本图标的进程id out dwProcessId); //获取当前进程id // StringBuilder sb = new StringBuilder(256); // GetModuleFileNameEx(OpenProcess(ProcessAccess.AllAccess, false, dwProcessId), IntPtr.Zero, sb, 256); // Console.WriteLine(sb.ToString()); if (dwProcessId == (UInt32) handel) { Rect rect = new Rect(); IntPtr lngRect = VirtualAllocEx(hProcess, //在目标进程中申请一块内存,放TBBUTTON信息 IntPtr.Zero, Marshal.SizeOf(typeof (Rect)), AllocationType.Commit, MemoryProtection.ReadWrite); i = j; SendMessage(hTrayWnd, TB_GETITEMRECT, i, lngRect); isTrue = ReadProcessMemory(hProcess, lngRect, out rect, Marshal.SizeOf(rect), out iTmp); //释放内存 VirtualFreeEx(hProcess, lngRect, Marshal.SizeOf(rect), FreeType.Decommit); VirtualFreeEx(hProcess, lngRect, 0, FreeType.Release); int left = rectTray.Left + rect.Left; int top = rectTray.Top + rect.Top; int botton = rectTray.Top + rect.Bottom; int right = rectTray.Left + rect.Right; rectNotify = new Rect(); rectNotify.Left = left; rectNotify.Right = right; rectNotify.Top = top; rectNotify.Bottom = botton; isFind = true; break; } } VirtualFreeEx(hProcess, address, 0x4096, FreeType.Decommit); VirtualFreeEx(hProcess, address, 0, FreeType.Release); CloseHandle(hProcess); } return isFind; }
7.如果没有找到,那么需要用相同的方法在托盘溢出区域内查找
private IntPtr FindTrayToolbarOverFlowWindow() { IntPtr hWnd = FindWindow("NotifyIconOverflowWindow", null); if (hWnd != IntPtr.Zero) { hWnd = FindWindowEx(hWnd, IntPtr.Zero, "ToolbarWindow32", null); } return hWnd; }
在查找中的难点:
1.对于32位操作系统和64位操作系统,系统内部处理方式不一样,所以许多时候当去取TBBUTTON结构到本地的时候得到的地址为0,这里查询了一些资料,网上一些资料TBBUTTON的结构体如下:
[StructLayout(LayoutKind.Sequential, Pack = 1)] public struct TBBUTTON { public int iBitmap; public int idCommand; public byte fsState; public byte fsStyle; public byte bReserved0; public byte bReserved1; public IntPtr dwData; public IntPtr iString; }
这个在32位下面没有问题,但是在64位系统下就出现了问题,后面参考网上一些资料,原来问题出在中间4个byte中,由于32位系统中4个byte刚好32位,但是在64位中这里就不对,所以就修改为如下:
[StructLayout(LayoutKind.Sequential, Pack = 1)] public struct TBBUTTON { public int iBitmap; public int idCommand; public IntPtr fsStateStylePadding; public IntPtr dwData; public IntPtr iString; }
修改过后在64位系统中运行通过了,在这样一位问题解决了,但是当我将解决方案迁移到程序当中的时候,却出了问题,一直不能得到地址,查找了很多原因,原来是在我程序编译的时候,生成的平台是X86,那么就造成了64位系统中使用32位平台时出现问题:
到这里我就猜想是不是 public IntPtr fsStateStylePadding;这一句出了问题,当时x86平台的时候,这个只占用了32位,但是实际64位系统这个位置应该要占用64位,造成地址不对,出错了。
那么接下来我就证实了下这个问题,在我获取地址的时候在public IntPtr dwData;字段中没有获取到,但是在public IntPtr iString;字段中获取到了,那么证明我的猜想是对的(真正是否正确还需要指正),
那么解决方案就来了,为了更好的兼容性,结构体不变,当获取dwData地址没有获取到的时候,我们查找iString字段就好了,方法在这里:
//这一步至关重要,不能省略 //主要解决64位系统电脑运行的是x86的程序 if (btnData.dwData == IntPtr.Zero) { btnData.dwData = btnData.iString; }
把这个主要的解决了,后面就是查找当前托盘图标相对于父窗体的位置了,使用了很多方法:
GetWindowRect
ScreenToClient
GetClientRect
这些方法都没有成功,最后发现网上有这么一种方法。
SendMessage(hTrayWnd, TB_GETITEMRECT, i, lngRect); isTrue = ReadProcessMemory(hProcess, lngRect, out rect, Marshal.SizeOf(rect), out iTmp);
在这里真正感受到c++的强大。
在解决这个问题的过程中,参考了很多方案,通过整合才解决了这个问题,如下:
http://blog.163.com/zjlovety@126/blog/static/22418624201142763542917/
http://blog.csdn.net/wzsy/article/details/47980317
http://www.cnblogs.com/hanf/archive/2011/08/09/2131641.html
等。
以下是调用方法:
private void Load() { this._notifyIcon.MouseDoubleClick += notifyIcon_MouseDoubleClick; _notifyIcon.MouseMove += new MouseEventHandler(notifyIcon_MouseMove); CreateNotifyMouseHelper(); } private NotifyIconMouseHelper notifyHelper; private Timer timer = null; private void CreateNotifyMouseHelper() { notifyHelper=new NotifyIconMouseHelper(); notifyHelper.MouseEnterNotifyStatusChanged+= MouseEnterNotifyStatusChanged; } private void MouseEnterNotifyStatusChanged(object sender, bool isEnter) { if (isEnter) { Console.WriteLine("鼠标进入"); } else { Console.WriteLine("鼠标离开"); } }
以下是检测的源代码:
using System; using System.Collections.Generic; using System.Data.Entity.Core.Metadata.Edm; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Timers; using System.Windows; namespace NotifyTest { /*托盘图标鼠标进入离开事件 */ public delegate void MouseEnterNotifyStatusChangedHandel(object sender, bool isEnter); public class NotifyIconMouseHelper { #region win32类库 [Flags()] public enum ProcessAccess : int { /// <summary>Specifies all possible access flags for the process object.</summary> AllAccess = CreateThread | DuplicateHandle | QueryInformation | SetInformation | Terminate | VMOperation | VMRead | VMWrite | Synchronize, /// <summary>Enables usage of the process handle in the CreateRemoteThread function to create a thread in the process.</summary> CreateThread = 0x2, /// <summary>Enables usage of the process handle as either the source or target process in the DuplicateHandle function to duplicate a handle.</summary> DuplicateHandle = 0x40, /// <summary>Enables usage of the process handle in the GetExitCodeProcess and GetPriorityClass functions to read information from the process object.</summary> QueryInformation = 0x400, /// <summary>Enables usage of the process handle in the SetPriorityClass function to set the priority class of the process.</summary> SetInformation = 0x200, /// <summary>Enables usage of the process handle in the TerminateProcess function to terminate the process.</summary> Terminate = 0x1, /// <summary>Enables usage of the process handle in the VirtualProtectEx and WriteProcessMemory functions to modify the virtual memory of the process.</summary> VMOperation = 0x8, /// <summary>Enables usage of the process handle in the ReadProcessMemory function to' read from the virtual memory of the process.</summary> VMRead = 0x10, /// <summary>Enables usage of the process handle in the WriteProcessMemory function to write to the virtual memory of the process.</summary> VMWrite = 0x20, /// <summary>Enables usage of the process handle in any of the wait functions to wait for the process to terminate.</summary> Synchronize = 0x100000 } [StructLayout(LayoutKind.Sequential)] private struct TRAYDATA { public IntPtr hwnd; public UInt32 uID; public UInt32 uCallbackMessage; public UInt32 bReserved0; public UInt32 bReserved1; public IntPtr hIcon; } [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct TBBUTTON { public int iBitmap; public int idCommand; public IntPtr fsStateStylePadding; public IntPtr dwData; public IntPtr iString; } [Flags] public enum AllocationType { Commit = 0x1000, Reserve = 0x2000, Decommit = 0x4000, Release = 0x8000, Reset = 0x80000, Physical = 0x400000, TopDown = 0x100000, WriteWatch = 0x200000, LargePages = 0x20000000 } [Flags] public enum MemoryProtection { Execute = 0x10, ExecuteRead = 0x20, ExecuteReadWrite = 0x40, ExecuteWriteCopy = 0x80, NoAccess = 0x01, ReadOnly = 0x02, ReadWrite = 0x04, WriteCopy = 0x08, GuardModifierflag = 0x100, NoCacheModifierflag = 0x200, WriteCombineModifierflag = 0x400 } [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); [DllImport("user32.dll", SetLastError = true)] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); [DllImport("kernel32.dll")] private static extern IntPtr OpenProcess(ProcessAccess dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwProcessId); [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern UInt32 SendMessage(IntPtr hWnd, UInt32 Msg, UInt32 wParam, IntPtr lParam); [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] private static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, int dwSize, AllocationType flAllocationType, MemoryProtection flProtect); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool ReadProcessMemory( IntPtr hProcess, IntPtr lpBaseAddress, out TBBUTTON lpBuffer, int dwSize, out int lpNumberOfBytesRead ); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool ReadProcessMemory( IntPtr hProcess, IntPtr lpBaseAddress, out Rect lpBuffer, int dwSize, out int lpNumberOfBytesRead ); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool ReadProcessMemory( IntPtr hProcess, IntPtr lpBaseAddress, out TRAYDATA lpBuffer, int dwSize, out int lpNumberOfBytesRead ); [DllImport("psapi.dll")] private static extern uint GetModuleFileNameEx(IntPtr hProcess, IntPtr hModule, [Out] StringBuilder lpBaseName, [In] [MarshalAs(UnmanagedType.U4)] int nSize); [Flags] public enum FreeType { Decommit = 0x4000, Release = 0x8000, } [DllImport("kernel32.dll")] private static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, int dwSize, FreeType dwFreeType); [DllImport("kernel32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CloseHandle(IntPtr hObject); [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; public POINT(int x, int y) { this.X = x; this.Y = y; } public override string ToString() { return ("X:" + X + ", Y:" + Y); } } [DllImport("user32")] public static extern bool GetClientRect( IntPtr hwnd, out Rect lpRect ); [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern bool GetCursorPos(out POINT pt); [StructLayout(LayoutKind.Sequential)] public struct Rect { public int Left; public int Top; public int Right; public int Bottom; } [DllImport("user32.dll")] private static extern int GetWindowRect(IntPtr hwnd, out Rect lpRect); public const int WM_USER = 0x0400; public const int TB_BUTTONCOUNT = WM_USER + 24; public const int TB_GETBUTTON = WM_USER + 23; public const int TB_GETBUTTONINFOW = WM_USER + 63; public const int TB_GETITEMRECT = WM_USER + 29; #endregion #region 检测托盘图标相对于屏幕位置 private bool FindNotifyIcon(ref Rect rect) { Rect rectNotify = new Rect(); IntPtr hTrayWnd = FindTrayToolbarWindow(); //找到托盘窗口句柄 var isTrue = FindNotifyIcon(hTrayWnd, ref rectNotify); if (isTrue == false) { hTrayWnd = FindTrayToolbarOverFlowWindow(); //找到托盘窗口句柄 isTrue = FindNotifyIcon(hTrayWnd, ref rectNotify); } rect = rectNotify; return isTrue; } private IntPtr FindTrayToolbarWindow() { IntPtr hWnd = FindWindow("Shell_TrayWnd", null); if (hWnd != IntPtr.Zero) { hWnd = FindWindowEx(hWnd, IntPtr.Zero, "TrayNotifyWnd", null); if (hWnd != IntPtr.Zero) { hWnd = FindWindowEx(hWnd, IntPtr.Zero, "SysPager", null); if (hWnd != IntPtr.Zero) { hWnd = FindWindowEx(hWnd, IntPtr.Zero, "ToolbarWindow32", null); } } } return hWnd; } private IntPtr FindTrayToolbarOverFlowWindow() { IntPtr hWnd = FindWindow("NotifyIconOverflowWindow", null); if (hWnd != IntPtr.Zero) { hWnd = FindWindowEx(hWnd, IntPtr.Zero, "ToolbarWindow32", null); } return hWnd; } private bool FindNotifyIcon(IntPtr hTrayWnd, ref Rect rectNotify) { UInt32 trayPid = 0; Rect rectTray = new Rect(); GetWindowRect(hTrayWnd, out rectTray); int count = (int) SendMessage(hTrayWnd, TB_BUTTONCOUNT, 0, IntPtr.Zero); //给托盘窗口发消息,得到托盘里图标 bool isFind = false; if (count > 0) { GetWindowThreadProcessId(hTrayWnd, out trayPid); //取得托盘窗口对应的进程id //获取托盘图标的位置 IntPtr hProcess = OpenProcess(ProcessAccess.VMOperation | ProcessAccess.VMRead | ProcessAccess.VMWrite, false, trayPid); //打开进程,取得进程句柄 IntPtr address = VirtualAllocEx(hProcess, //在目标进程中申请一块内存,放TBBUTTON信息 IntPtr.Zero, 1024, AllocationType.Commit, MemoryProtection.ReadWrite); TBBUTTON btnData = new TBBUTTON(); TRAYDATA trayData = new TRAYDATA(); // Console.WriteLine("Count:"+count); var handel = Process.GetCurrentProcess().Id; // Console.WriteLine("curHandel:" + handel); for (uint j = 0; j < count; j++) { // Console.WriteLine("j:"+j); var i = j; SendMessage(hTrayWnd, TB_GETBUTTON, i, address); //取得TBBUTTON结构到本地 int iTmp = 0; var isTrue = ReadProcessMemory(hProcess, address, out btnData, Marshal.SizeOf(btnData), out iTmp); if (isTrue == false) continue; //这一步至关重要,不能省略 //主要解决64位系统电脑运行的是x86的程序 if (btnData.dwData == IntPtr.Zero) { btnData.dwData = btnData.iString; } ReadProcessMemory(hProcess, //从目标进程address处存放的是TBBUTTON btnData.dwData, //取dwData字段指向的TRAYDATA结构 out trayData, Marshal.SizeOf(trayData), out iTmp); UInt32 dwProcessId = 0; GetWindowThreadProcessId(trayData.hwnd, //通过TRAYDATA里的hwnd字段取得本图标的进程id out dwProcessId); //获取当前进程id // StringBuilder sb = new StringBuilder(256); // GetModuleFileNameEx(OpenProcess(ProcessAccess.AllAccess, false, dwProcessId), IntPtr.Zero, sb, 256); // Console.WriteLine(sb.ToString()); if (dwProcessId == (UInt32) handel) { Rect rect = new Rect(); IntPtr lngRect = VirtualAllocEx(hProcess, //在目标进程中申请一块内存,放TBBUTTON信息 IntPtr.Zero, Marshal.SizeOf(typeof (Rect)), AllocationType.Commit, MemoryProtection.ReadWrite); i = j; SendMessage(hTrayWnd, TB_GETITEMRECT, i, lngRect); isTrue = ReadProcessMemory(hProcess, lngRect, out rect, Marshal.SizeOf(rect), out iTmp); //释放内存 VirtualFreeEx(hProcess, lngRect, Marshal.SizeOf(rect), FreeType.Decommit); VirtualFreeEx(hProcess, lngRect, 0, FreeType.Release); int left = rectTray.Left + rect.Left; int top = rectTray.Top + rect.Top; int botton = rectTray.Top + rect.Bottom; int right = rectTray.Left + rect.Right; rectNotify = new Rect(); rectNotify.Left = left; rectNotify.Right = right; rectNotify.Top = top; rectNotify.Bottom = botton; isFind = true; break; } } VirtualFreeEx(hProcess, address, 0x4096, FreeType.Decommit); VirtualFreeEx(hProcess, address, 0, FreeType.Release); CloseHandle(hProcess); } return isFind; } #endregion public MouseEnterNotifyStatusChangedHandel MouseEnterNotifyStatusChanged; private object moveObject = new object(); private bool isOver = false; private Timer timer = null; public void MouseEnter() { lock (moveObject) { if (isOver) return; //加载鼠标进入事件 MouseEnter(true); CreateCheckTimer(); timer.Enabled = true; } } private void CreateCheckTimer() { if (timer != null) return; timer = new Timer(); timer.Interval = 120; timer.Elapsed += TimerOnElapsed; } private void TimerOnElapsed(object sender, ElapsedEventArgs arg) { //300毫秒检测一次 //判断鼠标是否在托盘图标内 //如果在,那么就不管 //如果不在,就加载鼠标离开事件,同时停止timer var isEnter = CheckMouseIsEnter(); if (isEnter) return; timer.Enabled = false; MouseEnter(false); } private void MouseEnter(bool isEnter) { isOver = isEnter; if (MouseEnterNotifyStatusChanged == null) return; MouseEnterNotifyStatusChanged(this, isEnter); } public Point Point { get; set; } private bool CheckMouseIsEnter() { //这里怎么检测呢 //我很无语啊 //第一步:获取当前鼠标的坐标 //第二步:获取托盘图标的坐标 // ???? 难难难难难难难难难难 try { Rect rectNotify = new Rect(); var isTrue = FindNotifyIcon(ref rectNotify); if (isTrue == false) return false; POINT point = new POINT(); GetCursorPos(out point); // Console.WriteLine(string.Format(@" //Left={0} Top={1} Right={2} Bottom={3}", rectNotify.Left, rectNotify.Top, rectNotify.Right, rectNotify.Bottom)); // Console.WriteLine(point.X + " " + point.Y); //第三步:比较鼠标图标是否在托盘图标的范围内 if (point.X >= rectNotify.Left && point.X <= rectNotify.Right && point.Y >= rectNotify.Top && point.Y <= rectNotify.Bottom) { Point = new Point(point.X, point.Y); return true; } else { return false; } } catch (Exception) { return false; } } } }