Skip to content

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

Description

@sratz

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:
    Image

  • Run the snippet

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

return Win32DPIUtils.pixelToPoint(getBoundsInPixels(), DPIUtil.getDeviceZoom());

should be replaced with something like

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
    • Windows
    • Linux
    • macOS
  1. Additional OS info (e.g. OS version, Linux Desktop, etc)

  2. JRE/JDK version

Version since
Tested against 4.41 I-builds.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions