fix(automation): capture screenshot.window from the composited screen

Blitting from the MainFrame's own wxClientDC clips out child HWNDs, so all of
OrcaSlicer's custom child-window controls (sidebar buttons/combos/panels) and the
GL canvas came back as uninitialized black bitmap memory. Read the window's
on-screen rectangle from the DWM-composited desktop via wxScreenDC instead, which
includes every child window, the OpenGL surface, and ImGui overlays.

Document the visible/unobscured requirement and the HiDPI logical-vs-physical
pixel caveat; clarify how screenshot.viewport3d differs and why it stays.
This commit is contained in:
SoftFever
2026-06-03 17:19:36 +08:00
parent 6980d9c327
commit 952696fd1f
3 changed files with 46 additions and 11 deletions

View File

@@ -267,7 +267,7 @@ Return a high-level application-state snapshot. Takes no parameters.
### `screenshot.window`
Capture a window's own GDI/native surface as a PNG.
Capture a window as a PNG, exactly as it appears on screen.
**Params:**
@@ -280,15 +280,31 @@ Capture a window's own GDI/native surface as a PNG.
**Errors:** `1005` on screenshot failure; `1001` if a supplied `target` is not
found or ambiguous.
**LIMITATION:** `screenshot.window` captures the window's **own GDI surface only**.
It does **not** capture the OpenGL 3D viewport, and it may not capture some native
child controls. To capture the 3D scene, use
[`screenshot.viewport3d`](#screenshotviewport3d).
**How it works:** the window's on-screen rectangle is read back from the
DWM-composited desktop framebuffer (`wxScreenDC`), so the capture includes every
native child control, the OpenGL 3D viewport, and ImGui overlays — it is a faithful
image of what the user sees. (Capturing the parent window's own client DC instead
would clip out child HWNDs and the GL surface, leaving them black; that is why this
method reads from the screen.)
**Caveats:**
- The window must be **visible and unobscured**. Because the source is the on-screen
framebuffer, any overlapping window occludes the captured region. The backend
raises the target window before capturing.
- **HiDPI:** the reported `width`/`height` come from the window's logical client size,
while the screen framebuffer is in physical pixels. On per-monitor-DPI displays the
two can differ; the capture may be cropped or scaled relative to the logical size.
- For a clean, occlusion-independent, arbitrary-resolution render of the 3D scene
(including when the 3D tab is not the visible view), use
[`screenshot.viewport3d`](#screenshotviewport3d) instead.
### `screenshot.viewport3d`
Render the active 3D plate offscreen and return it as a PNG. This is the correct
way to capture the 3D scene that `screenshot.window` cannot.
Render a 3D plate offscreen and return it as a PNG. Unlike `screenshot.window`, this
renders into an offscreen framebuffer, so it is independent of window size and
occlusion, works even when the 3D tab is hidden, and supports arbitrary output
resolution — making it the right choice for clean, deterministic captures.
**Params (all optional):**
@@ -475,6 +491,11 @@ Consequences and conventions:
- **Input is asynchronous.** Do **not** rely on fixed sleeps. Use
[`sync.wait_for`](#syncwait_for) — for example, wait for `btn_export` to become
`enabled` after slicing completes — rather than sleeping for a guessed duration.
- **`screenshot.window` reads the screen.** It captures the on-screen, DWM-composited
framebuffer, so the target window must be visible and unobscured, and the result is
in physical pixels (see HiDPI caveat under [`screenshot.window`](#screenshotwindow)).
For occlusion-independent 3D captures use
[`screenshot.viewport3d`](#screenshotviewport3d).
- **Single-client / serialized.** v1 handles one request at a time; issue requests
sequentially from a single client.
- **GUI-thread marshaling.** Every backend call is marshaled onto the GUI thread

View File

@@ -17,7 +17,7 @@
#include <wx/choice.h>
#include <wx/checkbox.h>
#include <wx/uiaction.h> // wxUIActionSimulator (synthetic mouse/keyboard)
#include <wx/dcclient.h> // wxClientDC
#include <wx/dcscreen.h> // wxScreenDC
#include <wx/dcmemory.h> // wxMemoryDC
#include <wx/mstream.h> // wxMemoryOutputStream
@@ -296,10 +296,24 @@ PngImage WxUiBackend::screenshot_window(const UiNode* target) {
const wxSize sz = win->GetClientSize();
if (sz.x <= 0 || sz.y <= 0)
throw AutomationError(kErrScreenshotFail, "window has no client area");
// Capture from the on-screen (DWM-composited) framebuffer rather than the
// window's own client DC. A parent client DC clips out child HWNDs, so all of
// OrcaSlicer's custom child-window controls (sidebar buttons/combos/panels) and
// the GL canvas come back as uninitialized (black) bitmap memory. wxScreenDC
// reads the composited desktop, which includes every child window, the OpenGL
// surface, and ImGui overlays — pixel-perfect to what the user sees.
//
// Requirement: the window must be visible and unobscured (see doc/automation.md
// §platform caveats); the backend raises it before injecting input anyway.
// HiDPI note: GetClientSize is in logical units while wxScreenDC is in physical
// pixels; on per-monitor-DPI setups the captured size may differ from the logical
// client size (documented caveat, acceptable for v1).
win->Raise();
const wxPoint origin = win->ClientToScreen(wxPoint(0, 0));
wxBitmap bmp(sz.x, sz.y);
wxClientDC dc(win);
wxScreenDC sdc;
wxMemoryDC mdc(bmp);
mdc.Blit(0, 0, sz.x, sz.y, &dc, 0, 0);
mdc.Blit(0, 0, sz.x, sz.y, &sdc, origin.x, origin.y);
mdc.SelectObject(wxNullBitmap);
return wximage_to_png(bmp.ConvertToImage());
});

View File

@@ -59,7 +59,7 @@ def main() -> int:
with open("window.png", "wb") as f:
f.write(orca.screenshot())
with open("preview_3d.png", "wb") as f:
f.write(orca.screenshot_3d(width=1024, height=768))
f.write(orca.screenshot_3d(width=1920, height=1080))
print("wrote window.png and preview_3d.png")
return 0
finally: