11

Windows 8/10 has started to include a slider for how much GUI elements should scale, right click on desktop -> display. For a colleague with a laptop 4k-screen it's 250% while another colleague using the same resolution on a 4k 28" screen it's 150%.

How do I read that value programmatically? I need to adjust some graphics so it looks the same on all screens.

I'm working in Java on a Eclipse RCP application, but a way that uses C or C++ through JNI works too. I've been looking around but can't find anything.

dutt
  • 7,909
  • 11
  • 52
  • 85

7 Answers7

9

java.awt.Toolkit.getDefaultToolkit().getScreenResolution() see API

Returns the screen resolution in dots-per-inch.

Assumen your 100% is 96pixel you're able to calculate your scaling factor.

Christian
  • 1,664
  • 1
  • 23
  • 43
  • This is the only real solution, thank you indeed. All other solutions request JavaFX which is even more buggy. – Panayotis Nov 27 '18 at 23:39
  • 2
    Warning, this solution seems to always return `96dpi` on Java 8, regardless of the zoom factor. – tresf Jul 31 '19 at 17:12
  • @tresf Is this statement based on your experience or is it somewhere documented. For me it works with Java 8 (on Windows) tested since version u121. However I havn't tested the latest version u221 yet. – Christian Aug 02 '19 at 12:00
  • @ChristianLutz, 1.8.0 u211. `JNA found: 2.0; Toolkit found: 1.0`. Code is here: https://gist.github.com/tresf/00a8ed7c9860e3bd73cebf764a49789f. This may be environmental . I'm running the unit test under on Win10 Pro x64 u1903, but in a Parallels VM (Retina host allows the scaling in the guest). Strangely enough, running the same exact code from Java 11 returns `JNA found: 1.0; Toolkit found: 2.0`, so something is definitely off. – tresf Aug 06 '19 at 14:45
  • Just tested on physical hardware (Dell Spectre with 4K LCD, Win10 Pro x64) with identical results. 1.8.0 8u221. **JDK8**: `JNA found: 3.0, Toolkit found: 1.01`. **JDK11**: `JNA found: 1.0, Toolkit found: 3.0`. Edited my answer to warn about JDK11. – tresf Aug 06 '19 at 15:07
  • 2
    Just don't forget to cast res to double to prevent int/int approximation :-) `int res = java.awt.Toolkit.getDefaultToolkit().getScreenResolution(); double scale = (double)res/96;` – Stanislav Orlov Mar 05 '21 at 12:43
4

The following works for me. You need to get window actual device(for multi-monitor enviroment) and calc it's bound dimension agains display mode dimension.

public static double getWindowScale(Window window) {
    GraphicsDevice device = getWindowDevice(window);
    return device.getDisplayMode().getWidth() / (double) device.getDefaultConfiguration().getBounds().width;
}

public static GraphicsDevice getWindowDevice(Window window) {
    Rectangle bounds = window.getBounds();
    return asList(GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()).stream()

            // pick devices where window located
            .filter(d -> d.getDefaultConfiguration().getBounds().intersects(bounds))

            // sort by biggest intersection square
            .sorted((f, s) -> Long.compare(//
                    square(f.getDefaultConfiguration().getBounds().intersection(bounds)),
                    square(s.getDefaultConfiguration().getBounds().intersection(bounds))))

            // use one with the biggest part of the window
            .reduce((f, s) -> s) //

            // fallback to default device
            .orElse(window.getGraphicsConfiguration().getDevice());
}

public static long square(Rectangle rec) {
    return Math.abs(rec.width * rec.height);
}
  • This seems to give `2.0` when running under windows when set to 200% scaling and `0.5` when running at 200% scaling. – Charlie Nov 25 '21 at 23:15
1

Maybe this answer from here might help you:

[DllImport("gdi32.dll")]
static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
public enum DeviceCap
{
    VERTRES = 10,
    DESKTOPVERTRES = 117,

    // http://pinvoke.net/default.aspx/gdi32/GetDeviceCaps.html
}  


private float getScalingFactor()
{
    Graphics g = Graphics.FromHwnd(IntPtr.Zero);
    IntPtr desktop = g.GetHdc();
    int LogicalScreenHeight = GetDeviceCaps(desktop, (int)DeviceCap.VERTRES);
    int PhysicalScreenHeight = GetDeviceCaps(desktop, (int)DeviceCap.DESKTOPVERTRES); 

    float ScreenScalingFactor = (float)PhysicalScreenHeight / (float)LogicalScreenHeight;

    return ScreenScalingFactor; // 1.25 = 125%
}
Community
  • 1
  • 1
Seth Kitchen
  • 1,526
  • 19
  • 53
1

Adopting @seth-kitchen's example using JNA, this is possible, even on older JDKs like Java 8.

Note: The JNA portion of this technique doesn't work well on JDK11. A comment in the code explains how it will fallback to the Toolkit technique.

public static double getScaleFactor() {
    WinDef.HDC hdc = GDI32.INSTANCE.CreateCompatibleDC(null);
    if (hdc != null) {
        int actual = GDI32.INSTANCE.GetDeviceCaps(hdc, 10 /* VERTRES */);
        int logical = GDI32.INSTANCE.GetDeviceCaps(hdc, 117 /* DESKTOPVERTRES */);
        GDI32.INSTANCE.DeleteDC(hdc);
        // JDK11 seems to always return 1, use fallback below
        if (logical != 0 && logical/actual > 1) {
            return (double)logical/actual;
        }
    }
    return Toolkit.getDefaultToolkit().getScreenResolution() / 96.0d;
}

This above solution grabs the default display for simplicity purposes. You can enhance it to get the display of the current Window by finding the current Window handle (through Native.getComponentPointer(Component) or by title using User32.INSTANCE.FindWindow(...)) and then using CreateaCompatibleDC(GetDC(window)).

tresf
  • 7,103
  • 6
  • 40
  • 101
  • 1
    This code doesn't work properly because it casts everything to int. The values should all be float. In my case, the fallback solution worked, but only after I changed the return value to float, and cast both "Toolkit.getDefaultToolkit.getScreenResolution()" and "96.0" to float - then I get 1.25 as the result (my Windows is at 125% scaling). With all values as "int", the result is always 1.0. – MaeseDude Aug 06 '22 at 18:54
  • @MaeseDude thank you! Although your comment comes 3 years later, my code was still plagued by this rounding issue, only observable when fractional scaling was enabled (e.g. 250% instead of 200%). The example has been updated to use double instead. – tresf Aug 08 '22 at 15:20
  • Note, in the case of this example, `double` is likely overkill (since we're dealing with a scale factor 1.0x, 2.0x, 2.5x, 3.0x, 10.0x) but chosen to _better_ match the eventual `double` precision of `Point()`, which is often what the calculation is being cast to, without worry of accidental loss of precision. In practice, however, it's unlikely anything more precise than even a `float` will be needed for the foreseeable future, since even TEN conjoined 8K screens would not overflow an integer value in the mouse/windowing coordinate system. – tresf Aug 08 '22 at 15:40
1

Although the question asks about Windows scaling, the accepted solution(s), do not work properly with Linux running JDK11.

The below technique uses JNA to detect the screen scale factor on Linux, which is compatible with Linux desktops exposing the Gtk (Gimp Toolkit) libraries.

The snippet contains several Gtk version-dependent workarounds as the Gtk API exposing scaling information has changed several times. Furthermore, information may be inaccurate for specific use-cases (mixed standard and HiDPI monitors on the same desktop) as scaling is monitor-specific.

Warning: If Java fixes this behavior -- or equally if Gtk changes their API -- in a future version, this solution will need to be updated. Furthermore, this solution requires Linux-only components: reflection is required (included) for cross-compilation to succeed.

Usage: GtkUtilities.getScaleFactor()

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;

import java.lang.reflect.Method;

public class GtkUtilities {
    /**
    * Initializes Gtk2/3 and returns the desktop scaling factor, usually 1.0 or 2.0
    */
    public static double getScaleFactor() {
        GTK gtkHandle = getGtkInstance();
        if (gtkHandle != null && gtkHandle.gtk_init_check(0, null)) {
            System.out.println("Initialized Gtk");

            if (gtkHandle instanceof GTK2) {
                return getGtk2ScaleFactor((GTK2)gtkHandle);
            } else {
                return getGtk3ScaleFactor((GTK3)gtkHandle);
            }
        } else {
            System.err.println("An error occurred initializing the Gtk library");
        }
        return 0;
    }

    private static GTK getGtkInstance() {
        System.out.println("Finding preferred Gtk version...");
        switch(getGtkMajorVersion()) {
            case 2:
                return GTK2.INSTANCE;
            case 3:
                return GTK3.INSTANCE;
            default:
                System.err.println("Not a compatible Gtk version");
        }
        return null;
    }

    /**
    * Get the major version of Gtk (e.g. 2, 3)
    * UNIXToolkit is unavailable on Windows or Mac; reflection is required.
    * @return Major version if found, zero if not.
    */
    private static int getGtkMajorVersion() {
        try {
            Class toolkitClass = Class.forName("sun.awt.UNIXToolkit");
            Method versionMethod = toolkitClass.getDeclaredMethod("getGtkVersion");
            Enum versionInfo = (Enum)versionMethod.invoke(toolkitClass);
            Method numberMethod = versionInfo.getClass().getDeclaredMethod("getNumber");
            int version = ((Integer)numberMethod.invoke(versionInfo)).intValue();
            System.out.println("Found Gtk " + version);
            return version;
        } catch(Throwable t) {
            System.err.println("Could not obtain GtkVersion information from UNIXToolkit: {}", t.getMessage());
        }
        return 0;
    }

    private static double getGtk2ScaleFactor(GTK2 gtk2) {
        Pointer display = gtk2.gdk_display_get_default();
        System.out.println("Gtk 2.10+ detected, calling \"gdk_screen_get_resolution\"");
        Pointer screen = gtk2.gdk_display_get_default_screen(display);
        return gtk2.gdk_screen_get_resolution(screen) / 96.0d;
    }

    private static double getGtk3ScaleFactor(GTK3 gtk3) {
        Pointer display = gtk3.gdk_display_get_default();
        int gtkMinorVersion = gtk3.gtk_get_minor_version();
        if (gtkMinorVersion < 10) {
            System.err.println("Gtk 3.10+ is required to detect scaling factor, skipping.");
        } else if (gtkMinorVersion >= 22) {
            System.out.println("Gtk 3.22+ detected, calling \"gdk_monitor_get_scale_factor\"");
            Pointer monitor = gtk3.gdk_display_get_primary_monitor(display);
            return gtk3.gdk_monitor_get_scale_factor(monitor);
        } else if (gtkMinorVersion >= 10) {
            System.out.println("Gtk 3.10+ detected, calling \"gdk_screen_get_monitor_scale_factor\"");
            Pointer screen = gtk3.gdk_display_get_default_screen(display);
            return gtk3.gdk_screen_get_monitor_scale_factor(screen, 0);
        }
        return 0;
    }

    /**
    * Gtk2/Gtk3 wrapper
    */
    private interface GTK extends Library {
        // Gtk2.0+
        boolean gtk_init_check(int argc, String[] argv);
        Pointer gdk_display_get_default();
        Pointer gdk_display_get_default_screen (Pointer display);
    }

    private interface GTK3 extends GTK {
        GTK3 INSTANCE = Native.loadLibrary("gtk-3", GTK3.class);

        // Gtk 3.0+
        int gtk_get_minor_version ();

        // Gtk 3.10-3.21
        int gdk_screen_get_monitor_scale_factor (Pointer screen, int monitor_num);

        // Gtk 3.22+
        Pointer gdk_display_get_primary_monitor (Pointer display);
        int gdk_monitor_get_scale_factor (Pointer monitor);
    }

    private interface GTK2 extends GTK {
        GTK2 INSTANCE = Native.loadLibrary("gtk-x11-2.0", GTK2.class);

        // Gtk 2.1-3.0
        double gdk_screen_get_resolution(Pointer screen);
    }
}
tresf
  • 7,103
  • 6
  • 40
  • 101
1

Here's a "one liner" that should give you the scaling factor:

double scale = java.awt.GraphicsEnvironment
    .getLocalGraphicsEnvironment()
    .getDefaultScreenDevice() // or cycle your getScreenDevices()
    .getDefaultConfiguration()
    .getDefaultTransform()
    .getScaleX();

If you already have a java.awt.Component, you can use this instead:

double scale = myComponent.getGraphicsConfiguration().getDefaultTransform().getScaleX();

(based on this article)

MsxCowboy
  • 31
  • 3
0

This works on macOS and Windows with Java 17

public static Window getWindow(Component component) {
    return component instanceof Window ? (Window) component : getWindow(component.getParent());
}

@Override
public synchronized void paintComponent(Graphics g) {
    super.paintComponent(g);

    float displayScaling = (float) getWindow(this).getGraphicsConfiguration().getDefaultTransform().getScaleX();
}
Charlie
  • 8,530
  • 2
  • 55
  • 53