High DPI Scaling Improvements for Desktop Applications and “Mixed Mode” DPI Scaling in the Windows 10 Anniversary Update
As display technology has improved over time, the cutting edge has moved towards having more pixels packed into each physical square inch, and away from simply making displays physically larger. This trend has increased the dots per inch (DPI) of the displays on the market today. The Surface Pro 4, for example, has roughly 192 DPI (while legacy displays have 96 DPI). Although having more pixels packed into each physical square inch of a display can give you extremely sharp graphics and text, it can also cause problems for desktop application developers. Many desktop applications display blurry, incorrectly sized UI (too big or too small), or are unusable when using high DPI displays in combination with standard-DPI displays. Many desktop UI frameworks that developers rely on to create Windows desktop applications do not natively handle high DPI displays and work is required on the part of the developer to address resizing application UI on these displays. This can be a very expensive and time-consuming process for developers. In this post, I discuss some of the improvements introduced in the Windows 10 Anniversary Update that make it less-expensive for desktop application developers to develop applications that handle high-DPI displays properly.
Note that applications built upon the Windows Universal Platform (UWP) handle display scaling very well and that the content discussed in this post does not apply to UWP. If you’re creating a new Windows application or are in a position where migrating is possible, consider UWP to avoid the problems discussed in this post.
Some Background on DPI Scaling
Steve Wright has written on this topic extensively, but I thought I’d summarize some of the complexities around display scaling for desktop applications here. Many desktop applications (applications written in raw Win32, MFC, WPF, WinForms or other UI frameworks) can often become blurry, incorrectly sized or a combination of both, whenever the display scale factor, or DPI, of the display that they’re on is different than what it was when the Windows session was first started. This can happen under many circumstances:
- The application window is moved to a display that has a different display scale factor
- The user changes the display scale factor manually
- A remote-desktop connection is established from a device with a different scale factor
When the display scale factor changes, the application may be sized incorrectly for the new scale factor and therefore, Windows often jumps in and does a bitmap stretch of the application UI. This causes the application UI to be physically sized correctly, but it can also lead to the UI being blurry.
In the past, Windows offered no support for DPI scaling to applications at the platform level. When these type of “DPI Unaware” applications are run on Windows 10, they are almost always bitmap scaled by Windows when display scaling is > 100%. Later, Windows introduced a DPI-awareness mode called “System DPI Awareness.” System DPI Awareness provides information to applications about the display scale factor, the size of the screen, information on the correct fonts to use, etc., such that developers can have their applications scaled correctly for a high DPI display. Unfortunately, System DPI Awareness was not designed for dynamic-scaling scenarios such as docking/undocking, moving an application window to a display with a different display scale factor, etc. In other words: The model for system-DPI-awareness is one that assumes that only one display will be in use during the lifecycle of the application and that the scale factor will not change.
In dynamic-scale-factor scenarios applications will be bitmap stretched by Windows when the display-scale-factor changed (this even applies to system-DPI-aware processes). Windows 8.1 introduced support for “Per-Monitor-DPI Awareness” to enable developers to write applications that could resize on a per-DPI basis. Applications that register themselves as being Per-Monitor-DPI Aware are informed when the display scale factor changes and are expected to respond accordingly.
So… everything was good, right? Not quite.
Unfortunately, there were three big gaps with our implementation of Per-Monitor-DPI Awareness in the platform:
- There wasn’t enough platform support for desktop application developers to actually make their applications do the right thing when the display-scale-factor changed.
- It was very expensive to update application UI to respond correctly to a display-scale factor changes, if it was even possible to do at all.
- There was no way to directly disable Window’s bitmap-scaling of application UI. Some applications would register themselves as being Per-Monitor-DPI Aware not because they actually were DPI aware, but because they didn’t want Windows to bitmap stretch them.
These problems resulted in very few applications handling dynamic display scaling correctly. Many applications that registered themselves as being Per-Monitor-DPI Aware don’t scale at all and can render extremely large or extremely small on secondary displays.
Background on Explorer
As I mentioned in another blog post, during the development cycle for the first release of Windows 10 we decided to start improving the way Windows handled dynamic display scaling by updating some in-box UI components, such as the Windows File Explorer, to scale correctly.
This was a great learning experience for us because it taught us about the problems developers face when trying to update their applications to dynamically scale and where Windows was limited in this regard. One of the main lessons learned was that, even for simple applications, the model of registering an application as being either System DPI Aware or Per-Monitor-DPI Aware was too rigid of a requirement because it meant that if a developer decided to mark their application as conforming to one of these DPI-awareness modes, they would have had to update every top-level window in their application or live with some top-level windows being sized incorrectly. Any application that hosts third-party content, such as plugins or extensions, may not even have access to the source code for this content and therefore would not be able to validate that it handled display scaling properly. Furthermore, there were many system components (ComDlg32, for example) that didn’t scale on a per-DPI basis.
When we updated File Explorer (a codebase that’s been around and been added to for some time), we kept finding more and more UI that had to be updated to handle scaling correctly, even after we reached the point in the development process when the primary UI scaled correctly. At that point we faced the same choice other developers faced: we had to touch old code to implement dynamic scaling (which came with application-compatibility risks) or live with these UI components being sized incorrectly. This helped us feel the pain that developers face when trying to adhere to the rigid model that Windows required of them.
Mixed-Mode DPI Scaling and the DPI-Awareness Context
Lesson learned. It was clear to us that we needed to break apart this rigid, process-wide, model for display scaling that Windows required. Our goal was to make it easier for developers to update their desktop applications to handle dynamic display scaling so that more desktop applications would scale gracefully on Windows 10. The idea we came up with was to move the process-level constraint on display scaling to the top-level window level. The idea was that instead of requiring every single top-level window in a desktop application to be updated to scale using a single mode, we could instead enable developers to ease-in, so to speak, to the dynamic-DPI world by letting them choose the scaling mode for each top-level window. For an application with a main window and secondary UI, such as a CAD or illustration application, for example, developers can focus their time and energy updating the main UI while letting Windows handle scaling the less-important UI, possibly with bitmap stretching. While this would not be a perfect solution, it would enable application developers to update their UI at their own pace instead of requiring them to update every component of their UI at once, or suffer the consequences previously mentioned.
The Windows 10 Anniversary Update introduced the concept of “Mixed-Mode” DPI scaling, also known as sub-process DPI scaling, via the concept of the DPI-awareness context (DPI_AWARENESS_CONTEXT) and the SetThreadDpiAwarenessContext API. You can think of a DPI-awareness context as a mode that a thread can be in which can impact the DPI-behavior of API calls that are made by the thread (while in one of these modes). A thread’s mode, or context, can be changed via calls to SetThreadDpiAwarenessContext at any time. Here are some key points to consider:
- A thread can have its DPI Awareness Context changed at any time.
- Any API calls that are made after the context is changed will run in the corresponding DPI context (and may be virtualized).
- When a thread that is running with a given context creates a new top-level window, the new top-level window will be assigned the same context that the thread that created it had, at the time of creation.
Let’s discuss the first point: With SetThreadDpiAwarenessContext the context of a thread can be switched at will. Threads can also be switched in and out of different contexts multiple times.
Many Windows API calls in Windows will return different information to applications depending on the DPI awareness mode that the calling process is running in. For example, if an application is DPI-unaware (which means that it didn’t specify a DPI-Awareness mode) and is running on a display scale factor greater than 100%, and if this application queries Windows for the display size, Windows will return the display size scaled to the coordinate space of the application. This process is referred to as virtualization. Prior to the availability of Mixed-Mode DPI, this virtualization only took place at the process level. Now it can be done at the thread level.
Mixed-Mode DPI scaling should significantly reduce the barrier to entry for DPI support for desktop applications.
Making Notepad Per-Monitor DPI Aware
Now that I’ve introduced the concept of Mixed-Mode, let’s talk about how we applied it to an actual application. While we were working on Mixed Mode we decided to try it out on some in-box Windows applications. The first application we started with was Notepad. Notepad is essentially a single-window application with a single edit control. It also has several “level 2” UI such as the font dialog, print dialog and the find/replace dialog. Before the Windows 10 Anniversary Update, Notepad was a System-DPI-Aware process (crisp on the primary display, blurry on others or if the display scale factor changed). Our goal was to make it a first-class Per-Monitor-DPI-Aware process so that it would render crisply at any scale factor.
One of the first things we did was to change the application manifest for Notepad so that it would run in per-monitor mode. Once an application is running as per-monitor and the DPI changes, the process is sent a WM_DPICHANGE message. This message contains a suggested rectangle to size the application to using SetWindowPos. Once we did this and moved Notepad to a second display (a display with a different scale factor), we saw that the non-client area of the window wasn’t scaling automatically. The non-client area can be described as all of the window chrome that is drawn by the OS such as the min/max/close button, window borders, system menu, caption bar, etc.
Here is a picture of Notepad with its non-client area properly DPI scaling next to another per-monitor application that has non-client area that isn’t scaling. Notice how the non-client area of the second application is smaller. This is because the display that its image was captured on used 200% display scaling, while the non-client area was initialized at 100% (system) display scaling.
During the first Windows 10 release we developed functionality that would enable non-client area to scale dynamically, but it wasn’t ready for prime-time and wasn’t released publicly until we released the Anniversary Update.
We were able to use the EnableNonClientDpiScaling API to get Notepad’s non-client area to automatically DPI scale properly.
Using EnableNonClientDpiScaling will enable automatic DPI scaling of the non-client area for a window when the following conditions are satisfied:
- The API is called from the WM_NCCREATE handler for the window
- The process or window is running in per-monitor-DPI awareness
- The window passed to the API is a top-level window (only top-level windows are supported)
Font Size & the ChooseFont Dialog
The next thing that had to be done was to resize the font on a DPI change. Notepad uses an edit control for its primary UI and it needs to have a font-size specified. After a DPI change, the previous font size was either be too large or too small for the new scale factor, so this had to be recalculated. We used GetDpiForWindow to base the calculation for the new font size:
FontStruct.lfHeight = -MulDiv(iPointSize, GetDpiForWindow(hwndNP), 720);
This gave us a font size that was appropriate for the display-scale factor of the current display, but we next ran into an interesting problem: when choosing the font we ran up against the fact that the ChooseFont dialog was not per-monitor DPI aware. This meant that this dialog could be either too large or too small, depending on the display configuration at runtime. Notice in the image below that the ChooseFont dialog is twice as large as it should be:
To address this, we used mixed-mode to have the ChooseFont dialog run with a system-DPI-awareness context. This meant that this dialog would scale to the system DPI on the primary display and be bitmap stretched any time the display scale factor changed:
DPI_AWARENESS_CONTEXT previousDpiContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE);
BOOL cfResult = ChooseFont(cf);
SetThreadDpiAwarenessContext(previousDpiContext);
This code stores the DPI_AWARENESS_CONTEXT of the thread and then temporarily changes the context while the ChooseFont dialog is created. This ensures that the ChooseFont dialog will run with a system-DPI-awareness context. Immediately after the call to create the window, the thread’s awareness context is restored because we didn’t want the thread to have its awareness changed permanently.
We knew that the ChooseFont dialog did support system-DPI awareness so we chose DPI_AWARENESS_CONTEXT_SYSTEM_AWARE, otherwise we could have used DPI_AWARENESS_CONTEXT_UNAWARE to at least ensure that this dialog would have been bitmap stretched to the correct physical size.
Now we had the ChooseFont dialog scaling properly without touching any of the ChooseFont dialog’s code but this lead to our next challenge… and this is one of the most important concepts that developers should understand about the use of mixed-mode DPI scaling: data shared across DPI-awareness contexts can be using different scaling/coordinate spaces and can have different interpretations in each context. In the case of the ChooseFont dialog, this function returns a font size based off of the user’s input, but this font size returned is relative to the scale factor that the dialog is running in. When the main Notepad window is running at a scale factor that is different than that of the system scale factor, the values from the ChooseFont dialog must be translated to be meaningful for the main window’s scale factor. Here we scaled the font point size to the DPI of the display that the Notepad window was running on, again using GetDpiForWindow:
FontStruct.lfHeight = -MulDiv(cf.iPointSize, GetDpiForWindow(hwndNP), 720);
Windows Placement
Another place where we had to deal with handling data across coordinate spaces was with the way Notepad stores and reuses its window placement (position and dimensions). When Notepad is closed, it will store its window placement. The next time it’s launched, it reads this information in an attempt to restore the previous position. Once we started running the main Notepad thread in per-monitor-DPI awareness we ran into a problem: the Notepad window was opening in strange sizes when launched.
What was happening was that in some cases we would store Notepad’s size at one scale factor and then restore it for a different scale factor. If the display configuration of the PC that Notepad was run on hadn’t changed between when the information was stored and when Notepad was launched again, theoretically this wouldn’t have been a problem. However, Windows supports changing scale factors, connecting/disconnecting and rotating display at will. This meant that we needed Notepad handle these situations more gracefully.
The solution was again to use mixed-mode scaling, but this time not leverage Window’s bitmap-stretching functionality and instead to normalize the coordinates that Notepad used to set and restore its window placement. This involved changing the thread to a DPI-unaware context when saving window placement, and to do the same when restoring. This effectively normalized the coordinate space across all displays and display scale factors that Notepad would be restored to, approximately the same placement regardless of the display-topology changes:
DPI_AWARENESS_CONTEXT previousDpiContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
BOOL ret = SetWindowPlacement(hwnd, wp);
SetThreadDpiAwarenessContext(previousDpiContext);
Once all of these changes were made, we had Notepad scaling nicely whenever the DPI would change and the document text rendering natively for each DPI, which was a big improvement over having Windows bitmap stretch the application on DPI change.
Useful DPI Utilities
While working on Mixed Mode display scaling, we ran into the need to have DPI-aware variants of some commonly used APIs:
- AdjustWindowRectExForDpi
- GetDpiForSystem
- GetDpiForWindow
- GetSystemMetricForDpi
- SystemParametersInfoForDpi
A note about GetDpiForSystem: Calling GetDpiForSystem is more efficient than calling GetDC and GetDeviceCaps to obtain the system DPI.
Also, any component that could be running in an application that uses sub-process DPI awareness should not assume that the system DPI is static during the lifecycle of the process. For example, if a thread that is running under DPI_AWARENESS_CONTEXT_UNAWARE awareness context queries the system DPI, the answer will be 96. However, if that same thread switched to DPI_AWARENESS_CONTEXT_SYSTEM and queried the system DPI again, the answer could be different. To avoid the use of a cached — and possibly stale — system-DPI value, use GetDpiForSystem() to retrieve the system DPI relative to the DPI-awareness mode of the calling thread.
What We Didn’t Get To
The Windows 10 Anniversary Update delivers useful API for developers that want to update desktop applications to support dynamic DPI scaling in their applications, in particular EnableNonClientDpiScaling and SetThreadDpiAwarenessContext (also known as “mixed-mode”), but there is still some missing functionality that we weren’t able to deliver. Windows common controls (comctl32.dll) do not support per-monitor DPI scaling and non-client area DPI-scaling is only supported for top-level windows (child-window non-client area, such as child-window scroll bars do not automatically scale for DPI (even in the Anniversary Update)).
We recognize that these, and many other, platform features are going to be needed by developers before they’re fully unblocked from updating their desktop applications to handle display scaling well.
As mentioned in my other post, WPF now offers per-monitor DPI-awareness support as well.
Sample Mixed-Mode Application:
We put together a sample that shows the basics of how to use mixed-mode DPI awareness. The project linked below creates a top-level window that is per-monitor DPI aware and has its non-client area automatically scaled. From the menu you can create a secondary window that uses DPI_AWARENESS_CONTEXT_SYSTEM_AWARE context so that Windows will bitmap stretch the content when its rendered at a different DPI.
https://github.com/Microsoft/Windows-classic-samples/tree/master/Samples/DPIAwarenessPerWindow
Conclusion
Our aim was to reduce the cost for developers to update their desktop applications to be per-monitor DPI aware. We recognize that there are still gaps in the DPI-scaling functionality that Windows offers desktop application developers and the importance of fully unblocking developers in this space. Stay tuned for more goodness to come.
Download Visual Studio to get started.
The Windows team would love to hear your feedback. Please keep the feedback coming using our Windows Developer UserVoice site. If you have a direct bug, please use the Windows Feedback tool built directly into Windows 10.