VirtualDesktop 1.3.1__tar.gz → 1.4.1__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.3.1 → virtualdesktop-1.4.1}/PKG-INFO +106 -13
  2. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/README.md +104 -12
  3. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/__init__.py +135 -135
  4. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/_desktop_runtime.py +88 -0
  5. virtualdesktop-1.4.1/VirtualDesktop/_enumerate.py +97 -0
  6. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/agent.py +45 -0
  7. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/desktop_session.py +230 -20
  8. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/workspace.py +45 -1
  9. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop.egg-info/PKG-INFO +106 -13
  10. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop.egg-info/SOURCES.txt +1 -0
  11. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop.egg-info/requires.txt +1 -0
  12. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/pyproject.toml +69 -64
  13. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_packaging.py +3 -2
  14. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/LICENSE +0 -0
  15. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/__main__.py +0 -0
  16. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/_bundle.py +0 -0
  17. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/_compat.py +0 -0
  18. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/_devices.py +0 -0
  19. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/_download.py +0 -0
  20. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/_helper.py +0 -0
  21. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/_log.py +0 -0
  22. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/_process.py +0 -0
  23. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/_sigcheck.ps1 +0 -0
  24. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/_sigcheck.py +0 -0
  25. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/_winapi.py +0 -0
  26. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/capture.py +0 -0
  27. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/cli.py +0 -0
  28. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/config.py +0 -0
  29. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/desktop.py +0 -0
  30. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/desktop_capture.py +0 -0
  31. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/display_setup.py +0 -0
  32. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/driver.py +0 -0
  33. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/driver_manifest.json +0 -0
  34. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/errors.py +0 -0
  35. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/manifest.py +0 -0
  36. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/session.py +0 -0
  37. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop/window.py +0 -0
  38. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop.egg-info/dependency_links.txt +0 -0
  39. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop.egg-info/entry_points.txt +0 -0
  40. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/VirtualDesktop.egg-info/top_level.txt +0 -0
  41. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/setup.cfg +0 -0
  42. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_accelerators.py +0 -0
  43. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_agent.py +0 -0
  44. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_artifact_regression.py +0 -0
  45. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_capture.py +0 -0
  46. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_cli.py +0 -0
  47. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_compat.py +0 -0
  48. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_config_and_winapi.py +0 -0
  49. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_desktop_isolation.py +0 -0
  50. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_destructive_lifecycle.py +0 -0
  51. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_display_setup.py +0 -0
  52. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_download.py +0 -0
  53. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_driver.py +0 -0
  54. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_hide_modes.py +0 -0
  55. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_integration_windows.py +0 -0
  56. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_manifest.py +0 -0
  57. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_no_dialogs.py +0 -0
  58. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_process.py +0 -0
  59. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_virtual_desktop.py +0 -0
  60. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_workspace.py +0 -0
  61. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_workspace_instances.py +0 -0
  62. {virtualdesktop-1.3.1 → virtualdesktop-1.4.1}/tests/test_workspace_move.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: VirtualDesktop
3
- Version: 1.3.1
3
+ Version: 1.4.1
4
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.
5
5
  Author: VirtualDesktop contributors
6
6
  License: MIT License
@@ -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
@@ -78,20 +79,59 @@ pip install VirtualDesktop # pip
78
79
  uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
79
80
  ```
80
81
 
81
- 运行时依赖只有 **Pillow**。若国内镜像同步滞后导致找不到最新版,加
82
+ 运行时依赖:**Pillow**(帧图像)与 **comtypes**(UI Automation 是 COM API)。不需要 pywin32——
83
+ 库访问 Win32 全部走 ctypes。若国内镜像同步滞后导致找不到最新版,加
82
84
  `--default-index https://pypi.org/simple`(uv)或 `-i https://pypi.org/simple`(pip)。
83
85
 
84
86
  | | 私有桌面 | 虚拟显示器 |
85
87
  |---|---|---|
86
88
  | 一句话 | 另开一个桌面,窗口与你的桌面完全隔离 | 多一块屏幕,窗口仍在你的桌面上、只是画在看不见的地方 |
87
- | 隔离强度 | **强**:独立窗口命名空间,外部进程看不到、碰不到 | **弱**:窗口是普通窗口,任何程序都能找到并操作它 |
89
+ | 底层 | 内核**对象**(`CreateDesktopW`),独立窗口命名空间 + DACL | 显示**设备**(IddCx 驱动),系统多出一块 `\\.\DISPLAYn` |
88
90
  | 需要权限 | **零**:不需要驱动、不弹 UAC | 装一次驱动,弹**一次 UAC** |
89
- | 能截图吗 | **不能**(窗口截图、桌面截图都不行) | **能**(两种都行) |
90
- | 外部自动化库能用吗 | **不能**(要在桌面内跑代理) | **能**(pywinauto / UIA / pyautogui 直接用) |
91
- | 适合 | 多实例并行、强隔离、不要截图 | UI 自动化、截图 OCR、无人值守抓界面 |
91
+ | 系统要求 | Win10 / 11 任意 | Win10 1809 (17763) 起 |
92
+ | 程序窗口截图 | **不能** | **能** |
93
+ | 桌面截图 | **不能** | **能** |
94
+ | 读窗口属性 / 枚举子控件 | 能(桌面内代理) | 能(外部进程直接) |
95
+ | 点击控件 / 送入字符 | 能(窗口消息) | 能(消息 + 真实输入) |
96
+ | 真实鼠标 / 滚轮 / 键盘 | **不能** | **能** |
97
+ | 外部 pywinauto / pyautogui 直接驱动 | **不能** | **能** |
98
+ | 窗口跨桌面搬运 | **不能**(Windows 限制) | 能(在显示器之间移动) |
99
+ | 推荐场景 | 多实例并行、强隔离、不要截图 | UI 自动化、截图 OCR、无人值守抓界面 |
92
100
 
93
- > **只想跑一个窗口、要截图** → 用虚拟显示器侧的 `OffscreenSession`,最省事。
94
- > **要多个实例互不干扰、不需要截图** → 用私有桌面。
101
+ ### 隔离强度分级
102
+
103
+ 两套方案的隔离**不是"强/弱"两档,而是三个层面各不相同**,按你需要防住什么来选:
104
+
105
+ | 隔离层面 | 私有桌面 | 虚拟显示器 | 说明 |
106
+ |---|---|---|---|
107
+ | 窗口**发现**(能否枚举到) | **隔离** | 不隔离 | 私有桌面:`EnumWindows`/`FindWindow` 从外部看不到 |
108
+ | 窗口**输入**(真实鼠标键盘) | **隔离** | 不隔离 | 私有桌面:没有活动指针与键盘焦点 |
109
+ | 窗口**截图**(像素可读性) | **隔离** | 不隔离 | 私有桌面:从不参与系统合成,没有像素 |
110
+ | 窗口**消息**(`SendMessage` 等) | **不隔离** | 不隔离 | 拿到句柄即可投递消息,DACL 保护的是桌面对象而不是窗口消息 |
111
+ | 进程**生命周期** | **隔离** | 不隔离 | 私有桌面关闭时其上进程一并结束;虚拟显示器上的程序由你负责 |
112
+
113
+ > 关键提醒:私有桌面**不等于**"完全无法访问"。它挡的是**发现**、**输入**和**截图**,
114
+ > 挡不住"已知句柄后投递消息"。所以不要把私有桌面当作进程级安全沙箱使用——它没有降低
115
+ > 目标进程的权限,只是把它挪到了另一个窗口命名空间。
116
+
117
+ ### 推荐怎么选
118
+
119
+ ```
120
+ 要截图(窗口截图或桌面截图) → 虚拟显示器
121
+ 要用真实鼠标 / 滚轮 / 键盘,或 pyautogui → 虚拟显示器
122
+ 要用 pywinauto / UIA 从外部直接驱动窗口 → 虚拟显示器
123
+ 只处理一个窗口、要截图,越省事越好 → OffscreenSession(虚拟显示器侧)
124
+ ─────────────────────────────────────────────────────────────
125
+ 要多个实例互不干扰、且不需要截图 → 私有桌面
126
+ 不能弹 UAC、不能装驱动 → 私有桌面
127
+ 要"退出后什么都不留"(进程随桌面一起结束) → 私有桌面
128
+ 需要窗口完全不出现在任何可见位置 → 私有桌面
129
+ ```
130
+
131
+ 两条常用组合:
132
+
133
+ - **只是想安静地跑一个窗口并抓图** → `OffscreenSession` + `virtual_screen`
134
+ - **想并行跑很多互不干扰的实例** → 每个实例一个 `VirtualWorkspace("名字")`
95
135
 
96
136
  ---
97
137
 
@@ -107,7 +147,7 @@ uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
107
147
  | 系统 | Windows 10 / 11(无 IddCx 版本要求) |
108
148
  | 权限 | 当前用户即可,**不提权** |
109
149
  | 驱动 | 不需要 |
110
- | 依赖 | 仅 Pillow |
150
+ | 依赖 | Pillow + comtypes |
111
151
 
112
152
  ## 基本用法
113
153
 
@@ -169,13 +209,14 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
169
209
  | `agent.adopt(pid)` | 接上桌面内已在运行的程序 |
170
210
  | `agent.info(window)` | 标题、类名、矩形、`child_count`、`buttons` |
171
211
  | `agent.text(window)` | 窗口标题 |
212
+ | `agent.uia_tree(window)` | 完整 UIA 树(在桌面内遍历) |
172
213
  | `agent.click(window, text=/index=)` | 点击控件(走窗口消息,见下) |
173
214
  | `agent.keys(window, "...")` | 送入字符(走窗口消息) |
174
215
  | `agent.windows()` | 该桌面上的窗口清单 |
175
216
  | `agent.exec("python 代码")` | 在桌面内跑任意 Python,返回打印内容 |
176
217
 
177
218
  `exec` 是逃生舱:里面的代码能 `import VirtualDesktop`、能用 `list_windows()`、`CaptureEngine`,
178
- 也能用你自己装的任何库。代理本身只依赖 ctypes,不需要额外安装。
219
+ 也能用你自己装的任何库。代理本身只依赖 ctypes 与 comtypes。
179
220
 
180
221
  ### 私有桌面不能截图
181
222
 
@@ -210,6 +251,38 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
210
251
  要点:**能用消息驱动的都能用,需要真实输入设备的都不能用,两种截图都不能用**。需要截图或需要
211
252
  真实鼠标/滚轮的应用,请改用下面的虚拟显示器方案。
212
253
 
254
+ ## 支持的能力与调用方式
255
+
256
+ | 能力 | 是否支持 | 怎么调 |
257
+ |---|---|---|
258
+ | 在一个桌面里跑多个程序 | **支持** | 连续 `agent.launch([...])`,各自拿到不同句柄 |
259
+ | 多个独立桌面同时在线 | **支持** | 多个 `VirtualWorkspace("名字")` 并存,句柄与名字互不相同 |
260
+ | 拿到窗口句柄 | **支持** | `agent.launch([...]).hwnd` / `agent.adopt(pid).hwnd` / `desktop.find_window(pid)` |
261
+ | 读标题 / 类名 / 矩形 / 尺寸 | **支持** | `agent.info(window)` |
262
+ | 枚举子控件与按钮文字 | **支持** | `agent.info(window)["child_count"]` / `["buttons"]` |
263
+ | 完整 UIA 树 | **支持** | `agent.uia_tree(window)` |
264
+ | 点击控件 / 送入字符 | **支持** | `agent.click(window, text=...)` / `agent.keys(window, "...")` |
265
+ | 在桌面内跑任意 Python | **支持** | `agent.exec("...")`(可用 comtypes、pywinauto) |
266
+ | 程序窗口截图 / 桌面截图 | **不支持** | 见上方原因;改用虚拟显示器 |
267
+ | 跨桌面搬运窗口、移到主屏 | **不支持** | `move_to` 一律报 `cannot-cross-desktop`;只能在目标桌面重新 `launch` |
268
+
269
+ `agent.uia_tree()` 在**桌面内**遍历 UIA,这是私有桌面唯一可用的取树方式:
270
+
271
+ ```python
272
+ with VirtualWorkspace("lab") as space, space.agent() as agent:
273
+ window = agent.launch(["charmap.exe"])
274
+ tree = agent.uia_tree(window) # view="RawView" 默认;也可 ControlView / ContentView
275
+ print(tree["count"])
276
+ for node in tree["nodes"][:5]:
277
+ print(node["control"], node["name"], node["enabled"])
278
+ ```
279
+
280
+ > 不要用 `GetRootElement()`:它属于**输入桌面**,在私有桌面里走它拿不到任何目标窗口。
281
+ > 库内部用的是 `ElementFromHandle(hwnd)`。
282
+
283
+ > 在里面用 comtypes / pywinauto 是安全的:窗口枚举已不再绑定调用者线程,所以
284
+ > `from pywinauto import Application` 放在文件顶部也不会影响私有桌面。
285
+
213
286
  ---
214
287
 
215
288
  # 第二部分:虚拟显示器
@@ -225,7 +298,7 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
225
298
  | 系统 | **Windows 10 1809 (build 17763) 或更高**(更低版本没有 IddCx 框架) |
226
299
  | 权限 | 安装/卸载驱动需要管理员,弹**一次 UAC** |
227
300
  | 驱动 | 首次使用时自动下载安装(也可预先装好) |
228
- | 依赖 | 仅 Pillow |
301
+ | 依赖 | Pillow + comtypes |
229
302
 
230
303
  ## 装驱动
231
304
 
@@ -424,6 +497,26 @@ print(current_primary_size()) # (1920, 1080)
424
497
  > 用真实输入时,窗口所在显示器必须是活动桌面的一部分;`pyautogui` 这类库还需要窗口能取得前台
425
498
  > 焦点。若目标窗口拿不到焦点,请改用消息驱动(`agent.click()` / `WM_CHAR`)。
426
499
 
500
+ ## 支持的能力与调用方式
501
+
502
+ | 能力 | 是否支持 | 怎么调 |
503
+ |---|---|---|
504
+ | 在一个工作区里跑多个程序 | **支持** | 连续 `ws.launch([...])`,运行中可随时新增 |
505
+ | 多个工作区同时在线 | **支持** | 多个 `VirtualWorkspace(SHARED)` 并存,各自独立停靠位 |
506
+ | 拿到窗口句柄给外部用 | **支持** | `ws.describe()["windows"]`(真实 HWND,无包装) |
507
+ | 读标题 / 类名 / 矩形 | **支持** | 外部 pywinauto 直接读,或 `ws.windows()` |
508
+ | 枚举子控件 | **支持** | 外部 pywinauto `children(visible_only=False)` |
509
+ | 完整 UIA 树 | **支持** | 外部 UIA,或 `--ui-tree DIR` |
510
+ | 程序窗口截图 | **支持** | `ws.capture(hwnd, save_to=...)` / `CaptureEngine.capture(hwnd)` |
511
+ | 桌面截图 | **支持**(`virtual_screen`) | `ws.capture_desktop()` / `capture_monitor()` / `capture_all_screens()` |
512
+ | 真实鼠标 / 滚轮 / 键盘 | **支持**(`virtual_screen`) | pywinauto `click_input()` / `type_keys()`,或 pyautogui |
513
+ | 在显示器之间移动窗口 | **支持** | `move_to("\\\\.\\DISPLAY2")` / `move_all_to(...)` / `move_to_primary()` |
514
+ | 退出时还原窗口 | **支持** | `with` 退出自动还原;异常退出同样还原 |
515
+ | 退出时卸载驱动 | **支持** | `uninstall_on_exit=True` 或 `close(uninstall=True)` |
516
+ | 桌面截图(`offscreen` 模式) | **不支持** | 窗口在所有显示器之外,拍不到;改用 `virtual_screen` |
517
+ | 真实鼠标 / 滚轮 / 键盘(`offscreen`) | **不支持** | 同上:指针被钳制回屏幕边缘 |
518
+ | 跨私有桌面搬运窗口 | **不支持** | Windows 不允许;`move_to` 会明确报错 |
519
+
427
520
  ---
428
521
 
429
522
  # 通用功能
@@ -464,7 +557,8 @@ VIRTUALDESKTOP_LOG_FILE=<path>
464
557
  | `ElevationDenied` | UAC 被取消或被策略阻止 | 在管理员终端执行一次 `VirtualDesktop install-driver` |
465
558
  | `unsupported-windows-build` | 系统低于 Win10 1809,没有 IddCx | 换系统,或用私有桌面(不需要驱动) |
466
559
  | `capture-needs-shared-workspace` | 想截私有桌面里的窗口 | 私有桌面不支持截图,改用虚拟显示器 |
467
- | `SetThreadDesktop ... error 170` | 当前线程已有窗口,无法再绑定桌面 | 在创建窗口之前 `bind_thread()`,或用 `agent()` |
560
+ | `SetThreadDesktop ... error 170` | 你自己在绑定线程,而该线程已有窗口或已初始化 COM | 直接调用 `bind_thread()` 时才会遇到;库内部的枚举不再绑定调用者线程 |
561
+ | 某程序在私有桌面上找不到窗口 | 控制台类程序(`cmd.exe`、`ping.exe`)的窗口属于 `conhost.exe`,不在自己 pid 下 | 报错会提示;用 `agent.exec()` 在桌面内驱动它,或换 GUI 程序 |
468
562
  | 安装后没出现新的虚拟屏 | 驱动已注册但显示器未启用 | 设备管理器启用;`report.requires_reboot` 为真时重启 |
469
563
  | 驱动下载失败或极慢 | github.com 不可达 | 设 `VIRTUALDESKTOP_GITHUB_MIRRORS`,或预置离线缓存 |
470
564
  | 窗口截图是黑帧 | 应用为硬件加速 / 受保护内容 | 系统限制;换 `bitblt` 试试,或确认窗口有内容 |
@@ -472,7 +566,6 @@ VIRTUALDESKTOP_LOG_FILE=<path>
472
566
  | 桌面截图拍不到工作区窗口 | 用了 `offscreen`,窗口在所有显示器之外 | 改用 `hide_mode="virtual_screen"` |
473
567
  | `capture_desktop()` 报"workspace is not open" | offscreen 模式下没有绑定显示器 | 改用 `capture_all_screens()`,或用 `virtual_screen` |
474
568
  | 真实鼠标点击无效 | 窗口在屏幕之外,或拿不到前台焦点 | 用 `virtual_screen`,或改用消息驱动 |
475
- | 某程序在私有桌面上找不到窗口 | 控制台类(cmd)等窗口不参与常规枚举 | 换程序,或用 `agent.exec()` 在桌面内直接操作 |
476
569
 
477
570
  **离线 / 内网部署**:把安装包按清单里的文件名(`VirtualDesktop/driver_manifest.json` 的
478
571
  `artifact.filename`)放进 `VIRTUALDESKTOP_CACHE_DIR` 即可,不会联网;也可用
@@ -22,20 +22,59 @@ pip install VirtualDesktop # pip
22
22
  uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
23
23
  ```
24
24
 
25
- 运行时依赖只有 **Pillow**。若国内镜像同步滞后导致找不到最新版,加
25
+ 运行时依赖:**Pillow**(帧图像)与 **comtypes**(UI Automation 是 COM API)。不需要 pywin32——
26
+ 库访问 Win32 全部走 ctypes。若国内镜像同步滞后导致找不到最新版,加
26
27
  `--default-index https://pypi.org/simple`(uv)或 `-i https://pypi.org/simple`(pip)。
27
28
 
28
29
  | | 私有桌面 | 虚拟显示器 |
29
30
  |---|---|---|
30
31
  | 一句话 | 另开一个桌面,窗口与你的桌面完全隔离 | 多一块屏幕,窗口仍在你的桌面上、只是画在看不见的地方 |
31
- | 隔离强度 | **强**:独立窗口命名空间,外部进程看不到、碰不到 | **弱**:窗口是普通窗口,任何程序都能找到并操作它 |
32
+ | 底层 | 内核**对象**(`CreateDesktopW`),独立窗口命名空间 + DACL | 显示**设备**(IddCx 驱动),系统多出一块 `\\.\DISPLAYn` |
32
33
  | 需要权限 | **零**:不需要驱动、不弹 UAC | 装一次驱动,弹**一次 UAC** |
33
- | 能截图吗 | **不能**(窗口截图、桌面截图都不行) | **能**(两种都行) |
34
- | 外部自动化库能用吗 | **不能**(要在桌面内跑代理) | **能**(pywinauto / UIA / pyautogui 直接用) |
35
- | 适合 | 多实例并行、强隔离、不要截图 | UI 自动化、截图 OCR、无人值守抓界面 |
34
+ | 系统要求 | Win10 / 11 任意 | Win10 1809 (17763) 起 |
35
+ | 程序窗口截图 | **不能** | **能** |
36
+ | 桌面截图 | **不能** | **能** |
37
+ | 读窗口属性 / 枚举子控件 | 能(桌面内代理) | 能(外部进程直接) |
38
+ | 点击控件 / 送入字符 | 能(窗口消息) | 能(消息 + 真实输入) |
39
+ | 真实鼠标 / 滚轮 / 键盘 | **不能** | **能** |
40
+ | 外部 pywinauto / pyautogui 直接驱动 | **不能** | **能** |
41
+ | 窗口跨桌面搬运 | **不能**(Windows 限制) | 能(在显示器之间移动) |
42
+ | 推荐场景 | 多实例并行、强隔离、不要截图 | UI 自动化、截图 OCR、无人值守抓界面 |
36
43
 
37
- > **只想跑一个窗口、要截图** → 用虚拟显示器侧的 `OffscreenSession`,最省事。
38
- > **要多个实例互不干扰、不需要截图** → 用私有桌面。
44
+ ### 隔离强度分级
45
+
46
+ 两套方案的隔离**不是"强/弱"两档,而是三个层面各不相同**,按你需要防住什么来选:
47
+
48
+ | 隔离层面 | 私有桌面 | 虚拟显示器 | 说明 |
49
+ |---|---|---|---|
50
+ | 窗口**发现**(能否枚举到) | **隔离** | 不隔离 | 私有桌面:`EnumWindows`/`FindWindow` 从外部看不到 |
51
+ | 窗口**输入**(真实鼠标键盘) | **隔离** | 不隔离 | 私有桌面:没有活动指针与键盘焦点 |
52
+ | 窗口**截图**(像素可读性) | **隔离** | 不隔离 | 私有桌面:从不参与系统合成,没有像素 |
53
+ | 窗口**消息**(`SendMessage` 等) | **不隔离** | 不隔离 | 拿到句柄即可投递消息,DACL 保护的是桌面对象而不是窗口消息 |
54
+ | 进程**生命周期** | **隔离** | 不隔离 | 私有桌面关闭时其上进程一并结束;虚拟显示器上的程序由你负责 |
55
+
56
+ > 关键提醒:私有桌面**不等于**"完全无法访问"。它挡的是**发现**、**输入**和**截图**,
57
+ > 挡不住"已知句柄后投递消息"。所以不要把私有桌面当作进程级安全沙箱使用——它没有降低
58
+ > 目标进程的权限,只是把它挪到了另一个窗口命名空间。
59
+
60
+ ### 推荐怎么选
61
+
62
+ ```
63
+ 要截图(窗口截图或桌面截图) → 虚拟显示器
64
+ 要用真实鼠标 / 滚轮 / 键盘,或 pyautogui → 虚拟显示器
65
+ 要用 pywinauto / UIA 从外部直接驱动窗口 → 虚拟显示器
66
+ 只处理一个窗口、要截图,越省事越好 → OffscreenSession(虚拟显示器侧)
67
+ ─────────────────────────────────────────────────────────────
68
+ 要多个实例互不干扰、且不需要截图 → 私有桌面
69
+ 不能弹 UAC、不能装驱动 → 私有桌面
70
+ 要"退出后什么都不留"(进程随桌面一起结束) → 私有桌面
71
+ 需要窗口完全不出现在任何可见位置 → 私有桌面
72
+ ```
73
+
74
+ 两条常用组合:
75
+
76
+ - **只是想安静地跑一个窗口并抓图** → `OffscreenSession` + `virtual_screen`
77
+ - **想并行跑很多互不干扰的实例** → 每个实例一个 `VirtualWorkspace("名字")`
39
78
 
40
79
  ---
41
80
 
@@ -51,7 +90,7 @@ uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
51
90
  | 系统 | Windows 10 / 11(无 IddCx 版本要求) |
52
91
  | 权限 | 当前用户即可,**不提权** |
53
92
  | 驱动 | 不需要 |
54
- | 依赖 | 仅 Pillow |
93
+ | 依赖 | Pillow + comtypes |
55
94
 
56
95
  ## 基本用法
57
96
 
@@ -113,13 +152,14 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
113
152
  | `agent.adopt(pid)` | 接上桌面内已在运行的程序 |
114
153
  | `agent.info(window)` | 标题、类名、矩形、`child_count`、`buttons` |
115
154
  | `agent.text(window)` | 窗口标题 |
155
+ | `agent.uia_tree(window)` | 完整 UIA 树(在桌面内遍历) |
116
156
  | `agent.click(window, text=/index=)` | 点击控件(走窗口消息,见下) |
117
157
  | `agent.keys(window, "...")` | 送入字符(走窗口消息) |
118
158
  | `agent.windows()` | 该桌面上的窗口清单 |
119
159
  | `agent.exec("python 代码")` | 在桌面内跑任意 Python,返回打印内容 |
120
160
 
121
161
  `exec` 是逃生舱:里面的代码能 `import VirtualDesktop`、能用 `list_windows()`、`CaptureEngine`,
122
- 也能用你自己装的任何库。代理本身只依赖 ctypes,不需要额外安装。
162
+ 也能用你自己装的任何库。代理本身只依赖 ctypes 与 comtypes。
123
163
 
124
164
  ### 私有桌面不能截图
125
165
 
@@ -154,6 +194,38 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
154
194
  要点:**能用消息驱动的都能用,需要真实输入设备的都不能用,两种截图都不能用**。需要截图或需要
155
195
  真实鼠标/滚轮的应用,请改用下面的虚拟显示器方案。
156
196
 
197
+ ## 支持的能力与调用方式
198
+
199
+ | 能力 | 是否支持 | 怎么调 |
200
+ |---|---|---|
201
+ | 在一个桌面里跑多个程序 | **支持** | 连续 `agent.launch([...])`,各自拿到不同句柄 |
202
+ | 多个独立桌面同时在线 | **支持** | 多个 `VirtualWorkspace("名字")` 并存,句柄与名字互不相同 |
203
+ | 拿到窗口句柄 | **支持** | `agent.launch([...]).hwnd` / `agent.adopt(pid).hwnd` / `desktop.find_window(pid)` |
204
+ | 读标题 / 类名 / 矩形 / 尺寸 | **支持** | `agent.info(window)` |
205
+ | 枚举子控件与按钮文字 | **支持** | `agent.info(window)["child_count"]` / `["buttons"]` |
206
+ | 完整 UIA 树 | **支持** | `agent.uia_tree(window)` |
207
+ | 点击控件 / 送入字符 | **支持** | `agent.click(window, text=...)` / `agent.keys(window, "...")` |
208
+ | 在桌面内跑任意 Python | **支持** | `agent.exec("...")`(可用 comtypes、pywinauto) |
209
+ | 程序窗口截图 / 桌面截图 | **不支持** | 见上方原因;改用虚拟显示器 |
210
+ | 跨桌面搬运窗口、移到主屏 | **不支持** | `move_to` 一律报 `cannot-cross-desktop`;只能在目标桌面重新 `launch` |
211
+
212
+ `agent.uia_tree()` 在**桌面内**遍历 UIA,这是私有桌面唯一可用的取树方式:
213
+
214
+ ```python
215
+ with VirtualWorkspace("lab") as space, space.agent() as agent:
216
+ window = agent.launch(["charmap.exe"])
217
+ tree = agent.uia_tree(window) # view="RawView" 默认;也可 ControlView / ContentView
218
+ print(tree["count"])
219
+ for node in tree["nodes"][:5]:
220
+ print(node["control"], node["name"], node["enabled"])
221
+ ```
222
+
223
+ > 不要用 `GetRootElement()`:它属于**输入桌面**,在私有桌面里走它拿不到任何目标窗口。
224
+ > 库内部用的是 `ElementFromHandle(hwnd)`。
225
+
226
+ > 在里面用 comtypes / pywinauto 是安全的:窗口枚举已不再绑定调用者线程,所以
227
+ > `from pywinauto import Application` 放在文件顶部也不会影响私有桌面。
228
+
157
229
  ---
158
230
 
159
231
  # 第二部分:虚拟显示器
@@ -169,7 +241,7 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
169
241
  | 系统 | **Windows 10 1809 (build 17763) 或更高**(更低版本没有 IddCx 框架) |
170
242
  | 权限 | 安装/卸载驱动需要管理员,弹**一次 UAC** |
171
243
  | 驱动 | 首次使用时自动下载安装(也可预先装好) |
172
- | 依赖 | 仅 Pillow |
244
+ | 依赖 | Pillow + comtypes |
173
245
 
174
246
  ## 装驱动
175
247
 
@@ -368,6 +440,26 @@ print(current_primary_size()) # (1920, 1080)
368
440
  > 用真实输入时,窗口所在显示器必须是活动桌面的一部分;`pyautogui` 这类库还需要窗口能取得前台
369
441
  > 焦点。若目标窗口拿不到焦点,请改用消息驱动(`agent.click()` / `WM_CHAR`)。
370
442
 
443
+ ## 支持的能力与调用方式
444
+
445
+ | 能力 | 是否支持 | 怎么调 |
446
+ |---|---|---|
447
+ | 在一个工作区里跑多个程序 | **支持** | 连续 `ws.launch([...])`,运行中可随时新增 |
448
+ | 多个工作区同时在线 | **支持** | 多个 `VirtualWorkspace(SHARED)` 并存,各自独立停靠位 |
449
+ | 拿到窗口句柄给外部用 | **支持** | `ws.describe()["windows"]`(真实 HWND,无包装) |
450
+ | 读标题 / 类名 / 矩形 | **支持** | 外部 pywinauto 直接读,或 `ws.windows()` |
451
+ | 枚举子控件 | **支持** | 外部 pywinauto `children(visible_only=False)` |
452
+ | 完整 UIA 树 | **支持** | 外部 UIA,或 `--ui-tree DIR` |
453
+ | 程序窗口截图 | **支持** | `ws.capture(hwnd, save_to=...)` / `CaptureEngine.capture(hwnd)` |
454
+ | 桌面截图 | **支持**(`virtual_screen`) | `ws.capture_desktop()` / `capture_monitor()` / `capture_all_screens()` |
455
+ | 真实鼠标 / 滚轮 / 键盘 | **支持**(`virtual_screen`) | pywinauto `click_input()` / `type_keys()`,或 pyautogui |
456
+ | 在显示器之间移动窗口 | **支持** | `move_to("\\\\.\\DISPLAY2")` / `move_all_to(...)` / `move_to_primary()` |
457
+ | 退出时还原窗口 | **支持** | `with` 退出自动还原;异常退出同样还原 |
458
+ | 退出时卸载驱动 | **支持** | `uninstall_on_exit=True` 或 `close(uninstall=True)` |
459
+ | 桌面截图(`offscreen` 模式) | **不支持** | 窗口在所有显示器之外,拍不到;改用 `virtual_screen` |
460
+ | 真实鼠标 / 滚轮 / 键盘(`offscreen`) | **不支持** | 同上:指针被钳制回屏幕边缘 |
461
+ | 跨私有桌面搬运窗口 | **不支持** | Windows 不允许;`move_to` 会明确报错 |
462
+
371
463
  ---
372
464
 
373
465
  # 通用功能
@@ -408,7 +500,8 @@ VIRTUALDESKTOP_LOG_FILE=<path>
408
500
  | `ElevationDenied` | UAC 被取消或被策略阻止 | 在管理员终端执行一次 `VirtualDesktop install-driver` |
409
501
  | `unsupported-windows-build` | 系统低于 Win10 1809,没有 IddCx | 换系统,或用私有桌面(不需要驱动) |
410
502
  | `capture-needs-shared-workspace` | 想截私有桌面里的窗口 | 私有桌面不支持截图,改用虚拟显示器 |
411
- | `SetThreadDesktop ... error 170` | 当前线程已有窗口,无法再绑定桌面 | 在创建窗口之前 `bind_thread()`,或用 `agent()` |
503
+ | `SetThreadDesktop ... error 170` | 你自己在绑定线程,而该线程已有窗口或已初始化 COM | 直接调用 `bind_thread()` 时才会遇到;库内部的枚举不再绑定调用者线程 |
504
+ | 某程序在私有桌面上找不到窗口 | 控制台类程序(`cmd.exe`、`ping.exe`)的窗口属于 `conhost.exe`,不在自己 pid 下 | 报错会提示;用 `agent.exec()` 在桌面内驱动它,或换 GUI 程序 |
412
505
  | 安装后没出现新的虚拟屏 | 驱动已注册但显示器未启用 | 设备管理器启用;`report.requires_reboot` 为真时重启 |
413
506
  | 驱动下载失败或极慢 | github.com 不可达 | 设 `VIRTUALDESKTOP_GITHUB_MIRRORS`,或预置离线缓存 |
414
507
  | 窗口截图是黑帧 | 应用为硬件加速 / 受保护内容 | 系统限制;换 `bitblt` 试试,或确认窗口有内容 |
@@ -416,7 +509,6 @@ VIRTUALDESKTOP_LOG_FILE=<path>
416
509
  | 桌面截图拍不到工作区窗口 | 用了 `offscreen`,窗口在所有显示器之外 | 改用 `hide_mode="virtual_screen"` |
417
510
  | `capture_desktop()` 报"workspace is not open" | offscreen 模式下没有绑定显示器 | 改用 `capture_all_screens()`,或用 `virtual_screen` |
418
511
  | 真实鼠标点击无效 | 窗口在屏幕之外,或拿不到前台焦点 | 用 `virtual_screen`,或改用消息驱动 |
419
- | 某程序在私有桌面上找不到窗口 | 控制台类(cmd)等窗口不参与常规枚举 | 换程序,或用 `agent.exec()` 在桌面内直接操作 |
420
512
 
421
513
  **离线 / 内网部署**:把安装包按清单里的文件名(`VirtualDesktop/driver_manifest.json` 的
422
514
  `artifact.filename`)放进 `VIRTUALDESKTOP_CACHE_DIR` 即可,不会联网;也可用
@@ -1,135 +1,135 @@
1
- """``VirtualDesktop`` – an offscreen window workstation for Windows.
2
-
3
- The public surface is deliberately small:
4
-
5
- * :class:`~VirtualDesktop.session.OffscreenSession` – mount, capture, restore.
6
- * :func:`~VirtualDesktop.session.probe_environment` – diagnostics without side effects.
7
- * :class:`~VirtualDesktop.driver.DriverInstaller` – explicit driver control.
8
- * :mod:`VirtualDesktop.errors` – every exception, each with a ``code`` and a ``hint``.
9
-
10
- Typical use::
11
-
12
- from VirtualDesktop import OffscreenSession
13
-
14
- with OffscreenSession(target_window_title="WeChat") as session:
15
- session.mount()
16
- frame = session.capture() # PIL image
17
- frame.image.save("wechat.png")
18
-
19
- Nothing is installed until ``mount()`` actually needs a virtual display, and nothing is
20
- moved until a target window has been found, so a failed call leaves the machine as it was.
21
- """
22
-
23
- from __future__ import annotations
24
-
25
- from ._log import enable_debug_logging, get_logger
26
- from ._winapi import (
27
- IS_WINDOWS,
28
- MonitorInfo,
29
- enumerate_monitors,
30
- is_admin,
31
- list_windows,
32
- select_virtual_monitor,
33
- )
34
- from .capture import DEFAULT_METHODS, CaptureEngine, Frame, analyze_image
35
- from .config import DriverConfig, SessionConfig
36
- from .desktop import DesktopBinding, current_desktop_name, ensure_default_desktop
37
- from .driver import DriverInstaller, InstallReport, virtual_display_present
38
- from .errors import (
39
- AmbiguousWindowError,
40
- CaptureError,
41
- ConfigError,
42
- DesktopBindError,
43
- DownloadError,
44
- DriverInstallError,
45
- DriverNotFoundError,
46
- DriverVerificationError,
47
- ElevationDenied,
48
- ElevationRequired,
49
- ExtractionError,
50
- IntegrityError,
51
- MissingDependencyError,
52
- OffscreenMountError,
53
- SessionStateError,
54
- UnsupportedPlatformError,
55
- WindowMountError,
56
- WindowNotFoundError,
57
- )
58
- from .manifest import load_manifest
59
- from .session import OffscreenSession, probe_environment
60
- from .desktop_session import DesktopProcess, VirtualDesktop, current_desktop_name
61
- from .window import WindowMounter, WindowRecord, find_window, window_diagnostics
62
- from .workspace import SHARED, VirtualWorkspace, WorkspaceWindow
63
- from .agent import AgentProcess, AgentSession
64
- from .display_setup import configure_virtual_display, current_primary_dpi, current_primary_size
65
-
66
- __version__ = "1.3.1"
67
-
68
- __all__ = [
69
- "__version__",
70
- # session
71
- "OffscreenSession",
72
- "probe_environment",
73
- # drivers
74
- "DriverInstaller",
75
- "InstallReport",
76
- "virtual_display_present",
77
- "load_manifest",
78
- # windows and displays
79
- "WindowMounter",
80
- "WindowRecord",
81
- "find_window",
82
- "window_diagnostics",
83
- "enumerate_monitors",
84
- "select_virtual_monitor",
85
- "MonitorInfo",
86
- "list_windows",
87
- "ensure_default_desktop",
88
- "current_desktop_name",
89
- "DesktopBinding",
90
- # private virtual desktop (no admin, no driver)
91
- "VirtualDesktop",
92
- "DesktopProcess",
93
- # multi-window virtual-display workspace (externally controllable)
94
- "VirtualWorkspace",
95
- "WorkspaceWindow",
96
- "SHARED",
97
- # in-desktop agent (for isolated workspaces / private desktops)
98
- "AgentSession",
99
- "AgentProcess",
100
- # virtual display geometry
101
- "configure_virtual_display",
102
- "current_primary_size",
103
- "current_primary_dpi",
104
- # capture
105
- "CaptureEngine",
106
- "Frame",
107
- "analyze_image",
108
- "DEFAULT_METHODS",
109
- # configuration
110
- "DriverConfig",
111
- "SessionConfig",
112
- # platform helpers
113
- "IS_WINDOWS",
114
- "is_admin",
115
- "enable_debug_logging",
116
- # errors
117
- "OffscreenMountError",
118
- "UnsupportedPlatformError",
119
- "MissingDependencyError",
120
- "ConfigError",
121
- "IntegrityError",
122
- "DownloadError",
123
- "ExtractionError",
124
- "ElevationRequired",
125
- "ElevationDenied",
126
- "DriverInstallError",
127
- "DriverNotFoundError",
128
- "DriverVerificationError",
129
- "DesktopBindError",
130
- "WindowNotFoundError",
131
- "AmbiguousWindowError",
132
- "WindowMountError",
133
- "CaptureError",
134
- "SessionStateError",
135
- ]
1
+ """``VirtualDesktop`` 鈥?an offscreen window workstation for Windows.
2
+
3
+ The public surface is deliberately small:
4
+
5
+ * :class:`~VirtualDesktop.session.OffscreenSession` 鈥?mount, capture, restore.
6
+ * :func:`~VirtualDesktop.session.probe_environment` 鈥?diagnostics without side effects.
7
+ * :class:`~VirtualDesktop.driver.DriverInstaller` 鈥?explicit driver control.
8
+ * :mod:`VirtualDesktop.errors` 鈥?every exception, each with a ``code`` and a ``hint``.
9
+
10
+ Typical use::
11
+
12
+ from VirtualDesktop import OffscreenSession
13
+
14
+ with OffscreenSession(target_window_title="WeChat") as session:
15
+ session.mount()
16
+ frame = session.capture() # PIL image
17
+ frame.image.save("wechat.png")
18
+
19
+ Nothing is installed until ``mount()`` actually needs a virtual display, and nothing is
20
+ moved until a target window has been found, so a failed call leaves the machine as it was.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from ._log import enable_debug_logging, get_logger
26
+ from ._winapi import (
27
+ IS_WINDOWS,
28
+ MonitorInfo,
29
+ enumerate_monitors,
30
+ is_admin,
31
+ list_windows,
32
+ select_virtual_monitor,
33
+ )
34
+ from .capture import DEFAULT_METHODS, CaptureEngine, Frame, analyze_image
35
+ from .config import DriverConfig, SessionConfig
36
+ from .desktop import DesktopBinding, current_desktop_name, ensure_default_desktop
37
+ from .driver import DriverInstaller, InstallReport, virtual_display_present
38
+ from .errors import (
39
+ AmbiguousWindowError,
40
+ CaptureError,
41
+ ConfigError,
42
+ DesktopBindError,
43
+ DownloadError,
44
+ DriverInstallError,
45
+ DriverNotFoundError,
46
+ DriverVerificationError,
47
+ ElevationDenied,
48
+ ElevationRequired,
49
+ ExtractionError,
50
+ IntegrityError,
51
+ MissingDependencyError,
52
+ OffscreenMountError,
53
+ SessionStateError,
54
+ UnsupportedPlatformError,
55
+ WindowMountError,
56
+ WindowNotFoundError,
57
+ )
58
+ from .manifest import load_manifest
59
+ from .session import OffscreenSession, probe_environment
60
+ from .desktop_session import DesktopProcess, VirtualDesktop, current_desktop_name
61
+ from .window import WindowMounter, WindowRecord, find_window, window_diagnostics
62
+ from .workspace import SHARED, VirtualWorkspace, WorkspaceWindow
63
+ from .agent import AgentProcess, AgentSession
64
+ from .display_setup import configure_virtual_display, current_primary_dpi, current_primary_size
65
+
66
+ __version__ = "1.4.1"
67
+
68
+ __all__ = [
69
+ "__version__",
70
+ # session
71
+ "OffscreenSession",
72
+ "probe_environment",
73
+ # drivers
74
+ "DriverInstaller",
75
+ "InstallReport",
76
+ "virtual_display_present",
77
+ "load_manifest",
78
+ # windows and displays
79
+ "WindowMounter",
80
+ "WindowRecord",
81
+ "find_window",
82
+ "window_diagnostics",
83
+ "enumerate_monitors",
84
+ "select_virtual_monitor",
85
+ "MonitorInfo",
86
+ "list_windows",
87
+ "ensure_default_desktop",
88
+ "current_desktop_name",
89
+ "DesktopBinding",
90
+ # private virtual desktop (no admin, no driver)
91
+ "VirtualDesktop",
92
+ "DesktopProcess",
93
+ # multi-window virtual-display workspace (externally controllable)
94
+ "VirtualWorkspace",
95
+ "WorkspaceWindow",
96
+ "SHARED",
97
+ # in-desktop agent (for isolated workspaces / private desktops)
98
+ "AgentSession",
99
+ "AgentProcess",
100
+ # virtual display geometry
101
+ "configure_virtual_display",
102
+ "current_primary_size",
103
+ "current_primary_dpi",
104
+ # capture
105
+ "CaptureEngine",
106
+ "Frame",
107
+ "analyze_image",
108
+ "DEFAULT_METHODS",
109
+ # configuration
110
+ "DriverConfig",
111
+ "SessionConfig",
112
+ # platform helpers
113
+ "IS_WINDOWS",
114
+ "is_admin",
115
+ "enable_debug_logging",
116
+ # errors
117
+ "OffscreenMountError",
118
+ "UnsupportedPlatformError",
119
+ "MissingDependencyError",
120
+ "ConfigError",
121
+ "IntegrityError",
122
+ "DownloadError",
123
+ "ExtractionError",
124
+ "ElevationRequired",
125
+ "ElevationDenied",
126
+ "DriverInstallError",
127
+ "DriverNotFoundError",
128
+ "DriverVerificationError",
129
+ "DesktopBindError",
130
+ "WindowNotFoundError",
131
+ "AmbiguousWindowError",
132
+ "WindowMountError",
133
+ "CaptureError",
134
+ "SessionStateError",
135
+ ]