using System; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; using System.Text; namespace Notify.Interop; /// /// Windows Terminal 标签页的捕获与切换 /// /// save 时记录当前选中标签的 RuntimeId,点击通知激活窗口后再据此切回该标签 /// 全程走源生成 COM(GeneratedComInterface),保证 NativeAOT 兼容 /// vtable 顺序与 GUID 均取自 Windows SDK UIAutomationClient.h /// public static partial class WinTerminalTabs { private const string WtClass = "CASCADIA_HOSTING_WINDOW_CLASS"; private const int TreeScopeDescendants = 4; private const int ControlTypePropertyId = 30003; private const int IsSelectedPropertyId = 30079; private const int TabItemControlTypeId = 50019; private const int SelectionItemPatternId = 10010; private const int CLSCTX_INPROC_SERVER = 1; private static readonly StrategyBasedComWrappers ComWrappers = new(); private static IUIAutomation? _uia; private static bool _initTried; public static bool IsWindowsTerminal(IntPtr hwnd) => hwnd != IntPtr.Zero && Win32.GetClassNameOf(hwnd) == WtClass; // 返回当前选中标签的 RuntimeId 串,失败返回空串 public static string GetSelectedTabRuntimeId(IntPtr hwnd) { try { var uia = EnsureUia(); if (uia is null) { return ""; } var root = uia.ElementFromHandle(hwnd); var cond = uia.CreateTrueCondition(); var all = root.FindAll(TreeScopeDescendants, cond); var count = all.Length(); for (var i = 0; i < count; i++) { var el = all.GetElement(i); if (GetIntProperty(el, ControlTypePropertyId) != TabItemControlTypeId) { continue; } if (GetBoolProperty(el, IsSelectedPropertyId)) { return RuntimeIdOf(el); } } } catch { // 任何 COM 异常退回空串 } return ""; } // 在已激活的 WT 窗口里找到匹配 RuntimeId 的标签并选中 public static bool SelectTab(IntPtr hwnd, string runtimeId) { if (string.IsNullOrEmpty(runtimeId)) { return false; } try { var uia = EnsureUia(); if (uia is null) { return false; } var root = uia.ElementFromHandle(hwnd); var cond = uia.CreateTrueCondition(); var all = root.FindAll(TreeScopeDescendants, cond); var count = all.Length(); for (var i = 0; i < count; i++) { var el = all.GetElement(i); if (GetIntProperty(el, ControlTypePropertyId) != TabItemControlTypeId) { continue; } if (RuntimeIdOf(el) != runtimeId) { continue; } var pattern = el.GetCurrentPattern(SelectionItemPatternId); if (pattern is null) { return false; } pattern.Select(); return true; } } catch { // 切换失败不影响窗口已被激活 } return false; } private static IUIAutomation? EnsureUia() { if (_initTried) { return _uia; } _initTried = true; try { var clsid = new Guid("ff48dba4-60ef-4201-aa87-54103eef594e"); var iid = typeof(IUIAutomation).GUID; var hr = CoCreateInstance(ref clsid, IntPtr.Zero, CLSCTX_INPROC_SERVER, ref iid, out var ptr); if (hr >= 0 && ptr != IntPtr.Zero) { _uia = (IUIAutomation)ComWrappers.GetOrCreateObjectForComInstance(ptr, CreateObjectFlags.None); Marshal.Release(ptr); } } catch { _uia = null; } return _uia; } private static int GetIntProperty(IUIAutomationElement el, int propertyId) { var v = el.GetCurrentPropertyValue(propertyId); return v.lVal; } private static bool GetBoolProperty(IUIAutomationElement el, int propertyId) { var v = el.GetCurrentPropertyValue(propertyId); return v.boolVal != 0; } // RuntimeId 是一个 int 数组(SAFEARRAY),拼成点分串用于比较 private static string RuntimeIdOf(IUIAutomationElement el) { var psa = el.GetRuntimeId(); if (psa == IntPtr.Zero) { return ""; } try { if (SafeArrayGetLBound(psa, 1, out var lb) < 0 || SafeArrayGetUBound(psa, 1, out var ub) < 0) { return ""; } var sb = new StringBuilder(); for (var idx = lb; idx <= ub; idx++) { var i = idx; if (SafeArrayGetElement(psa, ref i, out var val) < 0) { return ""; } if (sb.Length > 0) { sb.Append('.'); } sb.Append(val); } return sb.ToString(); } finally { SafeArrayDestroy(psa); } } [LibraryImport("ole32.dll")] private static partial int CoCreateInstance(ref Guid rclsid, IntPtr pUnkOuter, int dwClsContext, ref Guid riid, out IntPtr ppv); [LibraryImport("oleaut32.dll")] private static partial int SafeArrayGetLBound(IntPtr psa, uint nDim, out int plLbound); [LibraryImport("oleaut32.dll")] private static partial int SafeArrayGetUBound(IntPtr psa, uint nDim, out int plUbound); [LibraryImport("oleaut32.dll")] private static partial int SafeArrayGetElement(IntPtr psa, ref int rgIndices, out int pv); [LibraryImport("oleaut32.dll")] private static partial int SafeArrayDestroy(IntPtr psa); } // VARIANT 的最小化布局(x64 为 24 字节),只读 VT_I4 / VT_BOOL [StructLayout(LayoutKind.Explicit, Size = 24)] internal struct VARIANT { [FieldOffset(0)] public ushort vt; [FieldOffset(8)] public int lVal; [FieldOffset(8)] public short boolVal; } [GeneratedComInterface] [Guid("30cbe57d-d9d0-452a-ab13-7ac5ac4825ee")] internal partial interface IUIAutomation { void _CompareElements(); void _CompareRuntimeIds(); void _GetRootElement(); IUIAutomationElement ElementFromHandle(IntPtr hwnd); void _ElementFromPoint(); void _GetFocusedElement(); void _GetRootElementBuildCache(); void _ElementFromHandleBuildCache(); void _ElementFromPointBuildCache(); void _GetFocusedElementBuildCache(); void _CreateTreeWalker(); void _get_ControlViewWalker(); void _get_ContentViewWalker(); void _get_RawViewWalker(); void _get_RawViewCondition(); void _get_ControlViewCondition(); void _get_ContentViewCondition(); void _CreateCacheRequest(); IUIAutomationCondition CreateTrueCondition(); } [GeneratedComInterface] [Guid("d22108aa-8ac5-49a5-837b-37bbb3d7591e")] internal partial interface IUIAutomationElement { void _SetFocus(); IntPtr GetRuntimeId(); void _FindFirst(); IUIAutomationElementArray FindAll(int scope, IUIAutomationCondition condition); void _FindFirstBuildCache(); void _FindAllBuildCache(); void _BuildUpdatedCache(); VARIANT GetCurrentPropertyValue(int propertyId); void _GetCurrentPropertyValueEx(); void _GetCachedPropertyValue(); void _GetCachedPropertyValueEx(); void _GetCurrentPatternAs(); void _GetCachedPatternAs(); [return: MarshalUsing(typeof(UniqueComInterfaceMarshaller))] IUIAutomationSelectionItemPattern? GetCurrentPattern(int patternId); } [GeneratedComInterface] [Guid("14314595-b4bc-4055-95f2-58f2e42c9855")] internal partial interface IUIAutomationElementArray { int Length(); IUIAutomationElement GetElement(int index); } [GeneratedComInterface] [Guid("352ffba8-0973-437c-a61f-f64cafd81df9")] internal partial interface IUIAutomationCondition { } [GeneratedComInterface] [Guid("a8efa66a-0fda-421a-9194-38021f3578ea")] internal partial interface IUIAutomationSelectionItemPattern { void Select(); }