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

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.

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 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.
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);

Related

Unity - Window size depending on Screen size

I would like my game to have a 1:1 aspect ratio, but scaled up to a certain amount. Meaning that the width and height must be identical, but never larger than the actual screen size. Ontop of that, to ensure consistent pixel sizes the width and height values must be power of 2 value.
I didn't have any problems figuring out the needed value.
int value = 2;
int limit = Screen.currentResolution.height;
while (value * 2 < limit) value *= 2;
Debug.Log(value);
I much rather have no idea how to set the window size BEFORE the splash image is even shown. Is there any way how to do this?
Yes, there is, but that means that you'll need to get rid of the launch window.
The reason is that, if you enable the launch window (from which you can select resolutions, quality, windowed or fullscreen mode etc.), Unity will show only the video card available resolutions - and this means no 1:1 aspect ratio resolutions available.
So, in order to do this, you need to setup the Player Settings as follows:
The important part is to disable the Display Resolution Dialog.
Then you set the Default Screen Width and Height by disabling the Default Is Native Resolution.
Notice that the standalone will be forced to this, and only this, resolution at start - after the splash screen you can set whatever resolution you want by calling the Screen.SetResolution method from any script in the first scene loaded.
Of course you can make the standalone start in windowed or fullscreen mode, by unticking/ticking the Default Is Full Screen option.
That's pretty much it, if you wanted to give the user the option to choose from a list of 1:1 AR resolutions, you simply just can't at the moment afaik.
Edit: The resolution info of the Player Settings are stored in the registry inside HKEY_CURRENT_USER\Software\[YourCompanyName]\[YourGameName].
The 3 keys are these:
Screenmanager Is Fullscreen mode
Screenmanager Resolution Height
Screenmanager Resolution Width
To change those from inside the game at runtime, you need to use:
PlayerPrefs.SetInt("Screenmanager Is Fullscreen mode", [0/1]);
PlayerPrefs.SetInt("Screenmanager Resolution Height", [HeighthRes]);
PlayerPrefs.SetInt("Screenmanager Resolution Width", [WidthRes]);
These will be read the next time the game is launched, setting the starting resolution before the splash screen.

Dealing with multiple DPI screens

Let's suppose that I have two screens, side by side:
1920x1080 100% DPI
1360x768 125% DPI
For my Window, this means:
1920x1080: Ok
1088x614: Not ok, it's divided by 1,25 because of the scaling factor.
Turning into this:
1920x1080 + 1088x614: 3008x1080
I want to use the CopyFromScreen/BitBlt methods.
These methods ignore all DPI info, making the Left and Top properties (of a window, for example) useless if inside a high dpi screen. Or left to a high dpi screen, since it behaves like 1 screen, example:
So whenever I need to get a screen point from within a set of screens with at least one having a high DPI, it will return a smaller point.
Is there any way to get the true (by true, ignoring the scaling factor) XY info from a set of screens with (at least one) high DPI?
I already tried the managed PointToScreen and the unmanaged ClientToScreen methods, both resulting the same "right" point.
Please, read
I want to take screenshots of the screen based of the position of my Window.
I have two monitors, one with 100% DPI, other with 125% DPI.
If my Window is inside the 1st monitor, the screenshot based on the Left/Top properties of my Window works.
If my Window is inside the second monitor, the screenshot won't take the right spot!
Because
The BitBlt API method ignores the scaling of the screens. Example:
Screenshot of the point 100;100 will be right, because it's inside the 1st screen.
Screenshot of the point 1950;100 won't be right, because it's inside the 2nd screen. Notice that it's 30 pixels to the right.
Why?
As said earlier, for my app, the 125% DPI reduces the screen resolution to 1088x614, but for the BitBlt method, it is still 1360x768.
So I can't convert the Left/Top properties, because it will be wrong, since there is a 100% DPI screen to the left.
Example of the Left property:
I believe this is the right way to convert:
1920px + 50px: 100% + 125%: 1920 + 62: 1982px
And this is the proposed version:
1920px + 50px: 100% + 125%: 2400 + 62: 2462px
See, if I simple convert the current Left property based on the DPI of the current Window, on this case my second screen, I'll also be converting the values of my first screen. This should not happen.

WPF: SystemParameters.WindowCaptionButtonHeight returns smaller number than expected

We have a window where we have set the WindowChrome GlassFrameThickness to a negative value so that we have glass frame extended over the entire window.
With this, the SystemParameters.WindowCaptionButtonHeight value returns 22 when I am actually expecting something in the range of ~36. I've provided some pictures showing what the current and expected behavior should be.
Current Behavior
Expected Behavior
Currently I am working around this issue by using the WindowCaptionButtonWidth instead of height since it returns the more expected value but this just feels wrong.
Under the DWM, these system parameters no longer give correct vales. You can still calculate the height by summing up SystemParameters.CaptionHeight, SystemParameters.WindowResizeBorderThickness.Left (or whatever other than Top), and SystemParameters.WindowNonClientFrameThickness.Left (or whatever other than Top).
However, things become quite complicated under Per-Monitor DPI environment like Windows 10. These system parameters turn to be useless without correct modifications by System DPI and Per-Monitor DPI. So, if you wish to completely redesign a window, the caption height will be a part of total considerations for Per-Monitor DPI.

MeasureString() gives different size for different screen resolutions

We are using Measurestring() to calculate size based on the length of text. For different screen resolution, MeasureString() gives different size.
Graphics g;
Size size = g.MeasureString(GetItemText(this.Items[n]), this.Font).ToSize();
width=size.width;
For Screen resolution 125%, size.width=76 and
For Screen resolution 100% and 150%, size.width=61.
How can i get same width in this code, please suggest me some ideas to measure size using measurestring().
Waiting for suggestions........
It's because the 125% behaves differently by default. For example, in Windows 7, if you change the DPI setting, because of the Windows XP style mode, the applications will aware of the current DPI setting. However, if you set 150%, this checkbox is not set by default, so the applications will work in DPI unaware mode, which means, that the MeasureString will return the same result as in case of 96 DPI, and the resizing will be performed automatically by Windows.
Normally you can ignore the result, because the sizes will be upscaled in your application anyway. If you still want to obtain the actual DPI value of Windows, see my answer here: https://stackoverflow.com/a/33412669/5114784
And then you can upscale your drawing like this (but as I said, normally this is not needed):
// See GetSystemDpi in the referenced post
float zoomFactor = (float)GetSystemDpi() / 96f;
size.Width = (int)(size.Width * zoomFactor);
size.Height = (int)(size.Height * zoomFactor);

How do I determine the true pixel size of my Monitor in .NET?

I want to display an image at 'true size' in my application. For that I need to know the pixel size of the display.
I know windows display resolution is nominally 96dpi, but for my purposes I want a better guess. I understand this information may not always be available or accurate (e.g. older CRT displays), but I imagine with the prevelance of LCD displays that this should be possible!
Is there a way to get the pixel size of my display?
Is there a way to determine if the pixel size is accurate?
.NET API's preferred (I couldn't find them), but Win32 is OK too, I'm happy to P/Invoke.
For the display size you'll want Screen.PrimaryScreen.Bounds.Size (or Screen.GetBounds(myform)).
If you want the DPI, use the DpiX and DpiY properties of Graphics:
PointF dpi = PointF.Empty;
using(Graphics g = this.CreateGraphics()){
dpi.X = g.DpiX;
dpi.Y = g.DpiY;
}
Oh, wait! You wanted actual, hold a ruler up to the monitor and measure, size?! No. Not possible using any OS services. The OS doesn't know the actual dimensions of the monitor, or how the user has it calibrated. Some of this information is theoretically detectable, but it's not deterministic enough for the OS to use it reliably, so it doesn't.
As a work around, you can try a couple of things.
You can try to query the display string of the installed monitor device (I'm not sure how to do that) and see if you can parse out a sensible size out of that. For example, the monitor might be a "ValueBin E17p", and you might deduce that it's a 17" monitor from that. Of course, this display string is likely to be "Plug and Play Monitor". This scheme is pretty sketchy at best.
You could ask the user what size monitor they have. Maybe they'll know.
Once you know (or think you know) the monitor's diagonal size, you need to find its physical aspect ratio. Again, a couple of things:
Assume the current pixel aspect ratio matches the monitor's physical aspect ratio. This assumes that (A) the user has chosen a resolution that is ideal for their monitor, and that (B) the monitor has square pixels. I don't know of a current consumer-oriented computer monitor that doesn't have square pixels, but older ones did and newer ones might.
Ask the user. Maybe they'll know.
Once you know (or think you know) what the monitor's diagonal size and physical aspect ratio are, then you you can calculate it's physical width and height. A2 + B2 = C2, so a few calculations will give it to you good:
If you found out that it's a 17" monitor, and its current resolution is 1280 x 1024:
12802 + 10242 = 2686976
Sqrt(2686976) = 1639.1998047828092637409837247032
17" * 1280 / 1639.2 = 13.274768179599804782820888238165"
17" * 1024 / 1639.2 = 10.619814543679843826256710590532"
This puts the physical width at 13.27" and the physical height at 10.62". This makes the pixels 13.27" / 1280 = 10.62" / 1024 = 0.01037" or about 0.263 mm.
Of course, all of this is invalid if the user doesn't have a suitable resolution, the monitor has wacky non-square pixels, or it's an older analog monitor and the controls aren't adjusted properly for the display to fill the entire physical screen. Or worse, it could be a projector.
In the end, you may be best off performing a calibration step where you have the user actually hold a ruler up to the screen, and measure the size of something for you. You could:
Have the user click the mouse on any two points an inch (or a centimeter) apart.
Draw a box on the screen and have the user press the up and down arrows to adjust its height, and the left and right arrows to adjust its width, until the box is exactly one inch (or centimeter) square according to their ruler.
Draw a box on the screen and have the user tell you how many inches/centimeters it is in each dimension.
No matter what you do, don't expect your results to be 100% accurate. There are way too many factors at play for you (or the user) to get this exactly correct, every time.
Be aware that 96 dpi is usually pretty close to accurate. Modern pixels on non-projected screens all tend to be about 0.25 mm, give or take, so you usually end up with about 100 physical pixels per inch, give or take, if the monitor is set to its native resolution. (Of course, this is a huge generalization and does not apply to all monitors. Eee PCs, for example, have pixels about 0.19 mm in size, if I remember the specs correctly.)
sorry, you've got to P/Invoke for this information.
Here's the link that I utilized for it a while ago:
http://www.davidthielen.info/programming/2007/05/get_screen_dpi_.html
You can check by just manually calculating from your screen size
cos(45)*LCD_SCREEN_DIAGONAL_IN_INCHES/sqrt(HORZ_RES^2 + VERT_RES^2)
That would give you the pixel width in inches
GetDeviceCaps can be P/Invoke'd to get some figures, but I've never known the figures to be that trustworthy...
You may obtain the physical dimensions of the display using the EDID information stored in the registry. You can obtain the appropriate monitor's registry key using the EnumDisplayDevices windows API call.
Physical Dimensions to the Screen object:
TL;DR
WPF's True Size = Pixels * DPI Magnification
DPI Magnification:
Matrix dpiMagnification
= PresentationSource.FromVisual(MyUserControl).CompositionTarget.TransformToDevice;
double magnificationX = dpiMagnification.M11;
double magnificationY = dpiMagnification.M22;
Discussion
I had trouble solving this question still in 2020. Back when this question was asked/answered in 2009, .NET C# probably meant Windows Forms. But WPF is the de facto standard of the day...
By asking about "true size" you have probably already figured out that the operating system does some calculation with actual pixels (say 1366x768, which I understand is usual laptop resolutions) and the DPI (hard to find) in order to give a control's true size. And you are trying to make an app that scales to different monitors.
This DPI actual number seems to be hidden, but it has been normalized (converted to a percentage). Assume 100% = 96 DPI, just because the actual number does not matter anymore. People can easily increase the system-wide text size by going to Desktop on Windows 10 > right click > Display settings > section Scale and layout > change the percentage to magnify text and other elements.
You can find the pixels another way, and multiple/divide the pixel by the DPI percentage in order to get true size. For instance, I want to drag a UserControl around a canvas element of a WPF window with the mouse. The user control's pixel count and the mouse xy-coordinates were off by the normalized DPI. In order to keep the mouse moving at the same rate as the user control, I use:
double newXCoord = System.Windows.Forms.Cursor.Position.X;
double newYCoord = System.Windows.Forms.Cursor.Position.Y;
double deltaX = newXCoord - oldXCoord;
double deltaY = newYCoord - oldYCoord;
double magnificationX = 1;
double magnificationY = 1;
Matrix dpiMagnification
= PresentationSource.FromVisual(visual).CompositionTarget.TransformToDevice;
if (magnificationMatrix != null)
{
magnificationX = dpiMagnification.M11;
magnificationY = dpiMagnification.M22;
}
PixelsFromLeft += deltaX / m_magnificationX;
PixelsFromTop += deltaY / m_magnificationY;

Categories