0

I have a non-DPI aware WPF application where I want to draw a set of polygons in a borderless window to fit exactly on a monitor. I have an algorithm in place to scale and draw my polygons to any given resolution. In my setup I have a 4K and a FullHD monitor next to each other. My 4K monitor has its scale set to 150% and the FullHD monitor is set to 100%. For the 4K monitor, this means that if I set a windows width and height to 3840x2160, the actual rendered resolution is 2560x1440. Now if I scale my set of polygons to 4K, the polygons get rendered outside the canvas and the window. I suspect this is because the polygons are not aware of the DPI setting of my 4K monitor. If I draw the polygons on my FullHD monitor, they fit perfectly since that monitors scale is set to 100%.

To combat this problem, I have tried the following:

  • Retrieve the DPI of each monitor and scale the polygons with the DPI in mind.

This works partly. Since my application is non-DPI aware (note that I am not willing to make it DPI aware since that introduces a whole new set of problems), any method for retrieving a monitors DPI results in getting 144 (150%) for both monitors. This results in the polygons fitting perfectly on my 4K monitor, but they will be scaled too small on my FullHD monitor. I have tried the following methods for retrieving DPI: GetDpiForMonitor from Shcore.dll, VisualTreeHelper and Matrixes. Note that these methods do work if I set my application to be DPI aware, but I can not do that for all the extra work that introduces.

  • ViewBox wrapping the Canvas

ViewBox does not automatically downscale the contents when I set the canvas width and height to 3840x2160 (ViewBox requires its contents, the canvas, to have a set width and height).

  • Retrieving the "real"/scaled resolution of a monitor

With this I mean I need to access an API of some kind which will return a resolution of 2560x1440 for my 4K monitor. I have tried the classic Windows.Forms.Screen API as well as the newer WindowsDispalyAPI. But both always return a 4K resolution for my 4K monitor.

So all my algorithms are working, I only need to find any of the following:

  • A reliable way to retrieve DPI of an individual monitor while keeping my application non-DPI aware.
  • A way to retrieve the scaled resolution of monitor.
  • Some other way to scale a set of polygons to fit the screen.

Any help is appreciated.

Edit:

Here is an xaml exmaple of a borderless window which reproduces the problem on a 4K screen with 150% scaling:

<Window x:Class="Test.Views.FullscreenPolygon"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" Width="3840" Height="2160"
        WindowStyle="None" AllowsTransparency="True" Background="Transparent">
    <Grid>
        <Canvas x:Name="CanvasArea">
            <Polygon Points="2888,0 3360,2140 3840,0" Fill="Black"></Polygon>
            <Polygon Points="1920,20 1450,2160 2400,2160" Fill="Black"></Polygon>
        </Canvas>
    </Grid>
</Window>

As you can see, both polygons (triangles) are scaled to fit the 4K resolution of the window. The window itself gets rendered as 2560x1440, because of the 150% scaling of the monitor. The polygons however, get rendered outside of it, partly onto my second screen.

Edit2: Got it working thanks to Jeff, using the GetScreenScaleFactorNonDpiAware method in his project.

Stijn
  • 33
  • 5

1 Answers1

1

I needed to account for screen scaling at some point and, as AlwaysLearning notes, I had to import and use user32.dll as I too use 4K monitors but mine are scaled to 125%. I created a separate class for this.

I have a test program to utilize this class. Here is the test output:

             monitor name| \\.\DISPLAY2
               native dpi| 96
               screen dpi| 120
             scale factor| 1.25
           scaling factor| 0.8
       native screen size| {X=0,Y=0,Width=3840,Height=2160}
       scaled screen size| {X=0,Y=0,Width=3072,Height=1728}

the above is created from this:

logMsgLn2("monitor name", ScreenParameters.GetMonitorName(this));
logMsgLn2("native dpi", ScreenParameters.GetNativeScreenDpi);
logMsgLn2("screen dpi", ScreenParameters.GetScreenDpi(this));
logMsgLn2("scale factor", ScreenParameters.GetScreenScaleFactor(this));
logMsgLn2("scaling factor", ScreenParameters.GetScreenScalingFactor(this));
logMsgLn2("native screen size", ScreenParameters.GetNativeScreenSize(this));
logMsgLn2("scaled screen size", ScreenParameters.GetScaledScreenSize(this));

Here is the whole class:

public class ScreenParameters
{
    private const double NativeScreenDpi = 96.0;
    private const int CCHDEVICENAME = 32;

    // private method to get the handle of the window
    // this keeps this class contained / not dependant
    public static double GetNativeScreenDpi
    {
        get => (int) NativeScreenDpi;
    }

    public static string GetMonitorName(Window win)
    {
        MONITORINFOEX mi = GetMonitorInfo(GetWindowHandle(win));

        return mi.DeviceName;
    }

    private static IntPtr GetWindowHandle(Window win)
    {
        return new WindowInteropHelper(win).Handle;
    }

    // the actual screen DPI adjusted for the scaling factor
    public static double GetScreenDpi(Window win)
    {
        return GetDpiForWindow(GetWindowHandle(win));
    }

    // this is the ratio of the current screen Dpi
    // and the base Dpi
    public static double GetScreenScaleFactor(Window win)
    {
        return (GetScreenDpi(win)  / NativeScreenDpi);
    }

    // this is the conversion factor between screen coordinates 
    // and sizes and their actual actual coordinate and size
    // e.g. for a screen set to 125%, this factor applied 
    // to the native screen dimensions, will provide the 
    // actual screen dimensions
    public static double GetScreenScalingFactor(Window win)
    {
        return (1 / (GetScreenDpi(win)  / NativeScreenDpi));
    }

    // get the dimensions of the physical / native screen
    // ignoring any applied scaling
    public static Rectangle GetNativeScreenSize(Window win)
    {
        MONITORINFOEX mi = GetMonitorInfo(GetWindowHandle(win));

        return ConvertRectToRectangle(mi.rcMonitor);
    }

    // get the screen dimensions taking the screen scaling into account
    public static Rectangle GetScaledScreenSize2(Window win)
    {
        double ScalingFactor = GetScreenScalingFactor(win);

        Rectangle rc = GetNativeScreenSize(win);

        if (ScalingFactor == 1) return rc;

        return rc.Scale(ScalingFactor);
    }

    public static Rectangle GetScaledScreenSize(Window win)
    {
        double dpi = GetScreenDpi(win);

        Rectangle rc = GetNativeScreenSize(win);

        return ScaleForDpi(rc, dpi);
    }

    internal static MONITORINFOEX GetMonitorInfo(IntPtr ptr)
    {
        IntPtr hMonitor = MonitorFromWindow(ptr, 0);

        MONITORINFOEX mi = new MONITORINFOEX();
        mi.Init();
        GetMonitorInfo(hMonitor, ref mi);

        return mi;
    }

    #region + Utility methods

    public static Rectangle ConvertRectToRectangle(RECT rc)
    {
        return new Rectangle(rc.Top, rc.Left, 
            rc.Right - rc.Left, rc.Bottom - rc.Top);
    }


    public static System.Drawing.Point ScaleForDpi(System.Drawing.Point pt, double dpi)
    {
        double factor = NativeScreenDpi / dpi;

        return new System.Drawing.Point((int) (pt.X * factor), (int) (pt.Y * factor));
    }


    public static Point ScaleForDpi(Point pt, double dpi)
    {
        double factor = NativeScreenDpi / dpi;

        return new Point(pt.X * factor, pt.Y * factor);
    }

    public static Size ScaleForDpi(Size size, double dpi)
    {
        double factor = NativeScreenDpi / dpi;

        return new Size(size.Width * factor, size.Height * factor);
    }

    public static System.Drawing.Size ScaleForDpi(System.Drawing.Size size, double dpi)
    {
        double factor = NativeScreenDpi / dpi;

        return new System.Drawing.Size((int) (size.Width * factor), (int) (size.Height * factor));
    }

    public static Rectangle ScaleForDpi(Rectangle rc, double dpi)
    {
        double factor = NativeScreenDpi / dpi;

        return new Rectangle(ScaleForDpi(rc.Location, dpi),
                ScaleForDpi(rc.Size, dpi));
    }

        #endregion

    #region + Dll Imports

    [DllImport("user32.dll")]
    internal static extern UInt16 GetDpiForWindow(IntPtr hwnd);


    [DllImport("user32.dll")]
    internal static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);


    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    internal static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);


    [DllImport("user32.dll")]
    internal static extern UInt16 GetProcessDpiAwareness(IntPtr hwnd);

    #endregion


    #region + Dll Enums

    internal enum dwFlags : uint
    {
        MONITORINFO_PRIMARY = 1
    }

        #endregion

    #region + Dll Structs

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
    internal struct MONITORINFOEX
    {
        public uint    cbSize;
        public RECT    rcMonitor;
        public RECT    rcWorkArea;
        public dwFlags Flags;

        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)]
        public string DeviceName;

        public void Init()
        {
            this.cbSize     = 40 + 2 * CCHDEVICENAME;
            this.DeviceName = String.Empty;
        }
    }

    public struct RECT
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }

    #endregion
}

I hope this helps.

For future reference, I updated the code a bit to allow for Non-DPI aware monitors. I placed the updated code here ScreenParameters

Jeff
  • 646
  • 1
  • 7
  • 13
  • If I understand it correctly, I have to create the full screen window on the desired monitor first and then use your helper class to get the monitor information? – Stijn Aug 12 '19 at 20:38
  • No, no. The helper class just needs any window as a starting point. Which monitor the window in on is the monitor that the data is provided. – Jeff Aug 13 '19 at 04:32
  • So I did some testing with your code and when I ask the scaling of a window which is opened on my second monitor, it still returns 1.5 (150%), while it should be 1 (100%) for my second monitor. What happens for you when you set your second monitor to a different scaling size as your first monitor and then try to get the scaling for the second monitor? – Stijn Aug 14 '19 at 18:03
  • Not sure I understand what is happening. I tested and I get screen specific results depending on which screen the window resides - which is what I expect and what I think you want. You say that you "ask the scaling" of a window. How did you do this? This may be a dumb question, but are you sure your second monitor is at 100% scale? – Jeff Aug 16 '19 at 02:16
  • Double checked and it is set to 100%. My method was as follows: I created a new window on a button click, set its X and Y such that it is visible on my second monitor and called `.Show()` on it. Thereafter I used your helper to retrieve the DPI and still got 1.5. – Stijn Aug 18 '19 at 19:07
  • probably a dumb question, but the new window includes a method to find its own screen parameters. That is, the "this" for the above is of the new window and not the window that creates the new window. Also, you have moved the primary window around to different screens and re-checked the screen parameters when on a different screen? – Jeff Aug 19 '19 at 01:56
  • Sorry. I did some further checking and my test program is set to be DPI aware which is why I got different results. I adjusted the helper file to incorporate Non-DPI aware values and created a test program for this and placed on github: [ScreenParameters](https://github.com/morbius1st/ScreenParameters) – Jeff Aug 25 '19 at 17:24
  • Finally had some time to take a look at your code and it works! I am using the `GetScreenScaleFactorNonDpiAware` method from your helper class. Thank you for all the effort. – Stijn Sep 08 '19 at 14:06