Channels ▼
RSS

.NET

Coding for High-DPI Displays in Windows


If you add the following lines to the _tWinMain function, you will notice the isSystemDPIAware variable holds the true value because the call to GetProcessDpiAwareness returns PROCESS_SYSTEM_DPI_AWARE in the awareness variable.

PROCESS_DPI_AWARENESS awareness;
auto hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, GetCurrentProcessId());
auto hr = GetProcessDpiAwareness(hProcess, &awareness);
if (hr != S_OK)
{
	MessageBox(NULL, (LPCWSTR)L"GetProcessDpiAwareness failed", (LPCWSTR)L"Notification", MB_OK);
	return FALSE;
}
auto isSystemDPIAware = (awareness == PROCESS_SYSTEM_DPI_AWARE);

The following lines make a call to GetDpiForMonitor to retrieve the DPI for the monitor in which the application window is running. If you add these lines to the InitInstance function, you will be able to check that the dpiX and dpiY values are different from 96 because the system DPI aware process isn't running on a virtualized 96 DPI environment. For example, if your monitor configuration has an extra large scaling of 200% or 192 DPI, the value for dpiX will be 192 and you can use this value to scale your content. The MDT_EFFECTIVE_DPI value specifies that you want the effective DPI that incorporates accessibility overrides and matches what DWM uses to scale desktop applications.

POINT    point;
UINT     dpiX = 0, dpiY = 0;

point.x = 1;
point.y = 1;
auto hMonitor = MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST);
auto hr = GetDpiForMonitor(hMonitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY);

if (hr != S_OK)
{
	MessageBox(NULL, (LPCWSTR)L"GetDpiForMonitor failed", (LPCWSTR)L"Notification", MB_OK);
	return FALSE;
}

It is a good idea to use a scale helper class with all the necessary methods to simplify the process of scaling the different values and elements. The following lines show the code for a sample ScaleHelper class. You need to provide the DPI retrieved from the monitor to the SetScaleFactor method in order to allow the class to calculate the appropriate scale factor and store it in m_nScaleFactor. For example, if the DPI is equal to 192, the value for m_nScaleFactor will be 200. Then, you can call different methods to scale from raw pixels to relative pixels by using the calculated scale factor:

  • ScaleValue: Scales a single value.
  • ScalePoint: Scales the x and y components of a POINT.
  • ScaleRectangle: Scales the left, right, top, and bottom components of a RECT.
  • CreateScaledFont: Receives a raw height value and scales it to create a logical font close to this scaled height.

class ScaleHelper
{
public:
	ScaleHelper()
	{
		m_nScaleFactor = 0;
	}

	UINT GetScaleFactor()
	{
		return m_nScaleFactor;
	}

	void SetScaleFactor(__in UINT iDPI)
	{
		m_nScaleFactor = MulDiv(iDPI, 100, 96);
	}

	int ScaleValue(int value)
	{
		return MulDiv(value, m_nScaleFactor, 100);
	}

	// Scale rectangle from raw pixels to relative pixels.
	void ScaleRectangle(__inout RECT *pRectangle)
	{
		pRectangle->left = ScaleValue(pRectangle->left);
		pRectangle->right = ScaleValue(pRectangle->right);
		pRectangle->top = ScaleValue(pRectangle->top);
		pRectangle->bottom = ScaleValue(pRectangle->bottom);
	}

	// Scale Point from raw pixels to relative pixels.
	void ScalePoint(__inout POINT *pPoint)
	{
		pPoint->x = ScaleValue(pPoint->x);
		pPoint->y = ScaleValue(pPoint->y);
	}

	HFONT CreateScaledFont(int nFontHeight)
	{
		int nScaledFontHeight = ScaleValue(nFontHeight);
		LOGFONT lf;
		memset(&lf, 0, sizeof(lf));
		lf.lfQuality = CLEARTYPE_QUALITY;
		lf.lfHeight = -nScaledFontHeight;
		auto hFont = CreateFontIndirect(&lf);
		return hFont;
	}

private:
	UINT m_nScaleFactor;
};

The following lines define the baseline padding and sizes for the window, the button, and the fonts. These are the default values for 96 DPI or 100% scaling. In addition, I define a global variable to call the ScaleHelper class methods within different functions and store two HFONT for the window text and the button text.

#define BASELINE_PADDING	15
#define BASELINE_WINDOW_WIDTH       800
#define BASELINE_WINDOW_HEIGHT      500
#define BASELINE_BUTTON_WIDTH       280
#define BASELINE_BUTTON_HEIGHT      60
#define BASELINE_FONT_HEIGHT 30

UINT      g_nBaselineFontHeight = BASELINE_FONT_HEIGHT;
HFONT     g_hTextFont, g_hButtonFont;
ScaleHelper      g_ScaleHelper;

The CreateFonts function calls the ScaleHelper.CreateScaledFont method twice to create fonts for the window text (g_hTextFont) and button text (g_hButtonFont). The function can be called many times if there are changes in the DPI settings (and when it is necessary to cleanup the existing fonts and create new fonts based on the new scale). In this case, the application is system DPI-aware, but the code for this function is ready to be reused when I convert this application to a per-monitor DPI aware one.

void CreateFonts(HWND hWnd)
{
	if (g_hTextFont != NULL)
	{
		DeleteObject(g_hTextFont);
		g_hTextFont = NULL;
	}

	g_hTextFont = g_ScaleHelper.CreateScaledFont(g_nBaselineFontHeight);
	if (g_hTextFont == NULL)
	{
		MessageBox(hWnd, (LPCWSTR)L"CreateScaledFont failed", (LPCWSTR)L"Notification", MB_OK);
	}

	if (g_hButtonFont != NULL)
	{
		DeleteObject(g_hButtonFont);
		g_hButtonFont = NULL;
	}

	g_hButtonFont = g_ScaleHelper.CreateScaledFont(BASELINE_FONT_HEIGHT);
	if (g_hButtonFont == NULL)
	{
		MessageBox(hWnd, (LPCWSTR)L"CreateScaledFont failed", (LPCWSTR)L"Notification", MB_OK);
	}
}

The RenderWindow function draws different elements of the window: the background color, two lines of text, and a button. The function calls the ScaleHelper.ScaleValue method to scale the different baseline values based on the DPI settings and uses the fonts for the window text (g_hTextFont) and button text (g_hButtonFont) created by the CreateFonts function. If the DPI settings changes, it is possible to call RenderWindow to update the elements based on the new DPI settings. This way, the RenderWindow function can be reused in the conversion to a per monitor DPI aware application.

void RenderWindow(HWND hWnd)
{
	PAINTSTRUCT ps;
	RECT        rcText, rcWindow, rcClient;
	LPCWSTR     text1 = L"Line #1: Sample scaled text";
	LPCWSTR     text2 = L"Line #2: Sample scaled text";
	
	// Cornflower blue background color
	auto color = RGB(100, 149, 237);

	GetWindowRect(hWnd, &rcWindow);
	GetClientRect(hWnd, &rcClient);
	// Scale the baseline padding value
	auto nScaledPadding = g_ScaleHelper.ScaleValue(BASELINE_PADDING);
	auto hdc = BeginPaint(hWnd, &ps);
	SetBkMode(hdc, TRANSPARENT);
	auto hBrush = CreateSolidBrush(color);
	FillRect(hdc, &rcClient, hBrush);

	// Render a button with the scaled text
	SelectObject(hdc, g_hButtonFont);
	auto hWndButton = GetWindow(hWnd, GW_CHILD);
	SetWindowPos(hWndButton, HWND_TOP, rcClient.left + nScaledPadding, rcClient.bottom - g_ScaleHelper.ScaleValue(BASELINE_BUTTON_HEIGHT) - nScaledPadding,
		g_ScaleHelper.ScaleValue(BASELINE_BUTTON_WIDTH), g_ScaleHelper.ScaleValue(BASELINE_BUTTON_HEIGHT), SWP_SHOWWINDOW);
	SendMessage(hWndButton, WM_SETFONT, (WPARAM)g_hButtonFont, TRUE);
	UpdateWindow(hWndButton);

	// Render two lines of text within the Window
	SelectObject(hdc, g_hTextFont);
	rcText.left = rcClient.left + nScaledPadding;
	rcText.top = rcClient.top + nScaledPadding;
	DrawText(hdc, text1, -1, &rcText, DT_CALCRECT | DT_LEFT | DT_TOP);
	DrawText(hdc, text1, -1, &rcText, DT_LEFT | DT_TOP);
	rcText.top = rcText.bottom + nScaledPadding;
	DrawText(hdc, text2, -1, &rcText, DT_CALCRECT | DT_LEFT | DT_TOP);
	DrawText(hdc, text2, -1, &rcText, DT_LEFT | DT_TOP);

	// Cleanup code
	EndPaint(hWnd, &ps);
	DeleteObject(hBrush);
	DeleteDC(hdc);
}

It is necessary to change the code for the InitInstance function. First, I add the previously shown lines that retrieve the DPI for the monitor in which the application window is going to be displayed. Then, I use the scale helper class to calculate the appropriate scale factor based on the retrieved DPI value, and I replace the code that creates the main window with new code that scales the baseline width (BASELINE_WINDOW_WIDTH) and height (BASELINE_WINDOW_HEIGHT) to create the window with the appropriate scaled size. In addition, the new code calls the previously explained CreateFonts function.

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   HWND hWnd;
   POINT    point;
   UINT     dpiX = 0, dpiY = 0;

   // Get the DPI for the main monitor, and set the scaling factor
   point.x = 1;
   point.y = 1;
   auto hMonitor = MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST);
   auto hr = GetDpiForMonitor(hMonitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY);

   if (hr != S_OK)
   {
	   MessageBox(NULL, (LPCWSTR)L"GetDpiForMonitor failed", (LPCWSTR)L"Notification", MB_OK);
	   return FALSE;
   }

   // Use the scale helper class to calculate the appropriate scale factor based on the retrieved DPI value
   g_ScaleHelper.SetScaleFactor(dpiX);

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

   // Create main window and pushbutton window
   hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, g_ScaleHelper.ScaleValue(100), g_ScaleHelper.ScaleValue(100),
	   g_ScaleHelper.ScaleValue(BASELINE_WINDOW_WIDTH), g_ScaleHelper.ScaleValue(BASELINE_WINDOW_HEIGHT), NULL, NULL, hInstance, NULL);

   if (!hWnd)
   {
	   return FALSE;
   }
   
   auto hWndButton = CreateWindow(L"BUTTON", L"Scaled button text", WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON, 0, 0, 0, 0, hWnd, NULL, hInstance, NULL);

   // Create the scaled fonts for the window text and the button text
   CreateFonts(hWnd);

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

   return TRUE;
}


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