Captures d'écran UI
Capturez des captures d'écran d'applications web et d'interfaces graphiques pendant le développement pour documenter les changements visuels.
Quand utiliser cette skill
Utilisez cette skill quand vous devez :
- Capturer l'état actuel d'une application web en cours d'exécution
- Documenter une interface avant et après une modification de code
- Capturer des états interactifs (tooltips, survols, éléments sélectionnés)
- Capturer des sections spécifiques d'une page sans refaire de capture
Prérequis
pip install playwright Pillow -q
playwright install chromium
Workflow principal
1. Prendre une capture brute de page complète
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) # let charts/animations render
await page.screenshot(path=out, full_page=True)
await browser.close()
- Utilisez une fenêtre d'affichage haute (height=5000) pour que la page affiche tout sans défilement
wait_until="networkidle"+wait_for_timeout(4000)garantit que les graphiques asynchrones se chargentfull_page=Truecapture tout le contenu déroulable
2. Regarder l'image brute, puis rogner avec PIL
N'essayez PAS d'obtenir des rognages parfaits via le paramètre clip de Playwright. C'est peu fiable avec les captures de page complète.
from PIL import Image
img = Image.open("screenshot-raw.png")
cropped = img.crop((left, top, right, bottom)) # adjust based on what you see
cropped.save("screenshot-final.png")
- Prenez la capture brute
- Regardez-la pour voir les positions réelles en pixels
- Rognez avec PIL en fonction de ce que vous voyez
- Regardez le résultat — si ce n'est pas correct, re-rognez (instantané, pas besoin de nouvelle capture)
3. Itérer sur le rognage, pas sur la capture
- Re-capturer est lent (lancement du navigateur + chargement de la page + attente de rendu)
- Re-rogner est instantané (juste PIL)
- Obtenez une bonne capture brute, puis découpez-la de autant de façons que nécessaire
4. États interactifs
element = page.locator("selector").first
await element.hover()
await page.wait_for_timeout(1000) # let tooltip appear
await page.screenshot(path="screenshot-hover.png", full_page=True)
Pour l'état « sélectionné » sans effet de survol, éloignez la souris après le clic :
await element.click()
await page.mouse.move(300, 300) # move away so hover doesn't show
await page.wait_for_timeout(500)
await page.screenshot(path="screenshot-selected.png", full_page=True)
5. Captures spécifiques à une section
Rognez différentes sections d'une capture de page complète unique :
img.crop((0, 200, 920, 900)).save("screenshot-header.png")
img.crop((0, 900, 920, 1600)).save("screenshot-main.png")
Directives
- Toujours capturer l'état avant AVANT de faire des modifications — si vous oubliez, vous devez revenir en arrière le code pour obtenir une capture avant
- Les paires avant/après doivent utiliser la même largeur de fenêtre d'affichage et rognage — sinon la comparaison est inutile
- Pour obtenir un « avant » après avoir déjà modifié le code : utilisez
git checkout HEAD~1 -- <files>pour revenir en arrière, prenez une capture, puisgit checkout HEAD -- <files>pour restaurer - Pour les états interactifs : capturez avant ET après pour chaque état — ne supposez pas que l'avant « normal » couvre tous les cas
- Utilisez
device_scale_factor=1dans Playwright pour forcer les pixels 1x afin que les captures correspondent à ce que les utilisateurs voient au zoom 100 % - Les graphiques nécessitent un temps d'attente supplémentaire — Plotly, D3, etc. se rendent de manière asynchrone ; 4s minimum après networkidle
- Une fenêtre d'affichage étroite révèle les bugs de rendu — certains problèmes de bordure/alignement n'apparaissent qu'à des largeurs spécifiques
Captures d'écran d'applications non web
Pour les applications de bureau (VS, WPF, WinForms, applications console, terminaux) où Playwright ne peut pas accéder.
mss + ctypes (recommandé pour les fenêtres de bureau)
Trouvez une fenêtre par titre via l'API Win32, capturez sa région avec mss. Testé à ~33ms par capture.
import ctypes
from ctypes import c_int, Structure, byref, windll
import mss
from PIL import Image
user32 = windll.user32
def find_window(title_contains):
"""Find visible windows matching a title substring."""
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):
"""Capture a window by title substring."""
windows = find_window(title_contains)
if not windows:
raise ValueError(f"No window matching '{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
# Usage:
capture_window('Visual Studio Code', 'vscode-capture.png')
Prérequis : pip install mss pillow
Limitation : La fenêtre doit être visible (pas cachée par d'autres fenêtres ou minimisée).
Applications Electron (VS Code, etc.)
Node.js Playwright seulement — Python Playwright n'a pas d'API electron. Les captures se font via CDP (Chrome DevTools Protocol), pas depuis l'écran — fonctionne même si minimisée.
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');
// Minimize immediately — captures still work via CDP
await app.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0].minimize();
});
await window.screenshot({ path: 'capture.png' }); // works while minimized!
await app.close();
Critique : --user-data-dir=<temp> est requis sinon VS Code délègue à l'instance existante et le processus lancé se termine immédiatement.
Arbre de décision
| Scénario | Outil | Notes |
|---|---|---|
| Application web (localhost) | Playwright | Éprouvé, accès DOM complet |
| Application Electron (VS Code) | Playwright Electron (Node.js) | Fonctionne minimisée via CDP |
| Application de bureau, fenêtre visible | mss + ctypes (trouver par titre) | ~33ms par capture |
| Application de bureau, derrière des fenêtres | Windows Graphics Capture API | Configuration complexe, Win10 1903+ |
| Plein écran rapide | mss | ~68ms |
Limitations
- La capture web nécessite une application s'exécutant localement ou une URL accessible
- La capture de bureau (mss) nécessite que la fenêtre soit visible et sans obstruction
- La capture Electron nécessite Node.js Playwright (pas Python)
- Certaines SPA avec un rendu lourd côté client peuvent avoir besoin d'une logique d'attente personnalisée au-delà de networkidle