fix: locate WeChat tray icons using public shell metadata
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -14,3 +14,6 @@ GetWindowRect
|
||||
AccessibleObjectFromWindow
|
||||
AccessibleChildren
|
||||
EnumChildWindows
|
||||
Shell_NotifyIconGetRect
|
||||
WindowFromPoint
|
||||
GetAncestor
|
||||
|
||||
@@ -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<Rectangle> FindKnownIconBounds()
|
||||
{
|
||||
var result = new List<Rectangle>();
|
||||
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<char> text = stackalloc char[256];
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user