using System; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; namespace Notify.Interop; /// /// 把窗口"钉"到所有虚拟桌面(Win+Tab 切桌面后仍可见) /// /// 走 Windows **未公开** 的 COM 接口:ImmersiveShell -> IApplicationViewCollection /// -> IVirtualDesktopPinnedApps.PinView。GUID/方法顺序随 build 变化,这里用 Win11 24H2 /// (build 26100) 的定义(来自 MScholtes/VirtualDesktop),实测在 26200 上 GUID 仍匹配 /// /// 关键点:IApplicationView 是 IInspectable,而现代 .NET 不支持 IInspectable 封送, /// 因此这里把 view 当作 **裸 IntPtr** 在 GetViewForHwnd / PinView 之间传递,绕开封送 /// 任何一步失败都被吞掉,退回"仅当前桌面显示" /// /// AOT 说明:接口用源生成 COM(GeneratedComInterface),ImmersiveShell 用 /// CoCreateInstance 直接拿 IUnknown 指针并经 StrategyBasedComWrappers 包装, /// 不再依赖内置 COM 封送(NativeAOT 下内置封送会被裁剪) /// public static partial class VirtualDesktopPinner { private static readonly StrategyBasedComWrappers ComWrappers = new(); private static bool _initialized; private static bool _available; private static IApplicationViewCollection? _views; private static IVirtualDesktopPinnedApps? _pinned; /// /// 最近一次失败的诊断信息(临时排查用) /// public static string LastError { get; private set; } = ""; /// /// 尝试把指定窗口钉到所有桌面;返回是否成功 /// public static bool TryPin(IntPtr hwnd) { if (hwnd == IntPtr.Zero) { LastError = "hwnd=0"; return false; } try { EnsureInit(); if (!_available || _views is null || _pinned is null) { LastError = "init failed: " + LastError; return false; } var view = IntPtr.Zero; try { _views.GetViewForHwnd(hwnd, out view); if (view == IntPtr.Zero) { LastError = "GetViewForHwnd returned null"; return false; } _pinned.PinView(view); return true; } catch (Exception ex) { LastError = "pin: " + ex.GetType().Name + " 0x" + ex.HResult.ToString("X8") + " " + ex.Message; return false; } finally { if (view != IntPtr.Zero) { Marshal.Release(view); // GetViewForHwnd 返回的指针已 AddRef } } } catch (Exception ex) { LastError = "outer: " + ex.GetType().Name + " " + ex.Message; return false; } } private static void EnsureInit() { if (_initialized) { return; } _initialized = true; var shellPtr = IntPtr.Zero; try { // 用 CoCreateInstance 直接拿 ImmersiveShell 的 IServiceProvider10 指针 // 等价于经典写法 Activator.CreateInstance(GetTypeFromCLSID(...)) 但 AOT 友好 var clsid = CLSID_ImmersiveShell; var iidServiceProvider = IID_IServiceProvider10; // ImmersiveShell 是本地服务器,必须用 CLSCTX_LOCAL_SERVER // 仅传 INPROC_SERVER 会得到 0x80040154 REGDB_E_CLASSNOTREG var hr = CoCreateInstance(ref clsid, IntPtr.Zero, CLSCTX_LOCAL_SERVER, ref iidServiceProvider, out shellPtr); if (hr < 0 || shellPtr == IntPtr.Zero) { throw new InvalidOperationException("CoCreateInstance(ImmersiveShell) 0x" + hr.ToString("X8")); } var shell = (IServiceProvider10)ComWrappers.GetOrCreateObjectForComInstance(shellPtr, CreateObjectFlags.None); var viewCollectionGuid = IID_IApplicationViewCollection; var viewsPtr = shell.QueryService(ref viewCollectionGuid, ref viewCollectionGuid); _views = WrapRequired(viewsPtr, "IApplicationViewCollection"); var pinnedGuid = IID_IVirtualDesktopPinnedApps; var clsidPinned = CLSID_VirtualDesktopPinnedApps; var pinnedPtr = shell.QueryService(ref clsidPinned, ref pinnedGuid); _pinned = WrapRequired(pinnedPtr, "IVirtualDesktopPinnedApps"); _available = true; } catch (Exception ex) { _available = false; LastError = "EnsureInit: " + ex.GetType().Name + " 0x" + ex.HResult.ToString("X8") + " " + ex.Message; } finally { if (shellPtr != IntPtr.Zero) { // GetOrCreateObjectForComInstance 持有了自己的引用,释放本地这一份 Marshal.Release(shellPtr); } } } // 把 QueryService 返回的裸 IUnknown 指针包装成托管 RCW,并释放本地引用 private static T WrapRequired(IntPtr unknown, string name) { if (unknown == IntPtr.Zero) { throw new InvalidOperationException("QueryService(" + name + ") 返回 null"); } try { return (T)ComWrappers.GetOrCreateObjectForComInstance(unknown, CreateObjectFlags.None); } finally { Marshal.Release(unknown); } } private const int CLSCTX_LOCAL_SERVER = 4; // --- CLSIDs / IIDs --- private static readonly Guid CLSID_ImmersiveShell = new("C2F03A33-21F5-47FA-B4BB-156362A2F239"); private static readonly Guid CLSID_VirtualDesktopPinnedApps = new("B5A399E7-1C87-46B8-88E9-FC5747B171BD"); private static readonly Guid IID_IServiceProvider10 = new("6D5140C1-7436-11CE-8034-00AA006009FA"); private static readonly Guid IID_IApplicationViewCollection = new("1841C6D7-4F9D-42C0-AF41-8747538F10E5"); private static readonly Guid IID_IVirtualDesktopPinnedApps = new("4CE81583-1E4C-4632-A621-07A53543148F"); [LibraryImport("ole32.dll")] private static partial int CoCreateInstance( ref Guid rclsid, IntPtr pUnkOuter, int dwClsContext, ref Guid riid, out IntPtr ppv); } // ImmersiveShell 的 IServiceProvider(与系统 IServiceProvider 不同) // QueryService 返回裸 IUnknown 指针(nint),由调用方用 ComWrappers 包装 [GeneratedComInterface] [Guid("6D5140C1-7436-11CE-8034-00AA006009FA")] internal partial interface IServiceProvider10 { IntPtr QueryService(ref Guid service, ref Guid riid); } // 只声明到 GetViewForHwnd(第 4 个方法);view 用 IntPtr,避免 IInspectable 封送 [GeneratedComInterface] [Guid("1841C6D7-4F9D-42C0-AF41-8747538F10E5")] internal partial interface IApplicationViewCollection { int GetViews(out IntPtr array); int GetViewsByZOrder(out IntPtr array); int GetViewsByAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] string id, out IntPtr array); int GetViewForHwnd(IntPtr hwnd, out IntPtr view); } // view 参数同样用 IntPtr [GeneratedComInterface] [Guid("4CE81583-1E4C-4632-A621-07A53543148F")] internal partial interface IVirtualDesktopPinnedApps { [return: MarshalAs(UnmanagedType.Bool)] bool IsAppIdPinned([MarshalAs(UnmanagedType.LPWStr)] string appId); void PinAppID([MarshalAs(UnmanagedType.LPWStr)] string appId); void UnpinAppID([MarshalAs(UnmanagedType.LPWStr)] string appId); [return: MarshalAs(UnmanagedType.Bool)] bool IsViewPinned(IntPtr applicationView); void PinView(IntPtr applicationView); void UnpinView(IntPtr applicationView); }