Channels ▼
RSS

Tools

Coding for High-DPI Displays in Windows


In the first article in this two-part series on coding for high-DPI displays on the Windows desktop, I explained high-DPI configurations in Windows 8.1 and their effect on the three different kinds of applications: DPI-unaware, system-DPI aware, and per-monitor DPI aware. In this article, I provide a complete example of a system DPI-aware Win32 application and then I'll convert it to a per-monitor DPI-aware application that always provides crisp text and images to Windows 8.1 users.

Checking the DPI Awareness of Each Process

The latest versions of Process Explorer allow you to easily check the DPI awareness of each process. The default view doesn't include the DPI Awareness column. You just need to right-click on the column headers, select Select Columns…, check the DPI Awareness checkbox, and click OK. This way, Process Explorer will add the DPI Awareness column and will display the following three possible values for each running process:

  • Unaware
  • System Aware
  • Per-Monitor Aware

I'll use Process Explorer to make sure that the changes in the DPI Awareness configuration for each new build are reflected in the process.

Creating a System DPI Aware Win32 Application in Visual Studio 2013

You can use the following two mechanisms to set the DPI-awareness level for a process:

  • Declaratively: Specify the desired level of awareness in the dpiAware entry of the application manifest. This way, DWM sets the specified DPI awareness level when the user launches the application. Visual Studio 2013 enables you to set the DPI-awareness level without having to edit the manifest tool.
  • Procedurally: Call the SetProcessDpiAwareness function with the appropriate value from the PROCESS_DPI_AWARENESS enumeration. The function must be called before any Win32 API call that makes DWM begin virtualization.

It is a good practice to use the application manifest instead of calling the SetProcessDpiAwareness function. However, there are specific cases in which you might find it more convenient to make the API call. For the system DPI-aware sample application, I use the recommended process for declaring the DPI awareness level of a sample application and I take advantage of the features included in Visual Studio 2013 to do so. However, it will be necessary to merge a manifest file for compatibility reasons when converting the application to per-monitor DPI aware.

Create a Visual C++ Win32 Project in Visual Studio 2013. I'll use DPIAware for the solution name. Make sure "Windows application" is selected in Application type in the Win32 Application Wizard. This way, you will have the basic structure for a C++ Win32 Windows application, and I can focus on the necessary changes in the code for the example. By default, Visual Studio 2013 creates a simple Win32 application with some menu items and an About dialog box. These elements are going to be useful to test how the app works with different DPI aware settings, even before I make any changes to the code.

To follow along, make sure your display settings in the Display Control Panel item use a scaling level higher than 100% (96 DPI) —you need at least 125% (120 DPI). If your settings specify a scaling level equal to 100% (96 DPI), you won't notice the changes I'll describe later. However, don't forget that any changes in the scaling level configuration will have an impact on all your applications.

By default, the generated Win32 application is DPI-unaware. Execute the application and you will notice the text in the menu bar is blurry because the DWM has virtualized the application window and is scaling it. Select Help | About… and you will also notice the text displayed within the About dialog box and the text for the OK button is blurry. If you use Process Explorer to check the DPI Awareness value for the generated executable (DPIAware.exe), you will see the Unaware value.

Now, stop the execution and right-click on the project name (DPIAware) within Solution Explorer and select Properties. Visual Studio will show the Property Pages dialog box for the project. Then, select Configuration Properties | Manifest Tool | Input and Output | DPI Awareness, select High DPI Aware from the dropdown menu and click OK. You can achieve the same effect by merging in a manifest file with the following XML to the application manifest. Notice that the true value for dpiAware indicates that the application is system DPI-aware.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
  manifestVersion="1.0">
  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
    </windowsSettings>
  </application>
</assembly>

As I explained earlier, in this case, it is easier to use the setting in the dialog box. I'll explain how to merge in a manifest file shortly.

Now, execute the application and you will notice the text in the menu bar is crisp and clear. Now, the application is DPI-aware and DWM doesn't virtualize it. There is no code to scale the fonts, and therefore, the text in the About… dialog box is probably going to be too small. If you use Process Explorer to check the DPI Awareness value for the generated executable (DPIAware.exe), you will see the System Aware value.

Open the stdafx.h header file and add a line that includes the ShellScalingAPI.h header. It is necessary to make a call to the GetDpiForMonitor API function. The following lines show the resulting code for stdafx.h.

#pragma once
#include "targetver.h"
#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
// Windows Header Files:
#include <windows.h>

// C RunTime Header Files
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <tchar.h>

// Additional header required to call GetDpiForMonitor, use enumerations and other API calls
#include <ShellScalingApi.h>

It is necessary to add shcore.lib in the linker dependencies. You just need to right-click on the project name, select Properties, and then Configuration Properties | Linker | Input | Additional Dependencies. Add shcore.lib; as a prefix to the existing additional dependencies and click OK.

The following lines show the initial code for the DPIAware.cpp source file:

// DPIAware.cpp : Defines the entry point for the application.
//

#include "stdafx.h"
#include "DPIAware.h"

#define MAX_LOADSTRING 100

// Global Variables:
HINSTANCE hInst;								// current instance
TCHAR szTitle[MAX_LOADSTRING];					// The title bar text
TCHAR szWindowClass[MAX_LOADSTRING];			// the main window class name

// Forward declarations of functions included in this code module:
ATOM				MyRegisterClass(HINSTANCE hInstance);
BOOL				InitInstance(HINSTANCE, int);
LRESULT CALLBACK	WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK	About(HWND, UINT, WPARAM, LPARAM);

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPTSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
	UNREFERENCED_PARAMETER(hPrevInstance);
	UNREFERENCED_PARAMETER(lpCmdLine);

 	// TODO: Place code here.
	MSG msg;
	HACCEL hAccelTable;

	// Initialize global strings
	LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
	LoadString(hInstance, IDC_DPIAWARE, szWindowClass, MAX_LOADSTRING);
	MyRegisterClass(hInstance);

	// Perform application initialization:
	if (!InitInstance (hInstance, nCmdShow))
	{
		return FALSE;
	}

	hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_DPIAWARE));

	// Main message loop:
	while (GetMessage(&msg, NULL, 0, 0))
	{
		if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}

	return (int) msg.wParam;
}



//
//  FUNCTION: MyRegisterClass()
//
//  PURPOSE: Registers the window class.
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
	WNDCLASSEX wcex;

	wcex.cbSize = sizeof(WNDCLASSEX);

	wcex.style			= CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc	= WndProc;
	wcex.cbClsExtra		= 0;
	wcex.cbWndExtra		= 0;
	wcex.hInstance		= hInstance;
	wcex.hIcon			= LoadIcon(hInstance, MAKEINTRESOURCE(IDI_DPIAWARE));
	wcex.hCursor		= LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground	= (HBRUSH)(COLOR_WINDOW+1);
	wcex.lpszMenuName	= MAKEINTRESOURCE(IDC_DPIAWARE);
	wcex.lpszClassName	= szWindowClass;
	wcex.hIconSm		= LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

	return RegisterClassEx(&wcex);
}

//
//   FUNCTION: InitInstance(HINSTANCE, int)
//
//   PURPOSE: Saves instance handle and creates main window
//
//   COMMENTS:
//
//        In this function, we save the instance handle in a global variable and
//        create and display the main program window.
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   HWND hWnd;

   hInst = hInstance; // Store instance handle in our global variable

   hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

   if (!hWnd)
   {
      return FALSE;
   }

   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   return TRUE;
}

//
//  FUNCTION: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  PURPOSE:  Processes messages for the main window.
//
//  WM_COMMAND	- process the application menu
//  WM_PAINT	- Paint the main window
//  WM_DESTROY	- post a quit message and return
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	int wmId, wmEvent;
	PAINTSTRUCT ps;
	HDC hdc;

	switch (message)
	{
	case WM_COMMAND:
		wmId    = LOWORD(wParam);
		wmEvent = HIWORD(wParam);
		// Parse the menu selections:
		switch (wmId)
		{
		case IDM_ABOUT:
			DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
			break;
		case IDM_EXIT:
			DestroyWindow(hWnd);
			break;
		default:
			return DefWindowProc(hWnd, message, wParam, lParam);
		}
		break;
	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);
		// TODO: Add any drawing code here...
		EndPaint(hWnd, &ps);
		break;
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

// Message handler for about box.
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
	UNREFERENCED_PARAMETER(lParam);
	switch (message)
	{
	case WM_INITDIALOG:
		return (INT_PTR)TRUE;

	case WM_COMMAND:
		if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
		{
			EndDialog(hDlg, LOWORD(wParam));
			return (INT_PTR)TRUE;
		}
		break;
	}
	return (INT_PTR)FALSE;
}


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video