Home » Blog
date 13.Feb.2022

■ Modernize old desktop apps to per-monitor DPI awareness

Once upon a time all screens were 1024x768 pixels big all programs were designed for the equivalent 96 DPI (Dots/pixels Per screen Inch). Toolbar buttons were 16x16 pixels and all was well. Then cometh super high resolution monitors UHD/4K, where 16x16 is hardly visible as a dot, and you needed 32 or 48 pixel toolbar buttons for your program to be usable. And eventually, in multi-monitor setups, programs must switch from 96 to 200 DPI as you move the window from the low resolution monitor to the 4K external one.

The life of desktop "old school" programmers in degrees got more complicated at each step of heightened display resolutions. Fixed icon sizes and magic constants had to be replaced with DPI-aware code and multiple resources that would cater for any resolution the program happened to run. All measurements had to be scaled to the prevailing DPI. For multi-monitor setups, even this isn't enough, as DPI can vary depending on the monitor too.

Microsoft realized that it would be hard for old tools to be converted for dynamic DPI environments, so it applied various automatic scaling techniques to make 16x16 pixel toolbars visible in UHD monitors. Stretched windows are bearable but are a fuzzy disgrace in a sleek high resolution screen. Even programs that are declared <dpiAware> through a manifest will look good only in the primary display, and an abomination everywhere else.

pixelated DPI

Figure 1. Pixelated window stretched to high DPI

Fast, cheap and good, pick any 2

Had providence allowed 48 hours per day, many things would have been possible, but under the circumstances one cannot re-write 20 year old code to make it perfectly multi-monitor aware. Some corners have to be cut. Thankfully windows 10 (and 11) do a lot of the work if you declare your app permonitorv2 DPI aware. Whenever a top level window switches monitors it is sent WM_DPICHANGED message with a reccommended new window size. Any standard controls that are part of the UI and the non-client area (menus etc) are automatically scaled for the new conditions, which "only" leaves the following matters to be sorted out: The idea is to adjust the fonts and dimensions so that they are appropriate for the new DPI. A 9 point font must be taller (in pixels) in higher DPI, to maintain its readability. We paint everything sharp ourselves, avoiding the automatic stretching of DPI-naive programs.
permonitorv2 awareness is only for windows 10 and later. For older windows the API doesn't exist or it is partially supported as in windows 8. So we only target w10+ and dynamically load the API so our code works for for older windows as well (with fallbacks)
To begin with we need a recent windows SDK that includes the latest definitions DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 etc. Then we must stop using system DPI values (GetDeviceCaps(LOGPIXELSX)) and use GetDpiForWindow instead. Unless you are doing something odd, the entire frame+child window hierarchy has the same DPI — but see this sample code for an exception. Per monitor API includes some replacement functions that accept the current DPI value such as GetSystemMetricsForDpi instead of the vanilla GetSystemMetrics that is based on the fixed system DPI (and should be avoided).

Then we add a handler for WM_DPICHANGED in our main window. This message is sent only if our manifest includes <dpiAwareness>=permonitorv2. Child windows don't get this message but I suspect all common controls handle WM_DPICHANGED_BEFOREPARENT, that's how they change their fonts before our WM_DPICHANGED handler. In there, we pass the notification to any custom controls we manage and do any other necessary actions for the new DPI.

Oddly, WM_DPICHANGED only tells us of the new DPI, so we must keep track of the old one in a member variable to calculate the DPI scale factor (new/old)

When updating old code to be permonitorv2 compliant, it's best to search the code base for fixed DPI culprits like GetDeviceCaps (especially LOGPIXELSY), GetSystemMetrics (especially for icon sizes SM_CXICON etc) and SystemParametersInfo (especially for menu font sizes). Also dig up "magic constants" like #define PADDING 2 in your custom controls. Some of these can be tolerated under "cut some corners" philosophy, but others must be dealt with to use the correct DPI scale. Below you will find a list that I dealt with for zabkat programs:

1. FONT MAPPING. Fonts for custom controls should be kept as point sizes (DPI independent), and adjusted for the correct pixel height for a window according to this formula (1 inch == 72 points)

LOGFONT.lfHeight = MulDiv(GetDpiForWindow(hwnd), pointSize, 72);
Another option is to use SystemParametersInfoForDpi for a suitable SPI_GETICONTITLELOGFONT size. Or if you are getting a font variation for listview custom drawing (e.g. an italic font), use WM_GETFONT to grab the updated common control font, then modify it (remember all windows controls have already updated internally by the time we receive WM_DPICHANGED).
Further complications surround common dialogs like ChooseFont that are only system DPI aware, so you must map font heights from window to system DPI (and the reverse) as this blog post explains for windows notepad.

2. TOOLBAR CONTROLS. If your buttons show text, your best bet is to delete and reinsert all of them, otherwise BTNS_AUTOSIZE doesn't work half of the time and you end up with cropped text or oversize buttons. If your toolbars host child combo or edit controls, whose height may change due to font resizing, you need to reposition them and adjust the buttons' vertical size to accommodate them (TB_SETBUTTONSIZE). Ideally you should also change the button imagelists if you can be bothered to load bigger icons. Sorry I couldn't be bothered :)

3. REBAR CONTROLS. As the toolbars they hold got resized (step 2 above), the rebar bands must be resized too, in terms of height (cyMinChild) and preferred width (cxIdeal) so that any chevrons show correctly

4. STATUS BAR. Any status panes must be repositioned and sized with SB_SETPARTS to match the latest font.

5. SETTINGS PERSISTENCE. For robustness, all window sizes and positions, font sizes and all DPI sensitive information should be saved in a DPI independent fashion (e.g. assuming fixed 96 DPI) and then mapped to the actual DPI when restoring the program window. Of course that's too much work for legacy code, so at least you should make sure the window is created in the same monitor where it was last saved (registry settings persistence). Most likely the DPI won't have changed and all will be well. But if the user changes desktop text size while the program is not running, then all saved settings will be smudged.

There will be numerous odds and sods that must be resized, e.g. the dimension of thumbnails, any other pixel size constants etc. But these will depend on your program. Most of the remaining "big stuff" will be supplied automatically by the framework:

Thanks permonitorv2 manifest!

Post a comment on this topic »

Share |

©2002-2022 ZABKAT LTD, all rights reserved | Privacy policy | Sitemap