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