项目文件夹

文件
2026-07-13 21:36:54 +08:00

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")
  1. 拍摄原始截图
  2. 查看截图以了解实际的像素位置
  3. 根据所见使用 PIL 进行裁剪
  4. 查看结果——如果不对,重新裁剪(瞬间完成,无需重新截图)

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")

使用指南

  1. 在进行任何更改之前,务必先捕获"之前"的状态——如果忘记,必须回退代码才能获得"之前"的截图
  2. "之前/之后"对比必须使用相同的视口宽度和裁剪区域——否则对比毫无意义
  3. 如果在代码更改后还需要"之前"的截图:使用 git checkout HEAD~1 -- <files> 回退代码,截图,然后使用 git checkout HEAD -- <files> 恢复
  4. 对于交互状态:为每个状态同时捕获"之前"和"之后"的截图——不要假设"正常"状态的"之前"截图适用于所有情况
  5. 在 Playwright 中使用 device_scale_factor=1 强制使用 1x 像素,使截图与用户在 100% 缩放时看到的效果一致
  6. 图表需要额外的等待时间——Plotly、D3 等是异步渲染的;在 networkidle 之后至少等待 4 秒
  7. 窄视口会暴露渲染错误——某些边框/对齐问题仅在特定宽度下才会出现

非 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。通过 CDPChrome 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 的自定义等待逻辑