skillhub-161-ui-screenshots
7.5 KiB
7.5 KiB
---
name: ui-screenshots
description: '使用 Playwright 和 PIL 在开发过程中捕获 Web 应用的截图。支持全页捕获、交互状态以及避免重复截图的迭代裁剪工作流。'
---
# UI 截图
在开发过程中捕获 Web 应用和图形用户界面的截图,用于记录视觉变化。
## 何时使用此技能
当您需要以下场景时使用此技能:
- 捕获正在运行的 Web 应用的当前状态
- 在代码变更前后记录 UI 状态
- 截图交互状态(工具提示、悬停效果、选中的元素)
- 捕获页面的特定区域而无需重新截图
## 前置条件
```bash
pip install playwright Pillow -q
playwright install chromium
核心工作流
1. 拍摄原始全页截图
from playwright.async_api import async_playwright
async def capture(url="http://localhost:3000", out="screenshot-raw.png", width=1400, height=5000):
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page(viewport={"width": width, "height": height})
await page.goto(url, wait_until="networkidle")
await page.wait_for_timeout(4000) # 等待图表/动画渲染
await page.screenshot(path=out, full_page=True)
await browser.close()
- 使用较高的视口(height=5000),使页面无需滚动即可渲染所有内容
wait_until="networkidle"+wait_for_timeout(4000)确保异步图表加载完成full_page=True捕获所有可滚动内容
2. 查看原始图片,然后使用 PIL 裁剪
不要试图通过 Playwright 的 clip 参数来获取完美的裁剪区域。在全页截图中这并不可靠。
from PIL import Image
img = Image.open("screenshot-raw.png")
cropped = img.crop((left, top, right, bottom)) # 根据所见进行调整
cropped.save("screenshot-final.png")
- 拍摄原始截图
- 查看截图以了解实际的像素位置
- 根据所见使用 PIL 进行裁剪
- 查看结果——如果不对,重新裁剪(瞬间完成,无需重新截图)
3. 迭代裁剪,而非重复截图
- 重新截图速度很慢(浏览器启动 + 页面加载 + 等待渲染)
- 重新裁剪是即时的(仅需 PIL)
- 获取一张好的原始截图,然后按需任意裁剪
4. 交互状态
element = page.locator("selector").first
await element.hover()
await page.wait_for_timeout(1000) # 等待工具提示出现
await page.screenshot(path="screenshot-hover.png", full_page=True)
对于"选中"状态(无需悬停效果),在点击后将鼠标移开:
await element.click()
await page.mouse.move(300, 300) # 移开鼠标,避免显示悬停效果
await page.wait_for_timeout(500)
await page.screenshot(path="screenshot-selected.png", full_page=True)
5. 特定区域截图
从一张全页截图中裁剪出不同区域:
img.crop((0, 200, 920, 900)).save("screenshot-header.png")
img.crop((0, 900, 920, 1600)).save("screenshot-main.png")
使用指南
- 在进行任何更改之前,务必先捕获"之前"的状态——如果忘记,必须回退代码才能获得"之前"的截图
- "之前/之后"对比必须使用相同的视口宽度和裁剪区域——否则对比毫无意义
- 如果在代码更改后还需要"之前"的截图:使用
git checkout HEAD~1 -- <files>回退代码,截图,然后使用git checkout HEAD -- <files>恢复 - 对于交互状态:为每个状态同时捕获"之前"和"之后"的截图——不要假设"正常"状态的"之前"截图适用于所有情况
- 在 Playwright 中使用
device_scale_factor=1强制使用 1x 像素,使截图与用户在 100% 缩放时看到的效果一致 - 图表需要额外的等待时间——Plotly、D3 等是异步渲染的;在 networkidle 之后至少等待 4 秒
- 窄视口会暴露渲染错误——某些边框/对齐问题仅在特定宽度下才会出现
非 Web 应用截图
适用于 Playwright 无法触及的桌面应用(VS、WPF、WinForms、控制台应用、终端)。
mss + ctypes(推荐用于桌面窗口)
通过 Win32 API 按标题查找窗口,使用 mss 捕获其区域。经测试,每次捕获约需 33ms。
import ctypes
from ctypes import c_int, Structure, byref, windll
import mss
from PIL import Image
user32 = windll.user32
def find_window(title_contains):
"""查找标题包含指定子串的可见窗口。"""
results = []
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
def cb(hwnd, _):
if user32.IsWindowVisible(hwnd):
buf = ctypes.create_unicode_buffer(256)
user32.GetWindowTextW(hwnd, buf, 256)
if title_contains.lower() in buf.value.lower():
results.append((hwnd, buf.value))
return True
user32.EnumWindows(WNDENUMPROC(cb), 0)
return results
def capture_window(title_contains, output_path):
"""按标题子串捕获窗口。"""
windows = find_window(title_contains)
if not windows:
raise ValueError(f"未找到匹配 '{title_contains}' 的窗口")
hwnd = windows[0][0]
class RECT(Structure):
_fields_ = [('left', c_int), ('top', c_int), ('right', c_int), ('bottom', c_int)]
rect = RECT()
user32.GetWindowRect(hwnd, byref(rect))
w, h = rect.right - rect.left, rect.bottom - rect.top
with mss.mss() as sct:
shot = sct.grab({'left': rect.left, 'top': rect.top, 'width': w, 'height': h})
img = Image.frombytes('RGB', shot.size, shot.rgb)
img.save(output_path)
return img
# 用法:
capture_window('Visual Studio Code', 'vscode-capture.png')
前置条件: pip install mss pillow
局限性: 窗口必须可见(不被其他窗口遮挡且未被最小化)。
Electron 应用(VS Code 等)
仅限 Node.js Playwright——Python Playwright 没有 electron API。通过 CDP(Chrome DevTools Protocol)捕获,而非从屏幕捕获——即使窗口最小化也能工作。
const { _electron: electron } = require('playwright');
const app = await electron.launch({
executablePath: 'C:\\Program Files\\Microsoft VS Code\\Code.exe',
args: ['--new-window', '--disable-extensions', '--user-data-dir=' + tmpDir]
});
const window = await app.firstWindow();
await window.waitForLoadState('domcontentloaded');
// 立即最小化——通过 CDP 仍然可以捕获
await app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0].minimize();
});
await window.screenshot({ path: 'capture.png' }); // 最小化时也能工作!
await app.close();
关键: 必须使用 --user-data-dir=<temp>,否则 VS Code 会将请求交给现有实例,启动的进程会立即退出。
决策树
| 场景 | 工具 | 备注 |
|---|---|---|
| Web 应用(localhost) | Playwright | 经过验证,具有完整的 DOM 访问能力 |
| Electron 应用(VS Code) | Playwright Electron (Node.js) | 通过 CDP 在最小化时也能工作 |
| 桌面应用,窗口可见 | mss + ctypes(按标题查找) | 每次捕获约需 33ms |
| 桌面应用,被其他窗口遮挡 | Windows Graphics Capture API | 设置复杂,需要 Win10 1903+ |
| 快速全屏截图 | mss | 约需 68ms |
局限性
- Web 捕获需要本地运行的应用或可访问的 URL
- 桌面捕获(mss)要求窗口可见且不被遮挡
- Electron 捕获需要 Node.js Playwright(而非 Python)
- 某些具有大量客户端渲染的 SPA 可能需要超越 networkidle 的自定义等待逻辑