An application that is not explicitly marked as high-DPI aware will be lied to by the system and told that there are 96 DPI with a scaling factor of 100%. In order to get the real DPI settings, and avoid automatic virtualization by DWM, you will need to include <dpiAware>True/PM</dpiAware>
in your application’s manifest. More information is available here.
In your case, it sounds like you are looking for the LogicalToPhysicalPointForPerMonitorDPI
and PhysicalToLogicalPointForPerMonitorDPI
pair of functions. As the linked documentation explains, by default, the system will return information about other windows based on the DPI awareness of the caller. So if a non-DPI aware application tries to get the bounds of a window of a high-DPI aware process, it will get bounds that have been translated into its own non-DPI aware coordinate space. This would be, in the vernacular of these functions, the “logical” coordinates. You can convert these to “physical” coordinates, which are those that are actually used by the operating system (and other high-DPI aware processes).
To answer your actual question, though: If you absolutely need to break through the operating system’s lies in a process that is not DPI aware, I can think of two ways to do so:
-
Call the
GetScaleFactorForMonitor
function. If the resultingDEVICE_SCALE_FACTOR
value is anything other thanSCALE_100_PERCENT
, then you are scaled. If your application is not DPI aware, then you are being virtualized.This is a quick-and-dirty solution, as a simple P/Invoke definition is all you need to call it from a WinForms application. However, you should not rely on its results for anything more than a Boolean “are we scaled/virtualized?” indicator. In other words, do not trust the scale factor that it returns!
On a Windows 10 system where the system DPI is 96, and a high-DPI monitor has a 144 DPI (150% scaling), the
GetScaleFactorForMonitor
function returnsSCALE_140_PERCENT
when it would be expected to returnSCALE_150_PERCENT
(144/96 == 1.5). I don’t really understand why this is the case. The only thing I can figure out is that it was designed for Metro/Modern/UWP apps on Windows 8.1, where 150% is not a valid scale factor but 140% is. The scaling factors have since been unified in Windows 10, but this function appears not to have been updated and still returns unreliable results for desktop applications. -
Calculate the scaling factor yourself, based on the logical and physical widths of the monitor.
First, of course, you’ll need to obtain an
HMONITOR
(handle to a specific physical monitor). You can do this by callingMonitorFromWindow
, passing a handle to your WinForms window, and specifyingMONITOR_DEFAULTTONEAREST
. That will get you a handle to the monitor that your window of interest is being displayed on.Then, you’ll use this monitor handle to get the logical width of that monitor by calling the
GetMonitorInfo
function. That fills in aMONITORINFOEX
structure that contains, as one of its members, aRECT
structure (rcMonitor
) that contains the virtual-screen coordinates of that monitor. (Remember that, unlike .NET, the Windows API represents rectangles in terms of their left, top, right, and bottom extents. The width is the right extent minus the left extent, while the height is the bottom extent minus the top extent.)The
MONITORINFOEX
structure filled in byGetMonitorInfo
will also have given you the name of that monitor (theszDevice
member). You can then use that name to call theEnumDisplaySettings
function, which will fill in aDEVMODE
structure with a bunch of information about the physical display modes for that monitor. The members you’re interested in aredmPelsWidth
anddmPelsHeight
, which give you the number of physical pixels per width and height, respectively.You can then divide the logical width by the physical width to determine the scaling factor for the width. Same thing for the height (except that all monitors I’m aware of have square pixels, so the vertical scaling factor will be equal to the horizontal scaling factor).
Example code, tested and working in Windows 10 (written in C++ because that’s what I have handy; sorry you’ll have to do your own translation to .NET):
// Get the monitor that the window is currently displayed on // (where hWnd is a handle to the window of interest). HMONITOR hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST); // Get the logical width and height of the monitor. MONITORINFOEX miex; miex.cbSize = sizeof(miex); GetMonitorInfo(hMonitor, &miex); int cxLogical = (miex.rcMonitor.right - miex.rcMonitor.left); int cyLogical = (miex.rcMonitor.bottom - miex.rcMonitor.top); // Get the physical width and height of the monitor. DEVMODE dm; dm.dmSize = sizeof(dm); dm.dmDriverExtra = 0; EnumDisplaySettings(miex.szDevice, ENUM_CURRENT_SETTINGS, &dm); int cxPhysical = dm.dmPelsWidth; int cyPhysical = dm.dmPelsHeight; // Calculate the scaling factor. double horzScale = ((double)cxPhysical / (double)cxLogical); double vertScale = ((double)cyPhysical / (double)cyLogical); ASSERT(horzScale == vertScale);