VirtualDesktop 1.4.0__tar.gz → 1.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/PKG-INFO +57 -32
  2. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/README.md +54 -30
  3. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/__init__.py +135 -135
  4. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/_desktop_runtime.py +85 -0
  5. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/_winapi.py +25 -0
  6. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/agent.py +109 -17
  7. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/capture.py +6 -1
  8. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/desktop_session.py +9 -2
  9. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/window.py +1240 -889
  10. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/workspace.py +166 -29
  11. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop.egg-info/PKG-INFO +57 -32
  12. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop.egg-info/requires.txt +1 -0
  13. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/pyproject.toml +69 -64
  14. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_agent.py +28 -5
  15. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_packaging.py +3 -2
  16. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_workspace_instances.py +12 -6
  17. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/LICENSE +0 -0
  18. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/__main__.py +0 -0
  19. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/_bundle.py +0 -0
  20. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/_compat.py +0 -0
  21. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/_devices.py +0 -0
  22. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/_download.py +0 -0
  23. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/_enumerate.py +0 -0
  24. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/_helper.py +0 -0
  25. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/_log.py +0 -0
  26. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/_process.py +0 -0
  27. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/_sigcheck.ps1 +0 -0
  28. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/_sigcheck.py +0 -0
  29. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/cli.py +0 -0
  30. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/config.py +0 -0
  31. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/desktop.py +0 -0
  32. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/desktop_capture.py +0 -0
  33. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/display_setup.py +0 -0
  34. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/driver.py +0 -0
  35. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/driver_manifest.json +0 -0
  36. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/errors.py +0 -0
  37. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/manifest.py +0 -0
  38. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop/session.py +0 -0
  39. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop.egg-info/SOURCES.txt +0 -0
  40. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop.egg-info/dependency_links.txt +0 -0
  41. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop.egg-info/entry_points.txt +0 -0
  42. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/VirtualDesktop.egg-info/top_level.txt +0 -0
  43. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/setup.cfg +0 -0
  44. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_accelerators.py +0 -0
  45. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_artifact_regression.py +0 -0
  46. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_capture.py +0 -0
  47. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_cli.py +0 -0
  48. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_compat.py +0 -0
  49. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_config_and_winapi.py +0 -0
  50. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_desktop_isolation.py +0 -0
  51. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_destructive_lifecycle.py +0 -0
  52. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_display_setup.py +0 -0
  53. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_download.py +0 -0
  54. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_driver.py +0 -0
  55. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_hide_modes.py +0 -0
  56. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_integration_windows.py +0 -0
  57. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_manifest.py +0 -0
  58. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_no_dialogs.py +0 -0
  59. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_process.py +0 -0
  60. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_virtual_desktop.py +0 -0
  61. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_workspace.py +0 -0
  62. {virtualdesktop-1.4.0 → virtualdesktop-1.5.0}/tests/test_workspace_move.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: VirtualDesktop
3
- Version: 1.4.0
4
- Summary: Multi-window virtual-display workspaces on Windows: one pip install bootstraps the virtual display driver, hosts programs on a virtual or private desktop, keeps them off the physical screens, and hands out real window handles for automation and PrintWindow capture.
3
+ Version: 1.5.0
4
+ Summary: win10-win11 虚拟桌面
5
5
  Author: VirtualDesktop contributors
6
6
  License: MIT License
7
7
 
@@ -47,6 +47,7 @@ Requires-Python: >=3.9
47
47
  Description-Content-Type: text/markdown
48
48
  License-File: LICENSE
49
49
  Requires-Dist: Pillow>=9.0
50
+ Requires-Dist: comtypes>=1.2.0
50
51
  Provides-Extra: test
51
52
  Requires-Dist: pytest>=7.0; extra == "test"
52
53
  Provides-Extra: build
@@ -56,7 +57,9 @@ Dynamic: license-file
56
57
 
57
58
  # VirtualDesktop
58
59
 
59
- 在 Windows 上把程序的窗口放到**你看不到的地方**去运行,并读取、操作它。窗口不出现在你的屏幕上,
60
+ win10-win11 虚拟桌面
61
+
62
+ 把程序的窗口放到**你看不到的地方**去运行,并读取、操作它。窗口不出现在你的屏幕上,
60
63
  退出时自动还原。
61
64
 
62
65
  ```python
@@ -78,7 +81,8 @@ pip install VirtualDesktop # pip
78
81
  uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
79
82
  ```
80
83
 
81
- 运行时依赖只有 **Pillow**。若国内镜像同步滞后导致找不到最新版,加
84
+ 运行时依赖:**Pillow**(帧图像)与 **comtypes**(UI Automation 是 COM API)。不需要 pywin32——
85
+ 库访问 Win32 全部走 ctypes。若国内镜像同步滞后导致找不到最新版,加
82
86
  `--default-index https://pypi.org/simple`(uv)或 `-i https://pypi.org/simple`(pip)。
83
87
 
84
88
  | | 私有桌面 | 虚拟显示器 |
@@ -87,40 +91,46 @@ uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
87
91
  | 底层 | 内核**对象**(`CreateDesktopW`),独立窗口命名空间 + DACL | 显示**设备**(IddCx 驱动),系统多出一块 `\\.\DISPLAYn` |
88
92
  | 需要权限 | **零**:不需要驱动、不弹 UAC | 装一次驱动,弹**一次 UAC** |
89
93
  | 系统要求 | Win10 / 11 任意 | Win10 1809 (17763) 起 |
90
- | 程序窗口截图 | **不能** | **能** |
91
- | 桌面截图 | **不能** | **能** |
94
+ | **程序窗口截图** | **能**(窗口需已显示) | **能** |
95
+ | **桌面截图** | **不能**(没有屏幕 DC) | **能** |
92
96
  | 读窗口属性 / 枚举子控件 | 能(桌面内代理) | 能(外部进程直接) |
93
97
  | 点击控件 / 送入字符 | 能(窗口消息) | 能(消息 + 真实输入) |
94
98
  | 真实鼠标 / 滚轮 / 键盘 | **不能** | **能** |
95
99
  | 外部 pywinauto / pyautogui 直接驱动 | **不能** | **能** |
96
100
  | 窗口跨桌面搬运 | **不能**(Windows 限制) | 能(在显示器之间移动) |
97
- | 推荐场景 | 多实例并行、强隔离、不要截图 | UI 自动化、截图 OCR、无人值守抓界面 |
101
+ | 推荐场景 | 多实例并行、强隔离、不需要桌面截图 | UI 自动化、截图 OCR、无人值守抓界面 |
98
102
 
99
103
  ### 隔离强度分级
100
104
 
101
- 两套方案的隔离**不是"强/弱"两档,而是三个层面各不相同**,按你需要防住什么来选:
105
+ 两套方案的隔离**不是"强/弱"两档,而是几个层面各不相同**,按你需要防住什么来选:
102
106
 
103
107
  | 隔离层面 | 私有桌面 | 虚拟显示器 | 说明 |
104
108
  |---|---|---|---|
105
109
  | 窗口**发现**(能否枚举到) | **隔离** | 不隔离 | 私有桌面:`EnumWindows`/`FindWindow` 从外部看不到 |
106
110
  | 窗口**输入**(真实鼠标键盘) | **隔离** | 不隔离 | 私有桌面:没有活动指针与键盘焦点 |
107
- | 窗口**截图**(像素可读性) | **隔离** | 不隔离 | 私有桌面:从不参与系统合成,没有像素 |
111
+ | 窗口**像素**(能否截图) | **不隔离** | 不隔离 | `PrintWindow` 让**应用自己绘制**,不需要屏幕,所以私有桌面同样能截 |
108
112
  | 窗口**消息**(`SendMessage` 等) | **不隔离** | 不隔离 | 拿到句柄即可投递消息,DACL 保护的是桌面对象而不是窗口消息 |
109
113
  | 进程**生命周期** | **隔离** | 不隔离 | 私有桌面关闭时其上进程一并结束;虚拟显示器上的程序由你负责 |
110
114
 
111
- > 关键提醒:私有桌面**不等于**"完全无法访问"。它挡的是**发现**、**输入**和**截图**,
112
- > 挡不住"已知句柄后投递消息"。所以不要把私有桌面当作进程级安全沙箱使用——它没有降低
113
- > 目标进程的权限,只是把它挪到了另一个窗口命名空间。
115
+ > 关键提醒:私有桌面**不等于**"完全无法访问"。它挡的是**发现**和**真实输入**,
116
+ > 挡不住"已知句柄后投递消息",也挡不住窗口自己绘制出来的像素。所以不要把私有桌面当作
117
+ > 进程级安全沙箱使用——它没有降低目标进程的权限,只是把它挪到了另一个窗口命名空间。
118
+ >
119
+ > 反过来,**"私有桌面截不了图"是个流传很广的误解**。真正的限制只有两条:
120
+ > ①**桌面截图**(拍整块屏幕)不行,因为那块桌面没有被合成到任何输出上;
121
+ > ②被 `SW_HIDE` 隐藏的窗口拍出来是纯色块,因为它压根不绘制。窗口只要**显示着**
122
+ > (本库默认如此),私有桌面上的窗口截图与共享模式**逐字节一致**。
114
123
 
115
124
  ### 推荐怎么选
116
125
 
117
126
  ```
118
- 要截图(窗口截图或桌面截图) → 虚拟显示器
127
+ 要桌面截图(拍整块屏幕/虚拟屏) → 虚拟显示器
119
128
  要用真实鼠标 / 滚轮 / 键盘,或 pyautogui → 虚拟显示器
120
129
  要用 pywinauto / UIA 从外部直接驱动窗口 → 虚拟显示器
121
130
  只处理一个窗口、要截图,越省事越好 → OffscreenSession(虚拟显示器侧)
131
+ 要窗口截图、但连驱动都不想装 → 私有桌面
122
132
  ─────────────────────────────────────────────────────────────
123
- 要多个实例互不干扰、且不需要截图 → 私有桌面
133
+ 要多个实例互不干扰 → 私有桌面
124
134
  不能弹 UAC、不能装驱动 → 私有桌面
125
135
  要"退出后什么都不留"(进程随桌面一起结束) → 私有桌面
126
136
  需要窗口完全不出现在任何可见位置 → 私有桌面
@@ -129,7 +139,7 @@ uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
129
139
  两条常用组合:
130
140
 
131
141
  - **只是想安静地跑一个窗口并抓图** → `OffscreenSession` + `virtual_screen`
132
- - **想并行跑很多互不干扰的实例** → 每个实例一个 `VirtualWorkspace("名字")`
142
+ - **想并行跑很多互不干扰的实例,同时要窗口截图** → 每个实例一个 `VirtualWorkspace("名字")`
133
143
 
134
144
  ---
135
145
 
@@ -145,7 +155,7 @@ uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
145
155
  | 系统 | Windows 10 / 11(无 IddCx 版本要求) |
146
156
  | 权限 | 当前用户即可,**不提权** |
147
157
  | 驱动 | 不需要 |
148
- | 依赖 | 仅 Pillow |
158
+ | 依赖 | Pillow + comtypes |
149
159
 
150
160
  ## 基本用法
151
161
 
@@ -214,19 +224,29 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
214
224
  | `agent.exec("python 代码")` | 在桌面内跑任意 Python,返回打印内容 |
215
225
 
216
226
  `exec` 是逃生舱:里面的代码能 `import VirtualDesktop`、能用 `list_windows()`、`CaptureEngine`,
217
- 也能用你自己装的任何库。代理本身只依赖 ctypes,不需要额外安装。
227
+ 也能用你自己装的任何库。代理本身只依赖 ctypes 与 comtypes。
218
228
 
219
- ### 私有桌面不能截图
229
+ ### 私有桌面的截图能力
220
230
 
221
- 两种截图都不行,且原因不同:
231
+ **程序窗口能截,桌面不能截**,原因是两条不同的路:
222
232
 
223
233
  | 类型 | 结果 | 原因 |
224
234
  |---|---|---|
225
- | 程序窗口截图 | **不能** | 该桌面的窗口从不被系统合成,没有像素可读 |
226
- | 桌面截图 | **不能** | 拿不到该桌面的屏幕 DC(`BitBlt` 返回拒绝访问) |
235
+ | 程序窗口截图 | **能** | `PrintWindow` 让**窗口自己绘制**到调用方给的 DC,不需要屏幕,也不需要合成 |
236
+ | 桌面截图 | **不能** | 拍整块屏幕需要该桌面的屏幕 DC,拿不到(`BitBlt` 返回拒绝访问) |
237
+
238
+ ```python
239
+ with VirtualWorkspace("lab") as space, space.agent() as agent:
240
+ window = agent.launch(["charmap.exe"])
241
+ frame = agent.screenshot(window, r"C:\绝对路径\window.png") # 路径必须绝对
242
+ print(frame.method, frame.size, frame.analysis.blank)
243
+ ```
244
+
245
+ 路径必须**绝对**:截图由桌面内的进程执行,它的工作目录不是调用者的。省略 `path` 则只返回
246
+ 内存中的帧、不落盘。
227
247
 
228
- `agent.screenshot()` 会**明确报错**(`code="capture-needs-shared-workspace"`)而不是产出空白图。
229
- 要读状态用 `agent.info()` / `agent.text()`;要像素请改用虚拟显示器。
248
+ > 窗口必须是**显示状态**。被 `SW_HIDE` 隐藏的窗口会返回纯色块(`code="capture-blank-window"`),
249
+ > 因为它压根不绘制——本库启动程序时会由放置流程负责在目标位置恢复显示。
230
250
 
231
251
  ## 能力边界
232
252
 
@@ -239,15 +259,15 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
239
259
  | 用外部进程的 pywinauto 连上并读属性 | **能** | 拿得到句柄就能读 |
240
260
  | 点击控件(`BM_CLICK` 等窗口消息) | **能** | 消息直接投递给窗口,不需要指针 |
241
261
  | 送入字符(`WM_CHAR` 消息) | **能** | 同上 |
242
- | **程序窗口截图** | **不能** | 桌面从不被系统合成,窗口没有像素 |
262
+ | **程序窗口截图** | **能** | `PrintWindow` 让窗口自己绘制,不需要屏幕或合成 |
243
263
  | **桌面截图** | **不能** | 拿不到该桌面的屏幕 DC(`BitBlt` 返回拒绝访问) |
244
264
  | 移动真实鼠标来点击 | **不能** | 该桌面不是活动桌面,没有指针 |
245
265
  | 滚轮(真实滚轮事件) | **不能** | 同"移动真实鼠标" |
246
266
  | 发送真实键盘输入(`SendInput`) | **不能** | 键盘只发给活动桌面 |
247
267
  | `pywinauto` 的 `click_input()` / `type_keys()` | **不能** | 依赖真实输入与前台焦点 |
248
268
 
249
- 要点:**能用消息驱动的都能用,需要真实输入设备的都不能用,两种截图都不能用**。需要截图或需要
250
- 真实鼠标/滚轮的应用,请改用下面的虚拟显示器方案。
269
+ 要点:**能用消息驱动的都能用,需要真实输入设备的都不能用;程序窗口截图可以,桌面截图不行**。
270
+ 需要桌面截图或真实鼠标/滚轮的应用,请改用下面的虚拟显示器方案。
251
271
 
252
272
  ## 支持的能力与调用方式
253
273
 
@@ -261,7 +281,8 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
261
281
  | 完整 UIA 树 | **支持** | `agent.uia_tree(window)` |
262
282
  | 点击控件 / 送入字符 | **支持** | `agent.click(window, text=...)` / `agent.keys(window, "...")` |
263
283
  | 在桌面内跑任意 Python | **支持** | `agent.exec("...")`(可用 comtypes、pywinauto) |
264
- | 程序窗口截图 / 桌面截图 | **不支持** | 见上方原因;改用虚拟显示器 |
284
+ | 程序窗口截图 | **支持** | `agent.screenshot(window, "绝对路径")` |
285
+ | 桌面截图 | **不支持** | 拿不到该桌面的屏幕 DC;需要拍屏幕请用虚拟显示器 |
265
286
  | 跨桌面搬运窗口、移到主屏 | **不支持** | `move_to` 一律报 `cannot-cross-desktop`;只能在目标桌面重新 `launch` |
266
287
 
267
288
  `agent.uia_tree()` 在**桌面内**遍历 UIA,这是私有桌面唯一可用的取树方式:
@@ -296,7 +317,7 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
296
317
  | 系统 | **Windows 10 1809 (build 17763) 或更高**(更低版本没有 IddCx 框架) |
297
318
  | 权限 | 安装/卸载驱动需要管理员,弹**一次 UAC** |
298
319
  | 驱动 | 首次使用时自动下载安装(也可预先装好) |
299
- | 依赖 | 仅 Pillow |
320
+ | 依赖 | Pillow + comtypes |
300
321
 
301
322
  ## 装驱动
302
323
 
@@ -554,7 +575,7 @@ VIRTUALDESKTOP_LOG_FILE=<path>
554
575
  | `DriverNotFoundError` | 未装虚拟显示器且不允许安装 | 去掉限制,或先跑 `VirtualDesktop install-driver` |
555
576
  | `ElevationDenied` | UAC 被取消或被策略阻止 | 在管理员终端执行一次 `VirtualDesktop install-driver` |
556
577
  | `unsupported-windows-build` | 系统低于 Win10 1809,没有 IddCx | 换系统,或用私有桌面(不需要驱动) |
557
- | `capture-needs-shared-workspace` | 想截私有桌面里的窗口 | 私有桌面不支持截图,改用虚拟显示器 |
578
+ | `capture-blank-window` | 窗口被 `SW_HIDE` 隐藏,不绘制,只能拍到纯色块 | 让它处于显示状态;本库启动程序时会自动在目标位置恢复显示 |
558
579
  | `SetThreadDesktop ... error 170` | 你自己在绑定线程,而该线程已有窗口或已初始化 COM | 直接调用 `bind_thread()` 时才会遇到;库内部的枚举不再绑定调用者线程 |
559
580
  | 某程序在私有桌面上找不到窗口 | 控制台类程序(`cmd.exe`、`ping.exe`)的窗口属于 `conhost.exe`,不在自己 pid 下 | 报错会提示;用 `agent.exec()` 在桌面内驱动它,或换 GUI 程序 |
560
581
  | 安装后没出现新的虚拟屏 | 驱动已注册但显示器未启用 | 设备管理器启用;`report.requires_reboot` 为真时重启 |
@@ -571,9 +592,13 @@ VIRTUALDESKTOP_LOG_FILE=<path>
571
592
 
572
593
  ## 已知限制
573
594
 
574
- - **私有桌面不能截图**:程序窗口截图和桌面截图都不行。窗口没有被系统合成、没有像素可读;
575
- 而该桌面的屏幕 DC 也拿不到(`BitBlt` 返回拒绝访问)。用 `agent.info()` / `agent.text()` 读状态,
576
- 需要像素请改用虚拟显示器。
595
+ - **私有桌面不能做桌面截图**:拍整块屏幕不行,因为该桌面没有被合成到任何输出上,屏幕 DC 也
596
+ 拿不到。**但程序窗口截图可以**——`PrintWindow` 让窗口自己绘制到调用方给的 DC,不需要屏幕,
597
+ 所以私有桌面上的窗口截图与共享模式逐字节一致。
598
+ - **窗口必须是显示状态才可截图**:被 `SW_HIDE` 隐藏的窗口会返回纯色块(它压根不绘制)。
599
+ 本库启动程序时按此处理,由放置流程负责在正确位置恢复显示。
600
+ - **共享模式下程序启动瞬间可能极短暂可见**:程序自己显示窗口,库随即隐藏并移走它。这个间隙在
601
+ 私有桌面上不存在(窗口创建在另一块桌面上,从不经过你的屏幕);需要完全杜绝请用私有桌面。
577
602
  - **私有桌面不能用真实鼠标 / 滚轮 / 真实键盘**:那里没有活动指针和键盘焦点;消息驱动可用。
578
603
  - **窗口无法在私有桌面之间搬运**:Windows 不允许跨桌面移动窗口。
579
604
  - 部分应用(UWP、硬件叠加层、DRM 受保护内容)任何离屏方式都抓不到——系统限制。
@@ -1,6 +1,8 @@
1
1
  # VirtualDesktop
2
2
 
3
- 在 Windows 上把程序的窗口放到**你看不到的地方**去运行,并读取、操作它。窗口不出现在你的屏幕上,
3
+ win10-win11 虚拟桌面
4
+
5
+ 把程序的窗口放到**你看不到的地方**去运行,并读取、操作它。窗口不出现在你的屏幕上,
4
6
  退出时自动还原。
5
7
 
6
8
  ```python
@@ -22,7 +24,8 @@ pip install VirtualDesktop # pip
22
24
  uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
23
25
  ```
24
26
 
25
- 运行时依赖只有 **Pillow**。若国内镜像同步滞后导致找不到最新版,加
27
+ 运行时依赖:**Pillow**(帧图像)与 **comtypes**(UI Automation 是 COM API)。不需要 pywin32——
28
+ 库访问 Win32 全部走 ctypes。若国内镜像同步滞后导致找不到最新版,加
26
29
  `--default-index https://pypi.org/simple`(uv)或 `-i https://pypi.org/simple`(pip)。
27
30
 
28
31
  | | 私有桌面 | 虚拟显示器 |
@@ -31,40 +34,46 @@ uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
31
34
  | 底层 | 内核**对象**(`CreateDesktopW`),独立窗口命名空间 + DACL | 显示**设备**(IddCx 驱动),系统多出一块 `\\.\DISPLAYn` |
32
35
  | 需要权限 | **零**:不需要驱动、不弹 UAC | 装一次驱动,弹**一次 UAC** |
33
36
  | 系统要求 | Win10 / 11 任意 | Win10 1809 (17763) 起 |
34
- | 程序窗口截图 | **不能** | **能** |
35
- | 桌面截图 | **不能** | **能** |
37
+ | **程序窗口截图** | **能**(窗口需已显示) | **能** |
38
+ | **桌面截图** | **不能**(没有屏幕 DC) | **能** |
36
39
  | 读窗口属性 / 枚举子控件 | 能(桌面内代理) | 能(外部进程直接) |
37
40
  | 点击控件 / 送入字符 | 能(窗口消息) | 能(消息 + 真实输入) |
38
41
  | 真实鼠标 / 滚轮 / 键盘 | **不能** | **能** |
39
42
  | 外部 pywinauto / pyautogui 直接驱动 | **不能** | **能** |
40
43
  | 窗口跨桌面搬运 | **不能**(Windows 限制) | 能(在显示器之间移动) |
41
- | 推荐场景 | 多实例并行、强隔离、不要截图 | UI 自动化、截图 OCR、无人值守抓界面 |
44
+ | 推荐场景 | 多实例并行、强隔离、不需要桌面截图 | UI 自动化、截图 OCR、无人值守抓界面 |
42
45
 
43
46
  ### 隔离强度分级
44
47
 
45
- 两套方案的隔离**不是"强/弱"两档,而是三个层面各不相同**,按你需要防住什么来选:
48
+ 两套方案的隔离**不是"强/弱"两档,而是几个层面各不相同**,按你需要防住什么来选:
46
49
 
47
50
  | 隔离层面 | 私有桌面 | 虚拟显示器 | 说明 |
48
51
  |---|---|---|---|
49
52
  | 窗口**发现**(能否枚举到) | **隔离** | 不隔离 | 私有桌面:`EnumWindows`/`FindWindow` 从外部看不到 |
50
53
  | 窗口**输入**(真实鼠标键盘) | **隔离** | 不隔离 | 私有桌面:没有活动指针与键盘焦点 |
51
- | 窗口**截图**(像素可读性) | **隔离** | 不隔离 | 私有桌面:从不参与系统合成,没有像素 |
54
+ | 窗口**像素**(能否截图) | **不隔离** | 不隔离 | `PrintWindow` 让**应用自己绘制**,不需要屏幕,所以私有桌面同样能截 |
52
55
  | 窗口**消息**(`SendMessage` 等) | **不隔离** | 不隔离 | 拿到句柄即可投递消息,DACL 保护的是桌面对象而不是窗口消息 |
53
56
  | 进程**生命周期** | **隔离** | 不隔离 | 私有桌面关闭时其上进程一并结束;虚拟显示器上的程序由你负责 |
54
57
 
55
- > 关键提醒:私有桌面**不等于**"完全无法访问"。它挡的是**发现**、**输入**和**截图**,
56
- > 挡不住"已知句柄后投递消息"。所以不要把私有桌面当作进程级安全沙箱使用——它没有降低
57
- > 目标进程的权限,只是把它挪到了另一个窗口命名空间。
58
+ > 关键提醒:私有桌面**不等于**"完全无法访问"。它挡的是**发现**和**真实输入**,
59
+ > 挡不住"已知句柄后投递消息",也挡不住窗口自己绘制出来的像素。所以不要把私有桌面当作
60
+ > 进程级安全沙箱使用——它没有降低目标进程的权限,只是把它挪到了另一个窗口命名空间。
61
+ >
62
+ > 反过来,**"私有桌面截不了图"是个流传很广的误解**。真正的限制只有两条:
63
+ > ①**桌面截图**(拍整块屏幕)不行,因为那块桌面没有被合成到任何输出上;
64
+ > ②被 `SW_HIDE` 隐藏的窗口拍出来是纯色块,因为它压根不绘制。窗口只要**显示着**
65
+ > (本库默认如此),私有桌面上的窗口截图与共享模式**逐字节一致**。
58
66
 
59
67
  ### 推荐怎么选
60
68
 
61
69
  ```
62
- 要截图(窗口截图或桌面截图) → 虚拟显示器
70
+ 要桌面截图(拍整块屏幕/虚拟屏) → 虚拟显示器
63
71
  要用真实鼠标 / 滚轮 / 键盘,或 pyautogui → 虚拟显示器
64
72
  要用 pywinauto / UIA 从外部直接驱动窗口 → 虚拟显示器
65
73
  只处理一个窗口、要截图,越省事越好 → OffscreenSession(虚拟显示器侧)
74
+ 要窗口截图、但连驱动都不想装 → 私有桌面
66
75
  ─────────────────────────────────────────────────────────────
67
- 要多个实例互不干扰、且不需要截图 → 私有桌面
76
+ 要多个实例互不干扰 → 私有桌面
68
77
  不能弹 UAC、不能装驱动 → 私有桌面
69
78
  要"退出后什么都不留"(进程随桌面一起结束) → 私有桌面
70
79
  需要窗口完全不出现在任何可见位置 → 私有桌面
@@ -73,7 +82,7 @@ uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
73
82
  两条常用组合:
74
83
 
75
84
  - **只是想安静地跑一个窗口并抓图** → `OffscreenSession` + `virtual_screen`
76
- - **想并行跑很多互不干扰的实例** → 每个实例一个 `VirtualWorkspace("名字")`
85
+ - **想并行跑很多互不干扰的实例,同时要窗口截图** → 每个实例一个 `VirtualWorkspace("名字")`
77
86
 
78
87
  ---
79
88
 
@@ -89,7 +98,7 @@ uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
89
98
  | 系统 | Windows 10 / 11(无 IddCx 版本要求) |
90
99
  | 权限 | 当前用户即可,**不提权** |
91
100
  | 驱动 | 不需要 |
92
- | 依赖 | 仅 Pillow |
101
+ | 依赖 | Pillow + comtypes |
93
102
 
94
103
  ## 基本用法
95
104
 
@@ -158,19 +167,29 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
158
167
  | `agent.exec("python 代码")` | 在桌面内跑任意 Python,返回打印内容 |
159
168
 
160
169
  `exec` 是逃生舱:里面的代码能 `import VirtualDesktop`、能用 `list_windows()`、`CaptureEngine`,
161
- 也能用你自己装的任何库。代理本身只依赖 ctypes,不需要额外安装。
170
+ 也能用你自己装的任何库。代理本身只依赖 ctypes 与 comtypes。
162
171
 
163
- ### 私有桌面不能截图
172
+ ### 私有桌面的截图能力
164
173
 
165
- 两种截图都不行,且原因不同:
174
+ **程序窗口能截,桌面不能截**,原因是两条不同的路:
166
175
 
167
176
  | 类型 | 结果 | 原因 |
168
177
  |---|---|---|
169
- | 程序窗口截图 | **不能** | 该桌面的窗口从不被系统合成,没有像素可读 |
170
- | 桌面截图 | **不能** | 拿不到该桌面的屏幕 DC(`BitBlt` 返回拒绝访问) |
178
+ | 程序窗口截图 | **能** | `PrintWindow` 让**窗口自己绘制**到调用方给的 DC,不需要屏幕,也不需要合成 |
179
+ | 桌面截图 | **不能** | 拍整块屏幕需要该桌面的屏幕 DC,拿不到(`BitBlt` 返回拒绝访问) |
180
+
181
+ ```python
182
+ with VirtualWorkspace("lab") as space, space.agent() as agent:
183
+ window = agent.launch(["charmap.exe"])
184
+ frame = agent.screenshot(window, r"C:\绝对路径\window.png") # 路径必须绝对
185
+ print(frame.method, frame.size, frame.analysis.blank)
186
+ ```
187
+
188
+ 路径必须**绝对**:截图由桌面内的进程执行,它的工作目录不是调用者的。省略 `path` 则只返回
189
+ 内存中的帧、不落盘。
171
190
 
172
- `agent.screenshot()` 会**明确报错**(`code="capture-needs-shared-workspace"`)而不是产出空白图。
173
- 要读状态用 `agent.info()` / `agent.text()`;要像素请改用虚拟显示器。
191
+ > 窗口必须是**显示状态**。被 `SW_HIDE` 隐藏的窗口会返回纯色块(`code="capture-blank-window"`),
192
+ > 因为它压根不绘制——本库启动程序时会由放置流程负责在目标位置恢复显示。
174
193
 
175
194
  ## 能力边界
176
195
 
@@ -183,15 +202,15 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
183
202
  | 用外部进程的 pywinauto 连上并读属性 | **能** | 拿得到句柄就能读 |
184
203
  | 点击控件(`BM_CLICK` 等窗口消息) | **能** | 消息直接投递给窗口,不需要指针 |
185
204
  | 送入字符(`WM_CHAR` 消息) | **能** | 同上 |
186
- | **程序窗口截图** | **不能** | 桌面从不被系统合成,窗口没有像素 |
205
+ | **程序窗口截图** | **能** | `PrintWindow` 让窗口自己绘制,不需要屏幕或合成 |
187
206
  | **桌面截图** | **不能** | 拿不到该桌面的屏幕 DC(`BitBlt` 返回拒绝访问) |
188
207
  | 移动真实鼠标来点击 | **不能** | 该桌面不是活动桌面,没有指针 |
189
208
  | 滚轮(真实滚轮事件) | **不能** | 同"移动真实鼠标" |
190
209
  | 发送真实键盘输入(`SendInput`) | **不能** | 键盘只发给活动桌面 |
191
210
  | `pywinauto` 的 `click_input()` / `type_keys()` | **不能** | 依赖真实输入与前台焦点 |
192
211
 
193
- 要点:**能用消息驱动的都能用,需要真实输入设备的都不能用,两种截图都不能用**。需要截图或需要
194
- 真实鼠标/滚轮的应用,请改用下面的虚拟显示器方案。
212
+ 要点:**能用消息驱动的都能用,需要真实输入设备的都不能用;程序窗口截图可以,桌面截图不行**。
213
+ 需要桌面截图或真实鼠标/滚轮的应用,请改用下面的虚拟显示器方案。
195
214
 
196
215
  ## 支持的能力与调用方式
197
216
 
@@ -205,7 +224,8 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
205
224
  | 完整 UIA 树 | **支持** | `agent.uia_tree(window)` |
206
225
  | 点击控件 / 送入字符 | **支持** | `agent.click(window, text=...)` / `agent.keys(window, "...")` |
207
226
  | 在桌面内跑任意 Python | **支持** | `agent.exec("...")`(可用 comtypes、pywinauto) |
208
- | 程序窗口截图 / 桌面截图 | **不支持** | 见上方原因;改用虚拟显示器 |
227
+ | 程序窗口截图 | **支持** | `agent.screenshot(window, "绝对路径")` |
228
+ | 桌面截图 | **不支持** | 拿不到该桌面的屏幕 DC;需要拍屏幕请用虚拟显示器 |
209
229
  | 跨桌面搬运窗口、移到主屏 | **不支持** | `move_to` 一律报 `cannot-cross-desktop`;只能在目标桌面重新 `launch` |
210
230
 
211
231
  `agent.uia_tree()` 在**桌面内**遍历 UIA,这是私有桌面唯一可用的取树方式:
@@ -240,7 +260,7 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
240
260
  | 系统 | **Windows 10 1809 (build 17763) 或更高**(更低版本没有 IddCx 框架) |
241
261
  | 权限 | 安装/卸载驱动需要管理员,弹**一次 UAC** |
242
262
  | 驱动 | 首次使用时自动下载安装(也可预先装好) |
243
- | 依赖 | 仅 Pillow |
263
+ | 依赖 | Pillow + comtypes |
244
264
 
245
265
  ## 装驱动
246
266
 
@@ -498,7 +518,7 @@ VIRTUALDESKTOP_LOG_FILE=<path>
498
518
  | `DriverNotFoundError` | 未装虚拟显示器且不允许安装 | 去掉限制,或先跑 `VirtualDesktop install-driver` |
499
519
  | `ElevationDenied` | UAC 被取消或被策略阻止 | 在管理员终端执行一次 `VirtualDesktop install-driver` |
500
520
  | `unsupported-windows-build` | 系统低于 Win10 1809,没有 IddCx | 换系统,或用私有桌面(不需要驱动) |
501
- | `capture-needs-shared-workspace` | 想截私有桌面里的窗口 | 私有桌面不支持截图,改用虚拟显示器 |
521
+ | `capture-blank-window` | 窗口被 `SW_HIDE` 隐藏,不绘制,只能拍到纯色块 | 让它处于显示状态;本库启动程序时会自动在目标位置恢复显示 |
502
522
  | `SetThreadDesktop ... error 170` | 你自己在绑定线程,而该线程已有窗口或已初始化 COM | 直接调用 `bind_thread()` 时才会遇到;库内部的枚举不再绑定调用者线程 |
503
523
  | 某程序在私有桌面上找不到窗口 | 控制台类程序(`cmd.exe`、`ping.exe`)的窗口属于 `conhost.exe`,不在自己 pid 下 | 报错会提示;用 `agent.exec()` 在桌面内驱动它,或换 GUI 程序 |
504
524
  | 安装后没出现新的虚拟屏 | 驱动已注册但显示器未启用 | 设备管理器启用;`report.requires_reboot` 为真时重启 |
@@ -515,9 +535,13 @@ VIRTUALDESKTOP_LOG_FILE=<path>
515
535
 
516
536
  ## 已知限制
517
537
 
518
- - **私有桌面不能截图**:程序窗口截图和桌面截图都不行。窗口没有被系统合成、没有像素可读;
519
- 而该桌面的屏幕 DC 也拿不到(`BitBlt` 返回拒绝访问)。用 `agent.info()` / `agent.text()` 读状态,
520
- 需要像素请改用虚拟显示器。
538
+ - **私有桌面不能做桌面截图**:拍整块屏幕不行,因为该桌面没有被合成到任何输出上,屏幕 DC 也
539
+ 拿不到。**但程序窗口截图可以**——`PrintWindow` 让窗口自己绘制到调用方给的 DC,不需要屏幕,
540
+ 所以私有桌面上的窗口截图与共享模式逐字节一致。
541
+ - **窗口必须是显示状态才可截图**:被 `SW_HIDE` 隐藏的窗口会返回纯色块(它压根不绘制)。
542
+ 本库启动程序时按此处理,由放置流程负责在正确位置恢复显示。
543
+ - **共享模式下程序启动瞬间可能极短暂可见**:程序自己显示窗口,库随即隐藏并移走它。这个间隙在
544
+ 私有桌面上不存在(窗口创建在另一块桌面上,从不经过你的屏幕);需要完全杜绝请用私有桌面。
521
545
  - **私有桌面不能用真实鼠标 / 滚轮 / 真实键盘**:那里没有活动指针和键盘焦点;消息驱动可用。
522
546
  - **窗口无法在私有桌面之间搬运**:Windows 不允许跨桌面移动窗口。
523
547
  - 部分应用(UWP、硬件叠加层、DRM 受保护内容)任何离屏方式都抓不到——系统限制。