VirtualDesktop 1.2.0__tar.gz → 1.3.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.
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/PKG-INFO +233 -218
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/README.md +232 -217
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/__init__.py +1 -1
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop.egg-info/PKG-INFO +233 -218
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/pyproject.toml +1 -1
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_workspace_instances.py +8 -2
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/LICENSE +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/__main__.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/_bundle.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/_compat.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/_desktop_runtime.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/_devices.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/_download.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/_helper.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/_log.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/_process.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/_sigcheck.ps1 +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/_sigcheck.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/_winapi.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/agent.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/capture.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/cli.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/config.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/desktop.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/desktop_capture.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/desktop_session.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/display_setup.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/driver.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/driver_manifest.json +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/errors.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/manifest.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/session.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/window.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop/workspace.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop.egg-info/SOURCES.txt +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop.egg-info/dependency_links.txt +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop.egg-info/entry_points.txt +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop.egg-info/requires.txt +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/VirtualDesktop.egg-info/top_level.txt +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/setup.cfg +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_accelerators.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_agent.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_artifact_regression.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_capture.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_cli.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_compat.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_config_and_winapi.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_desktop_isolation.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_destructive_lifecycle.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_display_setup.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_download.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_driver.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_hide_modes.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_integration_windows.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_manifest.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_no_dialogs.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_packaging.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_process.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_virtual_desktop.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/tests/test_workspace.py +0 -0
- {virtualdesktop-1.2.0 → virtualdesktop-1.3.0}/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
|
+
Version: 1.3.0
|
|
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
|
|
@@ -56,221 +56,254 @@ Dynamic: license-file
|
|
|
56
56
|
|
|
57
57
|
# VirtualDesktop
|
|
58
58
|
|
|
59
|
-
在 Windows
|
|
60
|
-
|
|
59
|
+
在 Windows 上把程序的窗口放到**你看不到的地方**去运行,并读取、操作它。窗口不出现在你的屏幕上,
|
|
60
|
+
退出时自动还原。
|
|
61
61
|
|
|
62
62
|
```python
|
|
63
63
|
from VirtualDesktop import VirtualWorkspace
|
|
64
64
|
|
|
65
65
|
with VirtualWorkspace(VirtualWorkspace.SHARED) as space:
|
|
66
|
-
space.launch(["
|
|
67
|
-
print(space.describe()["count"])
|
|
66
|
+
space.launch(["charmap.exe"])
|
|
67
|
+
print(space.describe()["count"])
|
|
68
68
|
# 退出时窗口还原、进程结束
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
# 一、先理解两个概念
|
|
71
|
+
本库提供两套**互不相通**的方案。它们的隔离强度、所需权限、以及"外部代码能对里面的窗口做什么"
|
|
72
|
+
都不一样,请按需要选一套,不要混用。
|
|
77
73
|
|
|
78
|
-
|
|
74
|
+
## 安装
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
```bash
|
|
77
|
+
pip install VirtualDesktop # pip
|
|
78
|
+
uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
|
|
79
|
+
```
|
|
84
80
|
|
|
85
|
-
|
|
81
|
+
运行时依赖只有 **Pillow**。若国内镜像同步滞后导致找不到最新版,加
|
|
82
|
+
`--default-index https://pypi.org/simple`(uv)或 `-i https://pypi.org/simple`(pip)。
|
|
86
83
|
|
|
87
|
-
|
|
|
84
|
+
| | 私有桌面 | 虚拟显示器 |
|
|
88
85
|
|---|---|---|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
86
|
+
| 一句话 | 另开一个桌面,窗口与你的桌面完全隔离 | 多一块屏幕,窗口仍在你的桌面上、只是画在看不见的地方 |
|
|
87
|
+
| 隔离强度 | **强**:独立窗口命名空间,外部进程看不到、碰不到 | **弱**:窗口是普通窗口,任何程序都能找到并操作它 |
|
|
88
|
+
| 需要权限 | **零**:不需要驱动、不弹 UAC | 装一次驱动,弹**一次 UAC** |
|
|
89
|
+
| 能截图吗 | **不能** | **能** |
|
|
90
|
+
| 外部自动化库能用吗 | **不能**(要在桌面内跑代理) | **能**(pywinauto / UIA / pyautogui 直接用) |
|
|
91
|
+
| 适合 | 多实例并行、强隔离、不要截图 | UI 自动化、截图 OCR、无人值守抓界面 |
|
|
94
92
|
|
|
95
|
-
|
|
93
|
+
> **只想跑一个窗口、要截图** → 用虚拟显示器侧的 `OffscreenSession`,最省事。
|
|
94
|
+
> **要多个实例互不干扰、不需要截图** → 用私有桌面。
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
只处理一个窗口、要截图 → OffscreenSession
|
|
99
|
-
要同时跑多个程序、外部要控制 → VirtualWorkspace(SHARED)
|
|
100
|
-
要多实例强隔离、不需要截图 → VirtualWorkspace("名字")
|
|
101
|
-
要自己管理桌面、不需要工作区封装 → VirtualDesktop
|
|
102
|
-
```
|
|
96
|
+
---
|
|
103
97
|
|
|
104
|
-
|
|
105
|
-
> (详见「已知限制」)。
|
|
98
|
+
# 第一部分:私有桌面
|
|
106
99
|
|
|
107
|
-
|
|
100
|
+
用 `CreateDesktopW` 建一个独立桌面。它有自己的窗口命名空间:你的桌面上看不到它的窗口,它上面的
|
|
101
|
+
程序也看不到你的窗口。**不需要任何驱动、不需要管理员权限、不弹 UAC。**
|
|
108
102
|
|
|
109
|
-
|
|
103
|
+
## 环境要求
|
|
110
104
|
|
|
111
|
-
|
|
105
|
+
| 项目 | 要求 |
|
|
106
|
+
|---|---|
|
|
107
|
+
| 系统 | Windows 10 / 11(无 IddCx 版本要求) |
|
|
108
|
+
| 权限 | 当前用户即可,**不提权** |
|
|
109
|
+
| 驱动 | 不需要 |
|
|
110
|
+
| 依赖 | 仅 Pillow |
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
pip install VirtualDesktop
|
|
115
|
-
```
|
|
112
|
+
## 基本用法
|
|
116
113
|
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
```python
|
|
115
|
+
from VirtualDesktop import VirtualDesktop
|
|
119
116
|
|
|
120
|
-
|
|
117
|
+
with VirtualDesktop("my-workspace") as desktop:
|
|
118
|
+
process = desktop.launch(["charmap.exe"], show=False)
|
|
119
|
+
desktop.bind_thread()
|
|
120
|
+
record = desktop.find_window(process.pid)
|
|
121
|
+
print(record.title, record.rect)
|
|
122
|
+
# 退出时桌面关闭,其上进程一并结束
|
|
123
|
+
```
|
|
121
124
|
|
|
122
|
-
|
|
125
|
+
工作区形式,一个桌面装多个窗口:
|
|
123
126
|
|
|
124
127
|
```python
|
|
125
128
|
from VirtualDesktop import VirtualWorkspace
|
|
126
129
|
|
|
127
|
-
#
|
|
128
|
-
with VirtualWorkspace(VirtualWorkspace.SHARED) as space:
|
|
129
|
-
space.launch(["charmap.exe"])
|
|
130
|
-
space.launch(["charmap.exe"]) # 运行中可随时继续新增
|
|
131
|
-
print(space.describe()["count"]) # 2
|
|
132
|
-
print([w.hwnd_hex for w in space.windows()])
|
|
133
|
-
|
|
134
|
-
# 隔离模式:每个工作区一个独立桌面,互不干扰
|
|
130
|
+
# 每个工作区一个独立桌面,互不干扰
|
|
135
131
|
with VirtualWorkspace("lab-a") as a, VirtualWorkspace("lab-b") as b:
|
|
136
132
|
a.launch(["charmap.exe"])
|
|
137
133
|
b.launch(["charmap.exe"])
|
|
138
|
-
print(
|
|
139
|
-
print(
|
|
134
|
+
print(a.describe()["count"]) # 1,只看到自己的
|
|
135
|
+
print(b.describe()["count"]) # 1
|
|
140
136
|
```
|
|
141
137
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
VirtualWorkspace(VirtualWorkspace.SHARED) # 共享:窗口留在交互桌面,外部可控
|
|
145
|
-
# VirtualWorkspace() # TypeError:不猜
|
|
146
|
-
# VirtualWorkspace(None) # ConfigError:并提示该传什么
|
|
147
|
-
```
|
|
138
|
+
名字**不会撞车**:`CreateDesktopW` 对同名桌面会复用同一对象,直接复用会让两个实例共享窗口命名
|
|
139
|
+
空间,所以默认加随机后缀去重。确实要共享同一个桌面时用 `unique=False`。
|
|
148
140
|
|
|
149
|
-
|
|
150
|
-
`CreateDesktopW` 对同名桌面会复用同一对象,直接复用会让两个实例共享窗口命名空间——所以默认去重。
|
|
151
|
-
确实要共享同一个桌面时才用 `VirtualDesktop("name", unique=False)`。
|
|
141
|
+
## 在桌面内操作窗口:`agent()`
|
|
152
142
|
|
|
153
|
-
|
|
143
|
+
私有桌面是隔离边界,外部进程拿不到它的窗口。**所有操作都要在桌面内执行**,`agent()` 提供一个
|
|
144
|
+
`with` 可用的常驻代理:
|
|
154
145
|
|
|
155
146
|
```python
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
147
|
+
from VirtualDesktop import VirtualDesktop
|
|
148
|
+
|
|
149
|
+
with VirtualDesktop("lab").create() as desktop, desktop.agent() as agent:
|
|
150
|
+
window = agent.launch(["charmap.exe"]) # 在里面启动
|
|
151
|
+
info = agent.info(window) # 读标题/类名/矩形/子控件数/按钮
|
|
152
|
+
print(info["title"], info["size"], info["buttons"][:3])
|
|
153
|
+
agent.click(window, text="选择") # 按按钮文字点击
|
|
154
|
+
agent.keys(window, "41") # 送入字符(不需要键盘焦点)
|
|
155
|
+
print(agent.windows())
|
|
159
156
|
```
|
|
160
157
|
|
|
161
|
-
|
|
158
|
+
隔离工作区同样可用:
|
|
162
159
|
|
|
163
160
|
```python
|
|
164
|
-
with VirtualWorkspace(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
src.move_to_primary() # 还给用户:还原到主屏、恢复任务栏
|
|
168
|
-
src.move_to(dst) # 交给另一个工作区
|
|
169
|
-
src.move_to("\\\\.\\DISPLAY2") # 移到指定的扩展屏 / 虚拟屏
|
|
170
|
-
src.move_all_to("primary") # 全部移走
|
|
171
|
-
src.move_to_primary(windows=[w.hwnd]) # 只移指定窗口(也接受标题)
|
|
161
|
+
with VirtualWorkspace("lab") as space, space.agent() as agent:
|
|
162
|
+
window = agent.launch(["charmap.exe"])
|
|
163
|
+
print(agent.info(window))
|
|
172
164
|
```
|
|
173
165
|
|
|
174
|
-
|
|
166
|
+
| 方法 | 作用 |
|
|
167
|
+
|---|---|
|
|
168
|
+
| `agent.launch([...])` | 在桌面内启动程序并等它的窗口 |
|
|
169
|
+
| `agent.adopt(pid)` | 接上桌面内已在运行的程序 |
|
|
170
|
+
| `agent.info(window)` | 标题、类名、矩形、`child_count`、`buttons` |
|
|
171
|
+
| `agent.text(window)` | 窗口标题 |
|
|
172
|
+
| `agent.click(window, text=/index=)` | 点击控件(走窗口消息,见下) |
|
|
173
|
+
| `agent.keys(window, "...")` | 送入字符(走窗口消息) |
|
|
174
|
+
| `agent.windows()` | 该桌面上的窗口清单 |
|
|
175
|
+
| `agent.exec("python 代码")` | 在桌面内跑任意 Python,返回打印内容 |
|
|
175
176
|
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
`exec` 是逃生舱:里面的代码能 `import VirtualDesktop`、能用 `list_windows()`、`CaptureEngine`,
|
|
178
|
+
也能用你自己装的任何库。代理本身只依赖 ctypes,不需要额外安装。
|
|
178
179
|
|
|
179
|
-
|
|
180
|
+
## 能力边界
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
with VirtualWorkspace(VirtualWorkspace.SHARED, uninstall_on_exit=True) as space:
|
|
183
|
-
...
|
|
184
|
-
print(space.uninstall_report) # {"ok": True, "message": "device node removed"}
|
|
185
|
-
```
|
|
182
|
+
私有桌面的窗口**不在交互桌面上**,所以按输入机制分,能做的事如下:
|
|
186
183
|
|
|
187
|
-
|
|
184
|
+
| 想做的事 | 私有桌面 | 原因 |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| 找到窗口、读标题 / 类名 / 矩形 | 能 | 读窗口属性,跨桌面可用 |
|
|
187
|
+
| 枚举子控件、读控件文字 | 能 | 同上 |
|
|
188
|
+
| 点击控件(`BM_CLICK` 等窗口消息) | **能** | 消息直接投递给窗口,不需要指针 |
|
|
189
|
+
| 送入字符(`WM_CHAR` 消息) | **能** | 同上 |
|
|
190
|
+
| **截图** | **不能** | 桌面从不被系统合成,没有像素可读 |
|
|
191
|
+
| 移动真实鼠标来点击 | **不能** | 该桌面不是活动桌面,没有指针 |
|
|
192
|
+
| 滚轮(真实滚轮事件) | **不能** | 同"移动真实鼠标" |
|
|
193
|
+
| 发送真实键盘输入(`SendInput`) | **不能** | 键盘只发给活动桌面 |
|
|
194
|
+
| `pywinauto` 的 `click_input()` / `type_keys()` | **不能** | 依赖真实输入与前台焦点 |
|
|
195
|
+
| 用外部进程的 pywinauto 直接连上它 | **不能** | 跨桌面拿不到窗口 |
|
|
196
|
+
|
|
197
|
+
要点:**能用消息驱动的都能用,需要真实输入设备的都不能用**。需要截图或需要真实鼠标/滚轮的应用,
|
|
198
|
+
请改用下面的虚拟显示器方案。
|
|
188
199
|
|
|
189
|
-
|
|
200
|
+
---
|
|
190
201
|
|
|
191
|
-
|
|
202
|
+
# 第二部分:虚拟显示器
|
|
192
203
|
|
|
193
|
-
|
|
194
|
-
|
|
204
|
+
装一个 IddCx 间接显示驱动,系统就多出一块屏幕(`\\.\DISPLAYn`)。把窗口搬到那块屏上:
|
|
205
|
+
**窗口仍是普通窗口,仍留在你的交互桌面上**,只是画在你看不到的地方。因此外部程序能正常找到并
|
|
206
|
+
操作它,也能正常截图。
|
|
195
207
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
208
|
+
## 环境要求
|
|
209
|
+
|
|
210
|
+
| 项目 | 要求 |
|
|
211
|
+
|---|---|
|
|
212
|
+
| 系统 | **Windows 10 1809 (build 17763) 或更高**(更低版本没有 IddCx 框架) |
|
|
213
|
+
| 权限 | 安装/卸载驱动需要管理员,弹**一次 UAC** |
|
|
214
|
+
| 驱动 | 首次使用时自动下载安装(也可预先装好) |
|
|
215
|
+
| 依赖 | 仅 Pillow |
|
|
216
|
+
|
|
217
|
+
## 装驱动
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
VirtualDesktop install-driver --dry-run # 先看将要执行的特权命令,不安装
|
|
221
|
+
VirtualDesktop install-driver # 安装(弹一次 UAC)
|
|
201
222
|
```
|
|
202
223
|
|
|
203
|
-
|
|
224
|
+
或在代码里让它按需装:`VirtualWorkspace(..., install_driver=True)`(默认)。
|
|
225
|
+
|
|
226
|
+
默认**不**自动卸载:否则下一次使用又要弹一次 UAC。只在"用完即还机器"的场景开启:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
with VirtualWorkspace(VirtualWorkspace.SHARED, uninstall_on_exit=True) as space:
|
|
230
|
+
...
|
|
231
|
+
print(space.uninstall_report) # {"ok": True, "message": "device node removed"}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## 两种隐藏方式
|
|
204
235
|
|
|
205
236
|
| `SessionConfig(hide_mode=...)` | 做法 | 需要驱动 |
|
|
206
237
|
|---|---|---|
|
|
207
|
-
| `virtual_screen`(默认) |
|
|
238
|
+
| `virtual_screen`(默认) | 移到虚拟显示器上 | 是 |
|
|
208
239
|
| `offscreen` | 移出所有显示器的坐标区 | 否 |
|
|
209
|
-
| `auto` | 先试前者,不可用则退到后者 | 否 |
|
|
210
240
|
|
|
211
|
-
`
|
|
241
|
+
> **`offscreen` 不需要驱动,但代价是窗口在所有显示器之外**,真实鼠标到不了那里(指针被钳制回
|
|
242
|
+
> 屏幕边缘),因此真实鼠标点击、滚轮、真实键盘输入都不可用;截图和消息驱动仍然可用。
|
|
243
|
+
> 需要真实输入或 `pyautogui`,请用 `virtual_screen`。
|
|
212
244
|
|
|
213
|
-
##
|
|
245
|
+
## 基本用法
|
|
214
246
|
|
|
215
247
|
```python
|
|
216
|
-
from VirtualDesktop import
|
|
248
|
+
from VirtualDesktop import VirtualWorkspace
|
|
217
249
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
print(
|
|
223
|
-
# 退出时桌面关闭,其上进程一并结束
|
|
250
|
+
# 共享模式:窗口可被外部自动化库直接控制
|
|
251
|
+
with VirtualWorkspace(VirtualWorkspace.SHARED) as space:
|
|
252
|
+
space.launch(["charmap.exe"])
|
|
253
|
+
space.launch(["charmap.exe"]) # 运行中可随时继续新增
|
|
254
|
+
print(space.describe()["count"]) # 2
|
|
224
255
|
```
|
|
225
256
|
|
|
226
|
-
|
|
257
|
+
`desktop` 是**必填参数**,因为它决定窗口能不能被外部进程控制,这不该由库替你猜:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
VirtualWorkspace(VirtualWorkspace.SHARED) # 共享:窗口留在交互桌面,外部可控
|
|
261
|
+
# VirtualWorkspace() # TypeError:不猜
|
|
262
|
+
# VirtualWorkspace(None) # ConfigError:并提示该传什么
|
|
263
|
+
```
|
|
227
264
|
|
|
228
|
-
##
|
|
265
|
+
## 单窗口会话
|
|
229
266
|
|
|
230
|
-
|
|
231
|
-
**`with` 可用的常驻代理**:
|
|
267
|
+
只处理一个窗口时最省事:
|
|
232
268
|
|
|
233
269
|
```python
|
|
234
|
-
from VirtualDesktop import
|
|
270
|
+
from VirtualDesktop import OffscreenSession
|
|
235
271
|
|
|
236
|
-
with
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
agent.click(window, index=0) # 或按下标点击
|
|
242
|
-
print(agent.windows()) # 该桌面上的所有窗口
|
|
243
|
-
print(agent.exec("from VirtualDesktop import list_windows; print(len(list_windows()))"))
|
|
272
|
+
with OffscreenSession(target_window_title="微信") as session:
|
|
273
|
+
session.mount() # 按需装驱动 + 隐藏窗口
|
|
274
|
+
frame = session.capture()
|
|
275
|
+
frame.image.save("wechat.png")
|
|
276
|
+
# 退出时窗口回到原位,任务栏样式恢复
|
|
244
277
|
```
|
|
245
278
|
|
|
246
|
-
|
|
279
|
+
也可用 `window_class=` / `process_name=` / `window_pid=` 定位。`unmount()` 幂等;异常退出同样会还原。
|
|
280
|
+
|
|
281
|
+
## 指定窗口放到哪块屏
|
|
247
282
|
|
|
248
283
|
```python
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
284
|
+
VirtualWorkspace(VirtualWorkspace.SHARED, monitor="virtual") # 第一块虚拟显示器(默认)
|
|
285
|
+
VirtualWorkspace(VirtualWorkspace.SHARED, monitor="\\\\.\\DISPLAY2") # 指定设备
|
|
286
|
+
VirtualWorkspace(VirtualWorkspace.SHARED, monitor="primary") # 主屏(不装驱动也能跑)
|
|
252
287
|
```
|
|
253
288
|
|
|
254
|
-
|
|
255
|
-
|---|---|
|
|
256
|
-
| `agent.launch([...])` | 在桌面内启动程序并等它的窗口 |
|
|
257
|
-
| `agent.adopt(pid)` | 接上桌面内已在运行的程序 |
|
|
258
|
-
| `agent.info(window)` | 标题、类名、矩形、子控件数、按钮文字 |
|
|
259
|
-
| `agent.text(window)` | 窗口标题 |
|
|
260
|
-
| `agent.click(window, text=/index=)` | 点击按钮 |
|
|
261
|
-
| `agent.keys(window, "...")` | 送入字符(不需要键盘焦点) |
|
|
262
|
-
| `agent.windows()` | 该桌面上的窗口清单 |
|
|
263
|
-
| `agent.exec("python 代码")` | 在桌面内跑任意 Python,返回打印内容 |
|
|
289
|
+
## 截图
|
|
264
290
|
|
|
265
|
-
`
|
|
266
|
-
也能用你自己装的任何库(包括 pywinauto)。代理本身**只依赖 ctypes**,不需要额外安装。
|
|
291
|
+
`capture()` 依次尝试多种后端直到拿到**有内容**的帧,默认顺序:
|
|
267
292
|
|
|
268
|
-
|
|
269
|
-
> 截图后端能读到像素。要截图请用 `VirtualWorkspace(SHARED)`。
|
|
293
|
+
`printwindow`(`PW_RENDERFULLCONTENT`)→ `printwindow_plain` → `wm_print` → `bitblt`
|
|
270
294
|
|
|
271
|
-
|
|
295
|
+
每帧都做统计判定:方差接近 0(未绘制/全黑/全白),或单色占比 ≥ 99.9% 且无结构,即判为失败并换
|
|
296
|
+
下一个后端。所以"偶尔拿到黑图"会变成确定性的回退,而不是静默产出坏图。
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
from VirtualDesktop import CaptureEngine
|
|
300
|
+
|
|
301
|
+
engine = CaptureEngine(methods=("bitblt",), client_only=False)
|
|
302
|
+
frame = engine.capture(hwnd, require_content=True)
|
|
303
|
+
print(frame.method, frame.size)
|
|
304
|
+
```
|
|
272
305
|
|
|
273
|
-
|
|
306
|
+
## 用自动化库控制窗口
|
|
274
307
|
|
|
275
308
|
窗口是普通窗口,直接从你的进程用即可:
|
|
276
309
|
|
|
@@ -278,73 +311,30 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
|
|
|
278
311
|
from VirtualDesktop import SessionConfig, VirtualWorkspace
|
|
279
312
|
from pywinauto import Application
|
|
280
313
|
|
|
281
|
-
config = SessionConfig(hide_mode="
|
|
314
|
+
config = SessionConfig(hide_mode="virtual_screen", keep_window_size=True)
|
|
282
315
|
|
|
283
|
-
with VirtualWorkspace(VirtualWorkspace.SHARED, config=config
|
|
316
|
+
with VirtualWorkspace(VirtualWorkspace.SHARED, config=config) as space:
|
|
284
317
|
first = space.launch(["charmap.exe"], timeout=45.0)
|
|
285
|
-
second = space.launch(["charmap.exe"], timeout=45.0)
|
|
286
318
|
|
|
287
319
|
dialog = Application(backend="win32").connect(process=first.pid).top_window()
|
|
288
320
|
print(dialog.window_text(), dialog.rectangle(), len(dialog.children()))
|
|
289
|
-
buttons = [c for c in dialog.children() if c.friendlyclassname == "Button"]
|
|
290
|
-
buttons[0].click_input() # 真实点击
|
|
291
321
|
|
|
292
322
|
space.capture(first.hwnd, save_to="window.png")
|
|
293
323
|
print(space.describe()) # count / externally_controllable ...
|
|
294
324
|
```
|
|
295
325
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
### 私有桌面:只能用代理
|
|
299
|
-
|
|
300
|
-
`agent()` 会话就是为它准备的(见上一节)。下面是**不用 agent 封装**的等价写法,便于理解协议:
|
|
301
|
-
|
|
302
|
-
```python
|
|
303
|
-
# agent.py —— 在桌面内运行
|
|
304
|
-
import argparse, ctypes, ctypes.wintypes as w, json
|
|
305
|
-
from pywinauto.controls.hwndwrapper import HwndWrapper
|
|
306
|
-
|
|
307
|
-
p = argparse.ArgumentParser(); p.add_argument("--out"); p.add_argument("--hwnd"); a = p.parse_args()
|
|
308
|
-
wrapper = HwndWrapper(int(a.hwnd, 16)) # 直接从句柄构造,绕开高层 API 的可见性检查
|
|
309
|
-
rect = wrapper.rectangle()
|
|
310
|
-
children = wrapper.children(visible_only=False) # 私有桌面窗口不带 WS_VISIBLE
|
|
311
|
-
json.dump({"title": wrapper.window_text(), "rect": [rect.left, rect.top, rect.right, rect.bottom],
|
|
312
|
-
"children": len(children)},
|
|
313
|
-
open(a.out, "w", encoding="utf-8"), ensure_ascii=False)
|
|
314
|
-
```
|
|
326
|
+
## 把窗口移到别的屏幕
|
|
315
327
|
|
|
316
328
|
```python
|
|
317
|
-
with VirtualWorkspace(
|
|
318
|
-
|
|
319
|
-
report = space.run_agent("agent.py", ["--out", "report.json", "--hwnd", hex(entry.hwnd)])
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
若要用 pywinauto 之类,这套写法在私有桌面内的可用范围:
|
|
323
|
-
|
|
324
|
-
| 操作 | 是否可用 | 说明 |
|
|
325
|
-
|---|---|---|
|
|
326
|
-
| 构造包装器 / 读标题 / 读矩形 | 可用 | 只是读窗口属性 |
|
|
327
|
-
| `children()` | 可用,需 `visible_only=False` | 私有桌面窗口不带 `WS_VISIBLE` |
|
|
328
|
-
| `is_visible()` | 返回 `False` | 桌面未被显示 |
|
|
329
|
-
| `click()` / `click_input()` | 不可用 | 前者做可见性前置检查,后者需要活动桌面移动光标 |
|
|
330
|
-
| 点击控件 | 发 `BM_CLICK` 消息 | `agent.click()` 内部就是这么做的 |
|
|
331
|
-
|
|
332
|
-
> pyautogui 这类依赖真实鼠标/键盘的库只能用于共享工作区——私有桌面上没有可移动的指针。
|
|
333
|
-
|
|
334
|
-
## 命令行
|
|
329
|
+
with VirtualWorkspace(VirtualWorkspace.SHARED) as src:
|
|
330
|
+
w = src.launch(["charmap.exe"])
|
|
335
331
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
VirtualDesktop install-driver --dry-run # 打印将要执行的特权命令,不安装
|
|
341
|
-
VirtualDesktop install-driver # 安装驱动(弹一次 UAC)
|
|
342
|
-
VirtualDesktop uninstall-driver --purge-files
|
|
332
|
+
src.move_to_primary() # 还给用户:还原到主屏、恢复任务栏
|
|
333
|
+
src.move_to("\\\\.\\DISPLAY2") # 移到指定的扩展屏 / 虚拟屏
|
|
334
|
+
src.move_all_to("primary") # 全部移走
|
|
335
|
+
src.move_to_primary(windows=[w.hwnd]) # 只移指定窗口(也接受标题)
|
|
343
336
|
```
|
|
344
337
|
|
|
345
|
-
`capture` / `mount` 还支持 `--hide-mode`、`--monitor`、`--methods`、`--virtual-size`、
|
|
346
|
-
`--virtual-dpi`、`--ui-tree`、`--desktop-shot`,用 `--help` 查看。
|
|
347
|
-
|
|
348
338
|
## 虚拟显示器大小与 DPI
|
|
349
339
|
|
|
350
340
|
默认与当前主屏一致:
|
|
@@ -360,23 +350,41 @@ configure_virtual_display(size="1920x1080", dpi="125%") # 传 None 表示跟
|
|
|
360
350
|
print(current_primary_size()) # (1920, 1080)
|
|
361
351
|
```
|
|
362
352
|
|
|
363
|
-
##
|
|
353
|
+
## 能力边界
|
|
364
354
|
|
|
365
|
-
|
|
355
|
+
| 想做的事 | 虚拟显示器 | offscreen |
|
|
356
|
+
|---|---|---|
|
|
357
|
+
| 找到窗口、读标题 / 类名 / 矩形 | 能 | 能 |
|
|
358
|
+
| 枚举子控件、读控件文字 | 能 | 能 |
|
|
359
|
+
| 点击控件(窗口消息) | 能 | 能 |
|
|
360
|
+
| 送入字符(窗口消息) | 能 | 能 |
|
|
361
|
+
| **截图** | **能** | **能** |
|
|
362
|
+
| 外部 pywinauto / UIA 直接连上 | 能 | 能 |
|
|
363
|
+
| 移动真实鼠标来点击 | 能 | **不能**(窗口在屏幕之外) |
|
|
364
|
+
| 滚轮 | 能 | **不能** |
|
|
365
|
+
| 真实键盘(`SendInput`) | 能 | **不能** |
|
|
366
|
+
|
|
367
|
+
> 用真实输入时,窗口所在显示器必须是活动桌面的一部分;`pyautogui` 这类库还需要窗口能取得前台
|
|
368
|
+
> 焦点。若目标窗口拿不到焦点,请改用消息驱动(`agent.click()` / `WM_CHAR`)。
|
|
366
369
|
|
|
367
|
-
|
|
370
|
+
---
|
|
368
371
|
|
|
369
|
-
|
|
370
|
-
下一个后端。所以"偶尔拿到黑图"会变成确定性的回退,而不是静默产出坏图。
|
|
372
|
+
# 通用功能
|
|
371
373
|
|
|
372
|
-
|
|
373
|
-
from VirtualDesktop import CaptureEngine
|
|
374
|
+
## 命令行
|
|
374
375
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
376
|
+
```bash
|
|
377
|
+
VirtualDesktop check # 只读体检:系统版本 / 显示器 / 驱动 / 缓存 / 签名
|
|
378
|
+
VirtualDesktop list-windows # 列出可选窗口
|
|
379
|
+
VirtualDesktop capture "微信" --out wx.png --unmount
|
|
380
|
+
VirtualDesktop install-driver --dry-run
|
|
381
|
+
VirtualDesktop install-driver
|
|
382
|
+
VirtualDesktop uninstall-driver --purge-files
|
|
378
383
|
```
|
|
379
384
|
|
|
385
|
+
`capture` / `mount` 还支持 `--hide-mode`、`--monitor`、`--methods`、`--virtual-size`、
|
|
386
|
+
`--virtual-dpi`、`--ui-tree`、`--desktop-shot`,用 `--help` 查看。
|
|
387
|
+
|
|
380
388
|
## 配置项
|
|
381
389
|
|
|
382
390
|
环境变量形式,便于计划任务与 CI:
|
|
@@ -397,20 +405,33 @@ VIRTUALDESKTOP_LOG_FILE=<path>
|
|
|
397
405
|
| `TypeError` 缺 `desktop` | `VirtualWorkspace` 必须显式选择模式 | 传工作区名或 `VirtualWorkspace.SHARED` |
|
|
398
406
|
| `DriverNotFoundError` | 未装虚拟显示器且不允许安装 | 去掉限制,或先跑 `VirtualDesktop install-driver` |
|
|
399
407
|
| `ElevationDenied` | UAC 被取消或被策略阻止 | 在管理员终端执行一次 `VirtualDesktop install-driver` |
|
|
400
|
-
| `unsupported-windows-build` | 系统低于 Win10 1809,没有 IddCx |
|
|
401
|
-
| `capture-needs-shared-workspace` | 想截私有桌面里的窗口 |
|
|
402
|
-
| `
|
|
408
|
+
| `unsupported-windows-build` | 系统低于 Win10 1809,没有 IddCx | 换系统,或用私有桌面(不需要驱动) |
|
|
409
|
+
| `capture-needs-shared-workspace` | 想截私有桌面里的窗口 | 私有桌面不支持截图,改用虚拟显示器 |
|
|
410
|
+
| `SetThreadDesktop ... error 170` | 当前线程已有窗口,无法再绑定桌面 | 在创建窗口之前 `bind_thread()`,或用 `agent()` |
|
|
403
411
|
| 安装后没出现新的虚拟屏 | 驱动已注册但显示器未启用 | 设备管理器启用;`report.requires_reboot` 为真时重启 |
|
|
404
412
|
| 驱动下载失败或极慢 | github.com 不可达 | 设 `VIRTUALDESKTOP_GITHUB_MIRRORS`,或预置离线缓存 |
|
|
405
413
|
| 截图是黑帧 | 应用为硬件加速 / 受保护内容 | 系统限制;换 `bitblt` 试试,或确认窗口有内容 |
|
|
414
|
+
| 真实鼠标点击无效 | 窗口在屏幕之外,或拿不到前台焦点 | 用 `virtual_screen`,或改用消息驱动 |
|
|
415
|
+
| 某程序在私有桌面上找不到窗口 | 控制台类(cmd)等窗口不参与常规枚举 | 换程序,或用 `agent.exec()` 在桌面内直接操作 |
|
|
406
416
|
|
|
407
417
|
**离线 / 内网部署**:把安装包按清单里的文件名(`VirtualDesktop/driver_manifest.json` 的
|
|
408
418
|
`artifact.filename`)放进 `VIRTUALDESKTOP_CACHE_DIR` 即可,不会联网;也可用
|
|
409
419
|
`VIRTUALDESKTOP_MANIFEST` 指向自建镜像的清单(含自算摘要)。
|
|
410
420
|
|
|
421
|
+
## 已知限制
|
|
422
|
+
|
|
423
|
+
- **私有桌面不能截图**:它的窗口没有被系统合成,没有任何后端能读到像素;`agent.screenshot()`
|
|
424
|
+
会明确报错。
|
|
425
|
+
- **私有桌面不能用真实鼠标 / 滚轮 / 真实键盘**:那里没有活动指针和键盘焦点;消息驱动可用。
|
|
426
|
+
- **窗口无法在私有桌面之间搬运**:Windows 不允许跨桌面移动窗口。
|
|
427
|
+
- 部分应用(UWP、硬件叠加层、DRM 受保护内容)任何离屏方式都抓不到——系统限制。
|
|
428
|
+
- 自绘界面的应用(如微信 3.9.x)没有子窗口,UI Automation 只暴露顶层 `Pane`,组件粒度只能到
|
|
429
|
+
窗口级;控件级定位需要靠截图像素。Electron/浏览器类应用的 UIA 树是完整的。
|
|
430
|
+
- ARM64 自动选用清单里的 ARM64 驱动变体;x64 / x86 共用同一份驱动包。
|
|
431
|
+
|
|
411
432
|
---
|
|
412
433
|
|
|
413
|
-
#
|
|
434
|
+
# 安全说明
|
|
414
435
|
|
|
415
436
|
## 驱动来源与校验
|
|
416
437
|
|
|
@@ -443,13 +464,17 @@ set VIRTUALDESKTOP_GITHUB_MIRRORS=https://gh-proxy.com/,https://internal.example
|
|
|
443
464
|
|
|
444
465
|
安装/卸载驱动需要管理员权限,会弹**一次 UAC**,由你确认。除此之外库以当前用户权限运行。
|
|
445
466
|
|
|
446
|
-
|
|
447
|
-
`hide_mode="offscreen"
|
|
467
|
+
**完全不提权**的方式:私有桌面(`VirtualWorkspace("name")` 或 `VirtualDesktop`),以及
|
|
468
|
+
`hide_mode="offscreen"`。
|
|
448
469
|
|
|
449
470
|
## 桌面隔离
|
|
450
471
|
|
|
451
|
-
|
|
452
|
-
|
|
472
|
+
- **私有桌面**:`CreateDesktopW` 建的桌面只属于当前用户会话,默认 DACL 已限定创建者(和管理员)
|
|
473
|
+
访问,其他用户无法打开。库另外会主动检测同名桌面并改用唯一名字,避免两个实例静默共享同一个
|
|
474
|
+
命名空间。
|
|
475
|
+
- **虚拟显示器 / offscreen**:窗口留在交互桌面上,**任何以你的身份运行的程序都能找到并操作它们**。
|
|
476
|
+
这既是它支持外部自动化的原因,也意味着它**不提供**针对同一台电脑上其他程序的隔离。需要隔离
|
|
477
|
+
请用私有桌面。
|
|
453
478
|
|
|
454
479
|
## 运行期行为
|
|
455
480
|
|
|
@@ -460,16 +485,6 @@ set VIRTUALDESKTOP_GITHUB_MIRRORS=https://gh-proxy.com/,https://internal.example
|
|
|
460
485
|
- **文件写入仅在缓存目录**:默认 `%LOCALAPPDATA%\VirtualDesktop\cache`,可用
|
|
461
486
|
`VIRTUALDESKTOP_CACHE_DIR` 更改。
|
|
462
487
|
|
|
463
|
-
## 已知限制
|
|
464
|
-
|
|
465
|
-
- **私有桌面里的窗口无法截图**:没有被系统合成,没有任何后端能读到像素;`agent.screenshot()`
|
|
466
|
-
会明确报错。需要截图就用共享工作区。
|
|
467
|
-
- **窗口无法在私有桌面之间搬运**:Windows 不允许跨桌面移动窗口。
|
|
468
|
-
- 部分应用(UWP、硬件叠加层、DRM 受保护内容)任何离屏方式都抓不到——系统限制。
|
|
469
|
-
- 自绘界面的应用(如微信 3.9.x)没有子窗口,UI Automation 只暴露顶层 `Pane`,组件粒度只能到
|
|
470
|
-
窗口级;控件级定位需要靠截图像素。Electron/浏览器类应用的 UIA 树是完整的。
|
|
471
|
-
- ARM64 自动选用清单里的 ARM64 驱动变体;x64 / x86 共用同一份驱动包。
|
|
472
|
-
|
|
473
488
|
---
|
|
474
489
|
|
|
475
490
|
# 许可证
|