fix: locate WeChat tray icons using public shell metadata

This commit is contained in:
2026-09-05 23:38:53 +08:00
parent 786932e892
commit 7e29041d61
4 changed files with 71 additions and 4 deletions
+6
View File
@@ -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"
+3
View File
@@ -14,3 +14,6 @@ GetWindowRect
AccessibleObjectFromWindow
AccessibleChildren
EnumChildWindows
Shell_NotifyIconGetRect
WindowFromPoint
GetAncestor
+50 -4
View File
@@ -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,