ITADN

[Win32] [Hi-DPI] Non-deterministic Display.getBounds() / no way to create a consistent screenshot without internal API

#3530Opensratz 创建于 12 天前
S
sratzcommented
**Describe the bug** The behavior of `Display.getBounds()` is currently non-deterministic, as this internally uses `DPIUtil.getDeviceZoom()`, which means _the autoscaling zoom for the shell whose zoom has last been changed (via moving the shell to another monitor or changing the zoom of a monitor)_. Due to this it's pretty much impossible to deterministically create a screenshot of the whole `Display` (all monitors). **To Reproduce** - Set up a multi-monitor setup as follows: <img width="701" height="205" alt="Image" src="https://github.com/user-attachments/assets/565a59f3-c3d4-4eaa-af3b-4d048e07d680" /> - Run the snippet ```java import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageLoader; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.internal.DPIUtil; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Monitor; import org.eclipse.swt.widgets.Shell; public final class ScreenshotProblemDisplayBounds { public static void main(String[] args) throws IOException { Display display = Display.getDefault(); Shell shell = new Shell(); shell.setSize(300, 200); shell.setLayout(new GridLayout(1, false)); Button buttonPrintBounds = new Button(shell, SWT.NONE); buttonPrintBounds.setText("Print Bounds"); buttonPrintBounds.addSelectionListener(SelectionListener.widgetSelectedAdapter(event -> { System.out.println("display bounds: " + display.getBounds()); System.out.println("shell bounds: " + shell.getBounds()); Monitor primaryMonitor = display.getPrimaryMonitor(); for (int i = 0; i < display.getMonitors().length; i++) { Monitor monitor = display.getMonitors()[i]; System.out.println("monitor" + i + " bounds: " + monitor.getBounds() + " zoom: " + monitor.getZoom() + (Objects.equals(monitor, primaryMonitor) ? " [primary]" : "")); } })); Button buttonScreenshot1 = new Button(shell, SWT.NONE); buttonScreenshot1.setText("Screenshot (without workaround)"); Button buttonScreenshot2 = new Button(shell, SWT.NONE); buttonScreenshot2.setText("Screenshot (with workaround)"); SelectionListener widgetSelectedAdapter = SelectionListener.widgetSelectedAdapter(event -> { Path tempFile; try { tempFile = Files.createTempFile("screenshot", ".png"); } catch (IOException e) { throw new IllegalStateException(e); } Rectangle rectangle = display.getBounds(); int factor = 1; if ("win32".equals(SWT.getPlatform()) && event.getSource() == buttonScreenshot2) { // display.getBounds(); on windows currently is non-deterministic, but uses DPIUtil.getDeviceZoom(); internally // which itself depends refers to the "zoom of the last Shell that changed zoom" // but gc.copyArea() lateron works with the zoom of the primary monitor. // we have to workaround that factor = DPIUtil.getDeviceZoom() / display.getPrimaryMonitor().getZoom(); } Image image = new Image(display, rectangle.width * factor, rectangle.height * factor); GC gc = new GC(display); gc.copyArea(image, rectangle.x * factor, rectangle.y * factor); ImageData imageData = image.getImageData(); image.dispose(); gc.dispose(); ImageLoader imageLoader = new ImageLoader(); imageLoader.data = new ImageData[] { imageData }; imageLoader.save(tempFile.toAbsolutePath().toString(), SWT.IMAGE_PNG); System.out.println(tempFile + " (" + imageData.width + "x" + imageData.height + ")"); }); buttonScreenshot1.addSelectionListener(widgetSelectedAdapter); buttonScreenshot2.addSelectionListener(widgetSelectedAdapter); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) { display.sleep(); } } display.dispose(); } } ``` - Move the opened shell to the primary (100%) monitor. - Press _Print Bounds_ / _Screenshot (without workaround)_ / _Screenshot (with workaround)_ buttons once each - Move the opened shell to the secondary (200%) monitor. - Press _Print Bounds_ / _Screenshot (without workaround)_ / _Screenshot (with workaround)_ buttons once each again Check console: ``` display bounds: Rectangle {0, 0, 5360, 1440} shell bounds: Rectangle {1559, 498, 300, 200} monitor0 bounds: Rectangle {3440, 236, 960, 600} zoom: 200 monitor1 bounds: Rectangle {0, 0, 3440, 1440} zoom: 100 [primary] C:\Users\user\AppData\Local\Temp\screenshot925734453198280273.png (5360x1440) C:\Users\user\AppData\Local\Temp\screenshot287834727171878990.png (5360x1440) display bounds: Rectangle {0, 0, 2680, 720} shell bounds: Rectangle {3822, 403, 300, 200} monitor0 bounds: Rectangle {3440, 236, 960, 600} zoom: 200 monitor1 bounds: Rectangle {0, 0, 3440, 1440} zoom: 100 [primary] C:\Users\user\AppData\Local\Temp\screenshot64546334595754013261.png (2680x720) C:\Users\user\AppData\Local\Temp\screenshot14362251863936518615.png (5360x1440) ``` When using the _Screenshot (without workaround)_ button, while the little helper program's shell is currently on the 2nd monitor, the screenshot created is only half the expected size and therefore cut-off. **Expected behavior** Screenshots should always be created with size 5360x1440, covering the whole virtual screen area. `Display.getBounds()` should return consistent values - no matter which Shell was last moved where, as the `Display` logically comprises **all** monitors and there is no notion of such a last-used Shell in that context. Maybe https://github.com/eclipse-platform/eclipse.platform.swt/blob/3db6f5d5ecf81d5b217f3af56ddeb09d0ae64b25/bundles/org.eclipse.swt/Eclipse%20SWT/win32/org/eclipse/swt/widgets/Display.java#L1595 should be replaced with something like ```java return Win32DPIUtils.pixelToPoint(getBoundsInPixels(), getPrimaryMonitor().getZoom()); ``` to be consistent and compatible with the coordinate system used by `GC.copyArea()`? The workaround applied in the snippet uses internal API. **Environment:** 1. Select the platform(s) on which the behavior is seen: - - [ ] All OS - - [x] Windows - - [ ] Linux - - [ ] macOS 2. Additional OS info (e.g. OS version, Linux Desktop, etc) 3. JRE/JDK version **Version since** Tested against 4.41 I-builds.
2 条评论