如何枚举Windows 11中通过WinKey+TAB创建的虚拟桌面?
枚举Windows 11(及Windows 10)的WinKey+TAB虚拟桌面
你说得完全正确——通过WinKey+TAB创建的虚拟桌面和Win32 API里EnumDesktops()枚举的传统「桌面」根本不是一个概念:
- 传统桌面(比如
Default、DummyDesktopForInstantResume)是Windows窗口站(Window Station)层面的系统对象,负责管理窗口的底层显示上下文; - 而WinKey+TAB的虚拟桌面是Windows Shell提供的逻辑分组功能,所有虚拟桌面共享同一个传统
Default桌面,所以你会看到窗口句柄始终不变、Spy++始终显示同一套窗口列表。
要枚举这类虚拟桌面,你需要使用Windows Shell提供的虚拟桌面COM接口,而非传统的窗口站API。下面是具体实现方法:
核心概念与所需接口
虚拟桌面的管理由User32.dll中的COM组件负责,主要用到两个接口:
IVirtualDesktopManager:公开API,仅用于查询窗口所属的虚拟桌面,没有枚举功能;IVirtualDesktopManagerInternal:内部接口(未正式公开,但在Win10/Win11中稳定可用),提供枚举、创建、切换虚拟桌面的完整能力。
C++实现示例
以下代码展示如何枚举所有虚拟桌面并获取它们的GUID和名称:
#include <windows.h> #include <shlobj.h> #include <atlbase.h> #include <vector> #include <iostream> // 声明未公开的IVirtualDesktopManagerInternal接口 MIDL_INTERFACE("a5cd92ff-29be-454c-8d04-d82879fb3f1b") IVirtualDesktopManagerInternal : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE GetCount(UINT *count) = 0; virtual HRESULT STDMETHODCALLTYPE GetDesktops(IObjectArray **desktops) = 0; virtual HRESULT STDMETHODCALLTYPE GetCurrentDesktop(IVirtualDesktop **desktop) = 0; virtual HRESULT STDMETHODCALLTYPE GetDesktopById(REFGUID desktopId, IVirtualDesktop **desktop) = 0; virtual HRESULT STDMETHODCALLTYPE CreateDesktop(IVirtualDesktop **desktop) = 0; virtual HRESULT STDMETHODCALLTYPE RemoveDesktop(IVirtualDesktop *desktop, IVirtualDesktop *fallbackDesktop) = 0; virtual HRESULT STDMETHODCALLTYPE SwitchDesktop(IVirtualDesktop *desktop) = 0; }; // 声明IVirtualDesktop接口 MIDL_INTERFACE("a1841308-3541-4fab-bc81-f71556f20b4a") IVirtualDesktop : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE GetID(GUID *desktopId) = 0; virtual HRESULT STDMETHODCALLTYPE GetName(LPWSTR *name) = 0; }; // 定义VirtualDesktopManager组件的CLSID const CLSID CLSID_VirtualDesktopManager = { 0xAA509086, 0x5CA9, 0x4C25, { 0x8F, 0x95, 0x58, 0x9D, 0x3C, 0x07, 0xB4, 0x8A } }; int main() { // 初始化COM,必须使用单线程单元(STA)模型 HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); if (FAILED(hr)) { std::cerr << "COM初始化失败,错误码:" << hr << std::endl; return 1; } // 获取IVirtualDesktopManagerInternal接口 CComPtr<IVirtualDesktopManagerInternal> pVDMInternal; hr = CoCreateInstance(CLSID_VirtualDesktopManager, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pVDMInternal)); if (FAILED(hr)) { std::cerr << "获取IVirtualDesktopManagerInternal失败,错误码:" << hr << std::endl; CoUninitialize(); return 1; } // 获取所有虚拟桌面的对象数组 CComPtr<IObjectArray> pDesktopArray; hr = pVDMInternal->GetDesktops(&pDesktopArray); if (FAILED(hr)) { std::cerr << "获取虚拟桌面数组失败,错误码:" << hr << std::endl; CoUninitialize(); return 1; } // 获取虚拟桌面数量 UINT desktopCount = 0; hr = pDesktopArray->GetCount(&desktopCount); if (FAILED(hr)) { std::cerr << "获取虚拟桌面数量失败,错误码:" << hr << std::endl; CoUninitialize(); return 1; } std::cout << "找到" << desktopCount << "个虚拟桌面:" << std::endl; // 遍历每个虚拟桌面,获取信息 for (UINT i = 0; i < desktopCount; ++i) { CComPtr<IVirtualDesktop> pDesktop; hr = pDesktopArray->GetAt(i, IID_PPV_ARGS(&pDesktop)); if (FAILED(hr)) { std::cerr << "获取第" << i+1 << "个虚拟桌面失败,错误码:" << hr << std::endl; continue; } // 获取虚拟桌面唯一GUID GUID desktopId; hr = pDesktop->GetID(&desktopId); if (SUCCEEDED(hr)) { wchar_t guidStr[39]; StringFromGUID2(desktopId, guidStr, sizeof(guidStr)/sizeof(wchar_t)); std::wcout << L"桌面" << i+1 << L" GUID: " << guidStr << std::endl; } // 获取虚拟桌面名称 LPWSTR pName = nullptr; hr = pDesktop->GetName(&pName); if (SUCCEEDED(hr) && pName) { std::wcout << L"桌面" << i+1 << L" 名称: " << pName << std::endl; CoTaskMemFree(pName); // 必须手动释放接口分配的内存 } std::cout << "------------------------" << std::endl; } CoUninitialize(); return 0; }
关键注意事项
- 接口兼容性:
IVirtualDesktopManagerInternal是内部接口,微软未承诺长期兼容,但从Windows 10 1607到Windows 11的所有版本中都能正常使用; - COM线程模型:必须用
COINIT_APARTMENTTHREADED初始化COM,否则接口调用会失败; - 内存管理:
GetName返回的字符串需要用CoTaskMemFree释放,避免内存泄漏; - 权限要求:普通用户权限即可运行,无需管理员权限。
补充说明
如果仅需查询某个窗口所属的虚拟桌面,可以使用公开的IVirtualDesktopManager接口,它的GetWindowDesktopId方法能直接获取窗口对应的虚拟桌面GUID,无需依赖内部接口。
内容的提问来源于stack exchange,提问作者Dimitrios Staikos
相关产品推荐
相关产品推荐

