Skip to main content

VirtualDesktop

在 Windows 上把程序的窗口放到你看不到的地方去运行并读取它:放到虚拟显示器、或放到独立的 私有桌面。窗口不会出现在你的屏幕上,退出时自动还原。

from VirtualDesktop import VirtualWorkspace

with VirtualWorkspace(VirtualWorkspace.SHARED) as space:
    space.launch(["notepad.exe"])          # 启动到"看不见的地方"
    print(space.describe()["count"])       # 1
# 退出时窗口还原、进程结束

适用于:UI 自动化、不在前台打扰用户的截图/OCR、无人值守环境抓取应用界面、以及把多个程序 隔离开并行运行。


一、先理解两个概念

整个库只建立在 Windows 的两套机制上。三个类只是它们的不同封装,搞清这点其余都好懂。

机制 是什么 需要什么
留在交互桌面 窗口仍是普通窗口,只把位置挪到虚拟显示器上(\\.\DISPLAYn),或挪到所有显示器之外的坐标区 虚拟显示器要装一次驱动(1 次 UAC);挪出坐标区什么都不需要
私有桌面 用 CreateDesktopW 建的独立窗口命名空间。与你的桌面完全隔离 什么都不需要

关键区别只有一条:窗口是否还留在交互桌面上。由此决定两件事——外部进程能不能直接控制它、能不能截图。

模式 窗口在交互桌面 外部库能直接控制 能截图 需要驱动
VirtualWorkspace(SHARED) + virtual_screen 是 能 能 是
VirtualWorkspace(SHARED) + offscreen 是 能 能 否
OffscreenSession + virtual_screen 是 能 能 是
OffscreenSession + offscreen 是 能 能 否
VirtualWorkspace("名字") 否 不能 不能 否
VirtualDesktop 否 不能 不能 否

也就是说,两种隐藏方式(virtual_screen / offscreen)都能截图,只是把窗口分别放到"另一块屏" 和"屏幕之外的坐标区"。唯一截不到图的是私有桌面——那里的窗口没有被系统合成,没有任何后端能 读到像素(详见「已知限制」)。

三个类与机制的关系:

类 用途 底层机制
VirtualWorkspace(SHARED) 多窗口工作区,外部可自动化 留在交互桌面
VirtualWorkspace("名字") 多窗口工作区,强隔离 私有桌面
OffscreenSession 单窗口:挂一个窗口、截图、还原 留在交互桌面
VirtualDesktop 私有桌面原语(VirtualWorkspace("名字") 内部就用它) 私有桌面
configure_virtual_display() 设置虚拟显示器的分辨率 / DPI (作用于显示器本身)

该用哪个:

只处理一个窗口、要截图            → OffscreenSession
要同时跑多个程序、外部要控制、要截图 → VirtualWorkspace(SHARED)
要多实例强隔离、不需要截图          → VirtualWorkspace("名字")
要自己管理桌面、不需要工作区封装     → VirtualDesktop

二、使用说明

安装

pip install VirtualDesktop        # pip
uv add VirtualDesktop             # uv(写入 pyproject.toml 并锁定)

运行时依赖只有 Pillow。需要 Windows 10 1809 (build 17763) 或更高(更低版本没有 IddCx 框架,装不了虚拟显示器)。不装驱动也能用私有桌面模式。

国内镜像(如清华源)同步新版本可能滞后;若 uv add / pip install 找不到最新版,加 --default-index https://pypi.org/simple(uv)或 -i https://pypi.org/simple(pip)即可。

多窗口工作区

desktop 是必填参数,因为它决定窗口能不能被外部进程控制,这不该由库替你猜:

from VirtualDesktop import VirtualWorkspace

# 共享模式:窗口可被外部自动化库直接控制
with VirtualWorkspace(VirtualWorkspace.SHARED) as space:
    space.launch(["charmap.exe"])
    space.launch(["charmap.exe"])          # 运行中可随时继续新增
    print(space.describe()["count"])       # 2
    print([w.hwnd_hex for w in space.windows()])

# 隔离模式:每个工作区一个独立桌面,互不干扰
with VirtualWorkspace("lab-a") as a, VirtualWorkspace("lab-b") as b:
    a.launch(["charmap.exe"])
    b.launch(["charmap.exe"])
    print([w.hwnd_hex for w in a.windows()])   # 只看到自己的
    print([w.hwnd_hex for w in b.windows()])   # 只看到自己的
VirtualWorkspace("my-lab")                 # 隔离:独立私有桌面
VirtualWorkspace(VirtualWorkspace.SHARED)   # 共享:窗口留在交互桌面,外部可控
# VirtualWorkspace()                       # TypeError:不猜
# VirtualWorkspace(None)                   # ConfigError:并提示该传什么

工作区名字不会撞车:若该名字已存在(包括别的进程建的),本实例自动改用带随机后缀的名字。 CreateDesktopW 对同名桌面会复用同一对象,直接复用会让两个实例共享窗口命名空间——所以默认去重。 确实要共享同一个桌面时才用 VirtualDesktop("name", unique=False)。

指定窗口放到哪块屏

VirtualWorkspace(VirtualWorkspace.SHARED, monitor="virtual")         # 第一块虚拟显示器(默认)
VirtualWorkspace(VirtualWorkspace.SHARED, monitor="\\\\.\\DISPLAY2")   # 指定设备
VirtualWorkspace(VirtualWorkspace.SHARED, monitor="primary")         # 主屏(不装驱动也能跑)

把窗口移到别的桌面

with VirtualWorkspace(VirtualWorkspace.SHARED) as src, VirtualWorkspace("lab-b") as dst:
    w = src.launch(["charmap.exe"])

    src.move_to_primary()             # 还给用户:还原到主屏、恢复任务栏
    src.move_to(dst)                  # 交给另一个工作区
    src.move_to("\\\\.\\DISPLAY2")      # 移到指定的扩展屏 / 虚拟屏
    src.move_all_to("primary")        # 全部移走
    src.move_to_primary(windows=[w.hwnd])    # 只移指定窗口(也接受标题)

交接后归属权一并转移:目标工作区负责后续还原,源工作区退出时不会再动它、也不会结束它的进程。

窗口无法在私有桌面之间搬运——Windows 不允许跨桌面移动窗口。这种模式请直接 launch 到目标 工作区;move_to 会明确报错说明原因。

退出时自动卸载驱动

with VirtualWorkspace(VirtualWorkspace.SHARED, uninstall_on_exit=True) as space:
    ...
print(space.uninstall_report)     # {"ok": True, "message": "device node removed"}

默认不卸载:否则下一次 with 又要弹一次 UAC。只在"用完即还机器"的场景开启。

单窗口会话

只要处理一个窗口时用这个,最省事:

from VirtualDesktop import OffscreenSession, SessionConfig

with OffscreenSession(target_window_title="微信") as session:
    session.mount()                 # 按需装驱动 + 隐藏窗口
    frame = session.capture()
    frame.image.save("wechat.png")
# 退出时窗口回到原位,任务栏样式恢复

也可用 window_class= / process_name= / window_pid= 定位。两种隐藏方式:

SessionConfig(hide_mode=...) 做法 需要驱动
virtual_screen(默认) 移到虚拟显示器 是
offscreen 移出所有显示器的坐标区 否
auto 先试前者,不可用则退到后者 否

unmount() 幂等;异常退出同样会还原。

私有桌面(零权限、零安装)

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)
# 退出时桌面关闭,其上进程一并结束

它上面运行的程序对交互桌面完全不可见。要在里面读写窗口、点击控件,用下面的代理。

在私有桌面里干活: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.click(window, index=0)                 # 或按下标点击
    print(agent.windows())                       # 该桌面上的所有窗口
    print(agent.exec("from VirtualDesktop import list_windows; print(len(list_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) 标题、类名、矩形、子控件数、按钮文字
agent.text(window) 窗口标题
agent.click(window, text=/index=) 点击按钮
agent.keys(window, "...") 送入字符(不需要键盘焦点)
agent.windows() 该桌面上的窗口清单
agent.exec("python 代码") 在桌面内跑任意 Python,返回打印内容

exec 是逃生舱:里面的代码能 import VirtualDesktop、能用 list_windows()、CaptureEngine, 也能用你自己装的任何库(包括 pywinauto)。代理本身只依赖 ctypes,不需要额外安装。

代理不能截图:私有桌面的窗口没有被系统合成,agent.screenshot() 会明确报错。需要截图就用 VirtualWorkspace(SHARED)。

用自动化库控制窗口

共享工作区:外部进程直接控制

窗口是普通窗口,直接从你的进程用即可:

from VirtualDesktop import SessionConfig, VirtualWorkspace
from pywinauto import Application

config = SessionConfig(hide_mode="offscreen", keep_window_size=True)   # offscreen 不需驱动

with VirtualWorkspace(VirtualWorkspace.SHARED, config=config, install_driver=False) as space:
    first = space.launch(["charmap.exe"], timeout=45.0)
    second = 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()))
    buttons = [c for c in dialog.children() if c.friendlyclassname == "Button"]
    buttons[0].click_input()                     # 真实点击

    space.capture(first.hwnd, save_to="window.png")
    print(space.describe())                      # count / externally_controllable ...

把 hide_mode 换成 virtual_screen 就搬到虚拟显示器上,自动化代码不用改。

私有桌面:只能用代理

agent() 会话就是为它准备的(见上一节)。下面是不用 agent 封装的等价写法,便于理解协议:

# agent.py —— 在桌面内运行
import argparse, ctypes, ctypes.wintypes as w, json
from pywinauto.controls.hwndwrapper import HwndWrapper

p = argparse.ArgumentParser(); p.add_argument("--out"); p.add_argument("--hwnd"); a = p.parse_args()
wrapper = HwndWrapper(int(a.hwnd, 16))            # 直接从句柄构造,绕开高层 API 的可见性检查
rect = wrapper.rectangle()
children = wrapper.children(visible_only=False)   # 私有桌面窗口不带 WS_VISIBLE
json.dump({"title": wrapper.window_text(), "rect": [rect.left, rect.top, rect.right, rect.bottom],
           "children": len(children)},
          open(a.out, "w", encoding="utf-8"), ensure_ascii=False)
with VirtualWorkspace("lab-a") as space:
    entry = space.launch(["charmap.exe"], timeout=45.0)
    report = space.run_agent("agent.py", ["--out", "report.json", "--hwnd", hex(entry.hwnd)])

若要用 pywinauto 之类,这套写法在私有桌面内的可用范围:

操作 是否可用 说明
构造包装器 / 读标题 / 读矩形 可用 只是读窗口属性
children() 可用,需 visible_only=False 私有桌面窗口不带 WS_VISIBLE
is_visible() 返回 False 桌面未被显示
click() / click_input() 不可用 前者做可见性前置检查,后者需要活动桌面移动光标
点击控件 发 BM_CLICK 消息 用 agent.click() 即可

pyautogui 这类依赖真实鼠标/键盘的库只能用于共享工作区——私有桌面上没有可移动的指针。

命令行

VirtualDesktop check                      # 只读体检:系统版本 / 显示器 / 驱动 / 缓存 / 签名
VirtualDesktop list-windows               # 列出可选窗口
VirtualDesktop capture "微信" --out wx.png --unmount
VirtualDesktop install-driver --dry-run   # 打印将要执行的特权命令,不安装
VirtualDesktop install-driver             # 安装驱动(弹一次 UAC)
VirtualDesktop uninstall-driver --purge-files

capture / mount 还支持 --hide-mode、--monitor、--methods、--virtual-size、 --virtual-dpi、--ui-tree、--desktop-shot,用 --help 查看。

虚拟显示器大小与 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)

截图方式

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)

配置项

环境变量形式,便于计划任务与 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 想截私有桌面里的窗口 改用 VirtualWorkspace(SHARED)
cannot-cross-desktop 想在私有桌面之间搬窗口 直接 launch 到目标工作区
安装后没出现新的虚拟屏 驱动已注册但显示器未启用 设备管理器启用;report.requires_reboot 为真时重启
驱动下载失败或极慢 github.com 不可达 设 VIRTUALDESKTOP_GITHUB_MIRRORS,或预置离线缓存
截图是黑帧 应用为硬件加速 / 受保护内容 系统限制;换 bitblt 试试,或确认窗口有内容

离线 / 内网部署:把安装包按清单里的文件名(VirtualDesktop/driver_manifest.json 的 artifact.filename)放进 VIRTUALDESKTOP_CACHE_DIR 即可,不会联网;也可用 VIRTUALDESKTOP_MANIFEST 指向自建镜像的清单(含自算摘要)。


三、安全说明

驱动来源与校验

虚拟显示器驱动使用 VirtualDrivers/Virtual-Display-Driver (MttVDD,IddCx 间接显示驱动,SignPath Foundation 签名)。

驱动与工具二进制不打进 wheel,首次使用时按需下载。下载链路三道校验,任一不过即中止:

  1. SHA-256 摘要:与随包发布的清单 driver_manifest.json 中固定的摘要逐字节比对。不匹配的 文件会被隔离(cache/quarantine)而不是使用。
  2. Authenticode 验签:比对签名者证书指纹与清单中固定的允许值。
  3. 系统安装校验:文件最后交给 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 已限定创建者(和管理员)访问, 其他用户无法打开。库另外会主动检测同名桌面并改用唯一名字,避免两个实例静默共享同一个命名空间。

运行期行为

  • 不改动你的桌面:窗口只被移动位置,退出(含异常退出)时还原到原位与原有样式。
  • 不注入目标进程:不写目标进程内存、不挂钩子。窗口操作走公开的 Win32 API,截图走 PrintWindow 等系统接口。
  • 网络访问仅限下载安装包:没有遥测、没有上传,运行期不与任何服务器通信。
  • 文件写入仅在缓存目录:默认 %LOCALAPPDATA%\VirtualDesktop\cache,可用 VIRTUALDESKTOP_CACHE_DIR 更改。

已知限制

  • 私有桌面里的窗口无法截图:两种隐藏方式(virtual_screen / offscreen)都支持截图,唯独 私有桌面不行——它的窗口没有被系统合成,没有任何后端能读到像素,agent.screenshot() 会明确报错。
  • 窗口无法在私有桌面之间搬运:Windows 不允许跨桌面移动窗口。
  • 部分应用(UWP、硬件叠加层、DRM 受保护内容)任何离屏方式都抓不到——系统限制。
  • 自绘界面的应用(如微信 3.9.x)没有子窗口,UI Automation 只暴露顶层 Pane,组件粒度只能到 窗口级;控件级定位需要靠截图像素。Electron/浏览器类应用的 UIA 树是完整的。
  • ARM64 自动选用清单里的 ARM64 驱动变体;x64 / x86 共用同一份驱动包。

许可证

MIT,见 LICENSE。

Metadata

Release files for VirtualDesktop 1.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for VirtualDesktop 1.2.1
File Size Uploaded
virtualdesktop-1.2.1.tar.gz 211.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for VirtualDesktop 1.2.1
File Interpreter ABI Platform
virtualdesktop-1.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 369.8 kB

Release files / virtualdesktop-1.2.1.tar.gz

Download URL virtualdesktop-1.2.1.tar.gz
Size 211.3 kB
Tags Source
SHA-256 checksum
How to use checksums
f7127f891ee79544e54fd5c4b44392e6e6e34ac49b56a64668a8bda4831832f9
BLAKE2b-256 checksum
How to use checksums
cbadd72528e554d9eca96c9e4a423da5d55e01dc29e243fc011bdff771662e02
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.23

Release files / virtualdesktop-1.2.1-py3-none-any.whl

Download URL virtualdesktop-1.2.1-py3-none-any.whl
Size 158.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
14b92a23a90d465d7b7fd2e268fd7729ca1d163305deac94f4f1cd43b7efe24f
BLAKE2b-256 checksum
How to use checksums
973a37bd521e10161742b2ef4c8eec67dd89e16989b61b5e18dda20f2905fa40
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.23

Release history Release notifications | RSS feed

1.5.0

2 release files

1.4.1

2 release files

1.4.0

2 release files

1.3.1

2 release files

1.3.0

2 release files

This release

1.2.1 This release

2 release files

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page