Windows with C++ : Write High-DPI Apps for Windows 8.1

Windows 8 introduced a new programming model for Windows apps, based on the Windows Runtime (WinRT), which lets users dynamically change the size of screen elements with a PC setting. You can see what this option looks like in the PC settings in Figure 1. On my desktop, the options are Default and Larger. On my Surface Pro, the options are Smaller and Default. It really depends on the device, and in particular, the vertical resolution of the attached displays. More important, Windows Store apps receive an event whenever this option is changed and can thus dynamically update their rendering code to reflect the current scaling factor.

Figure 1 Windows 8.1 PC Setting Affecting Windows Store Apps

The desktop on Windows 8, however, remained static. Desktop applications continued to be serviced by a system DPI setting, any changes to which would only take effect after the user signs out and back in again, effectively forcing all applications to shut down and restart. You can see this option, which is still available in Windows 8.1, in Figure 2, with its more granular Smaller, Medium, Larger and Extra Large options. It reminds me of a trip to the local coffee shop. Here, too, the options that may be available depend on the attached displays. My Surface Pro, for example, only includes Smaller, Medium and Larger.

Figure 2 Pre-Windows 8.1 PC Setting Affecting Desktop Applications

Needless to say, this split personality—like many things in Windows 8—can be quite confusing for the developer, let alone the user. While Windows 8.1 doesn’t really address the confusion in any meaningful way, it does finally allow desktop applications to similarly handle DPI scaling dynamically, thus the user is no longer forced to shut everything down and bring up a new logon session. But Windows 8.1 goes a lot further and really brings new life to multi-monitor configurations.

While the window in Figure 2 looks quite similar to what was available in Windows 8, it now sports a little checkbox that was added in Windows 8.1. Although it’s checked in Figure 2, the default is unchecked. The checkbox title, “Let me choose one scaling level for all my displays,” hints at the other capability that’s new in Windows 8.1: the ability for different monitors to have different scaling factors. The checkbox title is a little confusing, as clearing this checkbox still offers value for users who only have a single monitor. In that case, it still offers the user the option of changing the scaling factor dynamically. Checking this option really represents a legacy or compatibility mode for DPI behavior. So whether you have multiple monitors—or more important, regardless of how many monitors your users might normally use—you’re going to want to come to grips with these new options. They’ll affect your applications whether you like it or not. Clearing this checkbox reveals the window in Figure 3. Again, this is now the default on Windows 8.1.

If you think about it, there’s really no difference between Figure 2 and Figure 3. The former uses four radio buttons and the latter uses a slider with four possible positions. The only real difference is that changes to the slider take effect immediately, or at least as soon as you hit the Apply button. This is much the same experience, at least for the user, as the scaling option for Windows Store apps. Changes to the radio button selection, however, only take effect the next time the user signs in.

The four possible values, for either the slider or radio buttons, correspond to four DPI scaling factors and are illustrated in Figure 4. As a developer, I caution you not to read too much into the specific DPI values. They’re meant to reflect the resolution or pixel density of the screen, but in reality many factors influence the DPI value—such as form factor and distance to the screen—so the effective DPI value you end up using has little to do with an actual inch. These four options also represent the full spectrum of possibilities, but what a particular PC might offer depends on the vertical resolution of its displays.

Figure 4 System Scaling Factors

DPI

Percentage

96

100

120

125

144

150

192

200

This is illustrated in Figure 5. These limits are intended to keep UI elements from getting cropped off the bottom of the display. If your display has fewer than 900 lines of vertical resolution, then you won’t have any options and the 100 percent scaling factor will be all there is. As the vertical resolution of your display increases, you’re presented with more options until you reach 1,440 lines of vertical resolution, at which point you experience all four possible options. These options do not, however, affect the scaling on all monitors equally. This is where the concept of per-monitor DPI scaling comes from. It isn’t entirely obvious at first glance because the OS takes into account both the vertical resolution as well as the native DPI for the physical display.

Figure 5 Scaling Options Relative to Vertical Resolution

Resolution

Scaling Options

… – 900

100%

900 – 1079

100% – 125%

1080 – 1439

100% – 125% – 150%

1440 – ...

100% – 125% – 150% – 200%

As a developer of desktop applications, it’s important to realize there are now two scaling factors that may be at play. There’s the system DPI scaling factor and then there’s a per-monitor DPI scaling factor. The system DPI scaling factor corresponds to one of the values in Figure 4—with the exception of devices such as Windows Phone—and remains constant for the duration of the logon session. The system DPI value is based on the initial radio button shown in Figure 2or the slider position shown in Figure 3.

To retrieve the system DPI value, you start by getting hold of the desktop device context. Yes, this boils down to the old GDI API, but it has nothing to do with GDI rendering and is only a historical footnote. First, to get a handle representing the desktop device context, you call the GetDC function with a nullptr instead of a window handle. This special value indicates you want the device context for the desktop as a whole rather than a particular window:

The first parameter is the handle to the window to which the device context refers. Again, a nullptr value represents the desktop. Now, given the device context, you can use the GetDeviceCaps function to retrieve the system DPI scaling factor for the x and y axes as follows:

Having a different value for the x and y axes dates back to the dark ages when printers routinely offered different scaling factors horizontally and vertically. I’ve never come across a display that offers up non-square pixels, but I hear they do exist in some industries for which special graphics cards have been developed. LOGPIXELSX and LOGPIXELSY represent the number of pixels per logical inch along the width and height of the desktop. Again, this is a logical inch and isn’t meant to reflect reality. Also, given these are the system DPI values, it means they’re the same for all monitors that span the desktop regardless of how relatively big or how small they might be. Therein lies the problem.

If you plug your Dell Venue 8 Pro tablet with its 8-inch screen into an array of 30-inch Dell UltraSharp monitors, you’re going to have to make a difficult choice. I routinely plug two or three vastly different monitors into my desktop’s graphics card. A system-wide DPI scaling factor just doesn’t cut it. What’s needed is for each monitor to have a DPI scaling factor ideally suited to its relative size or resolution. This is exactly what Windows 8.1 offers with its per-monitor DPI scaling support.

Windows 8.1 offers up three different levels of DPI awareness. This is obvious when you look at the Process Explorer window, shown in Figure 6. You can see some applications are completely DPI-unaware, as in the case of the command prompt. Most applications that were written for Windows 7 and Windows 8 are system DPI-aware, or at least claim to be. Examples include Microsoft Outlook and the calculator. Per-monitor DPI awareness is the third and optimal level of awareness. Examples in Figure 6 include Internet Explorer and Microsoft Word. Interestingly, Word isn’t actually per-monitor DPI-aware at the moment, but I’ve disabled display scaling for Word to avoid blurriness. This has the effect of overriding the process DPI awareness so the desktop window manager won’t scale the window. This was done with a compatibility option.

Figure 6 Process Explorer Showing Purported DPI Awareness

The main point is you better make sure your apps are per-monitor DPI-aware and scale accordingly. How exactly can you do this? Keep reading.

Different applications make various assertions about their level of DPI awareness, and then the desktop window manager—the service responsible for composing the various application windows together—determines how to scale different application windows based on their individual DPI claims such that all windows are presented at some consistent scale. If an application claims to be per-monitor DPI-aware, the desktop window manager won’t scale the window at all and assumes the application knows what it’s doing. If an application is system DPI-aware, the desktop window manager will scale the window based on the assumption that it was rendered to the system DPI scaling factor returned by the GetDeviceCaps function I mentioned earlier. And if an application is DPI-unaware, the desktop window manager assumes the window was rendered to the traditional 96 DPI and scales it accordingly.

Before I consider how to actually write a well-behaving, per-­monitor, DPI-aware application, I’ll discuss what it takes to make such a claim. Windows Vista introduced the SetProcessDPIAware function to mark the calling process as DPI-aware. This function took no arguments and simply turned on DPI awareness, so to speak. This was before per-monitor awareness, so this was a simple binary state. Either you’re DPI-aware or you’re not. Windows 8.1 introduced a new function called SetProcessDpiAwareness that provides more control over this level of awareness:

This sets the process’s DPI awareness to the given level. Constants are available to cover the three possible states: DPI-unaware, system DPI-aware and per-monitor DPI-aware. There’s also a corresponding GetProcessDpiAwareness function to query this value for a given process. But using code to set the awareness level has a number of drawbacks. You need to be careful to call these functions early on in your application’s lifetime. Sometimes this isn’t practical and causes a number of issues when mixing executables and DLLs. A better solution is called for.

Applications can embed or distribute a manifest file—along with their binaries—that contains hints for the shell or Windows loader to use in determining how to prepare the application process before any code begins to run. The Windows shell uses this for a variety of purposes, including security features, assembly dependency information and, yes, DPI-awareness claims.

These manifest files are just text files. Either they’re embedded within the executable as a traditional Win32 resource or they’re simply shipped alongside the executable. You can, for example, take a peek at the manifest file for Word by using the manifest tool included with the Windows SDK using the following command:

I’ve elided the full path to conserve space. If all goes well, you’ll be rewarded with an XML document in the current directory. You can see what this looks like in Figure 7. In the middle of some otherwise unrelated information about the Word executable is the dpiAware XML element and its value of true.

Figure 7 The Microsoft Word Manifest File

If an application doesn’t have a manifest file and doesn’t use the programmatic approach of setting its awareness level, then it’s assumed to be DPI-unaware. If an application has a manifest file but doesn’t contain the dpiAware element, then it’s again assumed to be DPI-unaware. On Windows 7, the presence of the dpiAware element is enough for the system to assume it’s DPI-aware. It doesn’t matter what value this element contains or whether it even has a value. Window 8 is a little more specific. It expects a value starting with T, on the assumption that it says “True.” Case doesn’t matter.

So that deals with DPI-unaware and system DPI-aware applications. What about per-monitor DPI-aware applications? Well, when this feature was first developed, a new value was chosen for the dpiAware element: Per Monitor. The desire to let application developers produce a single binary capable of targeting both Windows 7 and Windows 8 with support for system DPI awareness, as well as per-monitor DPI awareness on Windows 8.1, led to a new value to be chosen: True/PM. This enables Windows 7 and Windows 8 to accept the binary as system DPI-aware, and Windows 8.1 accepts it as per-monitor DPI-aware. You can, of course, continue to use Per Monitor if you only need to support Windows 8.1.

Unfortunately, Visual C++ 2013 doesn’t yet support this new value, but there’s a way to make it happen. The Manifest Tool that’s part of the Visual C++ project build has an option to merge in additional manifest files. All you need to do is create a text file with the correct dpiAware element and value and merge it into your build. The following code shows the XML you need:

Once you’ve created the manifest file, you can simply update your project’s settings, as illustrated in Figure 8. The manifest is set as an additional manifest file and the DPI awareness is set to None.

Figure 8 Manifest Tool Options

When it comes to rendering your application’s window, you need to make sure you have the right DPI scaling factor. If you’re looking after a legacy application with deep roots in GDI graphics and USER controls, then you’re likely going to experience a lot of challenges in trying to make it per-­monitor DPI-aware. These are the same challenges faced by the Windows and Office teams at Microsoft. But if you’ve been following my column, you know that Direct2D is the way to go. Direct2D takes care of all your DPI scaling needs and presents you with a logical coordinate system independent of the physical pixels and the DPI scaling factor. Of course, for Direct2D to be able to do this effectively, you need to tell it what DPI values to use for a particular render target.

Traditionally, Direct2D applications simply retrieved the DPI values from the Direct2D factory, which in turn simply called GetDeviceCaps, as I illustrated earlier. But this is no longer sufficient. As I’ve shown, the DPI values may now change on the fly, and the value provided by GetDeviceCaps is only useful if you’re trying to fit in with the rendering of a system DPI-aware application.

Instead, right after you’ve created your Direct2D render target, you need to query the DPI value for the monitor nearest your window:

The effective DPI value for a given monitor won’t necessarily correspond exactly to options presented in Figure 4. It again depends on a number of factors, including resolution, the physical DPI of the display and the assumed distance to the display surface. Finally, you can call the Direct2D render target’s SetDpi method and Direct2D will take care of properly scaling your content as needed.

I did mention this can all happen dynamically. Your application may be running and suddenly the user changes the scale with the window shown in Figure 3 or drags your window to another monitor with a different DPI scaling factor. In those cases, the Windows shell will send per-monitor DPI-aware application windows a new window message called WM_DPICHANGED.

Inside your message handler, you can once again call the MonitorFromWindow and GetDpiForMonitor functions to get the effective DPI values for the current monitor and then simply update your Direct2D render target with its SetDpi method again. Alternatively, the message’s WPARAM packs both the x and y DPI values so this becomes a simple matter of extracting the low and high order words:

The WM_DPICHANGED message’s LPARAM is, however, indispensable. It provides a new suggested position and size of your window based on the new scale factor that’s now in effect. This ensures that while Direct2D takes care of scaling your content, you can also scale your window’s actual size on the desktop and position it appropriately. The message’s LPARAM is just a pointer to a RECT structure:

And that’s all it takes. If you’ve invested in Direct2D, you’re just a few steps away from being per-monitor DPI-aware. Few applications have made the leap, so your application is bound to stand out! If you’ve got an existing Windows Presentation Foundation (WPF) application, there’s also a sample available at bit.ly/IPDN3p that shows you how to integrate these same principles outlined in this article into your WPF code base.

Figure 9 provides an example of a per-monitor DPI-aware application, automatically scaling both its content and its window size based on WM_DPICHANGED messages. The emoticon indicates which monitor the window is currently positioned on. For a more interactive example, please check out my Pluralsight course at bit.ly/1fgTifi, where you can also get the sample code for this application.

Figure 9 Per-Monitor DPI-Aware Application

Kenny Kerris a computer programmer based in Canada, as well as an author for Pluralsight and a Microsoft MVP. He blogs at kennykerr.ca and you can follow him on Twitter at twitter.com/kennykerr.

Thanks to the following technical expert for reviewing this article: James Clarke (Microsoft)