diff --git a/src/WxAgent.Core/WechatLocators.cs b/src/WxAgent.Core/WechatLocators.cs index 97958e3..3771385 100644 --- a/src/WxAgent.Core/WechatLocators.cs +++ b/src/WxAgent.Core/WechatLocators.cs @@ -4,6 +4,12 @@ public static class WechatLocators { public const string MainView = "MainView"; public const string MainTabBar = "MainView.main_tabbar"; + // Observed with WeChat 4.1.13.63; query its public shell icon metadata, never post tray callback messages. + public const string TrayMessageWindowClass = "Qt51514WxTrayIconMessageWindowClass"; + public const uint TrayIconId = 0; + + public static bool IsNotificationAreaRoot(string className) => + className is "Shell_TrayWnd" or "Shell_SecondaryTrayWnd" or "NotifyIconOverflowWindow"; public static bool IsWechatTrayName(string name) => name is "微信" or "WeChat" or "Weixin" diff --git a/src/WxAgent.Windows/NativeMethods.txt b/src/WxAgent.Windows/NativeMethods.txt index 6d2a5ed..4b12a34 100644 --- a/src/WxAgent.Windows/NativeMethods.txt +++ b/src/WxAgent.Windows/NativeMethods.txt @@ -14,3 +14,6 @@ GetWindowRect AccessibleObjectFromWindow AccessibleChildren EnumChildWindows +Shell_NotifyIconGetRect +WindowFromPoint +GetAncestor diff --git a/src/WxAgent.Windows/WechatTray.cs b/src/WxAgent.Windows/WechatTray.cs index b6fdbc2..cba2a73 100644 --- a/src/WxAgent.Windows/WechatTray.cs +++ b/src/WxAgent.Windows/WechatTray.cs @@ -5,11 +5,13 @@ using FlaUI.UIA3; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.UI.Accessibility; +using Windows.Win32.UI.Shell; +using Windows.Win32.UI.WindowsAndMessaging; using WxAgent.Core; namespace WxAgent.Windows; -public sealed record WechatTrayDiagnostic(int ToolbarCount, int IconCount, int WechatIconCount); +public sealed record WechatTrayDiagnostic(int ToolbarCount, int IconCount, int WechatIconCount, string DetectionMethod = "MSAA"); internal static class WechatTray { @@ -23,7 +25,8 @@ internal static class WechatTray if (matches.Length == 0) return false; if (matches.Length != 1) throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, "Multiple WeChat notification icons found; refusing an ambiguous target."); - matches[0].DoubleClick(); + var bounds = matches[0].BoundingRectangle; + ActivateAt(new Point(bounds.Left + bounds.Width / 2, bounds.Top + bounds.Height / 2)); return true; } @@ -74,6 +77,20 @@ internal static class WechatTray if (WechatLocators.IsWechatTrayName(text)) matches.Add((owner, child)); } } + if (matches.Count == 0) + { + var nativeBounds = FindKnownIconBounds(); + if (activate) + { + if (nativeBounds.Count != 1) + throw new WxAgentException(WxAgentErrorCode.ControlNotFound, + "No unique WeChat notification icon was found through accessibility or public shell metadata; no application was started."); + cancellationToken.ThrowIfCancellationRequested(); + var bounds = nativeBounds[0]; + ActivateAt(new Point(bounds.Left + bounds.Width / 2, bounds.Top + bounds.Height / 2)); + } + return new WechatTrayDiagnostic(toolbars.Count, icons, nativeBounds.Count, "Shell_NotifyIconGetRect"); + } if (activate) { if (matches.Count != 1) @@ -84,8 +101,7 @@ internal static class WechatTray if (width <= 0 || height <= 0 || (Convert.ToInt32(match.Owner.get_accState(match.Child)) & 0x10000) != 0) throw new WxAgentException(WxAgentErrorCode.ControlNotFound, "The WeChat tray icon is in the hidden overflow area; reveal it before restoring."); // Qt opens the original main window on a tray double-click, not MSAA's single default action. - Mouse.MoveTo(left + width / 2, top + height / 2); - Mouse.DoubleClick(MouseButton.Left); + ActivateAt(new Point(left + width / 2, top + height / 2)); } return new WechatTrayDiagnostic(toolbars.Count, icons, matches.Count); } @@ -95,6 +111,36 @@ internal static class WechatTray } } + private static unsafe List FindKnownIconBounds() + { + var result = new List(); + foreach (var window in WechatNativeWindow.Enumerate().Where(window => window.ClassName == WechatLocators.TrayMessageWindowClass)) + { + var identifier = new NOTIFYICONIDENTIFIER + { + cbSize = (uint)sizeof(NOTIFYICONIDENTIFIER), + hWnd = new HWND((nint)window.Handle), + uID = WechatLocators.TrayIconId + }; + RECT bounds; + if (PInvoke.Shell_NotifyIconGetRect(&identifier, &bounds).Value == 0 + && bounds.right > bounds.left && bounds.bottom > bounds.top) + result.Add(Rectangle.FromLTRB(bounds.left, bounds.top, bounds.right, bounds.bottom)); + } + return result; + } + + private static void ActivateAt(Point point) + { + var hit = PInvoke.WindowFromPoint(point); + var root = PInvoke.GetAncestor(hit, GET_ANCESTOR_FLAGS.GA_ROOT); + if (!PInvoke.IsWindowVisible(root) || !WechatLocators.IsNotificationAreaRoot(ReadClass(root))) + throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, + "The notification icon is obscured or its overflow panel is closed; refusing an unverified click."); + Mouse.MoveTo(point); + Mouse.DoubleClick(MouseButton.Left); + } + private static string ReadClass(HWND hwnd) { Span text = stackalloc char[256]; diff --git a/tests/WxAgent.Core.Tests/WechatStabilityTests.cs b/tests/WxAgent.Core.Tests/WechatStabilityTests.cs index 90dc9ea..43d41ba 100644 --- a/tests/WxAgent.Core.Tests/WechatStabilityTests.cs +++ b/tests/WxAgent.Core.Tests/WechatStabilityTests.cs @@ -167,6 +167,18 @@ public sealed class WechatStabilityTests finally { Directory.Delete(directory, recursive: true); } } + [Theory] + [InlineData("Shell_TrayWnd", true)] + [InlineData("Shell_SecondaryTrayWnd", true)] + [InlineData("NotifyIconOverflowWindow", true)] + [InlineData("Qt51514QWindowIcon", false)] + [InlineData("Chrome_WidgetWin_1", false)] + [InlineData("", false)] + public void TrayClicks_RequireANotificationAreaHitTarget(string className, bool expected) + { + Assert.Equal(expected, WechatLocators.IsNotificationAreaRoot(className)); + } + private static WechatStabilitySample Sample( DateTimeOffset timestamp, long memory,