With JDK 9 my swing app works well on Windows with 4k highdpi and
normal 1080p normal dpi. Labels, Comboboxes etc. all look nice and are
scaled up on the 4k screen.
But so is my JPanel where i draw custom images. Can i disable
the scaling for this one JPanel to handle drawing myself? I am using
apache-commons bicubic interpolation to draw more details on the higher
unscaled resolution, but as it is scaled out of the box, i just have the "normal" dimensions to draw.
Kind regards
The scaling in Java 9 seems to work like this: Your paint(Component)() methods receive a Graphics2D object which is already scaled. Additionally, the component sizes (e.g. myJFrame.setSize(), myJPanel.getWidth()) are scaled invisibly to the program, meaning that when you say setSize(800,600) on a 200% desktop, the component will be 1600x1200 but getWidth/getHeight will return 800/600.
Can i disable the scaling for this one JPanel to handle drawing myself?
To "reset" your Graphics object to scaling 1, do this:
final Graphics2D g = (Graphics2D) graphics;
final AffineTransform t = g.getTransform();
final double scaling = t.getScaleX(); // Assuming square pixels :P
t.setToScale(1, 1);
g.setTransform(t);
To get the correct dimensions, e.g. for filling the whole background with blackness before drawing:
final int w = (int) Math.round(getWidth() * scaling);
If you do it like this, you should get the desired result on Java 9 and Java 8.
I just created a class for Java devs who strive for a more custom Component design and/or raw drawing, where the system's display scaling should be known and manual scaling is often necessary. It should solve all scaling problems on Java 8 and Java 9. Here it is:
import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
/**
* TL;DR:
* <p>
* Call GUIScaling.initialize() at application start on the Swing thread.
* <p>
* If you set your own Component font sizes or border sizes or window sizes, multiply them by
* GUIScaling.GUISCALINGFACTOR_COMPONENTSANDFONTS and/or use the helper methods newDimension() and scaleForComponent().
* Works on Java 8 and 9.
* <p>
* If you do your own custom graphics and want to have control down to the actual pixel, create an instance of
* GUIScalingCustomGraphics to obtain your Graphics2D at scaling 1 and your component's true physical width and height
* (Which Java 9 reports differently!), and scale all your graphics using GUIScaling.GUISCALINGFACTOR_CUSTOMGRAPHICS
* and/or use the helper method scaleForCustom(). The helper method scaleForRealComponentSize() can transform your mouse
* coordinates to the real physical coordinate, which Java 9 reports differently!
* <p>
* <p>
* <p>
* <p>
* <p>
* <p>
* <p>
* <p>
* <p>
* <p>
* GUIScaling class v[1, 2017-10-08 11!00 UTC] by dreamspace-president.com
* <p>
* This Swing class detects the system's display scaling setting, which is important to make your GUI and custom
* graphics scale properly like the user wants it. On a 4K display, for example, you'd probably set 200% in your
* system.
* <p>
* Not tested with Java less than 8!
* <p>
* On Java 8 (and with most but not all (e.g. no the default) LooksAndFeels), component sizes (e.g. JButton) and their
* font sizes will scale automatically, but if you have a certain border width in mind, or decided for a certain min and
* default window size or a certain font size, you have to upscale those on a non-100%-system. With this class, just
* multiply the values with GUISCALINGFACTOR_COMPONENTSANDFONTS. Done. newDimension() and scaleForComponent() help with
* that.
* <p>
* On Java 9, component sizes and their font sizes DO NOT SCALE from the perspective of the application, but in reality
* they are scaled: A window set to 800x600 size will really be 1600x1200, but it will still report half this size when
* asked. A border of 50 pixels will really be 100 pixels. A Graphics2D object (paint method etc.) will have a scaling
* of 2! (Not if you just create a BufferedImage object and do createGraphics(), the scale here will be 1.) So, you
* don't have to bother with GUI scaling here at all. YOU CAN STILL USE GUISCALINGFACTOR_COMPONENTSANDFONTS, because
* this class will set it to 1 on Java 9. This is detected by indeed checking the scaling of a Graphics2D object. So,
* your Java 8 and 9 component/font code will be exactly the same in regards to scaling.
* <p>
* CUSTOM GRAPHICS: If you do your own painting and want to insist on true physical pixels (in which case obviously
* you'd have to scale your fonts with GUISCALINGFACTOR_CUSTOMGRAPHICS instead of GUISCALINGFACTOR_COMPONENTSANDFONTS),
* on Java 9 you have to reset the scaling of the Graphics2D object the paint(Component)() method gives you from 2 to 1,
* and (also Java 9) you have to adjust the width/height reported by your component. Both is done by making an instance
* of GUIScalingCustomGraphics. You can do this blindly on Java 8 and 9, your code will stay the same. And then, apply
* this class' GUISCALINGFACTOR_CUSTOMGRAPHICS to scale everything according to system settings. Or, instead of
* insisting on true physical pixels, you could trust Java 9 and not mess with the initial scaling - but then you'd have
* to distinguish whether you're dealing with Java 8 or 9, because on 8, you'd still have to scale your custom graphics.
* In case you decide for this, use GUISCALINGFACTOR_COMPONENTSANDFONTS for your custom graphics instead of
* GUISCALINGFACTOR_CUSTOMGRAPHICS because the former will be ***1*** on Java 9 but will be proper (e.g. 2.0 for a 200%
* system) on Java 8.
* <p>
* A weird problem that comes with Java 9: If you use the mouse coordinates as reported by the system (instead of, say,
* quasi-fix the physical mouse pointer invisibly at the screen center and make your own pointer based on coordinate
* differences), you will have HALF THE USUAL RESOLUTION. On Java 8, a 3840x2160 screen will give you according mouse
* coordinates, but on Java 9, you get half these coordinates (if the system is set to scaling 200%). While
* scaleForRealComponentSize() helps correct this, a custom drawn mouse pointer will now step in 2 pixel distances, it
* can not reach every individual pixel any longer. I wish they had updated the MouseEvent class accordingly with
* additional float methods.
*/
final public class GUIScaling { // INITIAL TOUCHING of this class MUST be on Swing thread!
/**
* Call this at the start of your application ON THE SWING THREAD. This initializes the class and hence its values.
*/
public static void initialize() {
System.err.println(""); // To make sure an obfuscator doesn't remove this method and its calls.
}
/**
* By calling this, you ALSO initialize the class, so you don't HAVE TO use initialize() in that case (but it really
* doesn't matter). And you can indeed set a LookAndFeel of your choice, even though initialization of this class
* also sets AND TEMPORARILY USES a LookAndFeel.
*
* @param intendedLAFIs ANYTHING, but ideally a LookAndFeel name or several. The first value that equalsIgnoreCase
* an installed LookAndFeelInfo.getName() will be used.
*/
public static void setLookAndFeel(final String... intendedLAFIs) {
if (intendedLAFIs != null) {
final UIManager.LookAndFeelInfo[] installedLAFIs = UIManager.getInstalledLookAndFeels();
LAFILOOP:
for (String intendedLAFI : intendedLAFIs) {
for (final UIManager.LookAndFeelInfo lafi : UIManager.getInstalledLookAndFeels()) {
if (lafi.getName().equalsIgnoreCase(intendedLAFI)) {
try {
UIManager.setLookAndFeel(lafi.getClassName());
break LAFILOOP;
} catch (Exception e) {
continue LAFILOOP;
}
}
}
}
}
}
/**
* Convenience method, compatible with Java 8 and 9.
*/
public static Dimension newDimension(final int w, final int h) {
return new Dimension(scaleForComponent(w), scaleForComponent(h));
}
/**
* @param x E.g. the width of a component, or the size of a border.
* @return x scaled by the necessary display scaling factor for components and fonts, compatible with Java 8 and 9.
*/
public static int scaleForComponent(final double x) {
return (int) Math.round(x * GUISCALINGFACTOR_COMPONENTSANDFONTS);
}
/**
* @param x E.g. the width of a rectangle being drawn in a paint() or paintComponent() override.
* @return x scaled by the necessary display scaling factor for custom graphics, compatible with Java 8 and 9.
*/
public static int scaleForCustom(final double x) {
return (int) Math.round(x * GUISCALINGFACTOR_CUSTOMGRAPHICS);
}
/**
* @param x E.g. the width as reported by a component.
* @return x scaled so that it represents real physical pixels, compatible with Java 8 and 9.
*/
public static int scaleForRealComponentSize(final double x) {
return (int) Math.round(x * GUISCALINGFACTOR_REALCOMPONENTSIZE);
}
/**
* For Java 9, but can blindly be used in Java 8, too. Ensures that the scaling of a paint(Component)()'s Graphics2D
* object is 1. Conveniently does the usual casting, too.
* <p>
* Also calculates the physical pixel width/height of the component, which is reported differently on Java 9 if the
* display scaling is not 100%.
*/
final public static class GUIScalingCustomGraphics {
final public Component component; // Just for convenience. You can hand the whole instance down your paint call hierarchy.
final public int w; // The physical pixel width of the component.
final public int h; // dto. height
final public Graphics2D g; // Scale will be 1, even on Java 9 with a non-100% display scaling.
/**
* @param component NOT NULL. The component (e.g. JPanel or JFrame) whose paint() method you're overriding.
* @param graphics NOT NULL. The Graphics argument given to your paint() method.
*/
public GUIScalingCustomGraphics(final Component component, final Graphics graphics) {
this.component = component;
w = scaleForRealComponentSize(component.getWidth());
h = scaleForRealComponentSize(component.getHeight());
g = (Graphics2D) graphics;
final AffineTransform t = g.getTransform();
t.setToScale(1, 1);
g.setTransform(t);
}
}
final private static double JBUTTONFONTSIZE_ON_100PERCENTSCALE_JAVA8_W10_WITH_LOOKANDFEEL_WINDOWSORSYSTEMORXPLATFORMORWINCLASSIC = 11.0;
final public static double GUISCALINGFACTOR_SYSTEM; // The scaling set in the system.
final public static double GUISCALINGFACTOR_COMPONENTSANDFONTS; // The scaling necessary if you set component/font sizes yourself.
final public static double GUISCALINGFACTOR_CUSTOMGRAPHICS; // The scaling necessary if you want your custom graphics, too, to be scaled according to System settings.
final public static double GUISCALINGFACTOR_REALCOMPONENTSIZE; // The factor by which getWidth() and such return values have to be multiplied, because Java 9 reports them differently.
static {
// The last three (Nimbus etc.) DO NOT automatically scale their font sizes with the system's GUI scaling,
// so using the font size in those cases to derive the scaling WILL FAIL.
// Btw., the JButton font size at 100% Windows 10 system scaling is 11.0 in all cases but the last three.
GUIScaling.setLookAndFeel("Windows", UIManager.getSystemLookAndFeelClassName(), UIManager.getCrossPlatformLookAndFeelClassName(), "Windows Classic", "Nimbus", "Metal", "CDE/Motif");
final float jButtonFontSize_on_unknownScale_unknownJava_unknownOS_withLookAndFeelWindows = new JButton().getFont().getSize2D(); // 21.0 on 200% desktop on Java 8 // 11.0 on 100% desktop on Java 8
final Integer[] paintScalingInPercent = new Integer[1];
final JDialog bruteForceJava9ScalingCheck = new JDialog((Frame) null, "", true) {
{
setLocation(-1000, -1000); // Outamysight!
final Runnable fallbackInCaseOlderJavaVersionDoesNotEndUpClosingThisWindow = () -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
SwingUtilities.invokeLater(() -> {
paintScalingInPercent[0] = 100;
dispose();
});
};
final Thread t = new Thread(fallbackInCaseOlderJavaVersionDoesNotEndUpClosingThisWindow);
t.setDaemon(true);
t.setName("GUI scaling detector fallback thread");
t.start();
}
@Override
public void paint(final Graphics graphics) {
final Graphics2D g = (Graphics2D) graphics;
final AffineTransform originalTransform = g.getTransform();
paintScalingInPercent[0] = (int) Math.round(originalTransform.getScaleX() * 100);
dispose();
}
};
bruteForceJava9ScalingCheck.setVisible(true); // This call blocks until dispose() is reached.
if (paintScalingInPercent[0] == null) {
throw new Error("Unexpected behavior: Modal dialog did not block!");
} else if (paintScalingInPercent[0] != 100) {
GUISCALINGFACTOR_SYSTEM = paintScalingInPercent[0] * 0.01;
GUISCALINGFACTOR_COMPONENTSANDFONTS = 1; // Java 9 does everything. The developer's considerations are made unnecessary/harmless by this "1".
GUISCALINGFACTOR_CUSTOMGRAPHICS = GUISCALINGFACTOR_SYSTEM;
} else {
final double factorPreliminary = jButtonFontSize_on_unknownScale_unknownJava_unknownOS_withLookAndFeelWindows / JBUTTONFONTSIZE_ON_100PERCENTSCALE_JAVA8_W10_WITH_LOOKANDFEEL_WINDOWSORSYSTEMORXPLATFORMORWINCLASSIC;
// If we just divide the two, we get 1.454545... on a 150% desktop, because the font sizes
// chosen by Java are integer values, so we experience a rounding error.
// The crappy but probably in most cases nicely working solution is: We round the result to .25 steps!
GUISCALINGFACTOR_SYSTEM = Math.round(factorPreliminary * 4) / 4d;
GUISCALINGFACTOR_COMPONENTSANDFONTS = GUISCALINGFACTOR_SYSTEM;
GUISCALINGFACTOR_CUSTOMGRAPHICS = GUISCALINGFACTOR_SYSTEM;
}
GUISCALINGFACTOR_REALCOMPONENTSIZE = GUISCALINGFACTOR_CUSTOMGRAPHICS / GUISCALINGFACTOR_COMPONENTSANDFONTS;
System.err.println("GUISCALINGFACTOR_SYSTEM = " + GUISCALINGFACTOR_SYSTEM);
System.err.println("GUISCALINGFACTOR_COMPONENTSANDFONTS = " + GUISCALINGFACTOR_COMPONENTSANDFONTS);
System.err.println("GUISCALINGFACTOR_CUSTOMGRAPHICS = " + GUISCALINGFACTOR_CUSTOMGRAPHICS);
System.err.println("GUISCALINGFACTOR_REALCOMPONENTSIZE = " + GUISCALINGFACTOR_REALCOMPONENTSIZE);
}
}