Skip to content

Commit

Permalink
Dpi awareness support (#10)
Browse files Browse the repository at this point in the history
* Windows 7 and older versions fallback support

* Fix for scalefactor in primary monitor case

* Fix for window positioning

* Styling fix

* Update README.md

* bump to a breaking version v2.0.0

* add nuget badges

Co-authored-by: andbayd <[email protected]>
Co-authored-by: Michael Denny <[email protected]>
  • Loading branch information
3 people authored Feb 16, 2022
1 parent d9126ac commit c15b29e
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 233 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
WpfScreenHelper
===============

[![Build CI](https://github.com/micdenny/WpfScreenHelper/actions/workflows/dotnet.yml/badge.svg)](https://github.com/micdenny/WpfScreenHelper/actions/workflows/dotnet.yml)

[![NuGet Status](https://img.shields.io/nuget/v/WpfScreenHelper)](https://www.nuget.org/packages/WpfScreenHelper)
[![Nuget Status](https://img.shields.io/nuget/dt/WpfScreenHelper)](https://www.nuget.org/packages/WpfScreenHelper)

Porting of Windows Forms Screen helper for Windows Presentation Foundation (WPF). It avoids dependencies on Windows Forms libraries when developing in WPF.

### Release
Expand Down
2 changes: 1 addition & 1 deletion src/WpfScreenHelper/ExternDll.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
internal class ExternDll
{
public const string User32 = "user32.dll";
public const string Gdi32 = "gdi32.dll";
public const string Shcore = "shcore.dll";
public const string D2D1 = "d2d1.dll";
}
}
6 changes: 3 additions & 3 deletions src/WpfScreenHelper/MouseHelper.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace WpfScreenHelper
{
using System.Windows;
using System.Windows;

namespace WpfScreenHelper
{
/// <summary>
/// Provides helper functions for mouse cursor.
/// </summary>
Expand Down
53 changes: 40 additions & 13 deletions src/WpfScreenHelper/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,6 @@ public enum DpiType
RAW = 2
}

public enum PROCESS_DPI_AWARENESS
{
PROCESS_DPI_UNAWARE = 0,
PROCESS_SYSTEM_DPI_AWARE = 1,
PROCESS_PER_MONITOR_DPI_AWARE = 2
}

public enum SystemMetric
{
SM_CXSCREEN = 0,
Expand All @@ -49,21 +42,36 @@ public enum SPI : uint
public enum SPIF
{
None = 0x00,

/// <summary>Writes the new system-wide parameter setting to the user profile.</summary>
SPIF_UPDATEINIFILE = 0x01,

/// <summary>Broadcasts the WM_SETTINGCHANGE message after updating the user profile.</summary>
SPIF_SENDCHANGE = 0x02,

/// <summary>Same as SPIF_SENDCHANGE.</summary>
SPIF_SENDWININICHANGE = 0x02
}

public const int SPI_GETWORKAREA = 48;
public enum MonitorDefault
{
/// <summary>If the point is not contained within any display monitor, return a handle to the display monitor that is nearest to the point.</summary>
MONITOR_DEFAULTTONEAREST = 0x00000002,

public static readonly HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero);
/// <summary>If the point is not contained within any display monitor, return NULL.</summary>
MONITOR_DEFAULTTONULL = 0x00000000,

[DllImport(ExternDll.Shcore, CharSet = CharSet.Auto)]
[ResourceExposure(ResourceScope.None)]
public static extern int GetProcessDpiAwareness(IntPtr hprocess, out PROCESS_DPI_AWARENESS value);
/// <summary>If the point is not contained within any display monitor, return a handle to the primary display monitor.</summary>
MONITOR_DEFAULTTOPRIMARY = 0x00000001
}

public enum D2D1_FACTORY_TYPE
{
D2D1_FACTORY_TYPE_SINGLE_THREADED = 0,
D2D1_FACTORY_TYPE_MULTI_THREADED = 1,
}

public static readonly HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero);

[DllImport(ExternDll.Shcore, CharSet = CharSet.Auto)]
[ResourceExposure(ResourceScope.None)]
Expand Down Expand Up @@ -91,7 +99,7 @@ public enum SPIF

[DllImport(ExternDll.User32, ExactSpelling = true)]
[ResourceExposure(ResourceScope.None)]
public static extern IntPtr MonitorFromPoint(POINTSTRUCT pt, int flags);
public static extern IntPtr MonitorFromPoint(POINTSTRUCT pt, MonitorDefault flags);

[DllImport(ExternDll.User32, ExactSpelling = true, CharSet = CharSet.Auto)]
[ResourceExposure(ResourceScope.None)]
Expand All @@ -100,6 +108,12 @@ public enum SPIF
[DllImport(ExternDll.User32, SetLastError = true)]
public static extern bool IsProcessDPIAware();

[DllImport(ExternDll.User32, SetLastError = true)]
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);

[DllImport(ExternDll.D2D1)]
public static extern int D2D1CreateFactory(D2D1_FACTORY_TYPE factoryType, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, IntPtr pFactoryOptions, out ID2D1Factory ppIFactory);

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
Expand Down Expand Up @@ -164,10 +178,12 @@ public POINT(int x, int y)
}

#if DEBUG

public override string ToString()
{
return "{x=" + x + ", y=" + y + "}";
}

#endif
}

Expand Down Expand Up @@ -222,5 +238,16 @@ public override string ToString()
return "Left = " + left + " Top " + top + " Right = " + right + " Bottom = " + bottom;
}
}

[InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("06152247-6f50-465a-9245-118bfd3b6007")]
internal interface ID2D1Factory
{
int ReloadSystemMetrics();

[PreserveSig]
void GetDesktopDpi(out float dpiX, out float dpiY);

// the rest is not implemented as we don't need it
}
}
}
132 changes: 67 additions & 65 deletions src/WpfScreenHelper/Screen.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
namespace WpfScreenHelper
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;

namespace WpfScreenHelper
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;

/// <summary>
/// Represents a display device or multiple display devices on a single system.
/// </summary>
Expand All @@ -29,7 +29,6 @@ public class Screen
private const int PRIMARY_MONITOR = unchecked((int)0xBAADF00D);

private const int MONITORINFOF_PRIMARY = 0x00000001;
private const int MONITOR_DEFAULTTONEAREST = 0x00000002;

/// <summary>
/// The monitor handle.
Expand Down Expand Up @@ -62,7 +61,35 @@ private Screen(IntPtr monitor, IntPtr hdc)
{
if (NativeMethods.IsProcessDPIAware())
{
NativeMethods.GetDpiForMonitor(monitor, NativeMethods.DpiType.EFFECTIVE, out var dpiX, out _);
uint dpiX;

try
{
if (monitor == (IntPtr)PRIMARY_MONITOR)
{
var ptr = NativeMethods.MonitorFromPoint(new NativeMethods.POINTSTRUCT(0, 0), NativeMethods.MonitorDefault.MONITOR_DEFAULTTOPRIMARY);
NativeMethods.GetDpiForMonitor(ptr, NativeMethods.DpiType.EFFECTIVE, out dpiX, out _);
}
else
{
NativeMethods.GetDpiForMonitor(monitor, NativeMethods.DpiType.EFFECTIVE, out dpiX, out _);
}
}
catch
{
// Windows 7 fallback
var hr = NativeMethods.D2D1CreateFactory(NativeMethods.D2D1_FACTORY_TYPE.D2D1_FACTORY_TYPE_SINGLE_THREADED, typeof(NativeMethods.ID2D1Factory).GUID, IntPtr.Zero, out var factory);
if (hr < 0)
{
dpiX = 96;
}
else
{
factory.GetDesktopDpi(out var x, out _);
Marshal.ReleaseComObject(factory);
dpiX = (uint)x;
}
}

this.ScaleFactor = dpiX / 96.0;
}
Expand All @@ -73,7 +100,7 @@ private Screen(IntPtr monitor, IntPtr hdc)
NativeMethods.GetSystemMetrics(NativeMethods.SystemMetric.SM_CXSCREEN),
NativeMethods.GetSystemMetrics(NativeMethods.SystemMetric.SM_CYSCREEN));

this.PixelBounds = new Rect(0, 0, size.Width, size.Height);
this.Bounds = new Rect(0, 0, size.Width, size.Height);
this.Primary = true;
this.DeviceName = "DISPLAY";
}
Expand All @@ -83,7 +110,7 @@ private Screen(IntPtr monitor, IntPtr hdc)

NativeMethods.GetMonitorInfo(new HandleRef(null, monitor), info);

this.PixelBounds = new Rect(
this.Bounds = new Rect(
info.rcMonitor.left,
info.rcMonitor.top,
info.rcMonitor.right - info.rcMonitor.left,
Expand Down Expand Up @@ -134,14 +161,14 @@ public static Screen PrimaryScreen
/// Gets the bounds of the display in units.
/// </summary>
/// <returns>A <see cref="T:System.Windows.Rect" />, representing the bounds of the display in units.</returns>
public Rect Bounds =>
public Rect WpfBounds =>
this.ScaleFactor.Equals(1.0)
? this.PixelBounds
? this.Bounds
: new Rect(
this.PixelBounds.X / this.ScaleFactor,
this.PixelBounds.Y / this.ScaleFactor,
this.PixelBounds.Width / this.ScaleFactor,
this.PixelBounds.Height / this.ScaleFactor);
this.Bounds.X / this.ScaleFactor,
this.Bounds.Y / this.ScaleFactor,
this.Bounds.Width / this.ScaleFactor,
this.Bounds.Height / this.ScaleFactor);

/// <summary>
/// Gets the device name associated with a display.
Expand All @@ -153,7 +180,7 @@ public static Screen PrimaryScreen
/// Gets the bounds of the display in pixels.
/// </summary>
/// <returns>A <see cref="T:System.Windows.Rect" />, representing the bounds of the display in pixels.</returns>
public Rect PixelBounds { get; }
public Rect Bounds { get; }

/// <summary>
/// Gets a value indicating whether a particular display is the primary device.
Expand All @@ -169,9 +196,9 @@ public static Screen PrimaryScreen

/// <summary>
/// Gets the working area of the display. The working area is the desktop area of the display, excluding task bars,
/// docked windows, and docked tool bars in units.
/// docked windows, and docked tool bars in pixels.
/// </summary>
/// <returns>A <see cref="T:System.Windows.Rect" />, representing the working area of the display in units.</returns>
/// <returns>A <see cref="T:System.Windows.Rect" />, representing the working area of the display in pixels.</returns>
public Rect WorkingArea
{
get
Expand All @@ -184,36 +211,34 @@ public Rect WorkingArea

NativeMethods.SystemParametersInfo(NativeMethods.SPI.SPI_GETWORKAREA, 0, ref rc, NativeMethods.SPIF.SPIF_SENDCHANGE);

workingArea = this.ScaleFactor.Equals(1.0)
? new Rect(rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top)
: new Rect(
rc.left / this.ScaleFactor,
rc.top / this.ScaleFactor,
(rc.right - rc.left) / this.ScaleFactor,
(rc.bottom - rc.top) / this.ScaleFactor);
workingArea = new Rect(rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top);
}
else
{
var info = new NativeMethods.MONITORINFOEX();
NativeMethods.GetMonitorInfo(new HandleRef(null, this.monitorHandle), info);

workingArea = this.ScaleFactor.Equals(1.0)
? new Rect(
info.rcWork.left,
info.rcWork.top,
info.rcWork.right - info.rcWork.left,
info.rcWork.bottom - info.rcWork.top)
: new Rect(
info.rcWork.left / this.ScaleFactor,
info.rcWork.top / this.ScaleFactor,
(info.rcWork.right - info.rcWork.left) / this.ScaleFactor,
(info.rcWork.bottom - info.rcWork.top) / this.ScaleFactor);
workingArea = new Rect(info.rcWork.left, info.rcWork.top, info.rcWork.right - info.rcWork.left, info.rcWork.bottom - info.rcWork.top);
}

return workingArea;
}
}

/// <summary>
/// Gets the working area of the display. The working area is the desktop area of the display, excluding task bars,
/// docked windows, and docked tool bars in units.
/// </summary>
/// <returns>A <see cref="T:System.Windows.Rect" />, representing the working area of the display in units.</returns>
public Rect WpfWorkingArea =>
this.ScaleFactor.Equals(1.0)
? this.WorkingArea
: new Rect(
this.WorkingArea.X / this.ScaleFactor,
this.WorkingArea.Y / this.ScaleFactor,
this.WorkingArea.Width / this.ScaleFactor,
this.WorkingArea.Height / this.ScaleFactor);

/// <summary>
/// Retrieves a Screen for the display that contains the largest portion of the specified control.
/// </summary>
Expand Down Expand Up @@ -242,8 +267,9 @@ public static Screen FromPoint(Point point)
if (MultiMonitorSupport)
{
var pt = new NativeMethods.POINTSTRUCT((int)point.X, (int)point.Y);
return new Screen(NativeMethods.MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST));
return new Screen(NativeMethods.MonitorFromPoint(pt, NativeMethods.MonitorDefault.MONITOR_DEFAULTTONEAREST));
}

return new Screen((IntPtr)PRIMARY_MONITOR);
}

Expand All @@ -260,30 +286,6 @@ public static Screen FromWindow(Window window)
return FromHandle(new WindowInteropHelper(window).Handle);
}

/// <summary>
/// Retrieves a Screen for the display that contains the specified point in units.
/// </summary>
/// <param name="point">A <see cref="T:System.Windows.Point" /> that specifies the location for which to retrieve a Screen.</param>
/// <returns>
/// A Screen for the display that contains the point in units. In multiple display environments where no display contains
/// the point, the display closest to the specified point is returned.
/// </returns>
public static Screen FromWpfPoint(Point point)
{
if (MultiMonitorSupport)
{
foreach (var screen in AllScreens)
{
if (screen.Bounds.Contains(point))
{
return screen;
}
}
}

return new Screen((IntPtr)PRIMARY_MONITOR);
}

/// <summary>
/// Gets or sets a value indicating whether the specified object is equal to this Screen.
/// </summary>
Expand Down
14 changes: 10 additions & 4 deletions src/WpfScreenHelper/SystemInformation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ public static Rect WpfVirtualScreen
},
(accumulator, s) => new
{
xMin = Math.Min(s.Bounds.X, accumulator.xMin),
yMin = Math.Min(s.Bounds.Y, accumulator.yMin),
xMax = Math.Max(s.Bounds.Right, accumulator.xMax),
yMax = Math.Max(s.Bounds.Bottom, accumulator.yMax)
xMin = Math.Min(s.WpfBounds.X, accumulator.xMin),
yMin = Math.Min(s.WpfBounds.Y, accumulator.yMin),
xMax = Math.Max(s.WpfBounds.Right, accumulator.xMax),
yMax = Math.Max(s.WpfBounds.Bottom, accumulator.yMax)
});

return new Rect(values.xMin, values.yMin, values.xMax - values.xMin, values.yMax - values.yMin);
Expand All @@ -69,5 +69,11 @@ public static Rect WpfVirtualScreen
/// </summary>
/// <returns>A <see cref="T:System.Windows.Rect" /> that represents the size, in pixels, of the working area of the screen.</returns>
public static Rect WorkingArea => Screen.PrimaryScreen.WorkingArea;

/// <summary>
/// Gets the size, in units, of the working area of the screen.
/// </summary>
/// <returns>A <see cref="T:System.Windows.Rect" /> that represents the size, in units, of the working area of the screen.</returns>
public static Rect WpfWorkingArea => Screen.PrimaryScreen.WpfWorkingArea;
}
}
Loading

0 comments on commit c15b29e

Please sign in to comment.