Files

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);
}