Skip to content

Commit

Permalink
Wpf/WinForms: Fix PointTo/FromScreen in system dpi awareness mode
Browse files Browse the repository at this point in the history
  • Loading branch information
cwensley committed Aug 3, 2022
1 parent a85b467 commit 861f3a0
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 68 deletions.
68 changes: 53 additions & 15 deletions src/Eto.WinForms/Win32.dpi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ public static uint GetWindowDpi(IntPtr hwnd, bool onlyIfSupported = true)
return dpiX;
}

public static Eto.Drawing.Point LogicalToScreen(this Eto.Drawing.PointF point)
public static Eto.Drawing.Point LogicalToScreen(this Eto.Drawing.PointF point, Eto.Forms.Screen screen = null, bool usePerMonitor = true)
{
var screen = Eto.Forms.Screen.FromPoint(point);
screen = screen ?? Eto.Forms.Screen.FromPoint(point);
var sdscreen = ScreenHandler.GetControl(screen);
var pixelSize = sdscreen.GetLogicalPixelSize();
var pixelSize = sdscreen.GetLogicalPixelSize(usePerMonitor);
var location = sdscreen.GetBounds().Location;
var screenBounds = screen.Bounds;

Expand All @@ -78,16 +78,16 @@ public static Eto.Drawing.Point LogicalToScreen(this Eto.Drawing.PointF point)
return Drawing.Point.Round(new Drawing.PointF(x, y));
}

public static Eto.Drawing.PointF ScreenToLogical(this Eto.Drawing.Point point, swf.Screen sdscreen = null)
public static Eto.Drawing.PointF ScreenToLogical(this Eto.Drawing.Point point, swf.Screen sdscreen = null, bool usePerMonitor = true)
{
return ScreenToLogical(point.ToSD(), sdscreen);
return ScreenToLogical(point.ToSD(), sdscreen, usePerMonitor);
}

public static Eto.Drawing.PointF ScreenToLogical(this sd.Point point, swf.Screen sdscreen = null)
public static Eto.Drawing.PointF ScreenToLogical(this sd.Point point, swf.Screen sdscreen = null, bool usePerMonitor = true)
{
sdscreen = sdscreen ?? swf.Screen.FromPoint(point);
var location = sdscreen.GetLogicalLocation();
var pixelSize = sdscreen.GetLogicalPixelSize();
var pixelSize = sdscreen.GetLogicalPixelSize(usePerMonitor);
var sdscreenBounds = sdscreen.GetBounds();

var x = location.X + (point.X - sdscreenBounds.X) / pixelSize;
Expand All @@ -97,12 +97,12 @@ public static Eto.Drawing.PointF ScreenToLogical(this sd.Point point, swf.Screen
return new Drawing.PointF(x, y);
}

public static Eto.Drawing.RectangleF ScreenToLogical(this Eto.Drawing.Rectangle rect, swf.Screen screen)
public static Eto.Drawing.RectangleF ScreenToLogical(this Eto.Drawing.Rectangle rect, swf.Screen sdscreen, bool usePerMonitor = true)
{
screen = screen ?? swf.Screen.FromPoint(rect.Location.ToSD());
var location = screen.GetLogicalLocation();
var pixelSize = screen.GetLogicalPixelSize();
var screenBounds = screen.GetBounds();
sdscreen = sdscreen ?? swf.Screen.FromPoint(rect.Location.ToSD());
var location = sdscreen.GetLogicalLocation();
var pixelSize = sdscreen.GetLogicalPixelSize(usePerMonitor);
var screenBounds = sdscreen.GetBounds();
return new Eto.Drawing.RectangleF(
location.X + (rect.X - screenBounds.X) / pixelSize,
location.Y + (rect.Y - screenBounds.Y) / pixelSize,
Expand All @@ -117,6 +117,10 @@ public static Eto.Drawing.RectangleF GetLogicalBounds(this swf.Screen screen)
{
return new Eto.Drawing.RectangleF(GetLogicalLocation(screen), GetLogicalSize(screen));
}

public static bool IsSystemDpiAware => Win32.GetProcessDpiAwareness(IntPtr.Zero, out var awareness) == 0 && awareness == Win32.PROCESS_DPI_AWARENESS.SYSTEM_DPI_AWARE;

public static float SystemDpi => Win32.GetDpiForSystem() / 96f;

class ScreenHelper : LogicalScreenHelper<swf.Screen>
{
Expand Down Expand Up @@ -147,7 +151,7 @@ public sd.Rectangle GetWorkingArea(swf.Screen screen)
}


public override float GetLogicalPixelSize(swf.Screen screen)
public override float GetLogicalPixelSize(swf.Screen screen, bool usePerMonitor = true)
{
if (!MonitorDpiSupported)
{
Expand All @@ -161,7 +165,7 @@ public override float GetLogicalPixelSize(swf.Screen screen)
var mon = MonitorFromPoint(screen.Bounds.Location, MONITOR.DEFAULTTONEAREST);

// use per-monitor aware dpi awareness to get ACTUAL dpi here
var oldDpiAwareness = SetThreadDpiAwarenessContextSafe(DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2);
var oldDpiAwareness = usePerMonitor ? SetThreadDpiAwarenessContextSafe(DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2) : DPI_AWARENESS_CONTEXT.NONE;

uint dpiX, dpiY;
GetDpiForMonitor(mon, MDT.EFFECTIVE_DPI, out dpiX, out dpiY);
Expand All @@ -183,7 +187,7 @@ public override float GetLogicalPixelSize(swf.Screen screen)

public static Eto.Drawing.SizeF GetLogicalSize(this swf.Screen screen) => locationHelper.GetLogicalSize(screen);

public static float GetLogicalPixelSize(this swf.Screen screen) => locationHelper.GetLogicalPixelSize(screen);
public static float GetLogicalPixelSize(this swf.Screen screen, bool usePerMonitor = true) => locationHelper.GetLogicalPixelSize(screen, usePerMonitor);

public static void GetMonitorInfo(this swf.Screen screen, ref MONITORINFOEX info)
{
Expand Down Expand Up @@ -228,6 +232,40 @@ public static DPI_AWARENESS_CONTEXT SetThreadDpiAwarenessContextSafe(DPI_AWARENE
return DPI_AWARENESS_CONTEXT.NONE;
return SetThreadDpiAwarenessContext(dpiContext);
}

public static swf.Screen GetScreenFromWindow(IntPtr nativeHandle)
{
if (nativeHandle == IntPtr.Zero)
return swf.Screen.PrimaryScreen;

return swf.Screen.FromHandle(nativeHandle);

// var monitorPtr = Win32.MonitorFromWindow(nativeHandle, MONITOR.DEFAULTTONEAREST);
// var info = new MONITORINFOEX();
// Win32.GetMonitorInfo(new HandleRef(null, monitorPtr), info);
// var monitorBounds = info.rcMonitor.ToSD();
// foreach (var screen in swf.Screen.AllScreens)
// {
// if (screen.Bounds == monitorBounds)
// return screen;
// }
// return swf.Screen.PrimaryScreen;
}


public static T ExecuteInDpiAwarenessContext<T>(Func<T> func)
{
var oldDpiAwareness = Win32.SetThreadDpiAwarenessContextSafe(Win32.DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2);
try
{
return func();
}
finally
{
if (oldDpiAwareness != Win32.DPI_AWARENESS_CONTEXT.NONE)
Win32.SetThreadDpiAwarenessContextSafe(oldDpiAwareness);
}
}

[DllImport("User32.dll")]
public static extern DPI_AWARENESS_CONTEXT GetThreadDpiAwarenessContext();
Expand Down
10 changes: 2 additions & 8 deletions src/Eto.Wpf/Forms/MouseHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,13 @@ public PointF Position
get
{
var screen = swf.Screen.FromPoint(swf.Control.MousePosition);
var oldDpiAwareness = Win32.SetThreadDpiAwarenessContextSafe(Win32.DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2);
var result = swf.Control.MousePosition;
if (oldDpiAwareness != Win32.DPI_AWARENESS_CONTEXT.NONE)
Win32.SetThreadDpiAwarenessContextSafe(oldDpiAwareness);
var result = Win32.ExecuteInDpiAwarenessContext(() => swf.Control.MousePosition);
return result.ScreenToLogical(screen);
}
set
{
var pos = value.LogicalToScreen();
var oldDpiAwareness = Win32.SetThreadDpiAwarenessContextSafe(Win32.DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2);
swf.Cursor.Position = Point.Round(pos).ToSD();
if (oldDpiAwareness != Win32.DPI_AWARENESS_CONTEXT.NONE)
Win32.SetThreadDpiAwarenessContextSafe(oldDpiAwareness);
Win32.ExecuteInDpiAwarenessContext(() => swf.Cursor.Position = Point.Round(pos).ToSD());
}
}

Expand Down
39 changes: 36 additions & 3 deletions src/Eto.Wpf/Forms/WpfFrameworkElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,13 @@ public virtual Size Size
return newSize.Value;
if (!Widget.Loaded)
return UserPreferredSize.ToEtoSize();
// if (Win32.IsSystemDpiAware && !double.IsNaN(Control.ActualWidth) && !double.IsNaN(Control.ActualHeight))
// {
// // convert system dpi to logical
// var sizef = new SizeF((float)Control.ActualWidth, (float)Control.ActualHeight) / (Win32.GetDpiForSystem() / 96f) * SwfScreen.GetLogicalPixelSize();
// return Size.Round(sizef);
// }

return Control.GetSize();
}
set
Expand Down Expand Up @@ -938,20 +945,46 @@ public void MapPlatformCommand(string systemAction, Command command)
{
}

System.Windows.Forms.Screen SwfScreen => Win32.GetScreenFromWindow(Widget.ParentWindow?.NativeHandle ?? IntPtr.Zero);

public PointF PointFromScreen(PointF point)
{
if (!ContainerControl.IsLoaded)
return point;

point = point.LogicalToScreen();
return ContainerControl.PointFromScreen(point.ToWpf()).ToEto();
// ensure we're connected to a presentation source
var presentationSource = sw.PresentationSource.FromVisual(ContainerControl) as HwndSource;
if (presentationSource == null)
return point;

point = point.LogicalToScreen(Widget.ParentWindow?.Screen);
point = Win32.ExecuteInDpiAwarenessContext(() => ContainerControl.PointFromScreen(point.ToWpf())).ToEto();

if (Win32.IsSystemDpiAware)
{
point = point * Win32.SystemDpi / Win32.GetLogicalPixelSize(SwfScreen);
}
return point;

}

public PointF PointToScreen(PointF point)
{
if (!ContainerControl.IsLoaded)
return point;
return ContainerControl.PointToScreen(point.ToWpf()).ToEtoPoint().ScreenToLogical();

// ensure we're connected to a presentation source
var presentationSource = sw.PresentationSource.FromVisual(ContainerControl) as HwndSource;
if (presentationSource == null)
return point;

if (Win32.IsSystemDpiAware)
{
point = point / Win32.SystemDpi * Win32.GetLogicalPixelSize(SwfScreen);
}
var pt = Win32.ExecuteInDpiAwarenessContext(() => ContainerControl.PointToScreen(point.ToWpf()));

return pt.ToEtoPoint().ScreenToLogical(SwfScreen);
}

public Point Location
Expand Down
56 changes: 26 additions & 30 deletions src/Eto.Wpf/Forms/WpfWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -637,13 +637,13 @@ public override Size Size
if (handle != IntPtr.Zero && Control.IsLoaded)
{
// WPF doesn't always report the correct size when maximized
Win32.RECT rect;
if (Win32.GetWindowRect(handle, out rect))
var rect = Win32.ExecuteInDpiAwarenessContext(() => Win32.GetWindowRect(handle, out var r) ? r : (Win32.RECT?)null);
if (rect != null)
{
var scale = DpiScale;
return new Size(
(int)Math.Round(rect.width * scale),
(int)Math.Round(rect.height * scale)
(int)Math.Round(rect.Value.width * scale),
(int)Math.Round(rect.Value.height * scale)
);
}
}
Expand Down Expand Up @@ -676,19 +676,27 @@ protected bool LocationSet
set { Widget.Properties.Set(WpfWindow.LocationSet_Key, value); }
}

System.Windows.Forms.Screen SwfScreen
System.Windows.Forms.Screen SwfScreen => Win32.GetScreenFromWindow(NativeHandle);

double DpiScale
{
get
{
var handle = NativeHandle;
if (handle == IntPtr.Zero)
return System.Windows.Forms.Screen.PrimaryScreen;
return System.Windows.Forms.Screen.FromHandle(handle);
var source = sw.PresentationSource.FromVisual(Control);
double scale;
if (source != null)
{
scale = source.CompositionTarget.TransformFromDevice.M22;
if (Win32.IsSystemDpiAware)
{
scale = scale * Win32.SystemDpi / SwfScreen.GetLogicalPixelSize();
}
}
else scale = 1f / (Widget.Screen ?? Screen.PrimaryScreen).LogicalPixelSize;
return scale;
}
}

double DpiScale => sw.PresentationSource.FromVisual(Control)?.CompositionTarget.TransformFromDevice.M22 ?? 1f / Screen.PrimaryScreen.LogicalPixelSize;

public new Point Location
{
get
Expand All @@ -698,23 +706,14 @@ System.Windows.Forms.Screen SwfScreen
var handle = NativeHandle;
if (handle != IntPtr.Zero)
{
Point? location = null;
// Left/Top doesn't always report correct location when maximized, so use Win32 when we can.
var oldDpiAwareness = Win32.SetThreadDpiAwarenessContextSafe(Win32.DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2);
try
{
Win32.RECT rect;
if (Win32.GetWindowRect(handle, out rect))
location = new Point(rect.left, rect.top);
}
finally
var rect = Win32.ExecuteInDpiAwarenessContext(() => Win32.GetWindowRect(handle, out var r) ? r : (Win32.RECT?)null);

if (rect != null)
{
if (oldDpiAwareness != Win32.DPI_AWARENESS_CONTEXT.NONE)
Win32.SetThreadDpiAwarenessContextSafe(oldDpiAwareness);
var location = new Point(rect.Value.left, rect.Value.top);
return Point.Round(location.ScreenToLogical(SwfScreen));
}

if (location != null)
return Point.Round(location.Value.ScreenToLogical(SwfScreen));
}
// 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 @@ -762,12 +761,9 @@ void Control_SourceInitialized(object sender, EventArgs e)
void SetLocation(PointF location)
{
var handle = NativeHandle;
var loc = location.LogicalToScreen();

var oldDpiAwareness = Win32.SetThreadDpiAwarenessContextSafe(Win32.DPI_AWARENESS_CONTEXT.PER_MONITOR_AWARE_v2);
Win32.SetWindowPos(NativeHandle, IntPtr.Zero, loc.X, loc.Y, 0, 0, Win32.SWP.NOSIZE | Win32.SWP.NOACTIVATE);
if (oldDpiAwareness != Win32.DPI_AWARENESS_CONTEXT.NONE)
Win32.SetThreadDpiAwarenessContextSafe(oldDpiAwareness);
var loc = location.LogicalToScreen();
Win32.ExecuteInDpiAwarenessContext(() => Win32.SetWindowPos(NativeHandle, IntPtr.Zero, loc.X, loc.Y, 0, 0, Win32.SWP.NOSIZE | Win32.SWP.NOACTIVATE));
}

public WindowState WindowState
Expand Down
12 changes: 10 additions & 2 deletions src/Eto.Wpf/LogicalScreenHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ public abstract class LogicalScreenHelper<T>

public abstract Eto.Drawing.SizeF GetLogicalSize(T screen);

public abstract float GetLogicalPixelSize(T screen);
public abstract float GetLogicalPixelSize(T screen, bool usePerMonitor = true);

public virtual float GetMaxLogicalPixelSize() => AllScreens.Max((Func<T, float>)GetLogicalPixelSize);
public virtual float GetMaxLogicalPixelSize()
{
float logicalPixelSize = 0;
foreach (var screen in AllScreens)
{
logicalPixelSize = Math.Max(logicalPixelSize, GetLogicalPixelSize(screen));
}
return logicalPixelSize;
}

public Eto.Drawing.PointF GetLogicalLocation(T screen)
{
Expand Down
2 changes: 1 addition & 1 deletion test/Eto.Test.Wpf/UnitTests/ScreenTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class TestLogicalScreenHelper : LogicalScreenHelper<TestScreen>

public override sd.Rectangle GetBounds(TestScreen screen) => screen.Bounds;

public override float GetLogicalPixelSize(TestScreen screen) => screen.LogicalPixelSize;
public override float GetLogicalPixelSize(TestScreen screen, bool usePerMonitor = true) => screen.LogicalPixelSize;

public override SizeF GetLogicalSize(TestScreen screen) => screen.LogicalSize.ToEto();
}
Expand Down
Loading

0 comments on commit 861f3a0

Please sign in to comment.