VirtualDesktop
在 Windows 上把程序的窗口放到你看不到的地方去运行,并读取、操作它。窗口不出现在你的屏幕上, 退出时自动还原。
from VirtualDesktop import VirtualWorkspace
with VirtualWorkspace(VirtualWorkspace.SHARED) as space:
space.launch(["charmap.exe"])
print(space.describe()["count"])
# 退出时窗口还原、进程结束
本库提供两套互不相通的方案。它们的隔离强度、所需权限、以及"外部代码能对里面的窗口做什么" 都不一样,请按需要选一套,不要混用。
安装
pip install VirtualDesktop # pip
uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
运行时依赖只有 Pillow。若国内镜像同步滞后导致找不到最新版,加
--default-index https://pypi.org/simple(uv)或 -i https://pypi.org/simple(pip)。
| 私有桌面 | 虚拟显示器 | |
|---|---|---|
| 一句话 | 另开一个桌面,窗口与你的桌面完全隔离 | 多一块屏幕,窗口仍在你的桌面上、只是画在看不见的地方 |
| 底层 | 内核对象(CreateDesktopW),独立窗口命名空间 + DACL |
显示设备(IddCx 驱动),系统多出一块 \\.\DISPLAYn |
| 需要权限 | 零:不需要驱动、不弹 UAC | 装一次驱动,弹一次 UAC |
| 系统要求 | Win10 / 11 任意 | Win10 1809 (17763) 起 |
| 程序窗口截图 | 不能 | 能 |
| 桌面截图 | 不能 | 能 |
| 读窗口属性 / 枚举子控件 | 能(桌面内代理) | 能(外部进程直接) |
| 点击控件 / 送入字符 | 能(窗口消息) | 能(消息 + 真实输入) |
| 真实鼠标 / 滚轮 / 键盘 | 不能 | 能 |
| 外部 pywinauto / pyautogui 直接驱动 | 不能 | 能 |
| 窗口跨桌面搬运 | 不能(Windows 限制) | 能(在显示器之间移动) |
| 推荐场景 | 多实例并行、强隔离、不要截图 | UI 自动化、截图 OCR、无人值守抓界面 |
隔离强度分级
两套方案的隔离不是"强/弱"两档,而是三个层面各不相同,按你需要防住什么来选:
| 隔离层面 | 私有桌面 | 虚拟显示器 | 说明 |
|---|---|---|---|
| 窗口发现(能否枚举到) | 隔离 | 不隔离 | 私有桌面:EnumWindows/FindWindow 从外部看不到 |
| 窗口输入(真实鼠标键盘) | 隔离 | 不隔离 | 私有桌面:没有活动指针与键盘焦点 |
| 窗口截图(像素可读性) | 隔离 | 不隔离 | 私有桌面:从不参与系统合成,没有像素 |
窗口消息(SendMessage 等) |
不隔离 | 不隔离 | 拿到句柄即可投递消息,DACL 保护的是桌面对象而不是窗口消息 |
| 进程生命周期 | 隔离 | 不隔离 | 私有桌面关闭时其上进程一并结束;虚拟显示器上的程序由你负责 |
关键提醒:私有桌面不等于"完全无法访问"。它挡的是发现、输入和截图, 挡不住"已知句柄后投递消息"。所以不要把私有桌面当作进程级安全沙箱使用——它没有降低 目标进程的权限,只是把它挪到了另一个窗口命名空间。
推荐怎么选
要截图(窗口截图或桌面截图) → 虚拟显示器
要用真实鼠标 / 滚轮 / 键盘,或 pyautogui → 虚拟显示器
要用 pywinauto / UIA 从外部直接驱动窗口 → 虚拟显示器
只处理一个窗口、要截图,越省事越好 → OffscreenSession(虚拟显示器侧)
─────────────────────────────────────────────────────────────
要多个实例互不干扰、且不需要截图 → 私有桌面
不能弹 UAC、不能装驱动 → 私有桌面
要"退出后什么都不留"(进程随桌面一起结束) → 私有桌面
需要窗口完全不出现在任何可见位置 → 私有桌面
两条常用组合:
- 只是想安静地跑一个窗口并抓图 →
OffscreenSession+virtual_screen - 想并行跑很多互不干扰的实例 → 每个实例一个
VirtualWorkspace("名字")
第一部分:私有桌面
用 CreateDesktopW 建一个独立桌面。它有自己的窗口命名空间:你的桌面上看不到它的窗口,它上面的
程序也看不到你的窗口。不需要任何驱动、不需要管理员权限、不弹 UAC。
环境要求
| 项目 | 要求 |
|---|---|
| 系统 | Windows 10 / 11(无 IddCx 版本要求) |
| 权限 | 当前用户即可,不提权 |
| 驱动 | 不需要 |
| 依赖 | 仅 Pillow |
基本用法
from VirtualDesktop import VirtualDesktop
with VirtualDesktop("my-workspace") as desktop:
process = desktop.launch(["charmap.exe"], show=False)
desktop.bind_thread()
record = desktop.find_window(process.pid)
print(record.title, record.rect)
# 退出时桌面关闭,其上进程一并结束
工作区形式,一个桌面装多个窗口:
from VirtualDesktop import VirtualWorkspace
# 每个工作区一个独立桌面,互不干扰
with VirtualWorkspace("lab-a") as a, VirtualWorkspace("lab-b") as b:
a.launch(["charmap.exe"])
b.launch(["charmap.exe"])
print(a.describe()["count"]) # 1,只看到自己的
print(b.describe()["count"]) # 1
名字不会撞车:CreateDesktopW 对同名桌面会复用同一对象,直接复用会让两个实例共享窗口命名
空间,所以默认加随机后缀去重。确实要共享同一个桌面时用 unique=False。
在桌面内操作窗口:agent()
私有桌面是隔离边界,外部进程拿不到它的窗口。所有操作都要在桌面内执行,agent() 提供一个
with 可用的常驻代理:
from VirtualDesktop import VirtualDesktop
with VirtualDesktop("lab").create() as desktop, desktop.agent() as agent:
window = agent.launch(["charmap.exe"]) # 在里面启动
info = agent.info(window) # 读标题/类名/矩形/子控件数/按钮
print(info["title"], info["size"], info["buttons"][:3])
agent.click(window, text="选择") # 按按钮文字点击
agent.keys(window, "41") # 送入字符(不需要键盘焦点)
print(agent.windows())
隔离工作区同样可用:
with VirtualWorkspace("lab") as space, space.agent() as agent:
window = agent.launch(["charmap.exe"])
print(agent.info(window))
| 方法 | 作用 |
|---|---|
agent.launch([...]) |
在桌面内启动程序并等它的窗口 |
agent.adopt(pid) |
接上桌面内已在运行的程序 |
agent.info(window) |
标题、类名、矩形、child_count、buttons |
agent.text(window) |
窗口标题 |
agent.uia_tree(window) |
完整 UIA 树(在桌面内遍历) |
agent.click(window, text=/index=) |
点击控件(走窗口消息,见下) |
agent.keys(window, "...") |
送入字符(走窗口消息) |
agent.windows() |
该桌面上的窗口清单 |
agent.exec("python 代码") |
在桌面内跑任意 Python,返回打印内容 |
exec 是逃生舱:里面的代码能 import VirtualDesktop、能用 list_windows()、CaptureEngine,
也能用你自己装的任何库。代理本身只依赖 ctypes,不需要额外安装。
私有桌面不能截图
两种截图都不行,且原因不同:
| 类型 | 结果 | 原因 |
|---|---|---|
| 程序窗口截图 | 不能 | 该桌面的窗口从不被系统合成,没有像素可读 |
| 桌面截图 | 不能 | 拿不到该桌面的屏幕 DC(BitBlt 返回拒绝访问) |
agent.screenshot() 会明确报错(code="capture-needs-shared-workspace")而不是产出空白图。
要读状态用 agent.info() / agent.text();要像素请改用虚拟显示器。
能力边界
私有桌面的窗口不在交互桌面上,所以按输入机制分,能做的事如下:
| 想做的事 | 私有桌面 | 原因 |
|---|---|---|
| 找到窗口、读标题 / 类名 / 矩形 | 能 | 读窗口属性,跨桌面可用 |
| 枚举子控件、读控件文字 | 能 | 同上 |
| 用外部进程的 pywinauto 连上并读属性 | 能 | 拿得到句柄就能读 |
点击控件(BM_CLICK 等窗口消息) |
能 | 消息直接投递给窗口,不需要指针 |
送入字符(WM_CHAR 消息) |
能 | 同上 |
| 程序窗口截图 | 不能 | 桌面从不被系统合成,窗口没有像素 |
| 桌面截图 | 不能 | 拿不到该桌面的屏幕 DC(BitBlt 返回拒绝访问) |
| 移动真实鼠标来点击 | 不能 | 该桌面不是活动桌面,没有指针 |
| 滚轮(真实滚轮事件) | 不能 | 同"移动真实鼠标" |
发送真实键盘输入(SendInput) |
不能 | 键盘只发给活动桌面 |
pywinauto 的 click_input() / type_keys() |
不能 | 依赖真实输入与前台焦点 |
要点:能用消息驱动的都能用,需要真实输入设备的都不能用,两种截图都不能用。需要截图或需要 真实鼠标/滚轮的应用,请改用下面的虚拟显示器方案。
支持的能力与调用方式
| 能力 | 是否支持 | 怎么调 |
|---|---|---|
| 在一个桌面里跑多个程序 | 支持 | 连续 agent.launch([...]),各自拿到不同句柄 |
| 多个独立桌面同时在线 | 支持 | 多个 VirtualWorkspace("名字") 并存,句柄与名字互不相同 |
| 拿到窗口句柄 | 支持 | agent.launch([...]).hwnd / agent.adopt(pid).hwnd / desktop.find_window(pid) |
| 读标题 / 类名 / 矩形 / 尺寸 | 支持 | agent.info(window) |
| 枚举子控件与按钮文字 | 支持 | agent.info(window)["child_count"] / ["buttons"] |
| 完整 UIA 树 | 支持 | agent.uia_tree(window) |
| 点击控件 / 送入字符 | 支持 | agent.click(window, text=...) / agent.keys(window, "...") |
| 在桌面内跑任意 Python | 支持 | agent.exec("...")(可用 comtypes、pywinauto) |
| 程序窗口截图 / 桌面截图 | 不支持 | 见上方原因;改用虚拟显示器 |
| 跨桌面搬运窗口、移到主屏 | 不支持 | move_to 一律报 cannot-cross-desktop;只能在目标桌面重新 launch |
agent.uia_tree() 在桌面内遍历 UIA,这是私有桌面唯一可用的取树方式:
with VirtualWorkspace("lab") as space, space.agent() as agent:
window = agent.launch(["charmap.exe"])
tree = agent.uia_tree(window) # view="RawView" 默认;也可 ControlView / ContentView
print(tree["count"])
for node in tree["nodes"][:5]:
print(node["control"], node["name"], node["enabled"])
不要用
GetRootElement():它属于输入桌面,在私有桌面里走它拿不到任何目标窗口。 库内部用的是ElementFromHandle(hwnd)。
在里面用 comtypes / pywinauto 是安全的:窗口枚举已不再绑定调用者线程,所以
from pywinauto import Application放在文件顶部也不会影响私有桌面。
第二部分:虚拟显示器
装一个 IddCx 间接显示驱动,系统就多出一块屏幕(\\.\DISPLAYn)。把窗口搬到那块屏上:
窗口仍是普通窗口,仍留在你的交互桌面上,只是画在你看不到的地方。因此外部程序能正常找到并
操作它,也能正常截图。
环境要求
| 项目 | 要求 |
|---|---|
| 系统 | Windows 10 1809 (build 17763) 或更高(更低版本没有 IddCx 框架) |
| 权限 | 安装/卸载驱动需要管理员,弹一次 UAC |
| 驱动 | 首次使用时自动下载安装(也可预先装好) |
| 依赖 | 仅 Pillow |
装驱动
VirtualDesktop install-driver --dry-run # 先看将要执行的特权命令,不安装
VirtualDesktop install-driver # 安装(弹一次 UAC)
或在代码里让它按需装:VirtualWorkspace(..., install_driver=True)(默认)。
默认不自动卸载:否则下一次使用又要弹一次 UAC。只在"用完即还机器"的场景开启:
with VirtualWorkspace(VirtualWorkspace.SHARED, uninstall_on_exit=True) as space:
...
print(space.uninstall_report) # {"ok": True, "message": "device node removed"}
两种隐藏方式
SessionConfig(hide_mode=...) |
做法 | 需要驱动 |
|---|---|---|
virtual_screen(默认) |
移到虚拟显示器上 | 是 |
offscreen |
移出所有显示器的坐标区 | 否 |
offscreen不需要驱动,但代价是窗口在所有显示器之外,真实鼠标到不了那里(指针被钳制回 屏幕边缘),因此真实鼠标点击、滚轮、真实键盘输入都不可用;截图和消息驱动仍然可用。 需要真实输入或pyautogui,请用virtual_screen。
基本用法
from VirtualDesktop import VirtualWorkspace
# 共享模式:窗口可被外部自动化库直接控制
with VirtualWorkspace(VirtualWorkspace.SHARED) as space:
space.launch(["charmap.exe"])
space.launch(["charmap.exe"]) # 运行中可随时继续新增
print(space.describe()["count"]) # 2
desktop 是必填参数,因为它决定窗口能不能被外部进程控制,这不该由库替你猜:
VirtualWorkspace(VirtualWorkspace.SHARED) # 共享:窗口留在交互桌面,外部可控
# VirtualWorkspace() # TypeError:不猜
# VirtualWorkspace(None) # ConfigError:并提示该传什么
单窗口会话
只处理一个窗口时最省事:
from VirtualDesktop import OffscreenSession
with OffscreenSession(target_window_title="微信") as session:
session.mount() # 按需装驱动 + 隐藏窗口
frame = session.capture()
frame.image.save("wechat.png")
# 退出时窗口回到原位,任务栏样式恢复
也可用 window_class= / process_name= / window_pid= 定位。unmount() 幂等;异常退出同样会还原。
指定窗口放到哪块屏
VirtualWorkspace(VirtualWorkspace.SHARED, monitor="virtual") # 第一块虚拟显示器(默认)
VirtualWorkspace(VirtualWorkspace.SHARED, monitor="\\\\.\\DISPLAY2") # 指定设备
VirtualWorkspace(VirtualWorkspace.SHARED, monitor="primary") # 主屏(不装驱动也能跑)
截图
要分清两种,它们走完全不同的通路,能力也不同:
| 类型 | 拍什么 | 走哪条路 | 怎么调 |
|---|---|---|---|
| 程序窗口截图 | 某一个窗口自己的像素 | PrintWindow(hwnd) |
CaptureEngine.capture(hwnd) / space.capture(hwnd) |
| 桌面截图 | 整块屏幕(含其上所有窗口) | ImageGrab / BitBlt 屏幕 DC |
capture_all_screens() / capture_monitor() / space.capture_desktop() |
程序窗口截图
按句柄拍,窗口在屏幕外也能拍,是抓单个程序界面最可靠的方式。capture() 依次尝试多种后端直到
拿到有内容的帧,默认顺序:
printwindow(PW_RENDERFULLCONTENT)→ printwindow_plain → wm_print → bitblt
每帧都做统计判定:方差接近 0(未绘制/全黑/全白),或单色占比 ≥ 99.9% 且无结构,即判为失败并换 下一个后端。所以"偶尔拿到黑图"会变成确定性的回退,而不是静默产出坏图。
from VirtualDesktop import CaptureEngine
engine = CaptureEngine(methods=("bitblt",), client_only=False)
frame = engine.capture(hwnd, require_content=True)
print(frame.method, frame.size)
with VirtualWorkspace(VirtualWorkspace.SHARED) as space:
entry = space.launch(["charmap.exe"])
space.capture(entry.hwnd, save_to="window.png")
桌面截图
拍的是屏幕,不是某个窗口。virtual_screen_rect() 是整张虚拟桌面(含所有显示器,处理好负坐标),
capture_monitor() 是单块显示器。在虚拟显示器模式下,space.capture_desktop() 等价于
capture_monitor(space.monitor)。
from VirtualDesktop import CaptureEngine
from VirtualDesktop.desktop_capture import capture_all_screens, capture_monitor
CaptureEngine().ensure_dpi_aware() # 拍摄前必须做,见下方警告
whole = capture_all_screens() # 整张虚拟桌面
whole.image.save("desktop.png")
print(whole.rect, whole.size, whole.method)
one = capture_monitor(device="\\\\.\\DISPLAY2") # 只拍某一块屏
拍摄前先确保 DPI 感知。 矩形由
GetSystemMetrics/EnumDisplayMonitors计算,它们对 不感知 DPI 的进程返回的是缩放后的逻辑值;而像素抓取返回的是物理像素。两者不一致时 会静默裁掉画面右下角(125% 缩放下 1920×1080 的屏只拿到 1536×864,少 384×216 像素,且不报错)。 先调一次CaptureEngine().ensure_dpi_aware()即可,VirtualWorkspace内部已自动处理。
桌面截图只有虚拟显示器模式可用。 offscreen 模式下窗口被移到所有显示器之外,桌面截图按 定义拍不到它们;而私有桌面根本没有可拍的屏幕 DC。
用自动化库控制窗口
窗口是普通窗口,直接从你的进程用即可:
from VirtualDesktop import SessionConfig, VirtualWorkspace
from pywinauto import Application
config = SessionConfig(hide_mode="virtual_screen", keep_window_size=True)
with VirtualWorkspace(VirtualWorkspace.SHARED, config=config) as space:
first = space.launch(["charmap.exe"], timeout=45.0)
dialog = Application(backend="win32").connect(process=first.pid).top_window()
print(dialog.window_text(), dialog.rectangle(), len(dialog.children()))
space.capture(first.hwnd, save_to="window.png")
print(space.describe()) # count / externally_controllable ...
把窗口移到别的屏幕
with VirtualWorkspace(VirtualWorkspace.SHARED) as src:
w = src.launch(["charmap.exe"])
src.move_to_primary() # 还给用户:还原到主屏、恢复任务栏
src.move_to("\\\\.\\DISPLAY2") # 移到指定的扩展屏 / 虚拟屏
src.move_all_to("primary") # 全部移走
src.move_to_primary(windows=[w.hwnd]) # 只移指定窗口(也接受标题)
虚拟显示器大小与 DPI
默认与当前主屏一致:
VirtualDesktop capture "微信" --virtual-size 1920x1080 --virtual-dpi 125%
from VirtualDesktop import configure_virtual_display, current_primary_size
configure_virtual_display(size="1920x1080", dpi="125%") # 传 None 表示跟随主屏
print(current_primary_size()) # (1920, 1080)
能力边界
| 想做的事 | 虚拟显示器 | offscreen |
|---|---|---|
| 找到窗口、读标题 / 类名 / 矩形 | 能 | 能 |
| 枚举子控件、读控件文字 | 能 | 能 |
| 点击控件(窗口消息) | 能 | 能 |
| 送入字符(窗口消息) | 能 | 能 |
| 程序窗口截图 | 能 | 能 |
| 桌面截图 | 能(拍到窗口) | 不能(窗口在所有显示器之外) |
| 外部 pywinauto / UIA 直接连上 | 能 | 能 |
| 移动真实鼠标来点击 | 能 | 不能(窗口在屏幕之外) |
| 滚轮 | 能 | 不能 |
真实键盘(SendInput) |
能 | 不能 |
用真实输入时,窗口所在显示器必须是活动桌面的一部分;
pyautogui这类库还需要窗口能取得前台 焦点。若目标窗口拿不到焦点,请改用消息驱动(agent.click()/WM_CHAR)。
支持的能力与调用方式
| 能力 | 是否支持 | 怎么调 |
|---|---|---|
| 在一个工作区里跑多个程序 | 支持 | 连续 ws.launch([...]),运行中可随时新增 |
| 多个工作区同时在线 | 支持 | 多个 VirtualWorkspace(SHARED) 并存,各自独立停靠位 |
| 拿到窗口句柄给外部用 | 支持 | ws.describe()["windows"](真实 HWND,无包装) |
| 读标题 / 类名 / 矩形 | 支持 | 外部 pywinauto 直接读,或 ws.windows() |
| 枚举子控件 | 支持 | 外部 pywinauto children(visible_only=False) |
| 完整 UIA 树 | 支持 | 外部 UIA,或 --ui-tree DIR |
| 程序窗口截图 | 支持 | ws.capture(hwnd, save_to=...) / CaptureEngine.capture(hwnd) |
| 桌面截图 | 支持(virtual_screen) |
ws.capture_desktop() / capture_monitor() / capture_all_screens() |
| 真实鼠标 / 滚轮 / 键盘 | 支持(virtual_screen) |
pywinauto click_input() / type_keys(),或 pyautogui |
| 在显示器之间移动窗口 | 支持 | move_to("\\\\.\\DISPLAY2") / move_all_to(...) / move_to_primary() |
| 退出时还原窗口 | 支持 | with 退出自动还原;异常退出同样还原 |
| 退出时卸载驱动 | 支持 | uninstall_on_exit=True 或 close(uninstall=True) |
桌面截图(offscreen 模式) |
不支持 | 窗口在所有显示器之外,拍不到;改用 virtual_screen |
真实鼠标 / 滚轮 / 键盘(offscreen) |
不支持 | 同上:指针被钳制回屏幕边缘 |
| 跨私有桌面搬运窗口 | 不支持 | Windows 不允许;move_to 会明确报错 |
通用功能
命令行
VirtualDesktop check # 只读体检:系统版本 / 显示器 / 驱动 / 缓存 / 签名
VirtualDesktop list-windows # 列出可选窗口
VirtualDesktop capture "微信" --out wx.png --unmount
VirtualDesktop install-driver --dry-run
VirtualDesktop install-driver
VirtualDesktop uninstall-driver --purge-files
capture / mount 还支持 --hide-mode、--monitor、--methods、--virtual-size、
--virtual-dpi、--ui-tree、--desktop-shot,用 --help 查看。
配置项
环境变量形式,便于计划任务与 CI:
VIRTUALDESKTOP_DRIVER_POLICY=auto|never|always VIRTUALDESKTOP_MONITOR=auto|virtual|primary|DISPLAY2
VIRTUALDESKTOP_HIDE_MODE=virtual_screen|offscreen|auto
VIRTUALDESKTOP_GITHUB_MIRRORS=<url 前缀,逗号分隔> VIRTUALDESKTOP_CACHE_DIR=<path>
VIRTUALDESKTOP_MANIFEST=<path to manifest.json> VIRTUALDESKTOP_SIGNATURE_POLICY=warn|strict|off
VIRTUALDESKTOP_CAPTURE_METHODS=printwindow,bitblt VIRTUALDESKTOP_DEBUG=1
VIRTUALDESKTOP_LOG_FILE=<path>
故障排查
| 现象 | 原因 | 处理 |
|---|---|---|
TypeError 缺 desktop |
VirtualWorkspace 必须显式选择模式 |
传工作区名或 VirtualWorkspace.SHARED |
DriverNotFoundError |
未装虚拟显示器且不允许安装 | 去掉限制,或先跑 VirtualDesktop install-driver |
ElevationDenied |
UAC 被取消或被策略阻止 | 在管理员终端执行一次 VirtualDesktop install-driver |
unsupported-windows-build |
系统低于 Win10 1809,没有 IddCx | 换系统,或用私有桌面(不需要驱动) |
capture-needs-shared-workspace |
想截私有桌面里的窗口 | 私有桌面不支持截图,改用虚拟显示器 |
SetThreadDesktop ... error 170 |
你自己在绑定线程,而该线程已有窗口或已初始化 COM | 直接调用 bind_thread() 时才会遇到;库内部的枚举不再绑定调用者线程 |
| 某程序在私有桌面上找不到窗口 | 控制台类程序(cmd.exe、ping.exe)的窗口属于 conhost.exe,不在自己 pid 下 |
报错会提示;用 agent.exec() 在桌面内驱动它,或换 GUI 程序 |
| 安装后没出现新的虚拟屏 | 驱动已注册但显示器未启用 | 设备管理器启用;report.requires_reboot 为真时重启 |
| 驱动下载失败或极慢 | github.com 不可达 | 设 VIRTUALDESKTOP_GITHUB_MIRRORS,或预置离线缓存 |
| 窗口截图是黑帧 | 应用为硬件加速 / 受保护内容 | 系统限制;换 bitblt 试试,或确认窗口有内容 |
| 桌面截图少了右下角 | 进程不感知 DPI,矩形按逻辑坐标算 | 先调 CaptureEngine().ensure_dpi_aware() |
| 桌面截图拍不到工作区窗口 | 用了 offscreen,窗口在所有显示器之外 |
改用 hide_mode="virtual_screen" |
capture_desktop() 报"workspace is not open" |
offscreen 模式下没有绑定显示器 | 改用 capture_all_screens(),或用 virtual_screen |
| 真实鼠标点击无效 | 窗口在屏幕之外,或拿不到前台焦点 | 用 virtual_screen,或改用消息驱动 |
离线 / 内网部署:把安装包按清单里的文件名(VirtualDesktop/driver_manifest.json 的
artifact.filename)放进 VIRTUALDESKTOP_CACHE_DIR 即可,不会联网;也可用
VIRTUALDESKTOP_MANIFEST 指向自建镜像的清单(含自算摘要)。
已知限制
- 私有桌面不能截图:程序窗口截图和桌面截图都不行。窗口没有被系统合成、没有像素可读;
而该桌面的屏幕 DC 也拿不到(
BitBlt返回拒绝访问)。用agent.info()/agent.text()读状态, 需要像素请改用虚拟显示器。 - 私有桌面不能用真实鼠标 / 滚轮 / 真实键盘:那里没有活动指针和键盘焦点;消息驱动可用。
- 窗口无法在私有桌面之间搬运:Windows 不允许跨桌面移动窗口。
- 部分应用(UWP、硬件叠加层、DRM 受保护内容)任何离屏方式都抓不到——系统限制。
- 自绘界面的应用(如微信 3.9.x)没有子窗口,UI Automation 只暴露顶层
Pane,组件粒度只能到 窗口级;控件级定位需要靠截图像素。Electron/浏览器类应用的 UIA 树是完整的。 - ARM64 自动选用清单里的 ARM64 驱动变体;x64 / x86 共用同一份驱动包。
安全说明
驱动来源与校验
虚拟显示器驱动使用
VirtualDrivers/Virtual-Display-Driver
(MttVDD,IddCx 间接显示驱动,SignPath Foundation 签名)。
驱动与工具二进制不打进 wheel,首次使用时按需下载。下载链路三道校验,任一不过即中止:
- SHA-256 摘要:与随包发布的清单
driver_manifest.json中固定的摘要逐字节比对。不匹配的 文件会被隔离(cache/quarantine)而不是使用。 - Authenticode 验签:比对签名者证书指纹与清单中固定的允许值。
- 系统安装校验:文件最后交给 Windows 安装,由系统再验一次驱动签名。
国内下载加速是安全的
github.com 在国内经常不可达,因此清单为每个安装包列了多个加速镜像,主 URL 失败时按顺序重试。
加镜像不降低安全性:镜像只影响"从哪里拿字节",摘要校验照常执行——镜像要么给出完全相同的
文件,要么被第 1 道校验拒绝。它无法让不同的内容通过。
需要自建或更换镜像:
set VIRTUALDESKTOP_GITHUB_MIRRORS=https://gh-proxy.com/,https://internal.example/mirror/
同样只接受 https://,一样过摘要校验。
权限与提权
安装/卸载驱动需要管理员权限,会弹一次 UAC,由你确认。除此之外库以当前用户权限运行。
完全不提权的方式:私有桌面(VirtualWorkspace("name") 或 VirtualDesktop),以及
hide_mode="offscreen"。
桌面隔离
- 私有桌面:
CreateDesktopW建的桌面只属于当前用户会话,默认 DACL 已限定创建者(和管理员) 访问,其他用户无法打开。库另外会主动检测同名桌面并改用唯一名字,避免两个实例静默共享同一个 命名空间。 - 虚拟显示器 / offscreen:窗口留在交互桌面上,任何以你的身份运行的程序都能找到并操作它们。 这既是它支持外部自动化的原因,也意味着它不提供针对同一台电脑上其他程序的隔离。需要隔离 请用私有桌面。
运行期行为
- 不改动你的桌面:窗口只被移动位置,退出(含异常退出)时还原到原位与原有样式。
- 不注入目标进程:不写目标进程内存、不挂钩子。窗口操作走公开的 Win32 API,截图走
PrintWindow等系统接口。 - 网络访问仅限下载安装包:没有遥测、没有上传,运行期不与任何服务器通信。
- 文件写入仅在缓存目录:默认
%LOCALAPPDATA%\VirtualDesktop\cache,可用VIRTUALDESKTOP_CACHE_DIR更改。
许可证
MIT,见 LICENSE。
Metadata
Release files for VirtualDesktop 1.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| virtualdesktop-1.4.0.tar.gz | 223.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| virtualdesktop-1.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 391.0 kB
Release files / virtualdesktop-1.4.0.tar.gz
| Download URL | virtualdesktop-1.4.0.tar.gz |
|---|---|
| Size | 223.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6aaf18e3bbf843579cbd0cd6d90cd343e40c175ab8bf3b6dc01815ad7088ab65
|
|
BLAKE2b-256 checksum How to use checksums |
8228a3b385d5c34759ccfb3bf1142f9968501c5635ffea16e12c048822d265d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.23
|
Release files / virtualdesktop-1.4.0-py3-none-any.whl
| Download URL | virtualdesktop-1.4.0-py3-none-any.whl |
|---|---|
| Size | 167.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
90a0c39ed6136fc38d608d5e71a22829803202b04dca43eec07bf72a23739cdc
|
|
BLAKE2b-256 checksum How to use checksums |
d2ad6e5107954aa49de6ee5bdd389214161d8018684c19c468609bf3408ea20a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.23
|