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