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

1

13

I'm trying to detect in a WinForms application if it has been launched in scaled/virtualized mode due to the OS having a high DPI. Currently, in a system running at 3840x2400 with 200% scaling, the application sees the resolution as 1920x1200, the DPI as 96, and the scale factor is 1.

We are in the process of making the application DPI-aware, but until then, we need a "quick fix" that will allow us to detect if scaled. The reason for this is that it breaks a functionality in the application that takes a screenshot. We use the scaled dimensions in Graphics.CopyFromScreen, it takes a screenshot of the wrong size since it is expecting the non-scaled dimensions.

I am aware of the DPI-awareness setting, but for the moment, we still want the application to be scaled, but be able to detect that we are scaled and get the non-scaled dimensions, if possible.

Cortie answered 3/11, 2015 at 19:1 Comment(2)
Move the screen-shot taking code into another app that is dpiaware.Digraph
@HansPassant That's a great suggestion, but we serve this application through Remote Desktop Services as a RemoteApp, which really complicates this approach.Cortie
S
32

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);
    
Sacculate answered 26/4, 2016 at 12:18 Comment(10)
Thank you for this detailed response! This is exactly what I was looking for!Cortie
Note that the "GetScaleFactorForMonitor" approach shouldn't be used at all, even for just checking if scaling is being applied. Based on my tests on Windows 10 (only non-anniversary checked), with a scale factor of either 100% or 125%, this function still returns SCALE_100_PERCENT, with unusual values returned for higher numbers as mentioned in the answer. This is definitely a Windows bug. The enumeration values themselves map to integers, IE, SCALE_150_PERCENT = 150, which you'd expect they did so that any scale factor could be returned here accurately as an integer. Hopefully they'll fix it.Unitarian
Wow, I was looking all over trying to come up with solution for similar problem, and here it is beautifully explained and in great detail. This answer deserves way more upvotes.Piaffe
Monitor #1 is 125%, Monitor #2 remains 100%, if we didn't mark PerMonitor DpiAwareness but mark dpiAware = true in manifest file, your code still return double true for both monitor. Is it normal? Monitor #1:cxPhysical = 1366;cyPhysical = 768 cxLogical = 1366;cyLogical = 768 Monitor #2: cxPhysical = 1920;cyPhysical = 1080 cxLogical = 2400;cyLogical = 1350Reduce
Look like logic is:ASSERT(cxPhysical == cxLogical && cyPhysical == cyLogical );Reduce
@Reduce If you don't have per-monitor awareness (PMA) support enabled in the app manifest, then you're going to get scaling behavior as you change from a monitor with one DPI setting to a monitor with another DPI setting. See: learn.microsoft.com/en-us/windows/win32/hidpi/… So, yes, the system is going to return the same settings for both monitors in that case, because you haven't indicated in the app manifest that you are able to handle having different settings for different monitors.Sacculate
Perhaps you can also consider using GetDpiForMonitor, it worked in my tests. Use it along with temporary setting your thread as DPI-aware with SetThreadDpiAwarenessContext (Win10 only).Jacquiline
The < dpiaware > True < / dpiaware > works good when I have multiple monitors with different dpi set for each. With this above line added to app.manifest file, the GetMonitorInfo returns the correct rect values for the monitor's work area.Elisha
@CodyGray Did you try your functions? For Windows 10.0.19044.1889, non DPI awared app, GetDPIForMonitor returns 96 (instead of 120), GetScaleFactorForMonitor returns 100 (instead of 125), GetMonitorInfo returns 1536x864 (instead of 1920x1080). Was this behaviour changed in some Windows version?Equuleus
Thanks for the code! For some reason the code at the end of the post calculates the scale factor to be 1.0, where it should be 1.25 (I've set the Windows scale factor to 125% in the Settings app). I do have < dpiaware > True < / dpiaware > in my manifest. Any ideas?Gio

© 2022 - 2025 — McMap. All rights reserved.