If you’re developing native applications for Windows using Win32 or MFC and you want to support high DPIs so that the application looks crisp on any display, you have to do a lot of things by hand. That is because the technologies for building native UIs, that is MFC, GDI, GDI+, do not provide DPI scaling support. In this article, I will walk through some of the problems of supporting DPI scaling and the solutions for them.
Overview
Every display has a different value for the DPI (dots per inch), which, by the way, for displays is rather called PPI (pixels per inch); DPI is a term originated from printers. However, Windows allows virtualizing this value by enabling users to set a scaling factor for text and UI elements. This is possible from the Display Settings.


The scaling value is given as a percentage value, such as 100%, 125%, 150%, 175%, 200%, etc. Custom percentages (between 100 – 500) are also possible from Advanced scaling settings, although not recommended. These scaling factors correspond to DPI values as follows:
Scaling | DPI |
---|---|
100% | 96 |
125% | 120 |
150% | 144 |
175% | 168 |
200% | 192 |
Keep in mind that this effective DPI value may be different than the native (raw) DPI value of the display. You could have a 140 DPI display with the effective value reported as 96 when set to the 100% scaling.
You’re application may be running on a display with a scaling factor greater than the default 100%. This is especially reasonable to assume for newer laptop devices or displays (such as 4K ones, for instance). It is also more likely in multi-monitor environments, where different monitors are likely to be set up with different scaling.
The system can provide some automatic scaling for applications that can’t handle it by themselves (such as old applications that cannot or will not be updated). This is possible by changing the “Override High DPI scaling behavior” option in the Compatibility tab of the application’s (executable’s) properties. You can read more about this here:
- Improving the high-DPI experience in GDI based Desktop Apps
- High-DPI Scaling Improvements for Desktop Applications in the Windows 10 Creators Update (1703)
- How to Make Windows Work Better on High-DPI Displays and Fix Blurry Fonts
If you want to ensure your application’s user interface is crisp no matter the DPI, that text fonts, controls, windows, and others are adjusted appropriately (increase or decrease in size with the scaling) you need to make it adjust based on the settings on the monitor it’s running on. To do so, you must make it per-monitor DPI aware.
Per Monitor DPI Scaling Support
An application can run with one of four different DPI awareness modes:
- Unaware, views all displays as having 96 DPI
- System, introduced in Windows Vista, views all displays as having the DPI value of the primary display
- Per-Monitor, introduced in Windows 8.1, views the DPI of the display that the application window is primary located on. When the DPI changes, top-level windows are notified of the change, but there is no DPI scaling of any UI elements.
- Per-Monitor V2, introduced in Windows 10 1703, similar to Per-Monitor, but support automatic scaling of non-client area, theme-drawn bitmaps in common controls, and dialogs.
However, as already mentioned, GDI/GDI+ and MFC do not provide any per-monitor DPI awareness support. That means, if you’re using these technologies, you’re on your own to provide this support and appropriate scaling. Part of the solution is to replace Win32 APIs that only support a single DPI (the primary display DPI) with ones that support per-monitor settings, where available, or write your own, where that’s not available.
Here is a list of such APIs. If you’re using these in your code, then your app needs changes.
System (primary monitor) DPI | Per-monitor DPI |
---|---|
GetDeviceCaps | GetDpiForMonitor / GetDpiForWindow |
GetSystemMetrics | GetSystemMetricsForDpi |
SystemParametersInfo | SystemParametersInfoForDpi |
AdjustWindowRectEx | AdjustWindowRectExForDpi |
CWnd::CalcWindowRect | AdjustWindowRectExForDpi |
GetDpiForMonitor vs GetDpiForWindow
The GetDpiForMonitor function returns the DPI value of a monitor. The GetDpiForWindow function returns the DPI value for a window. However, their behavior, and, therefore, return value, depends on some settings.
GetDpiForMonitor
returns a different value based on the PROCESS_DPI_AWARENESS value. This is value set per application that indicates how much scaling is provided by the system and how much is done by the application. The behavior of GetDpiForMonitor
is described in the following table:
PROCESS_DPI_AWARENESS | Description | GetDpiForMonitor return value |
---|---|---|
PROCESS_DPI_UNAWARE | DPI unaware | 96 |
PROCESS_SYSTEM_DPI_AWARE | System DPI aware (all monitors have the DPI value of the primary monitor) | System DPI (the DPI value of the primary monitor) |
PROCESS_PER_MONITOR_DPI_AWARE | Per monitor DPI aware | The actual DPI value set by the user for the specified monitor |
GetDpiForWindow
also returns a different value based on the DPI_AWARENESS value. This is a value per thread, process, or window. This was introduced in Windows 10 1607 as an improvement over the per-application setting provided by PROCESS_DPI_AWARENESS
. The behavior of GetDpiForWindow
is described in the following table:
DPI_AWARENESS | Description | GetDpiForWindow return value |
---|---|---|
DPI_AWARENESS_UNAWARE | DPI unaware | 96 |
DPI_AWARENESS_SYSTEM_AWARE | System DPI aware | System DPI (the DPI value of the primary monitor) |
DPI_AWARENESS_PER_MONITOR_AWARE | Per monitor DPI aware | The actual DPI value set by the user for the monitor where the window is located. |
You can change the values for PROCESS_DPI_AWARENESS
and DPI_AWARENESS
either programmatically or with a manifest.
API | Modify API | Manifest |
---|---|---|
PROCESS_DPI_AWARENESS | SetProcessDpiAwareness | App manifest |
DPI_AWARENESS | SetThreadDpiAwarenessContext SetProcessDpiAwarenessContext | App manifest |
The application manifest should contain the following (for details, see the link from the above table):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> <asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True/PM</dpiAware> <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness> </asmv3:windowsSettings> </asmv3:application> </assembly>
Although in this example both <dpiAware>
and <dpiAwareness>
are set, the former is ignored in Windows 10 1607 or newer if the latter is present.
Retrieving monitor information
You can retrieve monitor information using EnumDisplayMonitors to enumerate the available monitors and functions such as GetMonitorInfo, GetDpiForMonitor, and EnumDisplaySettings to retrieve various monitor information. An example is shown in the following listing:
int main() { ::EnumDisplayMonitors( nullptr, nullptr, [](HMONITOR Arg1, HDC Arg2, LPRECT Arg3, LPARAM Arg4) { MONITORINFOEXA mif; mif.cbSize = sizeof(MONITORINFOEXA); if (::GetMonitorInfoA(Arg1, &mif) != 0) { std::cout << mif.szDevice << '\n'; std::cout << "monitor rect: " << '(' << mif.rcMonitor.left << ',' << mif.rcMonitor.top << ")-" << '(' << mif.rcMonitor.right << ',' << mif.rcMonitor.bottom << ")\n"; std::cout << "work rect: " << '(' << mif.rcWork.left << ',' << mif.rcWork.top << ")-" << '(' << mif.rcWork.right << ',' << mif.rcWork.bottom << ")\n"; } UINT xdpi, ydpi; LRESULT success = ::GetDpiForMonitor(Arg1, MDT_EFFECTIVE_DPI, &xdpi, &ydpi); if (success == S_OK) { std::cout << "DPI (effective): " << xdpi << ',' << ydpi << '\n'; } success = ::GetDpiForMonitor(Arg1, MDT_ANGULAR_DPI, &xdpi, &ydpi); if (success == S_OK) { std::cout << "DPI (angular): " << xdpi << ',' << ydpi << '\n'; } success = ::GetDpiForMonitor(Arg1, MDT_RAW_DPI, &xdpi, &ydpi); if (success == S_OK) { std::cout << "DPI (raw): " << xdpi << ',' << ydpi << '\n'; } DEVMODEA dm; dm.dmSize = sizeof(DEVMODEA); if (::EnumDisplaySettingsA(mif.szDevice, ENUM_CURRENT_SETTINGS, &dm) != 0) { std::cout << "BPP: " << dm.dmBitsPerPel << '\n'; std::cout << "resolution: " << dm.dmPelsWidth << ',' << dm.dmPelsHeight << '\n'; std::cout << "frequency: " << dm.dmDisplayFrequency << '\n'; } std::cout << '\n'; return TRUE; }, 0); }
With three monitors set at 100%, 125% and 150% scaling, this code displays the following for me:
\\.\DISPLAY1 monitor rect: (-1920,0)-(0,1080) work rect: (-1920,0)-(0,1040) DPI (effective): 96,96 DPI (angular): 123,123 DPI (raw): 141,141 BPP: 32 resolution: 1920,1080 frequency: 60 \\.\DISPLAY2 monitor rect: (0,0)-(2560,1440) work rect: (0,0)-(2560,1390) DPI (effective): 120,120 DPI (angular): 108,108 DPI (raw): 108,108 BPP: 32 resolution: 2560,1440 frequency: 59 \\.\DISPLAY3 monitor rect: (2560,0)-(4480,1200) work rect: (2560,0)-(4480,1140) DPI (effective): 144,144 DPI (angular): 93,93 DPI (raw): 94,94 BPP: 32 resolution: 1920,1200 frequency: 59
The value of the scaling is the ratio between the effective DPI (seen above) and 96. For instance, on the second display, 120 / 96 is 1.25, therefore the scaling for that display is set to 125%.
GetDeviceCaps
The use of GetDeviceCaps to retrieve the value of the DPI is a clear code smell that your code is not DPI aware. Typically, you could see code like the following to get the DPI:
int GetDpi(HWND hWnd) { HDC hDC = ::GetDC(hWnd); INT ydpi = ::GetDeviceCaps(hDC, LOGPIXELSY); ::ReleaseDC(hWnd, hDC); return ydpi; }
int dpi = GetDpi(GetDesktopWindow()); int scaling = static_cast<int>(100.0 * dpi / 96);
This is what the docs are saying about LOGPIXELSX
and LOGPIXELSY
:
LOGPIXELSX
Number of pixels per logical inch along the screen width. In a system with multiple display monitors, this value is the same for all monitors.
LOGPIXELSY
Number of pixels per logical inch along the screen height. In a system with multiple display monitors, this value is the same for all monitors.
Therefore, this function is not able to return a per-monitor DPI. For that, you should use GetDpiForWindow, available since Windows 10 1607.
int GetDpi(HWND hWnd) { return static_cast<int>(::GetDpiForWindow(hWnd)); }
If you’re targeting an earlier version, you can also use GetDpiForMonitor, which is available since Windows 8.1.
int GetDpi(HWND hWnd) { bool v81 = IsWindows8Point1OrGreater(); bool v10 = IsWindows10OrGreater(); if (v81 || v10) { HMONITOR hMonitor = ::MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST); UINT xdpi, ydpi; LRESULT success = ::GetDpiForMonitor(hMonitor, MDT_EFFECTIVE_DPI, &xdpi, &ydpi); if (success == S_OK) { return static_cast<int>(ydpi); } return 96; } else { HDC hDC = ::GetDC(hWnd); INT ydpi = ::GetDeviceCaps(hDC, LOGPIXELSY); ::ReleaseDC(NULL, hDC); return ydpi; } }
This implementation calls GetDpiForMonitor
if the code is running on Windows 8.1 or newer, and falls back to GetDeviceCaps
for older systems. This is, most likely, not necessarily, since prior to Windows 10 1607 it’s unlikely you can do truly per-monitor DPI aware native apps.
IsWindows8Point1OrGreater
and IsWindows10OrGreater
depend on the application manifest to specify the application support a particular operating system. Otherwise, they return false even if the application is running on Windows 8.1 or Windows 10. Notice these two functions are available since Windows 2000. You can setup the application manifest as following:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <application> <!-- Windows 10, Windows Server 2016 and Windows Server 2019 --> <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Windows 8.1 and Windows Server 2012 R2 --> <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Windows 8 and Windows Server 2012 --> <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Windows 7 and Windows Server 2008 R2 --> <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Windows Vista and Windows Server 2008 --> <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> </application> </compatibility> </assembly>
Remember both GetDpiForWindow
and GetDpiForMonitor
depend on the DPI awareness set in the application manifest or programmatically, as described above.
AdjustWindowRect, AdjustWindowRectEx, CWnd::CalcWindowRect
The AdjustWindowRect and its sister API, AdjustWindowRectEx, calculate the required size of the window rectangle based on the desired size of the client rectangle. Similarly, the MFC counter-part, CWnd::CalcWindowRect, does the same, by calling AdjustWindowRectEx
. However, these Windows APIs are not DPI aware and should be replaced with AdjustWindowsRectExForDPI. This function was introduced in Windows 10 1607.
The MFC implementation of CalcWindowRect
is the following:
void CWnd::CalcWindowRect(LPRECT lpClientRect, UINT nAdjustType) { DWORD dwExStyle = GetExStyle(); if (nAdjustType == 0) dwExStyle &= ~WS_EX_CLIENTEDGE; ::AdjustWindowRectEx(lpClientRect, GetStyle(), FALSE, dwExStyle); }
This should be replaced with the following implementation:
using AdjustWindowRectExForDpi_fn = BOOL(WINAPI *)(LPRECT, DWORD, BOOL, DWORD, UINT); BOOL CalcWindowRectForDpi( LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi, UINT nAdjustType = CWnd::adjustBorder) { if (nAdjustType == 0) dwExStyle &= ~WS_EX_CLIENTEDGE; HMODULE hModule = ::LoadLibrary(_T("User32.dll")); // don't call FreeLibrary() with this handle; the module was already loaded up, it would break the app if (hModule != nullptr) { AdjustWindowRectExForDpi_fn addr = (AdjustWindowRectExForDpi_fn)::GetProcAddress(hModule, _T("AdjustWindowRectExForDpi")); if (addr != nullptr) { return addr(lpRect, dwStyle, bMenu, dwExStyle, dpi); } } return ::AdjustWindowRectEx(lpRect, dwStyle, bMenu, dwExStyle); }
For calling this function, you need to the pass the DPI, after priory retrieving it as explained earlier.
Therefore, you should do the following replacements in your code:
Function | Replacement |
---|---|
AdjustWindowRect | AdjustWindowRectExForDpi |
AdjustWindowRectEx | AdjustWindowRectExForDpi |
CWnd::CalcWindowRect | CalcWindowRectForDpi |
CDC pixel conversion functions
The MFC’s CDC class contains several functions that perform conversions:
Function | Description |
---|---|
DPtoHIMETRIC | Converts device units into HIMETRIC units. |
HIMETRICtoDP | Converts HIMETRIC units into device units. |
LPtoHIMETRIC | Converts logical units into HIMETRIC units. |
HIMETRICtoLP | Converts HIMETRIC units into logical units. |
These functions need the DPI to perform the conversion, but depend on the GetDeviceCaps
function. Here is their implementation:
#define HIMETRIC_INCH 2540 // HIMETRIC units per inch void CDC::DPtoHIMETRIC(LPSIZE lpSize) const { ASSERT(AfxIsValidAddress(lpSize, sizeof(SIZE))); int nMapMode; if (this != NULL && (nMapMode = GetMapMode()) < MM_ISOTROPIC && nMapMode != MM_TEXT) { // when using a constrained map mode, map against physical inch ((CDC*)this)->SetMapMode(MM_HIMETRIC); DPtoLP(lpSize); ((CDC*)this)->SetMapMode(nMapMode); } else { // map against logical inch for non-constrained mapping modes int cxPerInch, cyPerInch; if (this != NULL) { ASSERT_VALID(this); ASSERT(m_hDC != NULL); // no HDC attached or created? cxPerInch = GetDeviceCaps(LOGPIXELSX); cyPerInch = GetDeviceCaps(LOGPIXELSY); } else { cxPerInch = afxData.cxPixelsPerInch; cyPerInch = afxData.cyPixelsPerInch; } ASSERT(cxPerInch != 0 && cyPerInch != 0); lpSize->cx = MulDiv(lpSize->cx, HIMETRIC_INCH, cxPerInch); lpSize->cy = MulDiv(lpSize->cy, HIMETRIC_INCH, cyPerInch); } } void CDC::HIMETRICtoDP(LPSIZE lpSize) const { ASSERT(AfxIsValidAddress(lpSize, sizeof(SIZE))); int nMapMode; if (this != NULL && (nMapMode = GetMapMode()) < MM_ISOTROPIC && nMapMode != MM_TEXT) { // when using a constrained map mode, map against physical inch ((CDC*)this)->SetMapMode(MM_HIMETRIC); LPtoDP(lpSize); ((CDC*)this)->SetMapMode(nMapMode); } else { // map against logical inch for non-constrained mapping modes int cxPerInch, cyPerInch; if (this != NULL) { ASSERT_VALID(this); ASSERT(m_hDC != NULL); // no HDC attached or created? cxPerInch = GetDeviceCaps(LOGPIXELSX); cyPerInch = GetDeviceCaps(LOGPIXELSY); } else { cxPerInch = afxData.cxPixelsPerInch; cyPerInch = afxData.cyPixelsPerInch; } ASSERT(cxPerInch != 0 && cyPerInch != 0); lpSize->cx = MulDiv(lpSize->cx, cxPerInch, HIMETRIC_INCH); lpSize->cy = MulDiv(lpSize->cy, cyPerInch, HIMETRIC_INCH); } } void CDC::LPtoHIMETRIC(LPSIZE lpSize) const { ASSERT(AfxIsValidAddress(lpSize, sizeof(SIZE))); LPtoDP(lpSize); DPtoHIMETRIC(lpSize); } void CDC::HIMETRICtoLP(LPSIZE lpSize) const { ASSERT(AfxIsValidAddress(lpSize, sizeof(SIZE))); HIMETRICtoDP(lpSize); DPtoLP(lpSize); }
These functions can be rewritten as following, to be DPI aware. However, the DPI is actually provided as an argument, just as in the case of CalcWindowRectForDpi
above.
constexpr int HIMETRIC_INCH = 2540; // HIMETRIC units per inch void Win32Utils::DPtoHIMETRIC(HDC hDC, LPSIZE lpSize, int const iDpi) { ASSERT(AfxIsValidAddress(lpSize, sizeof(SIZE))); int nMapMode; if ((nMapMode = ::GetMapMode(hDC)) < MM_ISOTROPIC && nMapMode != MM_TEXT) { // when using a constrained map mode, map against physical inch ::SetMapMode(hDC, MM_HIMETRIC); ::DPtoLP(hDC, reinterpret_cast<LPPOINT>(lpSize), 1); ::SetMapMode(hDC, nMapMode); } else { lpSize->cx = MulDiv(lpSize->cx, HIMETRIC_INCH, iDpi); lpSize->cy = MulDiv(lpSize->cy, HIMETRIC_INCH, iDpi); } } void Win32Utils::HIMETRICtoDP(HDC hDC, LPSIZE lpSize, int const iDpi) { ASSERT(AfxIsValidAddress(lpSize, sizeof(SIZE))); int nMapMode; if ((nMapMode = ::GetMapMode(hDC)) < MM_ISOTROPIC && nMapMode != MM_TEXT) { // when using a constrained map mode, map against physical inch ::SetMapMode(hDC, MM_HIMETRIC); ::LPtoDP(hDC, reinterpret_cast<LPPOINT>(lpSize), 1); ::SetMapMode(hDC, nMapMode); } else { lpSize->cx = MulDiv(lpSize->cx, iDpi, HIMETRIC_INCH); lpSize->cy = MulDiv(lpSize->cy, iDpi, HIMETRIC_INCH); } } void Win32Utils::LPtoHIMETRIC(HDC hDC, LPSIZE lpSize, int const iDpi) { ASSERT(AfxIsValidAddress(lpSize, sizeof(SIZE))); ::LPtoDP(hDC, reinterpret_cast<LPPOINT>(lpSize), 1); DPtoHIMETRIC(hDC, lpSize, iDpi); } void Win32Utils::HIMETRICtoLP(HDC hDC, LPSIZE lpSize, int const iDpi) { ASSERT(AfxIsValidAddress(lpSize, sizeof(SIZE))); HIMETRICtoDP(hDC, lpSize, iDpi); ::DPtoLP(hDC, reinterpret_cast<LPPOINT>(lpSize), 1); }
More of MFC
If you search for GetDeviceCaps
(or for LOGPIXELSY
) in the MFC source code, you’ll see there are other places where it’s used. These include CFrameWnd::RecalcLayout
and CWnd::RepositionBars
. You will have to look out for all these functions and replace them. CFrameWnd::RecalcLayout
, for instance, is a virtual method so you can override it. As for CWnd::RepositionBars
, you just need to replace it. You could copy the implementation from MFC and replace the parts that deal with DPI.
Working with fonts
The GDI CreateFont
API, the MFC’s CFont
class (based on the previous) and the GDI+ Font
class are not DPI aware. You can specify the height in various units, but the height is not adjusted based on the DPI. This is something you have to do explicitly. Let’s take a look at these functions and classes.
API | Library | Description |
---|---|---|
CreateFont | GDI | Creates a logical font with the specified characteristics. The height is given in logical units and indicates either the height of the character cell (if it’s a positive number) or the height of the character (if it’s a negative number). |
LOGFONTA / LOGFONTW | GDI | A structure that defines the attributes of a font. The height has the the same meaning as above. |
CFont::CreateFont | MFC | A wrapper of the GDI’s CreateFont function. |
CFont::CreateFontIndirect | MFC | Similar to CreateFont but takes a pointer to LOGFONT structure to describe the attributes of the font to be created. |
Font::Font | GDI+ | A set of overloaded constructors. Some take a pointer to a LOGFONT structure. Others take multiple arguments including the height in a specified unit. The default unit is the point (1/72 of an inch), but various other units are available. |
This is what the documentation is saying about the height of the GDI fonts:
For the
MM_TEXT
mapping mode, you can use the following formula to specify a height for a font with a specified point size:
nHeight = -MulDiv(PointSize, GetDeviceCaps(hDC, LOGPIXELSY), 72);
Therefore, we often see code that looks like this:
int pointSize = 12; int height = -MulDiv(pointSize, ::GetDeviceCaps(hDC, LOGPIXELSY), 72); HFONT hFont = CreateFont( height, // nHeight 0, // nWidth 0, // nEscapement 0, // nOrientation FW_DONTCARE, // nWeight FALSE, // bItalic FALSE, // bUnderline FALSE, // cStrikeOut ANSI_CHARSET, // nCharSet OUT_DEFAULT_PRECIS, // nOutPrecision CLIP_DEFAULT_PRECIS, // nClipPrecision DEFAULT_QUALITY, // nQuality DEFAULT_PITCH | FF_SWISS, // nPitchAndFamily "Tahoma")
The part that needs to be changed here is the computation of the height. GetDeviceCaps
needs to be replaced with one of the functions that can return the proper DPI of the window or monitor.
int pointSize = 12; int height = -MulDiv(pointSize, ::GetDpiForWindow(hWnd), 72);
If you’re working with GDI+ and creating fonts by specifying a unit points you need to consider that the library is using the system DPI, which is the DPI of the primary monitor or just 96. Therefore, you need to adjust your font size with a factor that is the ratio between the DPI of the monitor on which the window that uses the font is displayed and the DPI of the primary monitor.
Therefore, if you have code that looks like this:
Gdiplus::Font font(L"Tahoma", 12, Gdiplus::FontStyleRegular);
You need to modify it as follows:
int primaryMonitorDpi = ::GetDpiForWindow(::GetDesktopWindow()); // or GetDeviceCaps(), or GetDpiForMonitor() int currentMonitorDpi = ::GetDpiForWindow(hwnd); Gdiplus::REAL emSize = 12.0 * currentMonitorDpi / primaryMonitorDpi; Gdiplus::Font font(L"Tahoma", emSize, Gdiplus::FontStyleRegular);
Reacting to DPI changes
If you want your app to change on the fly to changes in DPI then you need to handle some windows messages and trigger the appropriate updates in your code. There are several messages related to DPI changes:
Message | Description |
---|---|
WM_DPICHANGED | Received by top-level windows when the effective DPI has changed. This message is only relevant for per-monitor DPI aware applications or threads. This was introduced in Windows 8.1. |
WM_DPICHANGED_BEFOREPARENT | For top-level windows that are per-monitor v2 DPI aware, this message is sent (from bottom-up) to all the windows in the child HWND tree of the window that is undergoing the DPI change. This is sent before the top-level window receives the WM_DPICHANGED message. |
WM_DPICHANGED_AFTERPARENT | For top-level windows that are per-monitor v2 DPI aware, this message is sent (from top-down) to all the windows in the child HWND tree of the window that is undergoing the DPI change. This is sent after the top-level window receives the WM_DPICHANGED message. |
Here I show you an example of a dialog application reacting to DPI changes and resizing and repositioning all the controls on the dialog. This is how the dialog resource looks like:

First, you need to register the handler for the WM_DPICHANGED
message.
BEGIN_MESSAGE_MAP(CDemoDlg, CDialogEx) // ... ON_MESSAGE(WM_DPICHANGED, OnDpiChanged) END_MESSAGE_MAP()
The implementation of this handler should do the following:
- resize and reposition the dialog (the top-level window that received the message); notice that the new window rectangle is received with the
LPARAM
argument. - enumerate all child windows and execute a callback that resizes and repositions each child window.
To do the latter step above, you have to:
- determine the relative position of the child window to the parent window; this is needed to adjust the top-left corner of the child window based on the new DPI
- know both the previous value of the DPI and the new DPI so that the position (left and top) and the size (width and height) can be adjusted accordingly (if you go from 100% to 125% the sizes must increase, but from 125% to 100% they have to decrease).
All these can be implemented as follows:
LRESULT CDemoDlg::OnDpiChanged(WPARAM wParam, LPARAM lParam) { if (m_dpi != 0) { RECT* const prcNewWindow = reinterpret_cast<RECT*>(lParam); ::SetWindowPos( m_hWnd, nullptr, prcNewWindow->left, prcNewWindow->top, prcNewWindow->right - prcNewWindow->left, prcNewWindow->bottom - prcNewWindow->top, SWP_NOZORDER | SWP_NOACTIVATE); ::EnumChildWindows( m_hWnd, [](HWND hWnd, LPARAM lParam) { int const dpi = ::GetDpiForWindow(hWnd); int const previousDpi = static_cast<int>(lParam); CRect rc; ::GetWindowRect(hWnd, rc); // child window rect in screen coordinates HWND parentWnd = ::GetParent(hWnd); CRect rcParent; ::GetWindowRect(parentWnd, rcParent); // parent window rect in screen coordinates POINT ptPos = { rc.left, rc.top }; ::ScreenToClient(parentWnd, &ptPos); // transforming the child window pos // from screen space to parent window space int dpiScaledX = ::MulDiv(ptPos.x, dpi, previousDpi); int dpiScaledY = ::MulDiv(ptPos.y, dpi, previousDpi); int dpiScaledWidth = ::MulDiv(rc.Width(), dpi, previousDpi); int dpiScaledHeight = ::MulDiv(rc.Height(), dpi, previousDpi); ::SetWindowPos( hWnd, nullptr, dpiScaledX, dpiScaledY, dpiScaledWidth, dpiScaledHeight, SWP_NOZORDER | SWP_NOACTIVATE); return TRUE; }, m_dpi); } m_dpi = HIWORD(wParam); return 0; }
Notice that m_dpi
is a class member, initialized with 0, that stores the value of the current DPI of the window.
The result is that if you move the window from one screen to another, with different DPIs, the application automatically adjust accordingly to the new DPI. Here are several screenshots from displays with 100%, 125%, and 150% scaling.



Conclusion
Making a native Windows application to be per-monitor DPI aware requires a certain amount of extra work. It is also going to work only for Windows 10 but at this point you shouldn’t be supporting any previous operating systems. To accomplish this task you need to loop up all those APIs (mentioned in this article) that are related to the DPI (return the DPI or fetch the DPI for various calculations) and replace them with other functions (either system ones or user-defined ones) that handle the DPI in a proper way.
Probably this is a piece of a good article but the title should be “How to build high DPI aware native Windows applications”. Most people now uses multiplatform solutions where they can.
That’s probably a good suggestion.
Is WTL supporting HIGH DPI?