2

I'm going to do some low-level rendering stuff, but I need to know real display DPI for making everything of correct size.

I've found one way to do this: java.awt.Toolkit.getDefaultToolkit().getScreenResolution() — but it returns incorrect result on OS X with "retina" display, it's 1/2 of the real DPI. (In my case it should be 220, but it's 110)

So either some other, more correct API must be available, or alternatively I need to implement a hack just for OS X — somehow find if the current display is "retina". But I couldn't find any way to query for this information too. There's this answer but on my machine Toolkit.getDefaultToolkit().getDesktopProperty("apple.awt.contentScaleFactor") just returns null.

How can I do it?

Display Name
  • 8,022
  • 3
  • 31
  • 66

2 Answers2

3

Looks like it's currently possible to get it from java.awt.GraphicsEnvironment. Here's commented code example which does work on latest JDK (8u112).

// find the display device of interest
final GraphicsDevice defaultScreenDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();

// on OS X, it would be CGraphicsDevice
if (defaultScreenDevice instanceof CGraphicsDevice) {
    final CGraphicsDevice device = (CGraphicsDevice) defaultScreenDevice;

    // this is the missing correction factor, it's equal to 2 on HiDPI a.k.a. Retina displays
    final int scaleFactor = device.getScaleFactor();

    // now we can compute the real DPI of the screen
    final double realDPI = scaleFactor * (device.getXResolution() + device.getYResolution()) / 2;
}
Display Name
  • 8,022
  • 3
  • 31
  • 66
  • This is platform dependent. On Linux it's `X11GraphicsDevice` and even that's a `sun` class that's normally hidden from the user. – Mark Jeronimus Apr 28 '20 at 18:27
2

Here's an example adopted from @sarge-borsch that won't throw compile errors on Windows and Linux.

public static int getScaleFactor() {
    try {
        // Use reflection to avoid compile errors on non-macOS environments
        Object screen = Class.forName("sun.awt.CGraphicsDevice").cast(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice());
        Method getScaleFactor = screen.getClass().getDeclaredMethod("getScaleFactor");
        Object obj = getScaleFactor.invoke(screen);
        if (obj instanceof Integer) {
            return ((Integer)obj).intValue();
        }
    } catch (Exception e) {
        System.out.println("Unable to determine screen scale factor.  Defaulting to 1.");
    }
    return 1;
}
tresf
  • 7,103
  • 6
  • 40
  • 101
  • 1
    I used something similar in the end as well, just forgot to add it to the answer. – Display Name Nov 16 '18 at 12:54
  • 1
    You don’t need the `null`s, and you don’t need the `Class.forClass` expression either: you can invoke the reflection directly on the `getDefaultScreenDevice()` instance. But beware that the *default* screen isn’t the same as the *current* screen: it’s better to get the graphics configuration of the `Component` to be painted on, and go from there. That way, moving a dialog around on multiple screens will correctly update the rendering. – Konrad Rudolph Nov 28 '18 at 13:48
  • I noticed the compile warnings after posting this. Example updated without the `null` parameters. In regards to the "Default screen", my example is used for the system tray (no parent Component) so I think it to be correct for my (very specific) use-case. In regards to `Class.forClass`, I'm not sure what you are recommending so it may make sense as a new answer or an improvement to this one. – tresf Nov 28 '18 at 19:37
  • @tresf You simply don’t need the cast at all: just use `GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice()` directly. – Konrad Rudolph Nov 29 '18 at 11:38
  • Thanks. If used on a non-apple device, wouldn't it be more intuitive to the programmer if the exception message fails on Class.forName rather than Method.invoke? – tresf Nov 30 '18 at 14:01