From 3aa0f09d3991d88eaef12373123ce9b7eca95237 Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Sat, 20 Apr 2019 23:36:44 +0800 Subject: [PATCH] improved notifyicon added mouseenter and mouseleave events --- .../Tools/Helper/AssemblyHelper.cs | 18 +- .../Controls/NotifyIconDemoCtl.xaml.cs | 7 + .../Controls/NotifyIconDemoViewModel.cs | 29 +++ .../ViewModel/Main/MainViewModel.cs | 5 +- .../Controls/Other/NotifyIcon.cs | 188 ++++++++++++++++-- .../Tools/Interop/NativeMethods.cs | 157 ++++++++++++++- 6 files changed, 388 insertions(+), 16 deletions(-) diff --git a/src/Shared/HandyControlDemo_Shared/Tools/Helper/AssemblyHelper.cs b/src/Shared/HandyControlDemo_Shared/Tools/Helper/AssemblyHelper.cs index ae55a7ee..da778226 100644 --- a/src/Shared/HandyControlDemo_Shared/Tools/Helper/AssemblyHelper.cs +++ b/src/Shared/HandyControlDemo_Shared/Tools/Helper/AssemblyHelper.cs @@ -1,15 +1,31 @@ using System; +using System.Collections.Generic; namespace HandyControlDemo.Tools { - public class AssemblyHelper + internal class AssemblyHelper { private static readonly string NameSpaceStr = typeof(AssemblyHelper).Assembly.GetName().Name; + private static readonly Dictionary CacheDic = new Dictionary(); + + public static void Register(string name, object instance) => CacheDic[name] = instance; + + public static object ResolveByKey(string key) + { + if (CacheDic.TryGetValue(key, out var value)) + { + return value; + } + + return null; + } + public static object CreateInternalInstance(string className) { try { + var type = Type.GetType($"{NameSpaceStr}.{className}"); return type == null ? null : Activator.CreateInstance(type); } diff --git a/src/Shared/HandyControlDemo_Shared/UserControl/Controls/NotifyIconDemoCtl.xaml.cs b/src/Shared/HandyControlDemo_Shared/UserControl/Controls/NotifyIconDemoCtl.xaml.cs index 4efd472c..402408dd 100644 --- a/src/Shared/HandyControlDemo_Shared/UserControl/Controls/NotifyIconDemoCtl.xaml.cs +++ b/src/Shared/HandyControlDemo_Shared/UserControl/Controls/NotifyIconDemoCtl.xaml.cs @@ -1,4 +1,6 @@ using System.Windows; +using HandyControlDemo.Tools; +using HandyControlDemo.ViewModel; namespace HandyControlDemo.UserControl { @@ -7,8 +9,13 @@ namespace HandyControlDemo.UserControl public NotifyIconDemoCtl() { InitializeComponent(); + + AssemblyHelper.Register(nameof(NotifyIconDemoCtl), this); + Unloaded += NotifyIconDemoCtl_Unloaded; } + private void NotifyIconDemoCtl_Unloaded(object sender, RoutedEventArgs e) => ViewModelLocator.Instance.NotifyIconDemo.Cleanup(); + private void ButtonPush_OnClick(object sender, RoutedEventArgs e) => NotifyIconContextContent.CloseContextControl(); } } diff --git a/src/Shared/HandyControlDemo_Shared/ViewModel/Controls/NotifyIconDemoViewModel.cs b/src/Shared/HandyControlDemo_Shared/ViewModel/Controls/NotifyIconDemoViewModel.cs index 72ae46b6..9b7750d1 100644 --- a/src/Shared/HandyControlDemo_Shared/ViewModel/Controls/NotifyIconDemoViewModel.cs +++ b/src/Shared/HandyControlDemo_Shared/ViewModel/Controls/NotifyIconDemoViewModel.cs @@ -12,6 +12,10 @@ namespace HandyControlDemo.ViewModel { public class NotifyIconDemoViewModel : ViewModelBase { + private bool _isCleanup; + + private bool _reversed; + private bool _contextMenuIsShow; public bool ContextMenuIsShow @@ -25,6 +29,12 @@ namespace HandyControlDemo.ViewModel Set(ref _contextMenuIsShow, value); #endif GlobalData.NotifyIconIsShow = ContextMenuIsShow || ContextContentIsShow; + if (!_isCleanup && !_reversed) + { + _reversed = true; + ContextContentIsShow = !value; + _reversed = false; + } } } @@ -53,6 +63,12 @@ namespace HandyControlDemo.ViewModel Set(ref _contextContentIsShow, value); #endif GlobalData.NotifyIconIsShow = ContextMenuIsShow || ContextContentIsShow; + if (!_isCleanup && !_reversed) + { + _reversed = true; + ContextMenuIsShow = !value; + _reversed = false; + } } } @@ -70,5 +86,18 @@ namespace HandyControlDemo.ViewModel public RelayCommand MouseCmd => new Lazy>(() => new RelayCommand(str=> Growl.Info(str.ToString()))).Value; + + public override void Cleanup() + { + base.Cleanup(); + + _isCleanup = true; + ContextMenuIsShow = false; + ContextMenuIsBlink = false; + ContextContentIsShow = false; + ContextContentIsBlink = false; + GlobalData.NotifyIconIsShow = false; + _isCleanup = false; + } } } \ No newline at end of file diff --git a/src/Shared/HandyControlDemo_Shared/ViewModel/Main/MainViewModel.cs b/src/Shared/HandyControlDemo_Shared/ViewModel/Main/MainViewModel.cs index d3a3d1bc..535fd329 100644 --- a/src/Shared/HandyControlDemo_Shared/ViewModel/Main/MainViewModel.cs +++ b/src/Shared/HandyControlDemo_Shared/ViewModel/Main/MainViewModel.cs @@ -82,7 +82,7 @@ namespace HandyControlDemo.ViewModel #endregion - #region 命令 +#region 命令 /// /// 切换例子命令 @@ -109,7 +109,8 @@ namespace HandyControlDemo.ViewModel _listBoxItemCurrent = item; ContentTitle = item.Content; Messenger.Default.Send(false, MessageToken.FullSwitch); - Messenger.Default.Send(AssemblyHelper.CreateInternalInstance($"UserControl.{tag}"), MessageToken.LoadShowContent); + var obj = AssemblyHelper.ResolveByKey(tag); + Messenger.Default.Send(obj ?? AssemblyHelper.CreateInternalInstance($"UserControl.{tag}"), MessageToken.LoadShowContent); } else { diff --git a/src/Shared/HandyControl_Shared/Controls/Other/NotifyIcon.cs b/src/Shared/HandyControl_Shared/Controls/Other/NotifyIcon.cs index badb1c40..fc87cbb4 100644 --- a/src/Shared/HandyControl_Shared/Controls/Other/NotifyIcon.cs +++ b/src/Shared/HandyControl_Shared/Controls/Other/NotifyIcon.cs @@ -1,4 +1,6 @@ using System; +using System.Diagnostics; +using System.Runtime.InteropServices; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; @@ -14,6 +16,8 @@ namespace HandyControl.Controls { public class NotifyIcon : FrameworkElement, IDisposable { + private bool _isMouseOver; + private bool _added; private readonly object _syncObj = new object(); @@ -44,7 +48,9 @@ namespace HandyControl.Controls private bool _doubleClick; - private DispatcherTimer _dispatcherTimer; + private DispatcherTimer _dispatcherTimerBlink; + + private DispatcherTimer _dispatcherTimerPos; private bool _isTransparent; @@ -87,6 +93,12 @@ namespace HandyControl.Controls OnIconChanged(); UpdateIcon(true); } + + _dispatcherTimerPos = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(200) + }; + _dispatcherTimerPos.Tick += DispatcherTimerPos_Tick; }; if (Application.Current != null) Application.Current.Exit += (s, e) => Dispose(); @@ -137,9 +149,9 @@ namespace HandyControl.Controls private static void OnBlinkIntervalChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var ctl = (NotifyIcon)d; - if (ctl._dispatcherTimer != null) + if (ctl._dispatcherTimerBlink != null) { - ctl._dispatcherTimer.Interval = (TimeSpan) e.NewValue; + ctl._dispatcherTimerBlink.Interval = (TimeSpan) e.NewValue; } } @@ -158,30 +170,175 @@ namespace HandyControl.Controls if (ctl.Visibility != Visibility.Visible) return; if ((bool) e.NewValue) { - if (ctl._dispatcherTimer == null) + if (ctl._dispatcherTimerBlink == null) { - ctl._dispatcherTimer = new DispatcherTimer + ctl._dispatcherTimerBlink = new DispatcherTimer { Interval = ctl.BlinkInterval }; - ctl._dispatcherTimer.Tick += ctl.DispatcherTimer_Tick; + ctl._dispatcherTimerBlink.Tick += ctl.DispatcherTimerBlinkTick; } - ctl._dispatcherTimer.Start(); + ctl._dispatcherTimerBlink.Start(); } else { - ctl._dispatcherTimer?.Stop(); - ctl._dispatcherTimer = null; + ctl._dispatcherTimerBlink?.Stop(); + ctl._dispatcherTimerBlink = null; ctl.UpdateIcon(true); } } - private void DispatcherTimer_Tick(object sender, EventArgs e) + private void DispatcherTimerBlinkTick(object sender, EventArgs e) { if (Visibility != Visibility.Visible || _iconCurrentHandle == IntPtr.Zero) return; UpdateIcon(true, !_isTransparent); } + private bool CheckMouseIsEnter() + { + var isTrue = FindNotifyIcon(out var rectNotify); + if (!isTrue) return false; + NativeMethods.GetCursorPos(out var point); + if (point.X >= rectNotify.Left && point.X <= rectNotify.Right && + point.Y >= rectNotify.Top && point.Y <= rectNotify.Bottom) + { + return true; + } + + return false; + } + + private void DispatcherTimerPos_Tick(object sender, EventArgs e) + { + if (CheckMouseIsEnter()) + { + if (!_isMouseOver) + { + _isMouseOver = true; + RaiseEvent(new MouseEventArgs(Mouse.PrimaryDevice, Environment.TickCount) + { + RoutedEvent = MouseEnterEvent + }); + _dispatcherTimerPos.Interval = TimeSpan.FromMilliseconds(500); + } + } + else + { + _dispatcherTimerPos.Stop(); + _isMouseOver = false; + RaiseEvent(new MouseEventArgs(Mouse.PrimaryDevice, Environment.TickCount) + { + RoutedEvent = MouseLeaveEvent + }); + } + } + + //referenced from http://www.cnblogs.com/sczmzx/p/5158127.html + private IntPtr FindTrayToolbarWindow() + { + var hWnd = NativeMethods.FindWindow("Shell_TrayWnd", null); + if (hWnd != IntPtr.Zero) + { + hWnd = NativeMethods.FindWindowEx(hWnd, IntPtr.Zero, "TrayNotifyWnd", null); + if (hWnd != IntPtr.Zero) + { + + hWnd = NativeMethods.FindWindowEx(hWnd, IntPtr.Zero, "SysPager", null); + if (hWnd != IntPtr.Zero) + { + hWnd = NativeMethods.FindWindowEx(hWnd, IntPtr.Zero, "ToolbarWindow32", null); + + } + } + } + return hWnd; + } + + //referenced from http://www.cnblogs.com/sczmzx/p/5158127.html + private IntPtr FindTrayToolbarOverFlowWindow() + { + var hWnd = NativeMethods.FindWindow("NotifyIconOverflowWindow", null); + if (hWnd != IntPtr.Zero) + { + hWnd = NativeMethods.FindWindowEx(hWnd, IntPtr.Zero, "ToolbarWindow32", null); + } + return hWnd; + } + + private bool FindNotifyIcon(out NativeMethods.RECT rect) + { + var rectNotify = new NativeMethods.RECT(); + var hTrayWnd = FindTrayToolbarWindow(); + var isTrue = FindNotifyIcon(hTrayWnd, ref rectNotify); + if (!isTrue) + { + hTrayWnd = FindTrayToolbarOverFlowWindow(); + isTrue = FindNotifyIcon(hTrayWnd, ref rectNotify); + } + rect = rectNotify; + return isTrue; + } + + //referenced from http://www.cnblogs.com/sczmzx/p/5158127.html + private bool FindNotifyIcon(IntPtr hTrayWnd, ref NativeMethods.RECT rectNotify) + { + NativeMethods.GetWindowRect(hTrayWnd, out var rectTray); + var count = (int)NativeMethods.SendMessage(hTrayWnd, NativeMethods.TB_BUTTONCOUNT, 0, IntPtr.Zero); + + var isFind = false; + if (count > 0) + { + NativeMethods.GetWindowThreadProcessId(hTrayWnd, out var trayPid); + var hProcess = NativeMethods.OpenProcess(NativeMethods.ProcessAccess.VMOperation | NativeMethods.ProcessAccess.VMRead | NativeMethods.ProcessAccess.VMWrite, false, trayPid); + var address = NativeMethods.VirtualAllocEx(hProcess, IntPtr.Zero, 1024, NativeMethods.AllocationType.Commit, NativeMethods.MemoryProtection.ReadWrite); + + var btnData = new NativeMethods.TBBUTTON(); + var trayData = new NativeMethods.TRAYDATA(); + var handel = Process.GetCurrentProcess().Id; + + for (uint i = 0; i < count; i++) + { + NativeMethods.SendMessage(hTrayWnd, NativeMethods.TB_GETBUTTON, i, address); + var isTrue = NativeMethods.ReadProcessMemory(hProcess, address, out btnData, Marshal.SizeOf(btnData), out _); + if (!isTrue) continue; + if (btnData.dwData == IntPtr.Zero) + { + btnData.dwData = btnData.iString; + } + NativeMethods.ReadProcessMemory(hProcess, btnData.dwData, out trayData, Marshal.SizeOf(trayData), out _); + NativeMethods.GetWindowThreadProcessId(trayData.hwnd, out var dwProcessId); + if (dwProcessId == (uint)handel) + { + var rect = new NativeMethods.RECT(); + var lngRect = NativeMethods.VirtualAllocEx(hProcess, IntPtr.Zero, Marshal.SizeOf(typeof(Rect)), NativeMethods.AllocationType.Commit, NativeMethods.MemoryProtection.ReadWrite); + NativeMethods.SendMessage(hTrayWnd, NativeMethods.TB_GETITEMRECT, i, lngRect); + NativeMethods.ReadProcessMemory(hProcess, lngRect, out rect, Marshal.SizeOf(rect), out _); + + NativeMethods.VirtualFreeEx(hProcess, lngRect, Marshal.SizeOf(rect), NativeMethods.FreeType.Decommit); + NativeMethods.VirtualFreeEx(hProcess, lngRect, 0, NativeMethods.FreeType.Release); + + var left = rectTray.Left + rect.Left; + var top = rectTray.Top + rect.Top; + var botton = rectTray.Top + rect.Bottom; + var right = rectTray.Left + rect.Right; + rectNotify = new NativeMethods.RECT + { + Left = left, + Right = right, + Top = top, + Bottom = botton + }; + isFind = true; + break; + } + } + NativeMethods.VirtualFreeEx(hProcess, address, 0x4096, NativeMethods.FreeType.Decommit); + NativeMethods.VirtualFreeEx(hProcess, address, 0, NativeMethods.FreeType.Release); + NativeMethods.CloseHandle(hProcess); + } + return isFind; + } + public bool IsBlink { get => (bool) GetValue(IsBlinkProperty); @@ -289,6 +446,13 @@ namespace HandyControl.Controls ShowContextMenu(); WmMouseUp(MouseButton.Right); break; + case NativeMethods.WM_MOUSEMOVE: + if (!_dispatcherTimerPos.IsEnabled) + { + _dispatcherTimerPos.Interval = TimeSpan.FromMilliseconds(200); + _dispatcherTimerPos.Start(); + } + break; } } } @@ -390,9 +554,9 @@ namespace HandyControl.Controls if (_isDisposed) return; if (disposing) { - if (_dispatcherTimer != null && IsBlink) + if (_dispatcherTimerBlink != null && IsBlink) { - _dispatcherTimer.Stop(); + _dispatcherTimerBlink.Stop(); } UpdateIcon(false); } diff --git a/src/Shared/HandyControl_Shared/Tools/Interop/NativeMethods.cs b/src/Shared/HandyControl_Shared/Tools/Interop/NativeMethods.cs index b5a01f22..dcff9ac8 100644 --- a/src/Shared/HandyControl_Shared/Tools/Interop/NativeMethods.cs +++ b/src/Shared/HandyControl_Shared/Tools/Interop/NativeMethods.cs @@ -41,10 +41,165 @@ namespace HandyControl.Tools.Interop WM_MBUTTONDOWN = 0x0207, WM_MBUTTONUP = 0x0208, WM_MBUTTONDBLCLK = 0x0209, - WM_USER = 0x0400; + WM_USER = 0x0400, + TB_GETBUTTON = WM_USER + 23, + TB_BUTTONCOUNT = WM_USER + 24, + TB_GETITEMRECT = WM_USER + 29, + STANDARD_RIGHTS_REQUIRED = 0x000F0000, + SYNCHRONIZE = 0x00100000, + PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0xFFF, + MEM_COMMIT = 0x1000, + MEM_RELEASE = 0x8000, + PAGE_READWRITE = 0x04, + TBSTATE_HIDDEN = 0x08; + + [Flags] + public enum ProcessAccess + { + AllAccess = CreateThread | DuplicateHandle | QueryInformation | SetInformation | Terminate | VMOperation | VMRead | VMWrite | Synchronize, + CreateThread = 0x2, + DuplicateHandle = 0x40, + QueryInformation = 0x400, + SetInformation = 0x200, + Terminate = 0x1, + VMOperation = 0x8, + VMRead = 0x10, + VMWrite = 0x20, + Synchronize = 0x100000 + } + + [Serializable, StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + + public RECT(int left, int top, int right, int bottom) + { + Left = left; + Top = top; + Right = right; + Bottom = bottom; + } + } + + [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 + } + + [StructLayout(LayoutKind.Sequential)] + public struct TRAYDATA + { + public IntPtr hwnd; + public uint uID; + public uint uCallbackMessage; + public uint bReserved0; + public uint bReserved1; + public IntPtr hIcon; + } + + [Flags] + public enum FreeType + { + Decommit = 0x4000, + Release = 0x8000, + } + + [StructLayout(LayoutKind.Sequential)] + public struct POINT + { + public int X; + public int Y; + + public POINT(int x, int y) + { + X = x; + Y = y; + } + } [DllImport(ExternDll.User32, CharSet = CharSet.Auto)] [ResourceExposure(ResourceScope.None)] public static extern int RegisterWindowMessage(string msg); + + [DllImport(ExternDll.User32, SetLastError = true, CharSet = CharSet.Auto)] + public static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, ref RECT lpPoints, uint cPoints); + + [DllImport(ExternDll.Kernel32, SetLastError = true, CharSet = CharSet.Auto)] + public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, out TBBUTTON lpBuffer, int dwSize, out int lpNumberOfBytesRead); + + [DllImport(ExternDll.Kernel32, SetLastError = true)] + public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, out RECT lpBuffer, int dwSize, out int lpNumberOfBytesRead); + + [DllImport(ExternDll.Kernel32, SetLastError = true)] + public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, out TRAYDATA lpBuffer, int dwSize, out int lpNumberOfBytesRead); + + [DllImport(ExternDll.User32, CharSet = CharSet.Auto)] + public static extern uint SendMessage(IntPtr hWnd, uint Msg, uint wParam, IntPtr lParam); + + [DllImport(ExternDll.User32, SetLastError = true, CharSet = CharSet.Auto)] + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [DllImport(ExternDll.Kernel32, SetLastError = true, CharSet = CharSet.Auto)] + public static extern IntPtr OpenProcess(ProcessAccess dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwProcessId); + + [DllImport(ExternDll.Kernel32, SetLastError = true, CharSet = CharSet.Auto)] + public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, int dwSize, AllocationType flAllocationType, MemoryProtection flProtect); + + [DllImport(ExternDll.Kernel32, SetLastError = true, CharSet = CharSet.Auto)] + public static extern int CloseHandle(IntPtr hObject); + + [DllImport(ExternDll.Kernel32, SetLastError = true, CharSet = CharSet.Auto)] + public static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, int dwSize, FreeType dwFreeType); + + [DllImport(ExternDll.User32, SetLastError = true)] + public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport(ExternDll.User32, SetLastError = true)] + public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); + + [DllImport(ExternDll.User32)] + public static extern int GetWindowRect(IntPtr hwnd, out RECT lpRect); + + [DllImport(ExternDll.User32, CharSet = CharSet.Auto)] + public static extern bool GetCursorPos(out POINT pt); } } \ No newline at end of file