📝 简介
在 Windows 10/11 中,虚拟桌面(Virtual Desktops)是一个非常实用的提升生产力的功能。然而,AutoHotkey 原生并没有提供直接操作虚拟桌面的内置命令。
这段代码提供了一个优雅、面向对象且完全独立的 AHK v2 类 —— VirtualDesktop。它不需要依赖任何第三方的 DLL 插件(如 VirtualDesktopAccessor.dll),而是通过底层的 COM 接口(IVirtualDesktopManagerInternal 等)直接与 Windows 系统通信,实现了对虚拟桌面的全面接管。
✨ 核心功能
通过简明的面向对象语法,你可以轻松实现以下操作:
-
状态获取:获取当前系统的虚拟桌面总数,以及当前所处的桌面序号。
-
桌面切换:通过索引(从0开始计)或相对位置(当前桌面的左侧/右侧)快速跳转桌面。
-
动态管理:通过代码自动创建新的虚拟桌面,或删除现有的虚拟桌面。
-
窗口侦测:精准判断指定的窗口(通过 HWND)是否处于特定的虚拟桌面中。
-
窗口移动:将窗口发送到指定的虚拟桌面(注:受限于系统安全机制,目前仅支持移动 AHK 脚本自身进程创建的窗口)。
🚀 代码亮点
-
完全纯净:全篇使用 ComCall 和原生 AHK v2 语法编写,无需下载和随脚本附带额外的依赖文件。
-
现代 OOP 设计:封装极为完善,使用属性(Properties)和方法(Methods)调用,代码可读性极高。例如 VirtualDesktop.Current.Right.Show() 即可直接切换到右侧桌面。
-
内存安全:利用 AHK v2 的 __Delete 元函数自动释放 COM 对象指针(ObjRelease),避免内存泄漏。
⚠️ 注意事项与已知局限
-
跨进程窗口移动限制:代码中的 ObtainWindow(hwnd) 方法目前只能移动当前 AHK 脚本进程所属的窗口(例如脚本自己创建的 Gui)。这是由于系统底层的权限限制,无法直接用此方法把其他程序(如浏览器、微信)的窗口移到别的桌面。
-
系统版本兼容性:由于微软并未公开虚拟桌面的完整 API,此代码使用的是 Windows 内部的非公开 COM 接口(GUID)。不同版本的 Windows 10 / Windows 11 可能会更改这些接口的 GUID。如果在你的系统上运行报错,可能需要针对你的 Win11/Win10 版本更新代码末尾 __New 中的 GUID 字符串。
💡 适用场景
-
需要编写自动化办公脚本,通过虚拟桌面隔离不同工作环境的极客玩家。
-
希望为鼠标手势、快捷键绑定更高级的虚拟桌面控制逻辑的用户。
-
研究 AHK v2 高级 COM 调用(ComCall、ComObjQuery)的开发者(这段代码是非常优秀的学习范例)。
💻 源码示例 (复制即用)
#Requires AutoHotkey v2.0
; 使用示例:
; 切换到第2个虚拟桌面, 注意要从0开始数:
desktop := VirtualDesktop.GetAt(1) ; GetAt(N) 返回第N+1个桌面对象
desktop.Show()
; 获取桌面总数,当前桌面是第几个:
count := VirtualDesktop.Count
index := VirtualDesktop.Current.Index
MsgBox("当前位于第" index + 1 "个桌面, 总共有" count "个桌面")
; 通过相对位置获取桌面对象:
rightDesktop := VirtualDesktop.Current.Right ; 获取当前桌面右边的桌面
leftDesktop := rightDesktop.Left ; 获取指定桌面左边的桌面
; 创建、删除虚拟桌面:
newDesktop := VirtualDesktop.Create() ; 创建桌面
newDesktop.Show()
Sleep(1000)
newDesktop.Remove() ; 删除桌面
; 判断窗口是否在某个桌面里:
MsgBox VirtualDesktop.Current.HasWindow(WinExist("A")) ; true
MsgBox VirtualDesktop.Current.Right.HasWindow(WinExist("A")) ; false
; 移动指定窗口到某个桌面,遗憾的是,只能移动本进程的窗口:
myGui := Gui()
myGui.Show("w300 h300")
Sleep(1000)
VirtualDesktop.GetAt(1).ObtainWindow(myGui.Hwnd)
class VirtualDesktop {
static Current => ((ComCall(6, this.IVirtualDesktopManagerInternal, "ptr*", ¤tDesktop := 0)), this(currentDesktop))
static Count => (ComCall(3, this.IVirtualDesktopManagerInternal, "int*", &count := 0), count)
static GetAt(index) {
ComCall(7, this.IVirtualDesktopManagerInternal, "ptr*", desktops := ComValue(13, 0))
ComCall(4, desktops, "uint", index, "ptr", this.IID_IVirtualDesktop, "ptr*", &desktop := 0)
return VirtualDesktop(desktop)
}
static Create() => (ComCall(10, this.IVirtualDesktopManagerInternal, "ptr*", &newDesktop := 0), VirtualDesktop(newDesktop))
Id => (ComCall(4, this, "ptr", id := Buffer(16)), id.ToString := (_) => (DllCall('ole32\StringFromGUID2', "ptr", _, "ptr", buf := Buffer(78), "int", 39), StrGet(buf)), id)
Left => (ComCall(8, VirtualDesktop.IVirtualDesktopManagerInternal, "ptr", this, "uint", 3, "ptr*", &leftDesktop := 0), VirtualDesktop(leftDesktop))
Right => (ComCall(8, VirtualDesktop.IVirtualDesktopManagerInternal, "ptr", this, "uint", 4, "ptr*", &rightDesktop := 0), VirtualDesktop(rightDesktop))
Visible => VirtualDesktop.Current.Equals(this)
Index {
get {
thisId := this.Id, thisIdH := NumGet(thisId, "int64"), thisIdL := NumGet(thisId, 8, "int64")
loop VirtualDesktop.Count {
id := VirtualDesktop.GetAt(A_Index - 1).Id
if NumGet(id, "int64") == thisIdH && NumGet(id, 8, "int64") == thisIdL
return A_Index - 1
}
}
}
Show() => ComCall(9, VirtualDesktop.IVirtualDesktopManagerInternal, "ptr", this)
Remove(fallbackDesktop?) => ComCall(11, VirtualDesktop.IVirtualDesktopManagerInternal, "ptr", this, "ptr", fallbackDesktop ?? VirtualDesktop.GetAt(0))
HasWindow(hwnd) {
ComCall(4, VirtualDesktop.IVirtualDesktopManager, "ptr", hwnd, "ptr", id1 := Buffer(16))
return NumGet(id1, "int64") == NumGet(id2 := this.Id, "int64") && NumGet(id1, 8, "int64") == NumGet(id2, 8, "int64")
}
ObtainWindow(hwnd) => ComCall(5, VirtualDesktop.IVirtualDesktopManager, "ptr", hwnd, "ptr", this.Id)
Equals(desktop) => NumGet(id1 := this.Id, "int64") == NumGet(id2 := desktop.Id, "int64") && NumGet(id1, 8, "int64") == NumGet(id2, 8, "int64")
static __New() {
iServiceProvider := ComObject("{C2F03A33-21F5-47FA-B4BB-156362A2F239}", "{6D5140C1-7436-11CE-8034-00AA006009FA}")
this.IVirtualDesktopManagerInternal := ComObjQuery(iServiceProvider, "{C5E0CDCA-7B6E-41B2-9FC4-D93975CC467B}", "{F31574D6-B682-4CDC-BD56-1827860ABEC6}")
this.IVirtualDesktopManager := ComObject("{AA509086-5CA9-4C25-8F95-589D3C07B48A}", "{A5CD92FF-29BE-454C-8D04-D82879FB3F1B}")
NumPut("int64", 0x43fcbe7eff72ffdd, "int64", 0xe4881e6881ad039c, iid := Buffer(16))
this.IID_IVirtualDesktop := iid
}
__New(ptr) {
if !this.Ptr := ptr
throw Error("Invalid pointer")
}
__Delete() => ObjRelease(this.Ptr)
}

评论(0)