VirtualDesktop 1.0.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.
- virtualdesktop-1.0.1/LICENSE +21 -0
- virtualdesktop-1.0.1/PKG-INFO +307 -0
- virtualdesktop-1.0.1/README.md +251 -0
- virtualdesktop-1.0.1/VirtualDesktop/__init__.py +121 -0
- virtualdesktop-1.0.1/VirtualDesktop/__main__.py +10 -0
- virtualdesktop-1.0.1/VirtualDesktop/_bundle.py +369 -0
- virtualdesktop-1.0.1/VirtualDesktop/_compat.py +576 -0
- virtualdesktop-1.0.1/VirtualDesktop/_devices.py +335 -0
- virtualdesktop-1.0.1/VirtualDesktop/_download.py +462 -0
- virtualdesktop-1.0.1/VirtualDesktop/_helper.py +31 -0
- virtualdesktop-1.0.1/VirtualDesktop/_log.py +85 -0
- virtualdesktop-1.0.1/VirtualDesktop/_process.py +912 -0
- virtualdesktop-1.0.1/VirtualDesktop/_sigcheck.ps1 +32 -0
- virtualdesktop-1.0.1/VirtualDesktop/_sigcheck.py +321 -0
- virtualdesktop-1.0.1/VirtualDesktop/_winapi.py +714 -0
- virtualdesktop-1.0.1/VirtualDesktop/capture.py +671 -0
- virtualdesktop-1.0.1/VirtualDesktop/cli.py +673 -0
- virtualdesktop-1.0.1/VirtualDesktop/config.py +255 -0
- virtualdesktop-1.0.1/VirtualDesktop/desktop.py +231 -0
- virtualdesktop-1.0.1/VirtualDesktop/desktop_capture.py +159 -0
- virtualdesktop-1.0.1/VirtualDesktop/desktop_session.py +533 -0
- virtualdesktop-1.0.1/VirtualDesktop/display_setup.py +790 -0
- virtualdesktop-1.0.1/VirtualDesktop/driver.py +831 -0
- virtualdesktop-1.0.1/VirtualDesktop/driver_manifest.json +118 -0
- virtualdesktop-1.0.1/VirtualDesktop/errors.py +211 -0
- virtualdesktop-1.0.1/VirtualDesktop/manifest.py +310 -0
- virtualdesktop-1.0.1/VirtualDesktop/session.py +514 -0
- virtualdesktop-1.0.1/VirtualDesktop/window.py +869 -0
- virtualdesktop-1.0.1/VirtualDesktop.egg-info/PKG-INFO +307 -0
- virtualdesktop-1.0.1/VirtualDesktop.egg-info/SOURCES.txt +51 -0
- virtualdesktop-1.0.1/VirtualDesktop.egg-info/dependency_links.txt +1 -0
- virtualdesktop-1.0.1/VirtualDesktop.egg-info/entry_points.txt +2 -0
- virtualdesktop-1.0.1/VirtualDesktop.egg-info/requires.txt +8 -0
- virtualdesktop-1.0.1/VirtualDesktop.egg-info/top_level.txt +1 -0
- virtualdesktop-1.0.1/pyproject.toml +64 -0
- virtualdesktop-1.0.1/setup.cfg +4 -0
- virtualdesktop-1.0.1/tests/test_accelerators.py +226 -0
- virtualdesktop-1.0.1/tests/test_artifact_regression.py +232 -0
- virtualdesktop-1.0.1/tests/test_capture.py +255 -0
- virtualdesktop-1.0.1/tests/test_cli.py +203 -0
- virtualdesktop-1.0.1/tests/test_compat.py +317 -0
- virtualdesktop-1.0.1/tests/test_config_and_winapi.py +214 -0
- virtualdesktop-1.0.1/tests/test_destructive_lifecycle.py +82 -0
- virtualdesktop-1.0.1/tests/test_display_setup.py +263 -0
- virtualdesktop-1.0.1/tests/test_download.py +282 -0
- virtualdesktop-1.0.1/tests/test_driver.py +622 -0
- virtualdesktop-1.0.1/tests/test_hide_modes.py +232 -0
- virtualdesktop-1.0.1/tests/test_integration_windows.py +339 -0
- virtualdesktop-1.0.1/tests/test_manifest.py +143 -0
- virtualdesktop-1.0.1/tests/test_no_dialogs.py +163 -0
- virtualdesktop-1.0.1/tests/test_packaging.py +247 -0
- virtualdesktop-1.0.1/tests/test_process.py +264 -0
- virtualdesktop-1.0.1/tests/test_virtual_desktop.py +168 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 offscreen-mount contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: VirtualDesktop
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Automated offscreen window mounting on a virtual display for Windows: driver bootstrap, desktop binding, window hiding and PrintWindow capture behind one context manager.
|
|
5
|
+
Author: VirtualDesktop contributors
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 offscreen-mount contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Documentation, https://github.com/Luofeng-Cloud/VirtualDesktop#readme
|
|
29
|
+
Project-URL: Source, https://github.com/Luofeng-Cloud/VirtualDesktop
|
|
30
|
+
Project-URL: Issues, https://github.com/Luofeng-Cloud/VirtualDesktop/issues
|
|
31
|
+
Keywords: windows,virtual-display,idd,printwindow,automation,screenshot,ocr
|
|
32
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
33
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 10
|
|
37
|
+
Classifier: Operating System :: Microsoft :: Windows :: Windows 11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
44
|
+
Classifier: Topic :: System :: Systems Administration
|
|
45
|
+
Classifier: Typing :: Typed
|
|
46
|
+
Requires-Python: >=3.9
|
|
47
|
+
Description-Content-Type: text/markdown
|
|
48
|
+
License-File: LICENSE
|
|
49
|
+
Requires-Dist: Pillow>=9.0
|
|
50
|
+
Provides-Extra: test
|
|
51
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
52
|
+
Provides-Extra: build
|
|
53
|
+
Requires-Dist: build>=1.0; extra == "build"
|
|
54
|
+
Requires-Dist: twine>=5.0; extra == "build"
|
|
55
|
+
Dynamic: license-file
|
|
56
|
+
|
|
57
|
+
# VirtualDesktop
|
|
58
|
+
|
|
59
|
+
在 Windows 上把窗口挂到**虚拟显示器**上截图:用户看不到、任务栏不留痕、退出自动还原。
|
|
60
|
+
一次 `pip install`,首次调用时自动完成虚拟显示器驱动安装。
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from VirtualDesktop import OffscreenSession
|
|
64
|
+
|
|
65
|
+
with OffscreenSession(target_window_title="微信") as session:
|
|
66
|
+
session.mount() # 按需装驱动 + 隐藏窗口
|
|
67
|
+
frame = session.capture() # PrintWindow,自动回退到可用后端
|
|
68
|
+
frame.image.save("wechat.png") # PIL.Image
|
|
69
|
+
# 退出时窗口回到原位,任务栏样式恢复
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
适用于 UI 自动化、不在前台打扰用户的截图/OCR、需要在无人值守环境里抓取应用界面。
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
# 一、使用说明
|
|
77
|
+
|
|
78
|
+
## 安装
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install VirtualDesktop
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
运行时依赖只有 **Pillow**。需要 **Windows 10 1809 (build 17763) 或更高**;更低版本没有
|
|
85
|
+
IddCx(间接显示驱动框架),无法承载虚拟显示器,`VirtualDesktop check` 会直接说明这一点。
|
|
86
|
+
|
|
87
|
+
## 快速开始
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from VirtualDesktop import OffscreenSession
|
|
91
|
+
|
|
92
|
+
session = OffscreenSession(
|
|
93
|
+
window_class="WeChatLoginWndForPC", # 按类名定位(也可用标题/进程名/PID)
|
|
94
|
+
window_pid=1234,
|
|
95
|
+
)
|
|
96
|
+
session.mount() # 首次会弹一次 UAC 装驱动
|
|
97
|
+
print(session.hide_mode_used) # 实际用了哪种隐藏方式
|
|
98
|
+
frame = session.capture()
|
|
99
|
+
frame.image.save("shot.png")
|
|
100
|
+
session.unmount() # 幂等
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
用 `with` 语句或 `try/finally` 都行:异常时同样会还原窗口。
|
|
104
|
+
|
|
105
|
+
窗口定位支持四种方式,可组合使用:`target_window_title`、`window_class`、`process_name`、
|
|
106
|
+
`window_pid`。不确定时先用 `VirtualDesktop list-windows` 查。
|
|
107
|
+
|
|
108
|
+
## 命令行
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
VirtualDesktop check # 只读体检:系统版本 / 显示器 / 驱动 / 缓存 / 签名
|
|
112
|
+
VirtualDesktop list-windows # 列出可选窗口
|
|
113
|
+
VirtualDesktop capture "微信" --out wx.png --unmount
|
|
114
|
+
VirtualDesktop install-driver --dry-run # 打印将要执行的特权命令,不安装
|
|
115
|
+
VirtualDesktop install-driver # 安装驱动(弹一次 UAC)
|
|
116
|
+
VirtualDesktop uninstall-driver --purge-files
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`capture` 和 `mount` 还支持 `--hide-mode`、`--monitor`、`--methods`、`--virtual-size`、
|
|
120
|
+
`--virtual-dpi`、`--ui-tree`、`--desktop-shot` 等开关,用 `--help` 查看全部。
|
|
121
|
+
|
|
122
|
+
## 两种隐藏方式
|
|
123
|
+
|
|
124
|
+
`SessionConfig.hide_mode` 三选一,默认 `virtual_screen`:
|
|
125
|
+
|
|
126
|
+
| 模式 | 做法 | 需要驱动 | 窗口在哪 |
|
|
127
|
+
|---|---|---|---|
|
|
128
|
+
| `virtual_screen`(默认) | 移到虚拟显示器 | 是 | 虚拟屏上 |
|
|
129
|
+
| `offscreen` | 移出所有显示器的坐标区 | 否 | 任何屏幕之外 |
|
|
130
|
+
| `auto` | 先试前者,不可用则退到后者 | 否 | 视情况 |
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from VirtualDesktop import OffscreenSession, SessionConfig
|
|
134
|
+
|
|
135
|
+
session = OffscreenSession(
|
|
136
|
+
target_window_title="微信",
|
|
137
|
+
config=SessionConfig(hide_mode="offscreen"),
|
|
138
|
+
)
|
|
139
|
+
session.mount(install_driver=False) # 完全不需要驱动
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
两种模式都会去掉任务栏按钮和 Alt+Tab 痕迹,并且都能在隐藏状态下截图;`unmount()` 会把
|
|
143
|
+
几何还原到挂载前的状态。
|
|
144
|
+
|
|
145
|
+
> 用 `offscreen` 时窗口不在任何显示器上,所以桌面截图里**看不到它**——这是设计如此。
|
|
146
|
+
> 窗口自身的截图请用 `session.capture()`。
|
|
147
|
+
|
|
148
|
+
## 虚拟显示器大小与 DPI
|
|
149
|
+
|
|
150
|
+
默认与当前主屏一致,也可以指定:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
VirtualDesktop capture "微信" --virtual-size 1920x1080 --virtual-dpi 125%
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from VirtualDesktop.display_setup import configure_virtual_display
|
|
158
|
+
|
|
159
|
+
result = configure_virtual_display(size="1920x1080", dpi="125%") # 传 None 表示跟随主屏
|
|
160
|
+
print(result.ok, result.effective_size, result.effective_dpi)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## 完全不安装驱动:私有桌面
|
|
164
|
+
|
|
165
|
+
`VirtualDesktop` 创建一个私有桌面,窗口在上面运行,**零权限、零安装**:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
import json, tempfile
|
|
169
|
+
from pathlib import Path
|
|
170
|
+
from VirtualDesktop import VirtualDesktop
|
|
171
|
+
|
|
172
|
+
# 代理脚本在目标桌面内运行,把结果写到 --out 指定的文件
|
|
173
|
+
agent = Path(tempfile.mkdtemp()) / "agent.py"
|
|
174
|
+
agent.write_text(
|
|
175
|
+
"import argparse, json\n"
|
|
176
|
+
"from VirtualDesktop import CaptureEngine, list_windows, window_diagnostics\n"
|
|
177
|
+
"p = argparse.ArgumentParser(); p.add_argument('--out'); a = p.parse_args()\n"
|
|
178
|
+
"CaptureEngine().ensure_dpi_aware()\n"
|
|
179
|
+
"data = [window_diagnostics(r.hwnd) for r in list_windows() if r.title]\n"
|
|
180
|
+
"open(a.out, 'w', encoding='utf-8').write(json.dumps(data))\n",
|
|
181
|
+
encoding="utf-8",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
with VirtualDesktop("my-workspace") as desktop:
|
|
185
|
+
process = desktop.launch(["charmap.exe"], show=False)
|
|
186
|
+
out = Path(tempfile.mkdtemp()) / "report.json"
|
|
187
|
+
windows = desktop.run_agent(agent, ["--out", str(out)])
|
|
188
|
+
# 退出时桌面关闭,其上进程一并结束
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
私有桌面是隔离边界:跨桌面的截图和 UI Automation 都不可用,所以枚举、截图、点击必须在目标
|
|
192
|
+
桌面**内的进程**里做。`run_agent` 就是为此准备的——它把你的脚本作为代理进程放进该桌面,再用
|
|
193
|
+
文件把结果传回来。
|
|
194
|
+
|
|
195
|
+
## 截图方式
|
|
196
|
+
|
|
197
|
+
`session.capture()` 会依次尝试多种后端,直到拿到**有内容**的帧。默认顺序:
|
|
198
|
+
|
|
199
|
+
`printwindow`(`PW_RENDERFULLCONTENT`)→ `printwindow_plain` → `wm_print` → `bitblt`
|
|
200
|
+
|
|
201
|
+
每个结果都做统计判定:方差接近 0(未绘制/全黑/全白),或单色占比 ≥ 99.9% 且无结构,即判为
|
|
202
|
+
失败并换下一个后端。所以"偶尔拿到黑图"会变成确定性的回退,而不是静默产出坏图。
|
|
203
|
+
|
|
204
|
+
需要指定后端或整窗(含标题栏)时:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from VirtualDesktop import CaptureEngine
|
|
208
|
+
|
|
209
|
+
engine = CaptureEngine(methods=("bitblt",), client_only=False)
|
|
210
|
+
frame = engine.capture(hwnd, require_content=True)
|
|
211
|
+
print(frame.method, frame.size) # 实际生效的后端与尺寸
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## 配置项
|
|
215
|
+
|
|
216
|
+
所有开关都可以用环境变量设置,便于计划任务和 CI:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
VIRTUALDESKTOP_DRIVER_POLICY=auto|never|always VIRTUALDESKTOP_MONITOR=auto|virtual|primary|DISPLAY2
|
|
220
|
+
VIRTUALDESKTOP_HIDE_MODE=virtual_screen|offscreen|auto
|
|
221
|
+
VIRTUALDESKTOP_GITHUB_MIRRORS=<url 前缀,逗号分隔> VIRTUALDESKTOP_CACHE_DIR=<path>
|
|
222
|
+
VIRTUALDESKTOP_MANIFEST=<path to manifest.json> VIRTUALDESKTOP_SIGNATURE_POLICY=warn|strict|off
|
|
223
|
+
VIRTUALDESKTOP_CAPTURE_METHODS=printwindow,bitblt VIRTUALDESKTOP_DEBUG=1
|
|
224
|
+
VIRTUALDESKTOP_LOG_FILE=<path>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## 故障排查
|
|
228
|
+
|
|
229
|
+
| 现象 | 原因 | 处理 |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `DriverNotFoundError` | 未装虚拟显示器,且 `install_driver=False` 或 `driver_policy=never` | 去掉限制,或先跑 `VirtualDesktop install-driver` |
|
|
232
|
+
| `ElevationDenied` | UAC 被取消或被策略阻止 | 在管理员终端执行一次 `VirtualDesktop install-driver` |
|
|
233
|
+
| `unsupported-windows-build` | 系统低于 Win10 1809,没有 IddCx | 换系统,或改用 `VirtualDesktop` / `hide_mode="offscreen"` |
|
|
234
|
+
| 安装后没出现新的虚拟屏 | 驱动已注册但显示器未启用 | 设备管理器启用该设备;`report.requires_reboot` 为真时重启 |
|
|
235
|
+
| 驱动下载失败或极慢 | github.com 不可达 | 加 `VIRTUALDESKTOP_GITHUB_MIRRORS`,或预置离线缓存(见下) |
|
|
236
|
+
| 截图是黑帧 | 应用为硬件加速 / 受保护内容 | 属系统限制;换 `bitblt` 试试,或确认窗口确实有内容 |
|
|
237
|
+
|
|
238
|
+
**离线 / 内网部署**:把安装包按清单里的文件名(`VirtualDesktop/driver_manifest.json` 的
|
|
239
|
+
`artifact.filename`)放进 `VIRTUALDESKTOP_CACHE_DIR`,就不会联网下载;也可以把
|
|
240
|
+
`VIRTUALDESKTOP_MANIFEST` 指向自建镜像的清单(含自算摘要)。
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
# 二、安全说明
|
|
245
|
+
|
|
246
|
+
## 驱动来源与校验
|
|
247
|
+
|
|
248
|
+
虚拟显示器驱动使用
|
|
249
|
+
[VirtualDrivers/Virtual-Display-Driver](https://github.com/VirtualDrivers/Virtual-Display-Driver)
|
|
250
|
+
(`MttVDD`,IddCx 间接显示驱动,SignPath Foundation 签名)。
|
|
251
|
+
|
|
252
|
+
驱动与工具二进制**不打进 wheel**,首次使用时按需下载。下载链路有三道校验,任何一道不过就
|
|
253
|
+
中止,不会把未经验证的文件交给系统:
|
|
254
|
+
|
|
255
|
+
1. **SHA-256 摘要**:与随包发布的清单 `driver_manifest.json` 中固定的摘要逐字节比对,决定了
|
|
256
|
+
"拿到的确实是那一个发布文件"。不匹配的文件会被隔离(`cache/quarantine`)而不是使用。
|
|
257
|
+
2. **Authenticode 验签**:比对签名者证书指纹与清单中固定的允许值,防止替换成其他发布者的
|
|
258
|
+
同版本文件。
|
|
259
|
+
3. **系统安装校验**:文件最后交给 Windows 安装,由系统再验一次驱动签名。
|
|
260
|
+
|
|
261
|
+
## 国内下载加速是安全的
|
|
262
|
+
|
|
263
|
+
`github.com` 在国内经常不可达,因此清单里为每个安装包列了多个加速镜像,主 URL 失败时按顺序
|
|
264
|
+
重试。**加镜像不会降低安全性**:镜像只影响"从哪里拿字节",摘要校验在拿到之后照常执行,所以
|
|
265
|
+
镜像要么给出完全相同的文件,要么被第 1 道校验拒绝、随后被隔离。它无法让不同的内容通过。
|
|
266
|
+
|
|
267
|
+
镜像返回的文件大小与摘要都会核对(例如驱动包固定为 132118 字节 /
|
|
268
|
+
`e24210692b442b39af763536330ce78b423f19342b7a7792c26de3944e418b3a`)。
|
|
269
|
+
|
|
270
|
+
需要自建或更换镜像时:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# 完整 URL 或 URL 前缀(前缀以 / 或 ? 结尾,会自动拼上原始 GitHub 链接),逗号分隔
|
|
274
|
+
set VIRTUALDESKTOP_GITHUB_MIRRORS=https://gh-proxy.com/,https://internal.example/mirror/
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
同样只接受 `https://`,且一样要过摘要校验。
|
|
278
|
+
|
|
279
|
+
## 权限与提权
|
|
280
|
+
|
|
281
|
+
安装/卸载驱动需要管理员权限,会弹 **一次 UAC**,由你确认。除此之外,库本身以当前用户权限
|
|
282
|
+
运行,不需要管理员。
|
|
283
|
+
|
|
284
|
+
如果希望完全不提权,用私有桌面(`VirtualDesktop`)或 `hide_mode="offscreen"`,
|
|
285
|
+
两者都不需要任何驱动。
|
|
286
|
+
|
|
287
|
+
## 运行期行为
|
|
288
|
+
|
|
289
|
+
- **不改动你的桌面**:窗口只是被移动位置,退出(含异常退出)时还原到原位与原有样式。
|
|
290
|
+
- **不注入目标进程**:不写目标进程内存、不挂钩子;窗口操作走公开的 Win32 窗口 API,
|
|
291
|
+
截图走 `PrintWindow` 等系统接口。
|
|
292
|
+
- **网络访问仅限下载安装包**:没有遥测、没有上传,运行期不与任何服务器通信。
|
|
293
|
+
- **文件写入仅在缓存目录**:默认 `%LOCALAPPDATA%\VirtualDesktop\cache`,可用
|
|
294
|
+
`VIRTUALDESKTOP_CACHE_DIR` 更改。
|
|
295
|
+
|
|
296
|
+
## 已知限制
|
|
297
|
+
|
|
298
|
+
- 部分应用(UWP、硬件叠加层、DRM 受保护内容)任何离屏方式都抓不到——系统限制,非本库缺陷。
|
|
299
|
+
- 自绘界面的应用(如微信 3.9.x)没有子窗口,UI Automation 只暴露顶层 `Pane`,组件粒度只能到
|
|
300
|
+
窗口级;控件级定位需要靠截图像素。Electron/浏览器类应用的 UIA 树是完整的。
|
|
301
|
+
- ARM64 自动选用清单里的 ARM64 驱动变体;x64 / x86 共用同一份驱动包。
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
# 许可证
|
|
306
|
+
|
|
307
|
+
MIT,见 [LICENSE](LICENSE)。
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# VirtualDesktop
|
|
2
|
+
|
|
3
|
+
在 Windows 上把窗口挂到**虚拟显示器**上截图:用户看不到、任务栏不留痕、退出自动还原。
|
|
4
|
+
一次 `pip install`,首次调用时自动完成虚拟显示器驱动安装。
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from VirtualDesktop import OffscreenSession
|
|
8
|
+
|
|
9
|
+
with OffscreenSession(target_window_title="微信") as session:
|
|
10
|
+
session.mount() # 按需装驱动 + 隐藏窗口
|
|
11
|
+
frame = session.capture() # PrintWindow,自动回退到可用后端
|
|
12
|
+
frame.image.save("wechat.png") # PIL.Image
|
|
13
|
+
# 退出时窗口回到原位,任务栏样式恢复
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
适用于 UI 自动化、不在前台打扰用户的截图/OCR、需要在无人值守环境里抓取应用界面。
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# 一、使用说明
|
|
21
|
+
|
|
22
|
+
## 安装
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install VirtualDesktop
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
运行时依赖只有 **Pillow**。需要 **Windows 10 1809 (build 17763) 或更高**;更低版本没有
|
|
29
|
+
IddCx(间接显示驱动框架),无法承载虚拟显示器,`VirtualDesktop check` 会直接说明这一点。
|
|
30
|
+
|
|
31
|
+
## 快速开始
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from VirtualDesktop import OffscreenSession
|
|
35
|
+
|
|
36
|
+
session = OffscreenSession(
|
|
37
|
+
window_class="WeChatLoginWndForPC", # 按类名定位(也可用标题/进程名/PID)
|
|
38
|
+
window_pid=1234,
|
|
39
|
+
)
|
|
40
|
+
session.mount() # 首次会弹一次 UAC 装驱动
|
|
41
|
+
print(session.hide_mode_used) # 实际用了哪种隐藏方式
|
|
42
|
+
frame = session.capture()
|
|
43
|
+
frame.image.save("shot.png")
|
|
44
|
+
session.unmount() # 幂等
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
用 `with` 语句或 `try/finally` 都行:异常时同样会还原窗口。
|
|
48
|
+
|
|
49
|
+
窗口定位支持四种方式,可组合使用:`target_window_title`、`window_class`、`process_name`、
|
|
50
|
+
`window_pid`。不确定时先用 `VirtualDesktop list-windows` 查。
|
|
51
|
+
|
|
52
|
+
## 命令行
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
VirtualDesktop check # 只读体检:系统版本 / 显示器 / 驱动 / 缓存 / 签名
|
|
56
|
+
VirtualDesktop list-windows # 列出可选窗口
|
|
57
|
+
VirtualDesktop capture "微信" --out wx.png --unmount
|
|
58
|
+
VirtualDesktop install-driver --dry-run # 打印将要执行的特权命令,不安装
|
|
59
|
+
VirtualDesktop install-driver # 安装驱动(弹一次 UAC)
|
|
60
|
+
VirtualDesktop uninstall-driver --purge-files
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`capture` 和 `mount` 还支持 `--hide-mode`、`--monitor`、`--methods`、`--virtual-size`、
|
|
64
|
+
`--virtual-dpi`、`--ui-tree`、`--desktop-shot` 等开关,用 `--help` 查看全部。
|
|
65
|
+
|
|
66
|
+
## 两种隐藏方式
|
|
67
|
+
|
|
68
|
+
`SessionConfig.hide_mode` 三选一,默认 `virtual_screen`:
|
|
69
|
+
|
|
70
|
+
| 模式 | 做法 | 需要驱动 | 窗口在哪 |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| `virtual_screen`(默认) | 移到虚拟显示器 | 是 | 虚拟屏上 |
|
|
73
|
+
| `offscreen` | 移出所有显示器的坐标区 | 否 | 任何屏幕之外 |
|
|
74
|
+
| `auto` | 先试前者,不可用则退到后者 | 否 | 视情况 |
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from VirtualDesktop import OffscreenSession, SessionConfig
|
|
78
|
+
|
|
79
|
+
session = OffscreenSession(
|
|
80
|
+
target_window_title="微信",
|
|
81
|
+
config=SessionConfig(hide_mode="offscreen"),
|
|
82
|
+
)
|
|
83
|
+
session.mount(install_driver=False) # 完全不需要驱动
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
两种模式都会去掉任务栏按钮和 Alt+Tab 痕迹,并且都能在隐藏状态下截图;`unmount()` 会把
|
|
87
|
+
几何还原到挂载前的状态。
|
|
88
|
+
|
|
89
|
+
> 用 `offscreen` 时窗口不在任何显示器上,所以桌面截图里**看不到它**——这是设计如此。
|
|
90
|
+
> 窗口自身的截图请用 `session.capture()`。
|
|
91
|
+
|
|
92
|
+
## 虚拟显示器大小与 DPI
|
|
93
|
+
|
|
94
|
+
默认与当前主屏一致,也可以指定:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
VirtualDesktop capture "微信" --virtual-size 1920x1080 --virtual-dpi 125%
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from VirtualDesktop.display_setup import configure_virtual_display
|
|
102
|
+
|
|
103
|
+
result = configure_virtual_display(size="1920x1080", dpi="125%") # 传 None 表示跟随主屏
|
|
104
|
+
print(result.ok, result.effective_size, result.effective_dpi)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 完全不安装驱动:私有桌面
|
|
108
|
+
|
|
109
|
+
`VirtualDesktop` 创建一个私有桌面,窗口在上面运行,**零权限、零安装**:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
import json, tempfile
|
|
113
|
+
from pathlib import Path
|
|
114
|
+
from VirtualDesktop import VirtualDesktop
|
|
115
|
+
|
|
116
|
+
# 代理脚本在目标桌面内运行,把结果写到 --out 指定的文件
|
|
117
|
+
agent = Path(tempfile.mkdtemp()) / "agent.py"
|
|
118
|
+
agent.write_text(
|
|
119
|
+
"import argparse, json\n"
|
|
120
|
+
"from VirtualDesktop import CaptureEngine, list_windows, window_diagnostics\n"
|
|
121
|
+
"p = argparse.ArgumentParser(); p.add_argument('--out'); a = p.parse_args()\n"
|
|
122
|
+
"CaptureEngine().ensure_dpi_aware()\n"
|
|
123
|
+
"data = [window_diagnostics(r.hwnd) for r in list_windows() if r.title]\n"
|
|
124
|
+
"open(a.out, 'w', encoding='utf-8').write(json.dumps(data))\n",
|
|
125
|
+
encoding="utf-8",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
with VirtualDesktop("my-workspace") as desktop:
|
|
129
|
+
process = desktop.launch(["charmap.exe"], show=False)
|
|
130
|
+
out = Path(tempfile.mkdtemp()) / "report.json"
|
|
131
|
+
windows = desktop.run_agent(agent, ["--out", str(out)])
|
|
132
|
+
# 退出时桌面关闭,其上进程一并结束
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
私有桌面是隔离边界:跨桌面的截图和 UI Automation 都不可用,所以枚举、截图、点击必须在目标
|
|
136
|
+
桌面**内的进程**里做。`run_agent` 就是为此准备的——它把你的脚本作为代理进程放进该桌面,再用
|
|
137
|
+
文件把结果传回来。
|
|
138
|
+
|
|
139
|
+
## 截图方式
|
|
140
|
+
|
|
141
|
+
`session.capture()` 会依次尝试多种后端,直到拿到**有内容**的帧。默认顺序:
|
|
142
|
+
|
|
143
|
+
`printwindow`(`PW_RENDERFULLCONTENT`)→ `printwindow_plain` → `wm_print` → `bitblt`
|
|
144
|
+
|
|
145
|
+
每个结果都做统计判定:方差接近 0(未绘制/全黑/全白),或单色占比 ≥ 99.9% 且无结构,即判为
|
|
146
|
+
失败并换下一个后端。所以"偶尔拿到黑图"会变成确定性的回退,而不是静默产出坏图。
|
|
147
|
+
|
|
148
|
+
需要指定后端或整窗(含标题栏)时:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from VirtualDesktop import CaptureEngine
|
|
152
|
+
|
|
153
|
+
engine = CaptureEngine(methods=("bitblt",), client_only=False)
|
|
154
|
+
frame = engine.capture(hwnd, require_content=True)
|
|
155
|
+
print(frame.method, frame.size) # 实际生效的后端与尺寸
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 配置项
|
|
159
|
+
|
|
160
|
+
所有开关都可以用环境变量设置,便于计划任务和 CI:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
VIRTUALDESKTOP_DRIVER_POLICY=auto|never|always VIRTUALDESKTOP_MONITOR=auto|virtual|primary|DISPLAY2
|
|
164
|
+
VIRTUALDESKTOP_HIDE_MODE=virtual_screen|offscreen|auto
|
|
165
|
+
VIRTUALDESKTOP_GITHUB_MIRRORS=<url 前缀,逗号分隔> VIRTUALDESKTOP_CACHE_DIR=<path>
|
|
166
|
+
VIRTUALDESKTOP_MANIFEST=<path to manifest.json> VIRTUALDESKTOP_SIGNATURE_POLICY=warn|strict|off
|
|
167
|
+
VIRTUALDESKTOP_CAPTURE_METHODS=printwindow,bitblt VIRTUALDESKTOP_DEBUG=1
|
|
168
|
+
VIRTUALDESKTOP_LOG_FILE=<path>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 故障排查
|
|
172
|
+
|
|
173
|
+
| 现象 | 原因 | 处理 |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| `DriverNotFoundError` | 未装虚拟显示器,且 `install_driver=False` 或 `driver_policy=never` | 去掉限制,或先跑 `VirtualDesktop install-driver` |
|
|
176
|
+
| `ElevationDenied` | UAC 被取消或被策略阻止 | 在管理员终端执行一次 `VirtualDesktop install-driver` |
|
|
177
|
+
| `unsupported-windows-build` | 系统低于 Win10 1809,没有 IddCx | 换系统,或改用 `VirtualDesktop` / `hide_mode="offscreen"` |
|
|
178
|
+
| 安装后没出现新的虚拟屏 | 驱动已注册但显示器未启用 | 设备管理器启用该设备;`report.requires_reboot` 为真时重启 |
|
|
179
|
+
| 驱动下载失败或极慢 | github.com 不可达 | 加 `VIRTUALDESKTOP_GITHUB_MIRRORS`,或预置离线缓存(见下) |
|
|
180
|
+
| 截图是黑帧 | 应用为硬件加速 / 受保护内容 | 属系统限制;换 `bitblt` 试试,或确认窗口确实有内容 |
|
|
181
|
+
|
|
182
|
+
**离线 / 内网部署**:把安装包按清单里的文件名(`VirtualDesktop/driver_manifest.json` 的
|
|
183
|
+
`artifact.filename`)放进 `VIRTUALDESKTOP_CACHE_DIR`,就不会联网下载;也可以把
|
|
184
|
+
`VIRTUALDESKTOP_MANIFEST` 指向自建镜像的清单(含自算摘要)。
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
# 二、安全说明
|
|
189
|
+
|
|
190
|
+
## 驱动来源与校验
|
|
191
|
+
|
|
192
|
+
虚拟显示器驱动使用
|
|
193
|
+
[VirtualDrivers/Virtual-Display-Driver](https://github.com/VirtualDrivers/Virtual-Display-Driver)
|
|
194
|
+
(`MttVDD`,IddCx 间接显示驱动,SignPath Foundation 签名)。
|
|
195
|
+
|
|
196
|
+
驱动与工具二进制**不打进 wheel**,首次使用时按需下载。下载链路有三道校验,任何一道不过就
|
|
197
|
+
中止,不会把未经验证的文件交给系统:
|
|
198
|
+
|
|
199
|
+
1. **SHA-256 摘要**:与随包发布的清单 `driver_manifest.json` 中固定的摘要逐字节比对,决定了
|
|
200
|
+
"拿到的确实是那一个发布文件"。不匹配的文件会被隔离(`cache/quarantine`)而不是使用。
|
|
201
|
+
2. **Authenticode 验签**:比对签名者证书指纹与清单中固定的允许值,防止替换成其他发布者的
|
|
202
|
+
同版本文件。
|
|
203
|
+
3. **系统安装校验**:文件最后交给 Windows 安装,由系统再验一次驱动签名。
|
|
204
|
+
|
|
205
|
+
## 国内下载加速是安全的
|
|
206
|
+
|
|
207
|
+
`github.com` 在国内经常不可达,因此清单里为每个安装包列了多个加速镜像,主 URL 失败时按顺序
|
|
208
|
+
重试。**加镜像不会降低安全性**:镜像只影响"从哪里拿字节",摘要校验在拿到之后照常执行,所以
|
|
209
|
+
镜像要么给出完全相同的文件,要么被第 1 道校验拒绝、随后被隔离。它无法让不同的内容通过。
|
|
210
|
+
|
|
211
|
+
镜像返回的文件大小与摘要都会核对(例如驱动包固定为 132118 字节 /
|
|
212
|
+
`e24210692b442b39af763536330ce78b423f19342b7a7792c26de3944e418b3a`)。
|
|
213
|
+
|
|
214
|
+
需要自建或更换镜像时:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
# 完整 URL 或 URL 前缀(前缀以 / 或 ? 结尾,会自动拼上原始 GitHub 链接),逗号分隔
|
|
218
|
+
set VIRTUALDESKTOP_GITHUB_MIRRORS=https://gh-proxy.com/,https://internal.example/mirror/
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
同样只接受 `https://`,且一样要过摘要校验。
|
|
222
|
+
|
|
223
|
+
## 权限与提权
|
|
224
|
+
|
|
225
|
+
安装/卸载驱动需要管理员权限,会弹 **一次 UAC**,由你确认。除此之外,库本身以当前用户权限
|
|
226
|
+
运行,不需要管理员。
|
|
227
|
+
|
|
228
|
+
如果希望完全不提权,用私有桌面(`VirtualDesktop`)或 `hide_mode="offscreen"`,
|
|
229
|
+
两者都不需要任何驱动。
|
|
230
|
+
|
|
231
|
+
## 运行期行为
|
|
232
|
+
|
|
233
|
+
- **不改动你的桌面**:窗口只是被移动位置,退出(含异常退出)时还原到原位与原有样式。
|
|
234
|
+
- **不注入目标进程**:不写目标进程内存、不挂钩子;窗口操作走公开的 Win32 窗口 API,
|
|
235
|
+
截图走 `PrintWindow` 等系统接口。
|
|
236
|
+
- **网络访问仅限下载安装包**:没有遥测、没有上传,运行期不与任何服务器通信。
|
|
237
|
+
- **文件写入仅在缓存目录**:默认 `%LOCALAPPDATA%\VirtualDesktop\cache`,可用
|
|
238
|
+
`VIRTUALDESKTOP_CACHE_DIR` 更改。
|
|
239
|
+
|
|
240
|
+
## 已知限制
|
|
241
|
+
|
|
242
|
+
- 部分应用(UWP、硬件叠加层、DRM 受保护内容)任何离屏方式都抓不到——系统限制,非本库缺陷。
|
|
243
|
+
- 自绘界面的应用(如微信 3.9.x)没有子窗口,UI Automation 只暴露顶层 `Pane`,组件粒度只能到
|
|
244
|
+
窗口级;控件级定位需要靠截图像素。Electron/浏览器类应用的 UIA 树是完整的。
|
|
245
|
+
- ARM64 自动选用清单里的 ARM64 驱动变体;x64 / x86 共用同一份驱动包。
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
# 许可证
|
|
250
|
+
|
|
251
|
+
MIT,见 [LICENSE](LICENSE)。
|