Detect if non DPI-aware application has been scaled/virtualized

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:

  1. Call the GetScaleFactorForMonitor function. If the resulting DEVICE_SCALE_FACTOR value is anything other than SCALE_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 returns SCALE_140_PERCENT when it would be expected to return SCALE_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.

  2. 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 calling MonitorFromWindow, passing a handle to your WinForms window, and specifying MONITOR_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 a MONITORINFOEX structure that contains, as one of its members, a RECT 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 by GetMonitorInfo will also have given you the name of that monitor (the szDevice member). You can then use that name to call the EnumDisplaySettings function, which will fill in a DEVMODE structure with a bunch of information about the physical display modes for that monitor. The members you’re interested in are dmPelsWidth and dmPelsHeight, 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);
    

Leave a Comment