VirtualDesktop 1.3.0__tar.gz → 1.3.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.3.0 → virtualdesktop-1.3.1}/PKG-INFO +72 -11
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/README.md +71 -10
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/__init__.py +1 -1
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop.egg-info/PKG-INFO +72 -11
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/pyproject.toml +1 -1
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/LICENSE +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/__main__.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/_bundle.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/_compat.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/_desktop_runtime.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/_devices.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/_download.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/_helper.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/_log.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/_process.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/_sigcheck.ps1 +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/_sigcheck.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/_winapi.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/agent.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/capture.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/cli.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/config.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/desktop.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/desktop_capture.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/desktop_session.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/display_setup.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/driver.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/driver_manifest.json +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/errors.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/manifest.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/session.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/window.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop/workspace.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop.egg-info/SOURCES.txt +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop.egg-info/dependency_links.txt +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop.egg-info/entry_points.txt +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop.egg-info/requires.txt +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/VirtualDesktop.egg-info/top_level.txt +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/setup.cfg +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_accelerators.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_agent.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_artifact_regression.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_capture.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_cli.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_compat.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_config_and_winapi.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_desktop_isolation.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_destructive_lifecycle.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_display_setup.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_download.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_driver.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_hide_modes.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_integration_windows.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_manifest.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_no_dialogs.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_packaging.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_process.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_virtual_desktop.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_workspace.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.1}/tests/test_workspace_instances.py +0 -0
- {virtualdesktop-1.3.0 → virtualdesktop-1.3.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.
|
|
3
|
+
Version: 1.3.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
|
|
@@ -86,7 +86,7 @@ uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
|
|
|
86
86
|
| 一句话 | 另开一个桌面,窗口与你的桌面完全隔离 | 多一块屏幕,窗口仍在你的桌面上、只是画在看不见的地方 |
|
|
87
87
|
| 隔离强度 | **强**:独立窗口命名空间,外部进程看不到、碰不到 | **弱**:窗口是普通窗口,任何程序都能找到并操作它 |
|
|
88
88
|
| 需要权限 | **零**:不需要驱动、不弹 UAC | 装一次驱动,弹**一次 UAC** |
|
|
89
|
-
| 能截图吗 |
|
|
89
|
+
| 能截图吗 | **不能**(窗口截图、桌面截图都不行) | **能**(两种都行) |
|
|
90
90
|
| 外部自动化库能用吗 | **不能**(要在桌面内跑代理) | **能**(pywinauto / UIA / pyautogui 直接用) |
|
|
91
91
|
| 适合 | 多实例并行、强隔离、不要截图 | UI 自动化、截图 OCR、无人值守抓界面 |
|
|
92
92
|
|
|
@@ -177,6 +177,18 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
|
|
|
177
177
|
`exec` 是逃生舱:里面的代码能 `import VirtualDesktop`、能用 `list_windows()`、`CaptureEngine`,
|
|
178
178
|
也能用你自己装的任何库。代理本身只依赖 ctypes,不需要额外安装。
|
|
179
179
|
|
|
180
|
+
### 私有桌面不能截图
|
|
181
|
+
|
|
182
|
+
两种截图都不行,且原因不同:
|
|
183
|
+
|
|
184
|
+
| 类型 | 结果 | 原因 |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| 程序窗口截图 | **不能** | 该桌面的窗口从不被系统合成,没有像素可读 |
|
|
187
|
+
| 桌面截图 | **不能** | 拿不到该桌面的屏幕 DC(`BitBlt` 返回拒绝访问) |
|
|
188
|
+
|
|
189
|
+
`agent.screenshot()` 会**明确报错**(`code="capture-needs-shared-workspace"`)而不是产出空白图。
|
|
190
|
+
要读状态用 `agent.info()` / `agent.text()`;要像素请改用虚拟显示器。
|
|
191
|
+
|
|
180
192
|
## 能力边界
|
|
181
193
|
|
|
182
194
|
私有桌面的窗口**不在交互桌面上**,所以按输入机制分,能做的事如下:
|
|
@@ -185,17 +197,18 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
|
|
|
185
197
|
|---|---|---|
|
|
186
198
|
| 找到窗口、读标题 / 类名 / 矩形 | 能 | 读窗口属性,跨桌面可用 |
|
|
187
199
|
| 枚举子控件、读控件文字 | 能 | 同上 |
|
|
200
|
+
| 用外部进程的 pywinauto 连上并读属性 | **能** | 拿得到句柄就能读 |
|
|
188
201
|
| 点击控件(`BM_CLICK` 等窗口消息) | **能** | 消息直接投递给窗口,不需要指针 |
|
|
189
202
|
| 送入字符(`WM_CHAR` 消息) | **能** | 同上 |
|
|
190
|
-
|
|
|
203
|
+
| **程序窗口截图** | **不能** | 桌面从不被系统合成,窗口没有像素 |
|
|
204
|
+
| **桌面截图** | **不能** | 拿不到该桌面的屏幕 DC(`BitBlt` 返回拒绝访问) |
|
|
191
205
|
| 移动真实鼠标来点击 | **不能** | 该桌面不是活动桌面,没有指针 |
|
|
192
206
|
| 滚轮(真实滚轮事件) | **不能** | 同"移动真实鼠标" |
|
|
193
207
|
| 发送真实键盘输入(`SendInput`) | **不能** | 键盘只发给活动桌面 |
|
|
194
208
|
| `pywinauto` 的 `click_input()` / `type_keys()` | **不能** | 依赖真实输入与前台焦点 |
|
|
195
|
-
| 用外部进程的 pywinauto 直接连上它 | **不能** | 跨桌面拿不到窗口 |
|
|
196
209
|
|
|
197
|
-
|
|
198
|
-
|
|
210
|
+
要点:**能用消息驱动的都能用,需要真实输入设备的都不能用,两种截图都不能用**。需要截图或需要
|
|
211
|
+
真实鼠标/滚轮的应用,请改用下面的虚拟显示器方案。
|
|
199
212
|
|
|
200
213
|
---
|
|
201
214
|
|
|
@@ -288,7 +301,17 @@ VirtualWorkspace(VirtualWorkspace.SHARED, monitor="primary") # 主屏
|
|
|
288
301
|
|
|
289
302
|
## 截图
|
|
290
303
|
|
|
291
|
-
|
|
304
|
+
要分清两种,它们走**完全不同的通路**,能力也不同:
|
|
305
|
+
|
|
306
|
+
| 类型 | 拍什么 | 走哪条路 | 怎么调 |
|
|
307
|
+
|---|---|---|---|
|
|
308
|
+
| **程序窗口截图** | 某一个窗口自己的像素 | `PrintWindow(hwnd)` | `CaptureEngine.capture(hwnd)` / `space.capture(hwnd)` |
|
|
309
|
+
| **桌面截图** | 整块屏幕(含其上所有窗口) | `ImageGrab` / `BitBlt` 屏幕 DC | `capture_all_screens()` / `capture_monitor()` / `space.capture_desktop()` |
|
|
310
|
+
|
|
311
|
+
### 程序窗口截图
|
|
312
|
+
|
|
313
|
+
按句柄拍,窗口在屏幕外也能拍,是抓单个程序界面最可靠的方式。`capture()` 依次尝试多种后端直到
|
|
314
|
+
拿到**有内容**的帧,默认顺序:
|
|
292
315
|
|
|
293
316
|
`printwindow`(`PW_RENDERFULLCONTENT`)→ `printwindow_plain` → `wm_print` → `bitblt`
|
|
294
317
|
|
|
@@ -303,6 +326,39 @@ frame = engine.capture(hwnd, require_content=True)
|
|
|
303
326
|
print(frame.method, frame.size)
|
|
304
327
|
```
|
|
305
328
|
|
|
329
|
+
```python
|
|
330
|
+
with VirtualWorkspace(VirtualWorkspace.SHARED) as space:
|
|
331
|
+
entry = space.launch(["charmap.exe"])
|
|
332
|
+
space.capture(entry.hwnd, save_to="window.png")
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### 桌面截图
|
|
336
|
+
|
|
337
|
+
拍的是**屏幕**,不是某个窗口。`virtual_screen_rect()` 是整张虚拟桌面(含所有显示器,处理好负坐标),
|
|
338
|
+
`capture_monitor()` 是单块显示器。在虚拟显示器模式下,`space.capture_desktop()` 等价于
|
|
339
|
+
`capture_monitor(space.monitor)`。
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
from VirtualDesktop import CaptureEngine
|
|
343
|
+
from VirtualDesktop.desktop_capture import capture_all_screens, capture_monitor
|
|
344
|
+
|
|
345
|
+
CaptureEngine().ensure_dpi_aware() # 拍摄前必须做,见下方警告
|
|
346
|
+
|
|
347
|
+
whole = capture_all_screens() # 整张虚拟桌面
|
|
348
|
+
whole.image.save("desktop.png")
|
|
349
|
+
print(whole.rect, whole.size, whole.method)
|
|
350
|
+
|
|
351
|
+
one = capture_monitor(device="\\\\.\\DISPLAY2") # 只拍某一块屏
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
> **拍摄前先确保 DPI 感知。** 矩形由 `GetSystemMetrics` / `EnumDisplayMonitors` 计算,它们对
|
|
355
|
+
> **不感知 DPI** 的进程返回的是**缩放后的逻辑值**;而像素抓取返回的是**物理像素**。两者不一致时
|
|
356
|
+
> 会**静默裁掉**画面右下角(125% 缩放下 1920×1080 的屏只拿到 1536×864,少 384×216 像素,且不报错)。
|
|
357
|
+
> 先调一次 `CaptureEngine().ensure_dpi_aware()` 即可,`VirtualWorkspace` 内部已自动处理。
|
|
358
|
+
|
|
359
|
+
> **桌面截图只有虚拟显示器模式可用。** offscreen 模式下窗口被移到所有显示器**之外**,桌面截图按
|
|
360
|
+
> 定义拍不到它们;而私有桌面根本没有可拍的屏幕 DC。
|
|
361
|
+
|
|
306
362
|
## 用自动化库控制窗口
|
|
307
363
|
|
|
308
364
|
窗口是普通窗口,直接从你的进程用即可:
|
|
@@ -358,7 +414,8 @@ print(current_primary_size()) # (1920, 1080)
|
|
|
358
414
|
| 枚举子控件、读控件文字 | 能 | 能 |
|
|
359
415
|
| 点击控件(窗口消息) | 能 | 能 |
|
|
360
416
|
| 送入字符(窗口消息) | 能 | 能 |
|
|
361
|
-
|
|
|
417
|
+
| **程序窗口截图** | **能** | **能** |
|
|
418
|
+
| **桌面截图** | **能**(拍到窗口) | **不能**(窗口在所有显示器之外) |
|
|
362
419
|
| 外部 pywinauto / UIA 直接连上 | 能 | 能 |
|
|
363
420
|
| 移动真实鼠标来点击 | 能 | **不能**(窗口在屏幕之外) |
|
|
364
421
|
| 滚轮 | 能 | **不能** |
|
|
@@ -410,7 +467,10 @@ VIRTUALDESKTOP_LOG_FILE=<path>
|
|
|
410
467
|
| `SetThreadDesktop ... error 170` | 当前线程已有窗口,无法再绑定桌面 | 在创建窗口之前 `bind_thread()`,或用 `agent()` |
|
|
411
468
|
| 安装后没出现新的虚拟屏 | 驱动已注册但显示器未启用 | 设备管理器启用;`report.requires_reboot` 为真时重启 |
|
|
412
469
|
| 驱动下载失败或极慢 | github.com 不可达 | 设 `VIRTUALDESKTOP_GITHUB_MIRRORS`,或预置离线缓存 |
|
|
413
|
-
|
|
|
470
|
+
| 窗口截图是黑帧 | 应用为硬件加速 / 受保护内容 | 系统限制;换 `bitblt` 试试,或确认窗口有内容 |
|
|
471
|
+
| 桌面截图少了右下角 | 进程不感知 DPI,矩形按逻辑坐标算 | 先调 `CaptureEngine().ensure_dpi_aware()` |
|
|
472
|
+
| 桌面截图拍不到工作区窗口 | 用了 `offscreen`,窗口在所有显示器之外 | 改用 `hide_mode="virtual_screen"` |
|
|
473
|
+
| `capture_desktop()` 报"workspace is not open" | offscreen 模式下没有绑定显示器 | 改用 `capture_all_screens()`,或用 `virtual_screen` |
|
|
414
474
|
| 真实鼠标点击无效 | 窗口在屏幕之外,或拿不到前台焦点 | 用 `virtual_screen`,或改用消息驱动 |
|
|
415
475
|
| 某程序在私有桌面上找不到窗口 | 控制台类(cmd)等窗口不参与常规枚举 | 换程序,或用 `agent.exec()` 在桌面内直接操作 |
|
|
416
476
|
|
|
@@ -420,8 +480,9 @@ VIRTUALDESKTOP_LOG_FILE=<path>
|
|
|
420
480
|
|
|
421
481
|
## 已知限制
|
|
422
482
|
|
|
423
|
-
-
|
|
424
|
-
|
|
483
|
+
- **私有桌面不能截图**:程序窗口截图和桌面截图都不行。窗口没有被系统合成、没有像素可读;
|
|
484
|
+
而该桌面的屏幕 DC 也拿不到(`BitBlt` 返回拒绝访问)。用 `agent.info()` / `agent.text()` 读状态,
|
|
485
|
+
需要像素请改用虚拟显示器。
|
|
425
486
|
- **私有桌面不能用真实鼠标 / 滚轮 / 真实键盘**:那里没有活动指针和键盘焦点;消息驱动可用。
|
|
426
487
|
- **窗口无法在私有桌面之间搬运**:Windows 不允许跨桌面移动窗口。
|
|
427
488
|
- 部分应用(UWP、硬件叠加层、DRM 受保护内容)任何离屏方式都抓不到——系统限制。
|
|
@@ -30,7 +30,7 @@ uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
|
|
|
30
30
|
| 一句话 | 另开一个桌面,窗口与你的桌面完全隔离 | 多一块屏幕,窗口仍在你的桌面上、只是画在看不见的地方 |
|
|
31
31
|
| 隔离强度 | **强**:独立窗口命名空间,外部进程看不到、碰不到 | **弱**:窗口是普通窗口,任何程序都能找到并操作它 |
|
|
32
32
|
| 需要权限 | **零**:不需要驱动、不弹 UAC | 装一次驱动,弹**一次 UAC** |
|
|
33
|
-
| 能截图吗 |
|
|
33
|
+
| 能截图吗 | **不能**(窗口截图、桌面截图都不行) | **能**(两种都行) |
|
|
34
34
|
| 外部自动化库能用吗 | **不能**(要在桌面内跑代理) | **能**(pywinauto / UIA / pyautogui 直接用) |
|
|
35
35
|
| 适合 | 多实例并行、强隔离、不要截图 | UI 自动化、截图 OCR、无人值守抓界面 |
|
|
36
36
|
|
|
@@ -121,6 +121,18 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
|
|
|
121
121
|
`exec` 是逃生舱:里面的代码能 `import VirtualDesktop`、能用 `list_windows()`、`CaptureEngine`,
|
|
122
122
|
也能用你自己装的任何库。代理本身只依赖 ctypes,不需要额外安装。
|
|
123
123
|
|
|
124
|
+
### 私有桌面不能截图
|
|
125
|
+
|
|
126
|
+
两种截图都不行,且原因不同:
|
|
127
|
+
|
|
128
|
+
| 类型 | 结果 | 原因 |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| 程序窗口截图 | **不能** | 该桌面的窗口从不被系统合成,没有像素可读 |
|
|
131
|
+
| 桌面截图 | **不能** | 拿不到该桌面的屏幕 DC(`BitBlt` 返回拒绝访问) |
|
|
132
|
+
|
|
133
|
+
`agent.screenshot()` 会**明确报错**(`code="capture-needs-shared-workspace"`)而不是产出空白图。
|
|
134
|
+
要读状态用 `agent.info()` / `agent.text()`;要像素请改用虚拟显示器。
|
|
135
|
+
|
|
124
136
|
## 能力边界
|
|
125
137
|
|
|
126
138
|
私有桌面的窗口**不在交互桌面上**,所以按输入机制分,能做的事如下:
|
|
@@ -129,17 +141,18 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
|
|
|
129
141
|
|---|---|---|
|
|
130
142
|
| 找到窗口、读标题 / 类名 / 矩形 | 能 | 读窗口属性,跨桌面可用 |
|
|
131
143
|
| 枚举子控件、读控件文字 | 能 | 同上 |
|
|
144
|
+
| 用外部进程的 pywinauto 连上并读属性 | **能** | 拿得到句柄就能读 |
|
|
132
145
|
| 点击控件(`BM_CLICK` 等窗口消息) | **能** | 消息直接投递给窗口,不需要指针 |
|
|
133
146
|
| 送入字符(`WM_CHAR` 消息) | **能** | 同上 |
|
|
134
|
-
|
|
|
147
|
+
| **程序窗口截图** | **不能** | 桌面从不被系统合成,窗口没有像素 |
|
|
148
|
+
| **桌面截图** | **不能** | 拿不到该桌面的屏幕 DC(`BitBlt` 返回拒绝访问) |
|
|
135
149
|
| 移动真实鼠标来点击 | **不能** | 该桌面不是活动桌面,没有指针 |
|
|
136
150
|
| 滚轮(真实滚轮事件) | **不能** | 同"移动真实鼠标" |
|
|
137
151
|
| 发送真实键盘输入(`SendInput`) | **不能** | 键盘只发给活动桌面 |
|
|
138
152
|
| `pywinauto` 的 `click_input()` / `type_keys()` | **不能** | 依赖真实输入与前台焦点 |
|
|
139
|
-
| 用外部进程的 pywinauto 直接连上它 | **不能** | 跨桌面拿不到窗口 |
|
|
140
153
|
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
要点:**能用消息驱动的都能用,需要真实输入设备的都不能用,两种截图都不能用**。需要截图或需要
|
|
155
|
+
真实鼠标/滚轮的应用,请改用下面的虚拟显示器方案。
|
|
143
156
|
|
|
144
157
|
---
|
|
145
158
|
|
|
@@ -232,7 +245,17 @@ VirtualWorkspace(VirtualWorkspace.SHARED, monitor="primary") # 主屏
|
|
|
232
245
|
|
|
233
246
|
## 截图
|
|
234
247
|
|
|
235
|
-
|
|
248
|
+
要分清两种,它们走**完全不同的通路**,能力也不同:
|
|
249
|
+
|
|
250
|
+
| 类型 | 拍什么 | 走哪条路 | 怎么调 |
|
|
251
|
+
|---|---|---|---|
|
|
252
|
+
| **程序窗口截图** | 某一个窗口自己的像素 | `PrintWindow(hwnd)` | `CaptureEngine.capture(hwnd)` / `space.capture(hwnd)` |
|
|
253
|
+
| **桌面截图** | 整块屏幕(含其上所有窗口) | `ImageGrab` / `BitBlt` 屏幕 DC | `capture_all_screens()` / `capture_monitor()` / `space.capture_desktop()` |
|
|
254
|
+
|
|
255
|
+
### 程序窗口截图
|
|
256
|
+
|
|
257
|
+
按句柄拍,窗口在屏幕外也能拍,是抓单个程序界面最可靠的方式。`capture()` 依次尝试多种后端直到
|
|
258
|
+
拿到**有内容**的帧,默认顺序:
|
|
236
259
|
|
|
237
260
|
`printwindow`(`PW_RENDERFULLCONTENT`)→ `printwindow_plain` → `wm_print` → `bitblt`
|
|
238
261
|
|
|
@@ -247,6 +270,39 @@ frame = engine.capture(hwnd, require_content=True)
|
|
|
247
270
|
print(frame.method, frame.size)
|
|
248
271
|
```
|
|
249
272
|
|
|
273
|
+
```python
|
|
274
|
+
with VirtualWorkspace(VirtualWorkspace.SHARED) as space:
|
|
275
|
+
entry = space.launch(["charmap.exe"])
|
|
276
|
+
space.capture(entry.hwnd, save_to="window.png")
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 桌面截图
|
|
280
|
+
|
|
281
|
+
拍的是**屏幕**,不是某个窗口。`virtual_screen_rect()` 是整张虚拟桌面(含所有显示器,处理好负坐标),
|
|
282
|
+
`capture_monitor()` 是单块显示器。在虚拟显示器模式下,`space.capture_desktop()` 等价于
|
|
283
|
+
`capture_monitor(space.monitor)`。
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
from VirtualDesktop import CaptureEngine
|
|
287
|
+
from VirtualDesktop.desktop_capture import capture_all_screens, capture_monitor
|
|
288
|
+
|
|
289
|
+
CaptureEngine().ensure_dpi_aware() # 拍摄前必须做,见下方警告
|
|
290
|
+
|
|
291
|
+
whole = capture_all_screens() # 整张虚拟桌面
|
|
292
|
+
whole.image.save("desktop.png")
|
|
293
|
+
print(whole.rect, whole.size, whole.method)
|
|
294
|
+
|
|
295
|
+
one = capture_monitor(device="\\\\.\\DISPLAY2") # 只拍某一块屏
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
> **拍摄前先确保 DPI 感知。** 矩形由 `GetSystemMetrics` / `EnumDisplayMonitors` 计算,它们对
|
|
299
|
+
> **不感知 DPI** 的进程返回的是**缩放后的逻辑值**;而像素抓取返回的是**物理像素**。两者不一致时
|
|
300
|
+
> 会**静默裁掉**画面右下角(125% 缩放下 1920×1080 的屏只拿到 1536×864,少 384×216 像素,且不报错)。
|
|
301
|
+
> 先调一次 `CaptureEngine().ensure_dpi_aware()` 即可,`VirtualWorkspace` 内部已自动处理。
|
|
302
|
+
|
|
303
|
+
> **桌面截图只有虚拟显示器模式可用。** offscreen 模式下窗口被移到所有显示器**之外**,桌面截图按
|
|
304
|
+
> 定义拍不到它们;而私有桌面根本没有可拍的屏幕 DC。
|
|
305
|
+
|
|
250
306
|
## 用自动化库控制窗口
|
|
251
307
|
|
|
252
308
|
窗口是普通窗口,直接从你的进程用即可:
|
|
@@ -302,7 +358,8 @@ print(current_primary_size()) # (1920, 1080)
|
|
|
302
358
|
| 枚举子控件、读控件文字 | 能 | 能 |
|
|
303
359
|
| 点击控件(窗口消息) | 能 | 能 |
|
|
304
360
|
| 送入字符(窗口消息) | 能 | 能 |
|
|
305
|
-
|
|
|
361
|
+
| **程序窗口截图** | **能** | **能** |
|
|
362
|
+
| **桌面截图** | **能**(拍到窗口) | **不能**(窗口在所有显示器之外) |
|
|
306
363
|
| 外部 pywinauto / UIA 直接连上 | 能 | 能 |
|
|
307
364
|
| 移动真实鼠标来点击 | 能 | **不能**(窗口在屏幕之外) |
|
|
308
365
|
| 滚轮 | 能 | **不能** |
|
|
@@ -354,7 +411,10 @@ VIRTUALDESKTOP_LOG_FILE=<path>
|
|
|
354
411
|
| `SetThreadDesktop ... error 170` | 当前线程已有窗口,无法再绑定桌面 | 在创建窗口之前 `bind_thread()`,或用 `agent()` |
|
|
355
412
|
| 安装后没出现新的虚拟屏 | 驱动已注册但显示器未启用 | 设备管理器启用;`report.requires_reboot` 为真时重启 |
|
|
356
413
|
| 驱动下载失败或极慢 | github.com 不可达 | 设 `VIRTUALDESKTOP_GITHUB_MIRRORS`,或预置离线缓存 |
|
|
357
|
-
|
|
|
414
|
+
| 窗口截图是黑帧 | 应用为硬件加速 / 受保护内容 | 系统限制;换 `bitblt` 试试,或确认窗口有内容 |
|
|
415
|
+
| 桌面截图少了右下角 | 进程不感知 DPI,矩形按逻辑坐标算 | 先调 `CaptureEngine().ensure_dpi_aware()` |
|
|
416
|
+
| 桌面截图拍不到工作区窗口 | 用了 `offscreen`,窗口在所有显示器之外 | 改用 `hide_mode="virtual_screen"` |
|
|
417
|
+
| `capture_desktop()` 报"workspace is not open" | offscreen 模式下没有绑定显示器 | 改用 `capture_all_screens()`,或用 `virtual_screen` |
|
|
358
418
|
| 真实鼠标点击无效 | 窗口在屏幕之外,或拿不到前台焦点 | 用 `virtual_screen`,或改用消息驱动 |
|
|
359
419
|
| 某程序在私有桌面上找不到窗口 | 控制台类(cmd)等窗口不参与常规枚举 | 换程序,或用 `agent.exec()` 在桌面内直接操作 |
|
|
360
420
|
|
|
@@ -364,8 +424,9 @@ VIRTUALDESKTOP_LOG_FILE=<path>
|
|
|
364
424
|
|
|
365
425
|
## 已知限制
|
|
366
426
|
|
|
367
|
-
-
|
|
368
|
-
|
|
427
|
+
- **私有桌面不能截图**:程序窗口截图和桌面截图都不行。窗口没有被系统合成、没有像素可读;
|
|
428
|
+
而该桌面的屏幕 DC 也拿不到(`BitBlt` 返回拒绝访问)。用 `agent.info()` / `agent.text()` 读状态,
|
|
429
|
+
需要像素请改用虚拟显示器。
|
|
369
430
|
- **私有桌面不能用真实鼠标 / 滚轮 / 真实键盘**:那里没有活动指针和键盘焦点;消息驱动可用。
|
|
370
431
|
- **窗口无法在私有桌面之间搬运**:Windows 不允许跨桌面移动窗口。
|
|
371
432
|
- 部分应用(UWP、硬件叠加层、DRM 受保护内容)任何离屏方式都抓不到——系统限制。
|
|
@@ -63,7 +63,7 @@ from .workspace import SHARED, VirtualWorkspace, WorkspaceWindow
|
|
|
63
63
|
from .agent import AgentProcess, AgentSession
|
|
64
64
|
from .display_setup import configure_virtual_display, current_primary_dpi, current_primary_size
|
|
65
65
|
|
|
66
|
-
__version__ = "1.3.
|
|
66
|
+
__version__ = "1.3.1"
|
|
67
67
|
|
|
68
68
|
__all__ = [
|
|
69
69
|
"__version__",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: VirtualDesktop
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.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
|
|
@@ -86,7 +86,7 @@ uv add VirtualDesktop # uv(写入 pyproject.toml 并锁定)
|
|
|
86
86
|
| 一句话 | 另开一个桌面,窗口与你的桌面完全隔离 | 多一块屏幕,窗口仍在你的桌面上、只是画在看不见的地方 |
|
|
87
87
|
| 隔离强度 | **强**:独立窗口命名空间,外部进程看不到、碰不到 | **弱**:窗口是普通窗口,任何程序都能找到并操作它 |
|
|
88
88
|
| 需要权限 | **零**:不需要驱动、不弹 UAC | 装一次驱动,弹**一次 UAC** |
|
|
89
|
-
| 能截图吗 |
|
|
89
|
+
| 能截图吗 | **不能**(窗口截图、桌面截图都不行) | **能**(两种都行) |
|
|
90
90
|
| 外部自动化库能用吗 | **不能**(要在桌面内跑代理) | **能**(pywinauto / UIA / pyautogui 直接用) |
|
|
91
91
|
| 适合 | 多实例并行、强隔离、不要截图 | UI 自动化、截图 OCR、无人值守抓界面 |
|
|
92
92
|
|
|
@@ -177,6 +177,18 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
|
|
|
177
177
|
`exec` 是逃生舱:里面的代码能 `import VirtualDesktop`、能用 `list_windows()`、`CaptureEngine`,
|
|
178
178
|
也能用你自己装的任何库。代理本身只依赖 ctypes,不需要额外安装。
|
|
179
179
|
|
|
180
|
+
### 私有桌面不能截图
|
|
181
|
+
|
|
182
|
+
两种截图都不行,且原因不同:
|
|
183
|
+
|
|
184
|
+
| 类型 | 结果 | 原因 |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| 程序窗口截图 | **不能** | 该桌面的窗口从不被系统合成,没有像素可读 |
|
|
187
|
+
| 桌面截图 | **不能** | 拿不到该桌面的屏幕 DC(`BitBlt` 返回拒绝访问) |
|
|
188
|
+
|
|
189
|
+
`agent.screenshot()` 会**明确报错**(`code="capture-needs-shared-workspace"`)而不是产出空白图。
|
|
190
|
+
要读状态用 `agent.info()` / `agent.text()`;要像素请改用虚拟显示器。
|
|
191
|
+
|
|
180
192
|
## 能力边界
|
|
181
193
|
|
|
182
194
|
私有桌面的窗口**不在交互桌面上**,所以按输入机制分,能做的事如下:
|
|
@@ -185,17 +197,18 @@ with VirtualWorkspace("lab") as space, space.agent() as agent:
|
|
|
185
197
|
|---|---|---|
|
|
186
198
|
| 找到窗口、读标题 / 类名 / 矩形 | 能 | 读窗口属性,跨桌面可用 |
|
|
187
199
|
| 枚举子控件、读控件文字 | 能 | 同上 |
|
|
200
|
+
| 用外部进程的 pywinauto 连上并读属性 | **能** | 拿得到句柄就能读 |
|
|
188
201
|
| 点击控件(`BM_CLICK` 等窗口消息) | **能** | 消息直接投递给窗口,不需要指针 |
|
|
189
202
|
| 送入字符(`WM_CHAR` 消息) | **能** | 同上 |
|
|
190
|
-
|
|
|
203
|
+
| **程序窗口截图** | **不能** | 桌面从不被系统合成,窗口没有像素 |
|
|
204
|
+
| **桌面截图** | **不能** | 拿不到该桌面的屏幕 DC(`BitBlt` 返回拒绝访问) |
|
|
191
205
|
| 移动真实鼠标来点击 | **不能** | 该桌面不是活动桌面,没有指针 |
|
|
192
206
|
| 滚轮(真实滚轮事件) | **不能** | 同"移动真实鼠标" |
|
|
193
207
|
| 发送真实键盘输入(`SendInput`) | **不能** | 键盘只发给活动桌面 |
|
|
194
208
|
| `pywinauto` 的 `click_input()` / `type_keys()` | **不能** | 依赖真实输入与前台焦点 |
|
|
195
|
-
| 用外部进程的 pywinauto 直接连上它 | **不能** | 跨桌面拿不到窗口 |
|
|
196
209
|
|
|
197
|
-
|
|
198
|
-
|
|
210
|
+
要点:**能用消息驱动的都能用,需要真实输入设备的都不能用,两种截图都不能用**。需要截图或需要
|
|
211
|
+
真实鼠标/滚轮的应用,请改用下面的虚拟显示器方案。
|
|
199
212
|
|
|
200
213
|
---
|
|
201
214
|
|
|
@@ -288,7 +301,17 @@ VirtualWorkspace(VirtualWorkspace.SHARED, monitor="primary") # 主屏
|
|
|
288
301
|
|
|
289
302
|
## 截图
|
|
290
303
|
|
|
291
|
-
|
|
304
|
+
要分清两种,它们走**完全不同的通路**,能力也不同:
|
|
305
|
+
|
|
306
|
+
| 类型 | 拍什么 | 走哪条路 | 怎么调 |
|
|
307
|
+
|---|---|---|---|
|
|
308
|
+
| **程序窗口截图** | 某一个窗口自己的像素 | `PrintWindow(hwnd)` | `CaptureEngine.capture(hwnd)` / `space.capture(hwnd)` |
|
|
309
|
+
| **桌面截图** | 整块屏幕(含其上所有窗口) | `ImageGrab` / `BitBlt` 屏幕 DC | `capture_all_screens()` / `capture_monitor()` / `space.capture_desktop()` |
|
|
310
|
+
|
|
311
|
+
### 程序窗口截图
|
|
312
|
+
|
|
313
|
+
按句柄拍,窗口在屏幕外也能拍,是抓单个程序界面最可靠的方式。`capture()` 依次尝试多种后端直到
|
|
314
|
+
拿到**有内容**的帧,默认顺序:
|
|
292
315
|
|
|
293
316
|
`printwindow`(`PW_RENDERFULLCONTENT`)→ `printwindow_plain` → `wm_print` → `bitblt`
|
|
294
317
|
|
|
@@ -303,6 +326,39 @@ frame = engine.capture(hwnd, require_content=True)
|
|
|
303
326
|
print(frame.method, frame.size)
|
|
304
327
|
```
|
|
305
328
|
|
|
329
|
+
```python
|
|
330
|
+
with VirtualWorkspace(VirtualWorkspace.SHARED) as space:
|
|
331
|
+
entry = space.launch(["charmap.exe"])
|
|
332
|
+
space.capture(entry.hwnd, save_to="window.png")
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### 桌面截图
|
|
336
|
+
|
|
337
|
+
拍的是**屏幕**,不是某个窗口。`virtual_screen_rect()` 是整张虚拟桌面(含所有显示器,处理好负坐标),
|
|
338
|
+
`capture_monitor()` 是单块显示器。在虚拟显示器模式下,`space.capture_desktop()` 等价于
|
|
339
|
+
`capture_monitor(space.monitor)`。
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
from VirtualDesktop import CaptureEngine
|
|
343
|
+
from VirtualDesktop.desktop_capture import capture_all_screens, capture_monitor
|
|
344
|
+
|
|
345
|
+
CaptureEngine().ensure_dpi_aware() # 拍摄前必须做,见下方警告
|
|
346
|
+
|
|
347
|
+
whole = capture_all_screens() # 整张虚拟桌面
|
|
348
|
+
whole.image.save("desktop.png")
|
|
349
|
+
print(whole.rect, whole.size, whole.method)
|
|
350
|
+
|
|
351
|
+
one = capture_monitor(device="\\\\.\\DISPLAY2") # 只拍某一块屏
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
> **拍摄前先确保 DPI 感知。** 矩形由 `GetSystemMetrics` / `EnumDisplayMonitors` 计算,它们对
|
|
355
|
+
> **不感知 DPI** 的进程返回的是**缩放后的逻辑值**;而像素抓取返回的是**物理像素**。两者不一致时
|
|
356
|
+
> 会**静默裁掉**画面右下角(125% 缩放下 1920×1080 的屏只拿到 1536×864,少 384×216 像素,且不报错)。
|
|
357
|
+
> 先调一次 `CaptureEngine().ensure_dpi_aware()` 即可,`VirtualWorkspace` 内部已自动处理。
|
|
358
|
+
|
|
359
|
+
> **桌面截图只有虚拟显示器模式可用。** offscreen 模式下窗口被移到所有显示器**之外**,桌面截图按
|
|
360
|
+
> 定义拍不到它们;而私有桌面根本没有可拍的屏幕 DC。
|
|
361
|
+
|
|
306
362
|
## 用自动化库控制窗口
|
|
307
363
|
|
|
308
364
|
窗口是普通窗口,直接从你的进程用即可:
|
|
@@ -358,7 +414,8 @@ print(current_primary_size()) # (1920, 1080)
|
|
|
358
414
|
| 枚举子控件、读控件文字 | 能 | 能 |
|
|
359
415
|
| 点击控件(窗口消息) | 能 | 能 |
|
|
360
416
|
| 送入字符(窗口消息) | 能 | 能 |
|
|
361
|
-
|
|
|
417
|
+
| **程序窗口截图** | **能** | **能** |
|
|
418
|
+
| **桌面截图** | **能**(拍到窗口) | **不能**(窗口在所有显示器之外) |
|
|
362
419
|
| 外部 pywinauto / UIA 直接连上 | 能 | 能 |
|
|
363
420
|
| 移动真实鼠标来点击 | 能 | **不能**(窗口在屏幕之外) |
|
|
364
421
|
| 滚轮 | 能 | **不能** |
|
|
@@ -410,7 +467,10 @@ VIRTUALDESKTOP_LOG_FILE=<path>
|
|
|
410
467
|
| `SetThreadDesktop ... error 170` | 当前线程已有窗口,无法再绑定桌面 | 在创建窗口之前 `bind_thread()`,或用 `agent()` |
|
|
411
468
|
| 安装后没出现新的虚拟屏 | 驱动已注册但显示器未启用 | 设备管理器启用;`report.requires_reboot` 为真时重启 |
|
|
412
469
|
| 驱动下载失败或极慢 | github.com 不可达 | 设 `VIRTUALDESKTOP_GITHUB_MIRRORS`,或预置离线缓存 |
|
|
413
|
-
|
|
|
470
|
+
| 窗口截图是黑帧 | 应用为硬件加速 / 受保护内容 | 系统限制;换 `bitblt` 试试,或确认窗口有内容 |
|
|
471
|
+
| 桌面截图少了右下角 | 进程不感知 DPI,矩形按逻辑坐标算 | 先调 `CaptureEngine().ensure_dpi_aware()` |
|
|
472
|
+
| 桌面截图拍不到工作区窗口 | 用了 `offscreen`,窗口在所有显示器之外 | 改用 `hide_mode="virtual_screen"` |
|
|
473
|
+
| `capture_desktop()` 报"workspace is not open" | offscreen 模式下没有绑定显示器 | 改用 `capture_all_screens()`,或用 `virtual_screen` |
|
|
414
474
|
| 真实鼠标点击无效 | 窗口在屏幕之外,或拿不到前台焦点 | 用 `virtual_screen`,或改用消息驱动 |
|
|
415
475
|
| 某程序在私有桌面上找不到窗口 | 控制台类(cmd)等窗口不参与常规枚举 | 换程序,或用 `agent.exec()` 在桌面内直接操作 |
|
|
416
476
|
|
|
@@ -420,8 +480,9 @@ VIRTUALDESKTOP_LOG_FILE=<path>
|
|
|
420
480
|
|
|
421
481
|
## 已知限制
|
|
422
482
|
|
|
423
|
-
-
|
|
424
|
-
|
|
483
|
+
- **私有桌面不能截图**:程序窗口截图和桌面截图都不行。窗口没有被系统合成、没有像素可读;
|
|
484
|
+
而该桌面的屏幕 DC 也拿不到(`BitBlt` 返回拒绝访问)。用 `agent.info()` / `agent.text()` 读状态,
|
|
485
|
+
需要像素请改用虚拟显示器。
|
|
425
486
|
- **私有桌面不能用真实鼠标 / 滚轮 / 真实键盘**:那里没有活动指针和键盘焦点;消息驱动可用。
|
|
426
487
|
- **窗口无法在私有桌面之间搬运**:Windows 不允许跨桌面移动窗口。
|
|
427
488
|
- 部分应用(UWP、硬件叠加层、DRM 受保护内容)任何离屏方式都抓不到——系统限制。
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "VirtualDesktop"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.1"
|
|
8
8
|
description = "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."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|