DPI Scaling in Windows GUIs

Microsoft had introduced a mechanism for dealing with high pixel densities (dots per inch, DPI) in Windows XP, and added another one in Windows Vista. Both are described in High DPI Settings in Windows. Vista’s new DPI virtualization barely looks acceptable even on modern monitors, and is only intended as a workaround for legacy software anyway.

Unless you are targeting the new Universal Windows Platform you will have to use XP-style scaling. Here the scaling of all user-drawn content is up to you, or rather your GUI framework. On this page I’ve collected samples from four GUI frameworks based on .NET and Java to demonstrate how they deal with XP style DPI scaling – or not.

Scaling Methods

Windows XP style DPI scaling enlarges the default system fonts and other system UI elements in response to a higher DPI setting, but always maps drawing coordinates to physical screen pixels. That leaves GUI frameworks three methods to deal with varying DPI settings.

  1. Adopt the scaled system font size, and internally scale all drawing coordinates as well. This approach completely hides the fact that any scaling takes place at all. Applications automatically display correctly at any DPI setting. WPF and to a lesser degree Windows Forms support this method, as well as JavaFX (since Java SE 8u60) and AWT/Swing (since Java SE 9).
  2. Adopt the scaled system font size, but do not scale drawing coordinates. This approach requires either manually scaling all drawing calls, or else relying entirely on UI components that automatically adapt to the current font size. JavaFX (prior to Java SE 8u60) quite successfully implemented the latter option, and to a lesser degree Swing’s System theme (prior to Java SE 9).
  3. Adopt nothing, scale nothing. Use hard-coded pixel sizes for all default fonts. This is what other Java Swing themes did prior to Java SE 9. The advantage is that the application layout is always correct. The disadvantage is that it’s unusably small at high DPI settings.

If you’re only targeting desktop Windows, your best option to handle diverse DPI settings is the Windows Presentation Foundation (WPF). For those cases where this isn’t possible, the rest of this page examines how other popular options compare to the WPF gold standard.

Note — The two enlarged modes showcased below are 120 DPI (125%) and 144 DPI (150%). 120 DPI defaults to XP style scaling while 144 DPI defaults to DPI virtualization. However, all tested GUI frameworks (surprisingly even Java 8 Swing) declare themselves DPI-aware and so use XP style scaling anyway. Windows Forms requires an extra manifest for DPI awareness but that’s typically present in real-world programs, so I added it to my test suite as well.

Test Programs

A suite of small test programs demonstrates the scaling behavior of the four standard .NET and Java GUI frameworks. There are two download packages (ZIP archives) with the precompiled executables and their complete source code. Please refer to the enclosed ReadMe.txt files and the various batch files for the required development tools and expected file paths. The two packages differ in their target environments:

There are two download packages and corresponding sets of screenshots because the DPI scaling of Java AWT/Swing changed massively (and that of JavaFX slightly) in Java 9. Some Windows Forms scaling improvements arrived in .NET 4.7, but they failed to affect my tests.

The sample screenshots on this page are quite large and appear scaled down to your viewport size. Click or tap each image to see a full-size version in a new browser window.

Application Windows

Each application window is set to size itself automatically to its content. That content is a 3×3 grid using the framework’s standard grid container. The two top rows hold text entry fields, and the bottom row holds three other commonly used controls. One text field shows the name and size of the window’s default font, the other any active display options.

Each application window is positioned at the exact same location for all DPI settings, allowing an easy comparison of relative sizes. Batch files included in the download package set all window positions and any display options. Run the batch files show.bat and showWinForms.bat to recreate these arrangements on your system.

WPF (.NET 4.6), JavaFX & Swing (Java 8)

First we’ll look at WPF and the two standard Java GUI frameworks, Swing and JavaFX. Back in 2015 the tested versions were .NET 4.6 and Java SE 8u60 on Windows 8. While it’s unlikely that anyone is still using .NET 4.6 today, the Java samples will be relevant for a while longer. Java 8 was the last major version before Oracle’s new semi-annual update policy, and therefore receives updates through December 2020. So here’s a series of screenshots, respectively at 96 DPI (100%), 120 DPI (125%), and 144 DPI (150%).

WPF/Java 96 DPI

WPF/Java 120 DPI

WPF/Java 144 DPI

WPF (.NET 4.6) & JavaFX (Java 8)

For WPF and JavaFX, the autoGrid option indicates that all controls are automatically sized to their contents. Omitting this option sets a fixed width of 70 pixels for each column in the bottom row, forcing all controls to conform to that width. (JavaFX uses 80 pixels for the third column since its ComboBox has a lot of internal padding.) The height of all rows is always set automatically by the frameworks.

WPF — As expected, WPF performs flawlessly on all DPI settings. The only criticism is somewhat insufficient button padding when controls are sized automatically (WPF …b). Fixed column widths scale perfectly, too, because WPF interprets the specified 70 pixels as device-independent units which are implicitly scaled with increasing DPI resolution (WPF …a).

JavaFX — JavaFX correctly adopts the default system font, reported as “System Regular” but it’s really Segoe UI – check the glyphs! The sizes are all correct as well. Fully automatic control sizing (JavaFX …b) looks even better than WPF, in my opinion. You’ll notice that JavaFX uses a spacious cross-platform styling rather than the standard Windows look. There are no plans for an official Windows style.

There’s a catch, though. 144 DPI and higher perform implicit coordinate scaling just like WPF, so explicitly and automatically sized columns both work fine, and the system font is reported with its 96 DPI size of 12 pixels. However, JavaFX 8 makes an exception for 120 DPI. Here only the default font and built-in controls are scaled whereas explicitly specified coordinates remain unscaled. Therefore, fixed 70/80-pixel columns lead to cut-off labels (JavaFX 120a). For the same reason, margins between controls (explicitly set to 8 pixels) are not widened.

This exception was added as a stopgap because the comprehensive DPI scaling introduced in Java SE 8u60 unexpectedly led to blurry text at 120 DPI. That’s no longer an issues in Java 9 where this exception has been removed and all DPI settings use implicit coordinate scaling. But when targeting Java 8, you must remember to manually test JavaFX layouts at 120 DPI to ensure no functionality is lost, as in the sample window with its cut-off labels.

Java Swing (Java 8)

The remaining four samples illustrate three different Java Swing “Look & Feel” packages, or themes for short. Swing on Java 8 does not support automatic coordinate scaling but does a good job of automatic control sizing, so the autoGrid option is always implied. Trying to use explicit sizing with the one Swing theme that features automatic font scaling (System) will again result in cut-off labels.

Swing System — System is a pseudo-theme that requests different concrete themes depending on the platform. On Windows, System is the only Swing theme whose font scales with DPI settings (Swing …a) but rather imperfectly. Its default font is Tahoma rather than the actual Windows system font, and its size is always ca. 10% smaller than the actual system font size! This strange defect makes the otherwise promising System rather useless for the purpose of enlarging the GUI at higher DPI settings. You would have to explicitly request a larger font, and hope that everything scales correctly to the greater size.

Swing Default — Simply labeled “Swing” in the screenshots (Swing …b), this is the theme informally known as “Metal.” That’s the old default appearance that gave Java desktop applications a bad name: chubby, ugly, non-standard in every conceivable way, not scaling to anything at all. Useless, unless perhaps you want to write a convincing Windows 3.1 emulator…

Swing Nimbus — This promising attempt at a scalable cross-platform Swing theme was abandoned halfway through, for unclear reasons. Nimbus is vector-based and could theoretically scale to any DPI resolution, but is in practice hard-coded to only four different sizes – two of which are smaller than the default 96 DPI size (Swing …d)! The “Large” variant (Swing …c) illustrates the largest available size. You must manually request this size for every single control, as it’s neither automatically selected on high DPI systems nor toggled with a global option. Nimbus strikes me as useful only if you must build a cross-platform Java 8 application with a consistent look that cannot use JavaFX.

WPF (.NET 4.7.1), JavaFX & Swing (Java 9)

The 2018 version of the test suites was run on Windows 10 Creators Update, using .NET Framework 4.7.1 and Java SE 9.0.4. No changes were expected (or evident) for WPF, but JavaFX dropped its earlier special-casing of 120 DPI and AWT/Swing even got the same implicit coordinate scaling as WPF and JavaFX. So here’s how the screenshots look now:

WPF/Java 96 DPI

WPF/Java 120 DPI

WPF/Java 144 DPI

You may notice that the non-client area (i.e. window borders) is much thinner on Windows 10. This makes the windows look rather different than on Windows 8 but that’s unrelated to the tested GUI frameworks, as they only draw the interior of these windows.

WPF — No changes whatsoever. Moving on.

JavaFX — As expected, JavaFX 120a/b now scale both font size and control margins correctly, just like JavaFX 144a/b. A new text rendering engine makes for prettier typgraphy, too. However, there was one nasty surprise that doesn’t appear in the screenshots.

JavaFX in Java SE 9.0.4 exhibits a layout bug regarding control labels that’s described in the announcement post for the new screenshots. As it only affected very simple layouts like my test suite, I decided to hack around it with an explicit minimum width for the checkboxes. Do read the linked post if you’re thinking of using JavaFX with Java 9, though!

Swing — Everything scales correctly… almost. Swing …a/b show increasing errors in line drawing at 120 and 144 DPI. These pixel-based themes just weren’t meant to scale. The vector-based Nimbus theme finally comes into its own, though (Swing …d). The “Large” variant (Swing …c) is no longer needed, unless perhaps accessibility is a concern. Only the ugly old Sans Serif font needs replacing, otherwise Nimbus looks like a good choice on Java 9.

(You’ll note that Swing 144d is the one window in all the tests that’s not at the exact same position as elsewhere. Swing 144b got so big in 2018, I had to move it a bit to the right to prevent overlap. There was no more room for a dedicated caption either.)

Obtaining the Scaling Factor

All test windows are supposed to appear at the same physical screen location regardless of current Windows DPI settings. That’s no problem for frameworks that don’t perform full implicit coordinate scaling, such as Windows Forms (discussed next), JavaFX prior to Java SE 8u60, or AWT/Swing prior to Java 9. But WPF and newer Java frameworks need to know the current scaling factor and divide their 96-DPI-equivalent window coordinates by that factor, so as to obtain the desired physical coordinates.

All current frameworks provide APIs for that purpose which I’ll summarize here. One exception was JavaFX in Java 8, which is why its 2015 test program takes the physical screen width as an additional parameter from which to figure out the scaling factor. As of Java 9 this is no longer necessary.

WPF — Obtain a Graphics object on the desired screen. Assuming a single screen, just use Graphics.FromHwnd with the argument IntPtr.Zero. The properties Graphics.DpiX/DpiY yield the current DPI resolution. Divide by the standard resolution 96 to obtain the scaling factor.

JavaFX (Java 9) — If you already have a Window you can read its outputScaleX/Y properties. Alternativey, use the Screen methods getOutputScaleX/Y on either a specific screen or the default one obtained by Screen.getPrimary. This is the scaling factor itself, not the DPI resolution.

AWT/Swing (Java 9) — Here it gets lengthy. The key class is GraphicsConfiguration. If you already have a window or other AWT Component you can simply call getGraphicsConfiguration on it. Then call getDefaultTransform to obtain the current AffineTransform. The methods AffineTransform.getScaleX/Y yield the scaling factor.

What if you don’t have a Component? First call getLocalGraphicsEnvironment to obtain your system’s GraphicsEnvironment. Then call either getScreenDevices for all screens or getDefaultScreenDevice for the current one. Now you have a GraphicsDevice on which you call getDefaultConfiguration for its present GraphicsConfiguration and proceed as above.

Interestingly, there’s a sister method to getDefaultTransform called getNormalizingTransform that yields a transformation to “normalize” coordinates – but to a resolution of 1/72" rather than 1/96"! This API is older than Java SE 9 and intended for printers or image files. Hence the normalized resolution of 1/72" which is a conventional desktop publishing point. (The Javadoc for getDefaultTransform still mentions 1/72", too, but that’s outdated. For screens and windows it now simply reports the current DPI scaling factor.)

Windows Forms (.NET 4.6)

I had originally intended to add two or three Windows Forms samples to the previous section’s overview. However, the framework’s various scaling options produced such a bizarre menagerie of layout failures that I decided to showcase them in a dedicated section.

The default settings are as follows: Automatic DPI scaling is disabled, row heights are always automatically sized, column widths in the bottom row are fixed at 70 pixels, and AutoSize is disabled for controls in the bottom row. Windows Forms always automatically adjusts control sizes, so it’s best to think of AutoSize as enabling an alternative sizing mode. The following settings are selectively overridden:

  • AutoScale — Automatic DPI scaling is enabled: AutoScaleMode = AutoScaleMode.Dpi
  • AutoGrid — Column widths in the bottom row are automatically sized: ColumnStyle(SizeType.AutoSize)
  • AutoSize — Controls in the bottom row are automatically sized: AutoSize = true

Here’s the resulting series of screenshots for Windows 8, again respectively set to 96 DPI (100%), 120 DPI (125%), and 144 DPI (150%). The window positioning at 96 DPI looks random, but that’s because it matches the seemingly random 144 DPI scaling of Windows Forms!

WinForms 96 DPI

WinForms 120 DPI

WinForms 144 DPI

No Automatic Sizing — Windows Forms looks best when not using automatic sizing for anything but grid row height. A fixed column width looks good at 96 DPI (WinForms 96a/c). Labels normally get cut off at higher DPI (WinForms 120/144c) but automatic DPI scaling prevents this (WinForms 120/144a). Indeed, this combination of fixed widths and automatic DPI scaling produces results comparable to WPF.

Automatic Sizing Without Automatic DPI Scaling — Enabling both automatic grid widths (autoGrid) and automatic control sizing (autoSize) but not automatic DPI scaling (autoScale) is the one other combination that works reasonably well (WinForms …e). The auto-sized combo box is rather too wide, but otherwise the only defect are the unscaled control margins, as in JavaFX 8 at 120 DPI.

Any Other Combination – Surprisingly, any other combination of automatic scaling and sizing destroys the layout! autoGrid on its own has very strange ideas of appropriate column widths (WinForms …b). autoSize on its own ignores DPI scaling and cuts off labels (WinForms …d). autoScale produces bizarrely bloated buttons with autoSize (WinForms …f) and bizarrely widened columns with autoGrid (WinForms …g). Enable all three options, and you get both defects at once (WinForms …h).

You may have noticed one other pervasive defect: the default font is always the correct size but the wrong family. Windows Forms always chooses the ancient Microsoft Sans Serif which hasn’t been the default system font since Windows XP. The usual workaround is to explicitly request SystemFonts.MenuFont or SystemFonts.MessageBoxFont. I haven’t bothered to do this here because I didn’t want to give Windows Forms too much of a leg up with manual corrections. The test program already contains an explicit call to Application.EnableVisualStyles, so as to at least show the correct control styling.

Windows Forms (.NET 4.7.1)

When the .NET Framework 4.7 was announced, Microsoft made a rather big deal of High DPI for Windows Forms when running on Windows 10. However, if you read the fine print it becomes clear that this was merely “a first investment” in proper high DPI support. The actual changes only affect five specific controls, none of which appear in my test suite.

Not expecting much (or any) change, I revised my Windows Forms test program anyway to target .NET 4.7.1 and Windows 10 Creators Update, as per the current Microsoft guide to High DPI Support in Windows Forms. As you can see below, absolutely nothing has changed in either the correctly or the incorrectly scaled samples.

WinForms 96 DPI

WinForms 120 DPI

WinForms 144 DPI