Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wpf/WinForms: Report per-monitor screen information when in system-dpi mode #1820

Merged
merged 1 commit into from
Nov 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions src/Eto.WinForms/Forms/ScreenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,28 @@ public ScreenHandler(swf.Screen screen)

public Image GetImage(RectangleF rect)
{
var ss = Bounds.Size;
var bounds = Bounds;
var realBounds = bounds;

// get scale based on actual pixel size, we don't support high DPI on winforms (yet)
// and the CopyFromScreen API always requires actual pixel size.
using (var g = sd.Graphics.FromHwnd(IntPtr.Zero))
var hmonitor = Win32.MonitorFromPoint(bounds.Location.ToSDPoint(), 0);
if (hmonitor != IntPtr.Zero)
{
var hdc = g.GetHdc();
// get actual monitor dimentions
var oldDpiAwareness = Win32.PerMonitorDpiSupported ? Win32.SetThreadDpiAwarenessContext(Win32.DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE) : Win32.DPI_AWARENESS_CONTEXT.NONE;

ss.Width = GetDeviceCaps(hdc, DESKTOPHORZRES);
ss.Height = GetDeviceCaps(hdc, DESKTOPVERTRES);
var info = new Win32.MONITORINFOEX();
Win32.GetMonitorInfo(new HandleRef(null, hmonitor), info);
realBounds = info.rcMonitor.ToSD().ToEto();

g.ReleaseHdc(hdc);
if (oldDpiAwareness != Win32.DPI_AWARENESS_CONTEXT.NONE)
Win32.SetThreadDpiAwarenessContext(oldDpiAwareness);
}

var realRect = Rectangle.Ceiling(rect * (float)(ss.Width / Bounds.Width));
var screenBmp = new sd.Bitmap(realRect.Width, realRect.Height, sd.Imaging.PixelFormat.Format32bppArgb);
var adjustedRect = rect;
adjustedRect.Size *= (float)(realBounds.Width / bounds.Width);
adjustedRect.Location += realBounds.Location;
var realRect = Rectangle.Ceiling(adjustedRect);
var screenBmp = new sd.Bitmap(realRect.Width, realRect.Height, sd.Imaging.PixelFormat.Format32bppRgb);
using (var bmpGraphics = sd.Graphics.FromImage(screenBmp))
{
bmpGraphics.CopyFromScreen(realRect.X, realRect.Y, 0, 0, realRect.Size.ToSD());
Expand Down
15 changes: 15 additions & 0 deletions src/Eto.WinForms/Win32.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ static partial class Win32
{
#pragma warning disable 0649
// Analysis disable InconsistentNaming
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int left;
Expand All @@ -18,6 +19,20 @@ public struct RECT
public int bottom;
public int width => right - left;
public int height => bottom - top;

public System.Drawing.Rectangle ToSD() => new System.Drawing.Rectangle(left, top, width, height);
}

[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int x;
public int y;
public POINT(int x, int y)
{
this.x = x;
this.y = y;
}
}
#pragma warning restore 0649

Expand Down
120 changes: 103 additions & 17 deletions src/Eto.WinForms/Win32.dpi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,9 @@ static partial class Win32
static Lazy<bool> perMonitorDpiSupported = new Lazy<bool>(() => MethodExists("shcore.dll", "SetProcessDpiAwareness"));
static Lazy<bool> monitorDpiSupported = new Lazy<bool>(() => MethodExists("shcore.dll", "GetDpiForMonitor"));

public static bool PerMonitorDpiSupported
{
get { return perMonitorDpiSupported.Value; }
}
public static bool PerMonitorDpiSupported => perMonitorDpiSupported.Value;

public static bool MonitorDpiSupported
{
get { return monitorDpiSupported.Value; }
}
public static bool MonitorDpiSupported => monitorDpiSupported.Value;

public enum PROCESS_DPI_AWARENESS : uint
{
Expand Down Expand Up @@ -92,23 +86,24 @@ public static Eto.Drawing.PointF ScreenToLogical(this sd.Point point, swf.Screen
sdscreen = sdscreen ?? swf.Screen.FromPoint(point);
var location = sdscreen.GetLogicalLocation();
var pixelSize = sdscreen.GetLogicalPixelSize();
var sdscreenBounds = sdscreen.GetBounds();

var x = location.X + (point.X - sdscreen.Bounds.X) / pixelSize;
var y = location.Y + (point.Y - sdscreen.Bounds.Y) / pixelSize;
var x = location.X + (point.X - sdscreenBounds.X) / pixelSize;
var y = location.Y + (point.Y - sdscreenBounds.Y) / pixelSize;

// Console.WriteLine($"In: {point}, out: {x},{y}");
return new Drawing.PointF(x, y);
}

public static Eto.Drawing.RectangleF ScreenToLogical(this Eto.Drawing.Rectangle rect) => ScreenToLogical(rect.ToSD());

public static Eto.Drawing.RectangleF ScreenToLogical(this sd.Rectangle rect)
public static Eto.Drawing.RectangleF ScreenToLogical(this Eto.Drawing.Rectangle rect)
{
var screen = swf.Screen.FromPoint(new sd.Point(rect.X + rect.Width / 2, rect.Y + rect.Height / 2));
var location = screen.GetLogicalLocation();
var pixelSize = screen.GetLogicalPixelSize();
var screenBounds = screen.GetBounds();
return new Eto.Drawing.RectangleF(
location.X + (rect.X - screen.Bounds.X) / pixelSize,
location.Y + (rect.Y - screen.Bounds.Y) / pixelSize,
location.X + (rect.X - screenBounds.X) / pixelSize,
location.Y + (rect.Y - screenBounds.Y) / pixelSize,
rect.Width / pixelSize,
rect.Height / pixelSize
);
Expand All @@ -127,7 +122,55 @@ class ScreenHelper : LogicalScreenHelper<swf.Screen>

public override swf.Screen PrimaryScreen => swf.Screen.PrimaryScreen;

public override sd.Rectangle GetBounds(swf.Screen screen) => screen.Bounds;
public override sd.Rectangle GetBounds(swf.Screen screen)
{
if (!PerMonitorDpiSupported)
return screen.Bounds;

var oldDpiAwareness = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2);

var hmonitor = MonitorFromPoint(screen.Bounds.Location, 0);
var info = new MONITORINFOEX();
GetMonitorInfo(new HandleRef(null, hmonitor), info);

//var bounds = screen.Bounds;
var bounds = info.rcMonitor.ToSD();

if (oldDpiAwareness != DPI_AWARENESS_CONTEXT.NONE)
SetThreadDpiAwarenessContext(oldDpiAwareness);
return bounds;
}

public sd.Rectangle GetWorkingArea(swf.Screen screen)
{
if (!PerMonitorDpiSupported)
return screen.WorkingArea;

var oldDpiAwareness = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2);

var hmonitor = MonitorFromPoint(screen.Bounds.Location, 0);
var info = new MONITORINFOEX();
GetMonitorInfo(new HandleRef(null, hmonitor), info);

// var bounds = screen.WorkingArea;
var bounds = info.rcWork.ToSD();

if (oldDpiAwareness != DPI_AWARENESS_CONTEXT.NONE)
SetThreadDpiAwarenessContext(oldDpiAwareness);
return bounds;
}

DPI_AWARENESS_CONTEXT? processDpiAwareness;

public DPI_AWARENESS_CONTEXT ProcessDpiAwareness
{
get
{
if (processDpiAwareness == null)
processDpiAwareness = GetThreadDpiAwarenessContext();
return processDpiAwareness.Value;
}
}

public override float GetLogicalPixelSize(swf.Screen screen)
{
Expand All @@ -140,19 +183,27 @@ public override float GetLogicalPixelSize(swf.Screen screen)
return (uint)graphics.DpiY / 96f;
}
}
// use per-monitor aware dpi awareness to get ACTUAL dpi here
var oldDpiAwareness = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2);

var pnt = new System.Drawing.Point(screen.Bounds.Left + 1, screen.Bounds.Top + 1);
var mon = MonitorFromPoint(pnt, MONITOR.DEFAULTTONEAREST);
uint dpiX, dpiY;
GetDpiForMonitor(mon, MDT.EFFECTIVE_DPI, out dpiX, out dpiY);

if (oldDpiAwareness != DPI_AWARENESS_CONTEXT.NONE)
SetThreadDpiAwarenessContext(oldDpiAwareness);
return dpiX / 96f;
}

public override SizeF GetLogicalSize(swf.Screen screen) => (SizeF)screen.Bounds.Size.ToEto() / screen.GetLogicalPixelSize();
public override SizeF GetLogicalSize(swf.Screen screen) => (SizeF)GetBounds(screen).Size.ToEto() / screen.GetLogicalPixelSize();
}

static ScreenHelper locationHelper = new ScreenHelper();

public static Eto.Drawing.Rectangle GetBounds(this swf.Screen screen) => locationHelper.GetBounds(screen).ToEto();
public static Eto.Drawing.Rectangle GetWorkingArea(this swf.Screen screen) => locationHelper.GetWorkingArea(screen).ToEto();

public static Eto.Drawing.PointF GetLogicalLocation(this swf.Screen screen) => locationHelper.GetLogicalLocation(screen);

public static Eto.Drawing.SizeF GetLogicalSize(this swf.Screen screen) => locationHelper.GetLogicalSize(screen);
Expand Down Expand Up @@ -180,7 +231,42 @@ public override float GetLogicalPixelSize(swf.Screen screen)
[DllImport("shcore.dll")]
public static extern uint GetProcessDpiAwareness(IntPtr handle, out PROCESS_DPI_AWARENESS awareness);

[DllImport("User32.dll")]
public static extern DPI_AWARENESS_CONTEXT SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT dpiContext);

[DllImport("User32.dll")]
public static extern DPI_AWARENESS_CONTEXT GetThreadDpiAwarenessContext();

[DllImport("User32.dll")]
public static extern bool EnableNonClientDpiScaling(IntPtr hwnd);

[DllImport("User32.dll", CharSet = CharSet.Auto)]
public static extern bool GetMonitorInfo(HandleRef hmonitor, [In, Out] MONITORINFOEX info);
[DllImport("User32.dll", ExactSpelling = true)]
public static extern IntPtr MonitorFromPoint(POINT pt, int flags);

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

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 4)]
public class MONITORINFOEX
{
public int cbSize = Marshal.SizeOf(typeof(MONITORINFOEX));
public RECT rcMonitor = new RECT();
public RECT rcWork = new RECT();
public int dwFlags = 0;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public char[] szDevice = new char[32];
}

public enum DPI_AWARENESS_CONTEXT
{
NONE = 0,
UNAWARE = -1,
SYSTEM_AWARE = -2,
PER_MONITOR_AWARE = -3,
PER_MONITOR_AWARE_v2 = -4,
UNAWARE_GDISCALED = -5
}
}
}
17 changes: 15 additions & 2 deletions src/Eto.Wpf/Forms/MouseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,21 @@ public void Initialize()

public PointF Position
{
get => swf.Control.MousePosition.ScreenToLogical();
set => swf.Cursor.Position = Point.Round(value.LogicalToScreen()).ToSD();
get
{
var oldDpiAwareness = Win32.PerMonitorDpiSupported ? Win32.SetThreadDpiAwarenessContext(Win32.DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2) : Win32.DPI_AWARENESS_CONTEXT.NONE;
var result = swf.Control.MousePosition.ScreenToLogical();
if (oldDpiAwareness != Win32.DPI_AWARENESS_CONTEXT.NONE)
Win32.SetThreadDpiAwarenessContext(oldDpiAwareness);
return result;
}
set
{
var oldDpiAwareness = Win32.PerMonitorDpiSupported ? Win32.SetThreadDpiAwarenessContext(Win32.DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2) : Win32.DPI_AWARENESS_CONTEXT.NONE;
swf.Cursor.Position = Point.Round(value.LogicalToScreen()).ToSD();
if (oldDpiAwareness != Win32.DPI_AWARENESS_CONTEXT.NONE)
Win32.SetThreadDpiAwarenessContext(oldDpiAwareness);
}
}

public static int s_CursorSetCount;
Expand Down
10 changes: 6 additions & 4 deletions src/Eto.Wpf/Forms/ScreenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ float GetRealScale()

public Image GetImage(RectangleF rect)
{
var realRect = Rectangle.Ceiling(rect * Widget.LogicalPixelSize);
using (var screenBmp = new sd.Bitmap(realRect.Width, realRect.Height, sd.Imaging.PixelFormat.Format32bppArgb))
var adjustedRect = rect * Widget.LogicalPixelSize;
adjustedRect.Location += Control.Bounds.Location.ToEto();
var realRect = Rectangle.Ceiling(adjustedRect);
using (var screenBmp = new sd.Bitmap(realRect.Width, realRect.Height, sd.Imaging.PixelFormat.Format32bppRgb))
{
using (var bmpGraphics = sd.Graphics.FromImage(screenBmp))
{
Expand All @@ -71,9 +73,9 @@ public Image GetImage(RectangleF rect)

public float Scale => 96f / 72f;

public RectangleF Bounds => Control.Bounds.ScreenToLogical();
public RectangleF Bounds => Control.GetBounds().ScreenToLogical();

public RectangleF WorkingArea => Control.WorkingArea.ScreenToLogical();
public RectangleF WorkingArea => Control.GetWorkingArea().ScreenToLogical();

public int BitsPerPixel => Control.BitsPerPixel;

Expand Down
35 changes: 30 additions & 5 deletions src/Eto.Wpf/Forms/WpfWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -595,9 +595,19 @@ public new Point Location
if (handle != IntPtr.Zero)
{
// Left/Top doesn't always report correct location when maximized, so use Win32 when we can.
Win32.RECT rect;
if (Win32.GetWindowRect(handle, out rect))
return Point.Round(new Point(rect.left, rect.top).ScreenToLogical(SwfScreen));
var oldDpiAwareness = Win32.PerMonitorDpiSupported ? Win32.SetThreadDpiAwarenessContext(Win32.DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2) : Win32.DPI_AWARENESS_CONTEXT.NONE;
try
{

Win32.RECT rect;
if (Win32.GetWindowRect(handle, out rect))
return Point.Round(new Point(rect.left, rect.top).ScreenToLogical(SwfScreen));
}
finally
{
if (oldDpiAwareness != Win32.DPI_AWARENESS_CONTEXT.NONE)
Win32.SetThreadDpiAwarenessContext(oldDpiAwareness);
}
}
// in WPF, left/top of a window is transformed by the (current) screen dpi, which makes absolutely no sense.
var left = Control.Left;
Expand Down Expand Up @@ -644,10 +654,14 @@ void Control_SourceInitialized(object sender, EventArgs e)

void SetLocation(PointF location)
{
var oldDpiAwareness = Win32.PerMonitorDpiSupported ? Win32.SetThreadDpiAwarenessContext(Win32.DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2) : Win32.DPI_AWARENESS_CONTEXT.NONE;

var handle = WindowHandle;
var loc = location.LogicalToScreen();

Win32.SetWindowPos(WindowHandle, IntPtr.Zero, loc.X, loc.Y, 0, 0, Win32.SWP.NOSIZE | Win32.SWP.NOACTIVATE);
if (oldDpiAwareness != Win32.DPI_AWARENESS_CONTEXT.NONE)
Win32.SetThreadDpiAwarenessContext(oldDpiAwareness);
}

public WindowState WindowState
Expand Down Expand Up @@ -693,7 +707,17 @@ public WindowState WindowState

public Rectangle RestoreBounds
{
get { return Control.WindowState == sw.WindowState.Normal || Control.RestoreBounds.IsEmpty ? Widget.Bounds : Control.RestoreBounds.ToEto(); }
get
{
if (Control.WindowState == sw.WindowState.Normal || Control.RestoreBounds.IsEmpty)
return Widget.Bounds;

var restoreBounds = Control.RestoreBounds.ToEto();
var scale = DpiScale;
var position = new Point((int)(restoreBounds.X / scale), (int)(restoreBounds.Y / scale));
restoreBounds.Location = Point.Truncate(position.ScreenToLogical(SwfScreen));
return restoreBounds;
}
}

sw.Window IWpfWindow.Control
Expand Down Expand Up @@ -814,7 +838,8 @@ public double WpfScale

public float LogicalPixelSize
{
get {
get
{
var scale = (float)(dpiHelper?.Scale ?? sw.PresentationSource.FromVisual(Control)?.CompositionTarget.TransformToDevice.M11 ?? 1.0);
// will be zero after the window is closed, but should always be a positive number
if (scale <= 0)
Expand Down
Loading