using System.Runtime.InteropServices; using FlaUI.Core.Input; using FlaUI.Core.Definitions; 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, string DetectionMethod = "MSAA"); internal static class WechatTray { internal static bool TryActivateUia(UIA3Automation automation) { var regions = automation.GetDesktop().FindAllChildren(cf => cf.ByClassName("Shell_TrayWnd")) .SelectMany(window => window.FindAllDescendants(cf => cf.ByClassName("TrayNotifyWnd"))); var matches = regions.SelectMany(region => region.FindAllDescendants(cf => cf.ByControlType(ControlType.Button))) .Where(icon => WechatLocators.IsWechatTrayName(icon.Name) && !icon.IsOffscreen && !icon.BoundingRectangle.IsEmpty).ToArray(); if (matches.Length == 0) return false; if (matches.Length != 1) throw new WxAgentException(WxAgentErrorCode.InvalidOperationState, "Multiple WeChat notification icons found; refusing an ambiguous target."); var bounds = matches[0].BoundingRectangle; ActivateAt(new Point(bounds.Left + bounds.Width / 2, bounds.Top + bounds.Height / 2)); return true; } // On Windows 10 the notification toolbar may expose no buttons through UIA3. // Use its public MSAA accessibility API, never Explorer process memory or tray callback message guesses. internal static unsafe WechatTrayDiagnostic Inspect(bool activate, CancellationToken cancellationToken) { var toolbars = new List(); PInvoke.EnumWindows((root, _) => { var className = ReadClass(root); if (className is not ("Shell_TrayWnd" or "NotifyIconOverflowWindow")) return true; PInvoke.EnumChildWindows(root, (child, _) => { if (ReadClass(child) == "ToolbarWindow32") toolbars.Add(child); return true; }, default); return true; }, default); var icons = 0; var matches = new List<(IAccessible Owner, object Child)>(); var owners = new List(); try { foreach (var toolbar in toolbars) { cancellationToken.ThrowIfCancellationRequested(); void* pointer; var hr = PInvoke.AccessibleObjectFromWindow(toolbar, 0xfffffffc, typeof(IAccessible).GUID, out pointer); if (hr.Value < 0 || pointer == null) continue; IAccessible owner; try { owner = (IAccessible)Marshal.GetObjectForIUnknown((nint)pointer); } finally { Marshal.Release((nint)pointer); } owners.Add(owner); var count = Math.Clamp(owner.accChildCount, 0, 256); var children = new object[count]; PInvoke.AccessibleChildren(owner, 0, children, out var obtained); for (var index = 0; index < obtained; index++) { cancellationToken.ThrowIfCancellationRequested(); var child = children[index]; if (child is not int) continue; icons++; var name = owner.get_accName(child); string text; try { text = name.ToString() ?? string.Empty; } finally { if (name.Value != null) Marshal.FreeBSTR((nint)name.Value); } 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) throw new WxAgentException(WxAgentErrorCode.ControlNotFound, $"Expected one WeChat notification icon; found {matches.Count}. No application was started."); var match = matches[0]; match.Owner.accLocation(out var left, out var top, out var width, out var height, match.Child); 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. ActivateAt(new Point(left + width / 2, top + height / 2)); } return new WechatTrayDiagnostic(toolbars.Count, icons, matches.Count); } finally { foreach (var owner in owners) Marshal.ReleaseComObject(owner); } } 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]; return new string(text[..PInvoke.GetClassName(hwnd, text)]); } } public static partial class WechatChatClient { public static WechatTrayDiagnostic InspectTray(CancellationToken cancellationToken = default) => WechatTray.Inspect(false, cancellationToken); }