Fullscreen WPF window on a MS Windows system with multiple Monitors and different DPI settings

218 views Asked by At

I have a Windows 11 system with 2 monitors:

  • One 4k monitor with scale factor of 150% and
  • One FullHD monitor on scale factor 100% (Of course this needs to work with differnt system settings too)

I want to show a new WPF Window on the other monitor - meaning on the monitor my main app is currently not being displayed on. So if my main window is on the 4k monitor then I want to show the new window on the FullHD monitor.

The new windows should be showing a fullscreen view of my webcam. But having different DPI settings makes it hard to show a fullscreen window in the correct position and size.

My Window XAML is straight forward and looks like this

<Window
    x:Class="MyApp.Fullscreen"
    x:ClassModifier="internal"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Width="800"
    Height="600"
    AllowsTransparency="False"
    Background="Black"
    ResizeMode="NoResize"
    ShowInTaskbar="False"
    Topmost="True"
    WindowStyle="None">
    <Grid>
        <Image Name="WebCam" />
    </Grid>
</Window>

I initialize it like this

 var allSystemScreens = System.Windows.Forms.Screen.AllScreens;
 var otherScreen = DetermineOtherScreen(allSystemScreens);

 var window = new Fullscreen();
 window.WindowStartupLocation = WindowStartupLocation.Manual;

 window.Left = otherScreen.WorkingArea.Left;
 window.Top = otherScreen.WorkingArea.Top;
 window.Width = otherScreen.WorkingArea.Width;
 window.Height = otherScreen.WorkingArea.Height;

 window.Show();

But it does not work. It does not show the window fully on the monitor or is way bigger than the monitor and is reaching into the second one.

I played around with the values from the WorkingArea. But it never works.

My app is DPI per-monitor-aware. I tried to apply the scale factor that I read from the system but no matter what values I use - it is never correct positioned or sized.

How to do it correctly? Or is there an easier or better way to achieve this? Maybe without using the WindowStartupLocation.Manual.

2

There are 2 answers

6
Freeman On BEST ANSWER

OK, so instead of using the WorkingArea property, which may not take into account the DPI scaling, we can use the Bounds property of the monitor to calculate the correct position and size, in my new suggestion, I'm using the Bounds property of the monitor, which represents the entire monitor area, including any taskbars or other system elements and by dividing the Bounds values by the DPI scale factor, I can ensure that the position and size are calculated correctly for the specific monitor's DPI scaling!

Additionally, I've added window.WindowState = WindowState.Maximized; to set the window to fullscreen mode.

var otherScreen = DetermineOtherScreen(allSystemScreens);

var otherScreenScaleFactor = VisualTreeHelper.GetDpi(window);
double otherScreenScale = otherScreenScaleFactor.DpiScaleX;

double left = otherScreen.Bounds.Left / otherScreenScale;
double top = otherScreen.Bounds.Top / otherScreenScale;
double width = otherScreen.Bounds.Width / otherScreenScale;
double height = otherScreen.Bounds.Height / otherScreenScale;
    
window.Left = left;
window.Top = top;
window.Width = width;
window.Height = height;

window.WindowState = WindowState.Maximized;
6
emoacht On

You can calculate Left, Top, Width and Height of Window but as I commented, such calculation can be screwed up depending on the number of screens and their placement. Instead, I recommend to use Win32 functions that are not affected by DPI of each monitor. In addition, if WindowStyle of Window is not WindowStyle.None, you will need to take into account thickness of extended frame bounds which is known to be 7 when DPI is 96.

EDIT:

I identified the cause of incorrect size of window after this method is applied. If dpiAwareness is specifiled in the application manifest, the size will not correctly set in the case of newly created window. The version of .NET Framework or .NET does not matter.

The workaround is pretty simple. Just call this method again at Loaded event. I modified the code and it should work when dpiAwareness is specified.

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;

public static class WindowHelper
{
    public static void SetWindowRect(Window window, System.Drawing.Rectangle rect)
    {
        SetWindowRect(window, rect.X, rect.Y, rect.Width, rect.Height);
    }

    public static void SetWindowRect(Window window, int x, int y, int width, int height)
    {
        IntPtr windowHandle = new WindowInteropHelper(window).EnsureHandle();
        GetWindowPlacement(windowHandle, out WINDOWPLACEMENT windowPlacement);

        int left = x;
        int top = y;
        int right = x + width;
        int bottom = y + height;

        if (window.WindowStyle != WindowStyle.None)
        {
            IntPtr monitorHandle = MonitorFromPoint(new POINT(x, y), MONITOR_DEFAULTTO.MONITOR_DEFAULTTONULL);
            if (monitorHandle != IntPtr.Zero)
            {
                if (TryGetMonitorDpi(monitorHandle, out DpiScale dpi))
                {
                    const double DefaultExtendedFrameBoundsThickness = 7D;
                    int extendedFrameBoundsThickness = (int)(DefaultExtendedFrameBoundsThickness * dpi.PixelsPerDip);

                    left -= extendedFrameBoundsThickness;
                    right += extendedFrameBoundsThickness;
                    bottom += extendedFrameBoundsThickness;
                }
            }
        }

        windowPlacement.rcNormalPosition = new RECT(left, top, right, bottom);
        SetWindowPlacement(windowHandle, ref windowPlacement);
    }

    private static bool TryGetMonitorDpi(IntPtr monitorHandle, out DpiScale dpi)
    {
        const double DefaultPixelsPerInch = 96D;

        if (GetDpiForMonitor(monitorHandle, MONITOR_DPI_TYPE.MDT_Default, out uint dpiX, out uint dpiY) == S_OK)
        {
            dpi = new DpiScale(dpiX / DefaultPixelsPerInch, dpiY / DefaultPixelsPerInch);
            return true;
        }
        dpi = default;
        return false;
    }

    [DllImport("User32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetWindowPlacement(
        IntPtr hWnd,
        out WINDOWPLACEMENT lpwndpl);

    [DllImport("User32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetWindowPlacement(
        IntPtr hWnd,
        [In] ref WINDOWPLACEMENT lpwndpl);

    [StructLayout(LayoutKind.Sequential)]
    private struct WINDOWPLACEMENT
    {
        public uint length;
        public uint flags;
        public uint showCmd;
        public POINT ptMinPosition;
        public POINT ptMaxPosition;
        public RECT rcNormalPosition;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        public int x;
        public int y;

        public POINT(int x, int y)
        {
            this.x = x;
            this.y = y;
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
        public int left;
        public int top;
        public int right;
        public int bottom;

        public RECT(int left, int top, int right, int bottom)
        {
            this.left = left;
            this.top = top;
            this.right = right;
            this.bottom = bottom;
        }
    }

    [DllImport("User32.dll")]
    private static extern IntPtr MonitorFromPoint(
        POINT pt,
        MONITOR_DEFAULTTO dwFlags);

    private enum MONITOR_DEFAULTTO : uint
    {
        MONITOR_DEFAULTTONULL = 0x00000000,
        MONITOR_DEFAULTTOPRIMARY = 0x00000001,
        MONITOR_DEFAULTTONEAREST = 0x00000002,
    }

    [DllImport("Shcore.dll", SetLastError = true)]
    private static extern int GetDpiForMonitor(
        IntPtr hmonitor,
        MONITOR_DPI_TYPE dpiType,
        out uint dpiX,
        out uint dpiY);

    private enum MONITOR_DPI_TYPE
    {
        MDT_Effective_DPI = 0,
        MDT_Angular_DPI = 1,
        MDT_Raw_DPI = 2,
        MDT_Default = MDT_Effective_DPI
    }

    private const int S_OK = 0x0;
}

Then you can use this method like the following.

var window = new Fullscreen();
WindowHelper.SetWindowRect(window, otherScreen.WorkingArea); // 1st call
window.Loaded += OnLoaded;
window.Show();

void OnLoaded(object sender, RoutedEventArgs e)
{
    window.Loaded -= OnLoaded;
    WindowHelper.SetWindowRect(window, otherScreen.WorkingArea); // 2nd call
}