Important (2015-06-30): Starting with Java 8u60, JavaFX provides WPF-like DPI scaling on Windows by implicitly scaling all coordinates and image sizes to the current system DPI setting. So the manual scaling workaround described below is now only required for previous versions. The original post follows but now applies only to Java 8u45 and older. End of addendum
JavaFX is one of the GUI frameworks covered in my overview, DPI Scaling in Windows GUIs. As noted there, JavaFX 2.2 is partly but not fully suited for high DPI scaling on Windows. The default font and all standard controls are automatically scaled whereas explicitly specified sizes are not, with the exception of (some?) CSS em sizes. This post shows how Oracle’s own Windows samples break in high DPI mode, and how to work around this defect until perhaps one day JavaFX scales its coordinate system.
Oracle’s Layout Samples
Oracle’s official JavaFX documentation includes Joni Gordon’s three-part tutorial, Working With Layouts in JavaFX. The code showcased there is typical for JavaFX sample code you can find on the web: explicit sizes in Java or CSS are mixed freely (and thoughtlessly) with default-sized fonts and self-sized controls. We’ll pick the second and third tutorial to demonstrate why this is a bad idea.
Procedure. The following is based on the June 2013 revision of Oracle’s tutorials. I downloaded the ZIP archives for each tutorial and recompiled the source code in JavaFX 2.2 without changes, except to complete various tutorial steps. Then I ran each program at four different DPI settings on my Windows 8 system and took the screenshots you see below. The DPI settings are all standard ones:
- 100% (96 DPI) — same as Joni Gordon’s, except for different window decoration since she was using Windows 7. This verifies that I’m indeed running the same programs with the same results, so any layout errors are due to DPI scaling.
- 125% (120 DPI) — approximately the physical dot pitch of my Dell 2711 desktop monitor, and already a very common setting today.
- 150% (144 DPI) — what I’m actually using on my monitor for comfortable viewing at my preferred distance, and also the default setting for Microsoft’s Surface Pro tablet. Necessary to accommodate modern high-resolution tablet screens.
- 200% (192 DPI) — trumpeted for the upcoming Windows 8.1 but actually available since Windows 7. Expect people to use this setting on high-resolution tablets, too.
All JavaFX applications register themselves as DPI-aware on Windows, so they always run with XP style scaling and never use Vista DPI virtualization, regardless of whether XP style scaling is forced or not. Please read High DPI Settings in Windows if you don’t understand the last sentence. The bottom line is that there are no Windows settings to work around JavaFX layout failures at high DPI settings. The developers must fix their programs.
First Sample. The first sample is taken from the second tutorial, Tips for Sizing and Aligning Nodes. Here all controls are sized in Java code, with some inline CSS for button fonts. The unchanged sample code produces “Figure 2-2 Desired Sizes” on the tutorial page. Observe how the 96 DPI version looks great, the 120 DPI version starts misarranging buttons, the 144 DPI version has ugly overlaps, and the 192 DPI version is entirely unusable.
What happened? The
Scene size was explicitly set to 300×400 pixels. While the buttons scaled with the Windows default font, the
Scene and its nested containers could not grow to accommodate the growing buttons. Also note that the “Exit” label was explicitly set to 15pt – intended as 25% bigger than the default font, but actually much smaller at high DPI.
Second Sample. The second sample is taken from the third tutorial, Styling Layout Panes with CSS. I completed all suggested changes to the CSS stylesheet to produce “Figure 3-3 Styled with Images,” the last picture on the tutorial page. Once again, the 96 DPI version looks great and then everything falls apart.
- The fixed width of the two top buttons eventually cuts off their labels to the point of becoming incomprehensible.
- The text on the picture buttons to the right starts out barely smaller than Windows-rendered text, but ends up downright microscopic. If the regular button text is close to the smallest acceptable size, the picture button text will be unreadable.
- The cloud pattern behind the two rotated buttons at the bottom was originally sized to exactly fit them. As the buttons grow larger, they protrude beyond the pattern and clouds appear between the buttons rather than around them.
Scalable JavaFX Layout
To reiterate, all of the above layout failures are the result of mixing explicit sizes with default sizes. JavaFX automatically scales its default font and standard controls, but not explicitly specified positions and sizes. That means you must scale them manually, based on the JavaFX default font size. The idea is similar to the concept of root em sizing in HTML/CSS. To obtain a scalable page layout, you define all positions and sizes in terms of “root em” (rem), the em size of the default font. Obtaining a JavaFX application’s root em is easy:
final double rem = javafx.scene.text.Font.getDefault().getSize();
That’s the default font size in typographical points (1/72″, CSS pt). You can obtain this value whenever you need it, or cache it since it won’t change while the application is running. (That is, until JavaFX supports the new dynamic per-monitor scaling in Windows 8.1.) Now, instead of supplying explicit pixel measures to positioning and sizing methods, you specify fractions or multiples of
rem, like so:
// scalable version of 300x400 scene, assuming rem=12 at 100%
Scene scene = new Scene(tabs, 25 * rem, 33.33 * rem);
2013-09-08: As Cay Horstmann suggests below, it’s probably smarter to define
rem in terms of pixels rather than points. This requires some additional rounding to obtain integral sizes on Windows, however. The alternative pixel-based definition looks like this:
final double rem = Math.rint(new Text("").getLayoutBounds().getHeight());
CSS and FXML
What about CSS? JavaFX exposes only a fraction of its sizing & positioning properties in CSS, and pixel or point sizes (px/pt) are never scaled anyway. Em sizes do appear to scale to the current default font size, but Oracle makes no guarantees to this effect. I recall em sometimes producing a hard-coded 12pt rather than the current default font size – possibly the behavior depends on when the style is loaded or applied. The safest route is to either specify no sizes in CSS, or else to calculate the necessary styles at runtime using
Node.setStyle with the current root em. Alternatively, you could prepare multiple stylesheets at different scales and select one at runtime, Android-style.
What about FXML? No help there either, as any coordinates are simply passed through to the corresponding Java methods, meaning they are interpreted as unscaled pixels.
Moreover, FXML does not support computational coordinate scaling or em equivalents. You could create multiple FXML files at different scales and select one at runtime – or avoid specifying any layout measures in FXML.
2013-08-17: As John Smith points out below, you can use computed expressions for FXML coordinates! See his example in the OpenJDK discussion thread FXML and high dpi screens. So you should be able to scale FXML layouts by root em without too much hassle.
The Road Ahead?
Although JavaFX is certainly better suited to scalable GUIs than Swing, thanks to its scaled default font and intelligently self-sizing controls, it’s still rather lagging behind the state of the scaling art. Microsoft’s Windows Presentation Foundation (WPF) scales all coordinates, whether in code or XAML, and the new WinRT API maps different pixel densities to a standardized virtual screen size. Apple OS-X and Android both offer dual APIs so that developers can use either scaled coordinates for portable layout or unscaled coordinates for precise drawing, at their discretion. Apple had customized Java 1.6 rendering to provide high DPI support in Swing, but JavaFX 2.2 still offers nothing comparable.
Planned for Spring 2014, JavaFX 8 and its new Modena theme will finally support Apple’s “retina” displays. Android support is currently being prototyped at Oracle but there’s no release date that I’m aware of. Back in December 2012, Richard Bair promised that JavaFX will “in the future” replicate Apple’s dual API so that developers can opt into or out of coordinate scaling as necessary. I certainly hope that this feature will be available for all platforms, including Windows, and sooner rather than later. To put it bluntly, a single unscaled coordinate system is not good enough for a GUI framework in 2013.
2013-09-11: Looks like Christmas came early for Apple users. The current Java SE 7u40 claims to already support Apple Retina displays in both Swing/AWT and JavaFX.