Skip to content

Commit

Permalink
Merge pull request #1820 from cwensley/curtis/wpf-screen-positions
Browse files Browse the repository at this point in the history
Wpf/WinForms: Report per-monitor screen information when in system-dpi mode
  • Loading branch information
cwensley committed Nov 10, 2020
2 parents 8b1fefb + 7bf88e6 commit 5b0fa98
Show file tree
Hide file tree
Showing 10 changed files with 374 additions and 118 deletions.
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

0 comments on commit 5b0fa98

Please sign in to comment.