156 lines
7.4 KiB
C#
156 lines
7.4 KiB
C#
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<HWND>();
|
|
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<IAccessible>();
|
|
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<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];
|
|
return new string(text[..PInvoke.GetClassName(hwnd, text)]);
|
|
}
|
|
}
|
|
|
|
public static partial class WechatChatClient
|
|
{
|
|
public static WechatTrayDiagnostic InspectTray(CancellationToken cancellationToken = default) =>
|
|
WechatTray.Inspect(false, cancellationToken);
|
|
}
|