From 6714ca888dcc3a035793e373bd68b14095a78c5b Mon Sep 17 00:00:00 2001 From: Andrew <30706733+FoxPride@users.noreply.github.com> Date: Thu, 23 Sep 2021 19:48:42 +0300 Subject: [PATCH] Dpi awareness support (#5) * Rework library for supporting dpi awareness: - All values returns in wpf units - Project update for StyleCop rules - Bugfix for SystemInformation.VirtualScreen returning only primary screen * Added test project for window position check (keep in mind that you should set WindowStyle="None" ResizeMode="NoResize" properties for window or you will get position problem. See https://github.com/dotnet/wpf/issues/4127) * Fix for MONITORINFOEX initialization after code cleanup * Final changes WindowHelper added; Some fixes for SystemInformation; * code review * simplify build batch using dotnet tool * upgrade to netcoreapp3.1 because netcoreapp3.0 is out of support * bump version to v1.1.0 Co-authored-by: andbayd Co-authored-by: Michael Denny --- WpfScreenHelper.sln | 14 + build.bat | 2 +- src/WpfScreenHelper/Enum/WindowPositions.cs | 16 ++ src/WpfScreenHelper/ExternDll.cs | 1 + src/WpfScreenHelper/MouseHelper.cs | 16 +- src/WpfScreenHelper/NativeMethods.cs | 113 ++++++-- src/WpfScreenHelper/Screen.cs | 248 +++++++++++++----- src/WpfScreenHelper/SystemInformation.cs | 63 ++++- src/WpfScreenHelper/WindowHelper.cs | 144 ++++++++++ src/WpfScreenHelper/WpfScreenHelper.csproj | 4 +- test/WpfScreenHelper.DpiTestWpfApp/App.xaml | 8 + .../WpfScreenHelper.DpiTestWpfApp/App.xaml.cs | 11 + .../AssemblyInfo.cs | 10 + .../MainWindow.xaml | 31 +++ .../MainWindow.xaml.cs | 147 +++++++++++ .../WpfScreenHelper.DpiTestWpfApp.csproj | 14 + .../app.manifest | 18 ++ 17 files changed, 747 insertions(+), 113 deletions(-) create mode 100644 src/WpfScreenHelper/Enum/WindowPositions.cs create mode 100644 src/WpfScreenHelper/WindowHelper.cs create mode 100644 test/WpfScreenHelper.DpiTestWpfApp/App.xaml create mode 100644 test/WpfScreenHelper.DpiTestWpfApp/App.xaml.cs create mode 100644 test/WpfScreenHelper.DpiTestWpfApp/AssemblyInfo.cs create mode 100644 test/WpfScreenHelper.DpiTestWpfApp/MainWindow.xaml create mode 100644 test/WpfScreenHelper.DpiTestWpfApp/MainWindow.xaml.cs create mode 100644 test/WpfScreenHelper.DpiTestWpfApp/WpfScreenHelper.DpiTestWpfApp.csproj create mode 100644 test/WpfScreenHelper.DpiTestWpfApp/app.manifest diff --git a/WpfScreenHelper.sln b/WpfScreenHelper.sln index a16338c..d6478a5 100644 --- a/WpfScreenHelper.sln +++ b/WpfScreenHelper.sln @@ -14,6 +14,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfScreenHelper.DpiTestWpfApp", "test\WpfScreenHelper.DpiTestWpfApp\WpfScreenHelper.DpiTestWpfApp.csproj", "{FB65A9A4-4376-4CBC-A53E-0415B8D41DBB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9C9345A1-727D-4C83-98C8-BB3084B4CFD8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0F6F0B6D-1C32-4681-9B6C-0CE92D22C5D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,10 +30,18 @@ Global {8346EB2E-BFBB-4D63-B974-352A4D8C07F1}.Debug|Any CPU.Build.0 = Debug|Any CPU {8346EB2E-BFBB-4D63-B974-352A4D8C07F1}.Release|Any CPU.ActiveCfg = Release|Any CPU {8346EB2E-BFBB-4D63-B974-352A4D8C07F1}.Release|Any CPU.Build.0 = Release|Any CPU + {FB65A9A4-4376-4CBC-A53E-0415B8D41DBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB65A9A4-4376-4CBC-A53E-0415B8D41DBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB65A9A4-4376-4CBC-A53E-0415B8D41DBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB65A9A4-4376-4CBC-A53E-0415B8D41DBB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {8346EB2E-BFBB-4D63-B974-352A4D8C07F1} = {9C9345A1-727D-4C83-98C8-BB3084B4CFD8} + {FB65A9A4-4376-4CBC-A53E-0415B8D41DBB} = {0F6F0B6D-1C32-4681-9B6C-0CE92D22C5D1} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BAF8370C-EA3E-4F99-A94C-FFC687353E0F} EndGlobalSection diff --git a/build.bat b/build.bat index e648ce2..68d442e 100644 --- a/build.bat +++ b/build.bat @@ -4,7 +4,7 @@ echo Removing old packages... del Package\*.nupkg >nul 2>&1 echo Building... -"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe" "WpfScreenHelper.sln" /property:Configuration=Release +dotnet build -c Release echo. echo ------------------------------------------------ diff --git a/src/WpfScreenHelper/Enum/WindowPositions.cs b/src/WpfScreenHelper/Enum/WindowPositions.cs new file mode 100644 index 0000000..3067662 --- /dev/null +++ b/src/WpfScreenHelper/Enum/WindowPositions.cs @@ -0,0 +1,16 @@ +namespace WpfScreenHelper.Enum +{ + public enum WindowPositions + { + Center, + Left, + Top, + Right, + Bottom, + TopLeft, + TopRight, + BottomRight, + BottomLeft, + Maximize + } +} diff --git a/src/WpfScreenHelper/ExternDll.cs b/src/WpfScreenHelper/ExternDll.cs index 6d7c50a..f476d35 100644 --- a/src/WpfScreenHelper/ExternDll.cs +++ b/src/WpfScreenHelper/ExternDll.cs @@ -4,5 +4,6 @@ internal class ExternDll { public const string User32 = "user32.dll"; public const string Gdi32 = "gdi32.dll"; + public const string Shcore = "shcore.dll"; } } \ No newline at end of file diff --git a/src/WpfScreenHelper/MouseHelper.cs b/src/WpfScreenHelper/MouseHelper.cs index 0c117d4..2387f0b 100644 --- a/src/WpfScreenHelper/MouseHelper.cs +++ b/src/WpfScreenHelper/MouseHelper.cs @@ -1,17 +1,23 @@ -using System.Windows; - -namespace WpfScreenHelper +namespace WpfScreenHelper { + using System.Windows; + + /// + /// Provides helper functions for mouse cursor. + /// public static class MouseHelper { + /// + /// Gets the position of the mouse cursor in screen coordinates. + /// public static Point MousePosition { get { - NativeMethods.POINT pt = new NativeMethods.POINT(); + var pt = new NativeMethods.POINT(); NativeMethods.GetCursorPos(pt); return new Point(pt.x, pt.y); } } } -} +} \ No newline at end of file diff --git a/src/WpfScreenHelper/NativeMethods.cs b/src/WpfScreenHelper/NativeMethods.cs index 1ee9326..fd2e841 100644 --- a/src/WpfScreenHelper/NativeMethods.cs +++ b/src/WpfScreenHelper/NativeMethods.cs @@ -7,25 +7,87 @@ namespace WpfScreenHelper { internal static class NativeMethods { + public delegate bool MonitorEnumProc(IntPtr monitor, IntPtr hdc, IntPtr lprcMonitor, IntPtr lParam); + + public enum DpiType + { + EFFECTIVE = 0, + ANGULAR = 1, + 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, + SM_CYSCREEN = 1, + SM_XVIRTUALSCREEN = 76, + SM_YVIRTUALSCREEN = 77, + SM_CXVIRTUALSCREEN = 78, + SM_CYVIRTUALSCREEN = 79, + SM_CMONITORS = 80 + } + + public enum SPI : uint + { + /// + /// Retrieves the size of the work area on the primary display monitor. The work area is the portion of the screen not obscured + /// by the system taskbar or by application desktop toolbars. The pvParam parameter must point to a RECT structure that receives + /// the coordinates of the work area, expressed in virtual screen coordinates. + /// To get the work area of a monitor other than the primary display monitor, call the GetMonitorInfo function. + /// + SPI_GETWORKAREA = 0x0030 + } + + [Flags] + public enum SPIF + { + None = 0x00, + /// Writes the new system-wide parameter setting to the user profile. + SPIF_UPDATEINIFILE = 0x01, + /// Broadcasts the WM_SETTINGCHANGE message after updating the user profile. + SPIF_SENDCHANGE = 0x02, + /// Same as SPIF_SENDCHANGE. + SPIF_SENDWININICHANGE = 0x02 + } + + public const int SPI_GETWORKAREA = 48; + + public static readonly HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero); + + [DllImport(ExternDll.Shcore, CharSet = CharSet.Auto)] + [ResourceExposure(ResourceScope.None)] + public static extern int GetProcessDpiAwareness(IntPtr hprocess, out PROCESS_DPI_AWARENESS value); + + [DllImport(ExternDll.Shcore, CharSet = CharSet.Auto)] + [ResourceExposure(ResourceScope.None)] + public static extern IntPtr GetDpiForMonitor([In] IntPtr hmonitor, [In] DpiType dpiType, [Out] out uint dpiX, [Out] out uint dpiY); + [DllImport(ExternDll.User32, CharSet = CharSet.Auto)] [ResourceExposure(ResourceScope.None)] - public static extern bool GetMonitorInfo(HandleRef hmonitor, [In, Out] MONITORINFOEX info); + public static extern bool GetMonitorInfo(HandleRef hmonitor, [In][Out] MONITORINFOEX info); [DllImport(ExternDll.User32, ExactSpelling = true)] [ResourceExposure(ResourceScope.None)] public static extern bool EnumDisplayMonitors(HandleRef hdc, COMRECT rcClip, MonitorEnumProc lpfnEnum, IntPtr dwData); - + [DllImport(ExternDll.User32, ExactSpelling = true)] [ResourceExposure(ResourceScope.None)] public static extern IntPtr MonitorFromWindow(HandleRef handle, int flags); [DllImport(ExternDll.User32, ExactSpelling = true, CharSet = CharSet.Auto)] [ResourceExposure(ResourceScope.None)] - public static extern int GetSystemMetrics(int nIndex); + public static extern int GetSystemMetrics(SystemMetric nIndex); [DllImport(ExternDll.User32, CharSet = CharSet.Auto)] [ResourceExposure(ResourceScope.None)] - public static extern bool SystemParametersInfo(int nAction, int nParam, ref RECT rc, int nUpdate); + public static extern bool SystemParametersInfo(SPI nAction, int nParam, ref RECT rc, SPIF nUpdate); [DllImport(ExternDll.User32, ExactSpelling = true)] [ResourceExposure(ResourceScope.None)] @@ -33,11 +95,10 @@ internal static class NativeMethods [DllImport(ExternDll.User32, ExactSpelling = true, CharSet = CharSet.Auto)] [ResourceExposure(ResourceScope.None)] - public static extern bool GetCursorPos([In, Out] POINT pt); + public static extern bool GetCursorPos([In][Out] POINT pt); - public static readonly HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero); - - public delegate bool MonitorEnumProc(IntPtr monitor, IntPtr hdc, IntPtr lprcMonitor, IntPtr lParam); + [DllImport(ExternDll.User32, SetLastError = true)] + public static extern bool IsProcessDPIAware(); [StructLayout(LayoutKind.Sequential)] public struct RECT @@ -57,10 +118,10 @@ public RECT(int left, int top, int right, int bottom) public RECT(Rect r) { - this.left = (int)r.Left; - this.top = (int)r.Top; - this.right = (int)r.Right; - this.bottom = (int)r.Bottom; + left = (int)r.Left; + top = (int)r.Top; + right = (int)r.Right; + bottom = (int)r.Bottom; } public static RECT FromXYWH(int x, int y, int width, int height) @@ -68,10 +129,7 @@ public static RECT FromXYWH(int x, int y, int width, int height) return new RECT(x, y, x + width, y + height); } - public Size Size - { - get { return new Size(this.right - this.left, this.bottom - this.top); } - } + public Size Size => new Size(right - left, bottom - top); } // use this in cases where the Native API takes a POINT not a POINT* @@ -81,6 +139,7 @@ public struct POINTSTRUCT { public int x; public int y; + public POINTSTRUCT(int x, int y) { this.x = x; @@ -116,19 +175,22 @@ public override string ToString() public class MONITORINFOEX { internal int cbSize = Marshal.SizeOf(typeof(MONITORINFOEX)); + internal RECT rcMonitor = new RECT(); internal RECT rcWork = new RECT(); internal int dwFlags = 0; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] internal char[] szDevice = new char[32]; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + internal char[] szDevice = new char[32]; } [StructLayout(LayoutKind.Sequential)] public class COMRECT { + public int bottom; public int left; - public int top; public int right; - public int bottom; + public int top; public COMRECT() { @@ -136,10 +198,10 @@ public COMRECT() public COMRECT(Rect r) { - this.left = (int)r.X; - this.top = (int)r.Y; - this.right = (int)r.Right; - this.bottom = (int)r.Bottom; + left = (int)r.X; + top = (int)r.Y; + right = (int)r.Right; + bottom = (int)r.Bottom; } public COMRECT(int left, int top, int right, int bottom) @@ -160,10 +222,5 @@ public override string ToString() return "Left = " + left + " Top " + top + " Right = " + right + " Bottom = " + bottom; } } - - public const int SM_CMONITORS = 80, - SM_CXSCREEN = 0, - SM_CYSCREEN = 1, - SPI_GETWORKAREA = 48; } } \ No newline at end of file diff --git a/src/WpfScreenHelper/Screen.cs b/src/WpfScreenHelper/Screen.cs index 388f13f..83e57fa 100644 --- a/src/WpfScreenHelper/Screen.cs +++ b/src/WpfScreenHelper/Screen.cs @@ -1,12 +1,13 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Windows; - -namespace WpfScreenHelper +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; + /// /// Represents a display device or multiple display devices on a single system. /// @@ -17,7 +18,10 @@ public class Screen // http://msdn.microsoft.com/en-us/library/windows/desktop/dd145072.aspx // http://msdn.microsoft.com/en-us/library/windows/desktop/dd183314.aspx - private readonly IntPtr hmonitor; + /// + /// Indicates if we have more than one monitor. + /// + private static readonly bool MultiMonitorSupport; // This identifier is just for us, so that we don't try to call the multimon // functions if we just need the primary monitor... this is safer for @@ -27,23 +31,49 @@ public class Screen private const int MONITORINFOF_PRIMARY = 0x00000001; private const int MONITOR_DEFAULTTONEAREST = 0x00000002; - private static bool multiMonitorSupport; + /// + /// The monitor handle. + /// + private readonly IntPtr monitorHandle; + /// + /// Initializes static members of the class. + /// static Screen() { - multiMonitorSupport = NativeMethods.GetSystemMetrics(NativeMethods.SM_CMONITORS) != 0; + MultiMonitorSupport = NativeMethods.GetSystemMetrics(NativeMethods.SystemMetric.SM_CMONITORS) != 0; } + /// + /// Initializes a new instance of the class. + /// + /// The monitor. private Screen(IntPtr monitor) : this(monitor, IntPtr.Zero) { } + /// + /// Initializes a new instance of the class. + /// + /// The monitor. + /// The hdc. private Screen(IntPtr monitor, IntPtr hdc) { - if (!multiMonitorSupport || monitor == (IntPtr)PRIMARY_MONITOR) + if (NativeMethods.IsProcessDPIAware()) + { + NativeMethods.GetDpiForMonitor(monitor, NativeMethods.DpiType.EFFECTIVE, out var dpiX, out _); + + this.ScaleFactor = dpiX / 96.0; + } + + if (!MultiMonitorSupport || monitor == (IntPtr)PRIMARY_MONITOR) { - this.Bounds = SystemInformation.VirtualScreen; + var size = new Size( + NativeMethods.GetSystemMetrics(NativeMethods.SystemMetric.SM_CXSCREEN), + NativeMethods.GetSystemMetrics(NativeMethods.SystemMetric.SM_CYSCREEN)); + + this.PixelBounds = new Rect(0, 0, size.Width, size.Height); this.Primary = true; this.DeviceName = "DISPLAY"; } @@ -53,16 +83,16 @@ private Screen(IntPtr monitor, IntPtr hdc) NativeMethods.GetMonitorInfo(new HandleRef(null, monitor), info); - this.Bounds = new Rect( - info.rcMonitor.left, info.rcMonitor.top, + this.PixelBounds = new Rect( + info.rcMonitor.left, + info.rcMonitor.top, info.rcMonitor.right - info.rcMonitor.left, info.rcMonitor.bottom - info.rcMonitor.top); - - this.Primary = ((info.dwFlags & MONITORINFOF_PRIMARY) != 0); - + this.Primary = (info.dwFlags & MONITORINFOF_PRIMARY) != 0; this.DeviceName = new string(info.szDevice).TrimEnd((char)0); } - hmonitor = monitor; + + this.monitorHandle = monitor; } /// @@ -73,7 +103,7 @@ public static IEnumerable AllScreens { get { - if (multiMonitorSupport) + if (MultiMonitorSupport) { var closure = new MonitorEnumCallback(); var proc = new NativeMethods.MonitorEnumProc(closure.Callback); @@ -83,62 +113,104 @@ public static IEnumerable AllScreens return closure.Screens.Cast(); } } + return new[] { new Screen((IntPtr)PRIMARY_MONITOR) }; } } /// - /// Gets the bounds of the display. + /// Gets the primary display. + /// + /// The primary display. + public static Screen PrimaryScreen + { + get + { + return MultiMonitorSupport ? AllScreens.FirstOrDefault(t => t.Primary) : new Screen((IntPtr)PRIMARY_MONITOR); + } + } + + /// + /// Gets the bounds of the display in units. /// - /// A , representing the bounds of the display. - public Rect Bounds { get; private set; } + /// A , representing the bounds of the display in units. + public Rect Bounds => + this.ScaleFactor.Equals(1.0) + ? this.PixelBounds + : new Rect( + this.PixelBounds.X / this.ScaleFactor, + this.PixelBounds.Y / this.ScaleFactor, + this.PixelBounds.Width / this.ScaleFactor, + this.PixelBounds.Height / this.ScaleFactor); /// /// Gets the device name associated with a display. /// /// The device name associated with a display. - public string DeviceName { get; private set; } + public string DeviceName { get; } + + /// + /// Gets the bounds of the display in pixels. + /// + /// A , representing the bounds of the display in pixels. + public Rect PixelBounds { get; } /// /// Gets a value indicating whether a particular display is the primary device. /// /// true if this display is primary; otherwise, false. - public bool Primary { get; private set; } + public bool Primary { get; } /// - /// Gets the primary display. + /// Gets the scale factor of the display. /// - /// The primary display. - public static Screen PrimaryScreen - { - get - { - if (multiMonitorSupport) - { - return AllScreens.FirstOrDefault(t => t.Primary); - } - return new Screen((IntPtr)PRIMARY_MONITOR); - } - } + /// The scale factor of the display. + public double ScaleFactor { get; } = 1.0; /// - /// Gets the working area of the display. The working area is the desktop area of the display, excluding taskbars, docked windows, and docked tool bars. + /// 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. /// - /// A , representing the working area of the display. + /// A , representing the working area of the display in units. public Rect WorkingArea { get { - if (!multiMonitorSupport || hmonitor == (IntPtr)PRIMARY_MONITOR) + Rect workingArea; + + if (!MultiMonitorSupport || this.monitorHandle == (IntPtr)PRIMARY_MONITOR) { - return SystemInformation.WorkingArea; + var rc = new NativeMethods.RECT(); + + 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); } - var info = new NativeMethods.MONITORINFOEX(); - NativeMethods.GetMonitorInfo(new HandleRef(null, hmonitor), info); - return new Rect( - info.rcWork.left, info.rcWork.top, - info.rcWork.right - info.rcWork.left, - info.rcWork.bottom - info.rcWork.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); + } + + return workingArea; } } @@ -146,24 +218,28 @@ public Rect WorkingArea /// Retrieves a Screen for the display that contains the largest portion of the specified control. /// /// The window handle for which to retrieve the Screen. - /// A Screen for the display that contains the largest region of the object. In multiple display environments where no display contains any portion of the specified window, the display closest to the object is returned. + /// + /// A Screen for the display that contains the largest region of the object. In multiple display environments + /// where no display contains any portion of the specified window, the display closest to the object is returned. + /// public static Screen FromHandle(IntPtr hwnd) { - if (multiMonitorSupport) - { - return new Screen(NativeMethods.MonitorFromWindow(new HandleRef(null, hwnd), 2)); - } - return new Screen((IntPtr)PRIMARY_MONITOR); + return MultiMonitorSupport + ? new Screen(NativeMethods.MonitorFromWindow(new HandleRef(null, hwnd), 2)) + : new Screen((IntPtr)PRIMARY_MONITOR); } /// - /// Retrieves a Screen for the display that contains the specified point. + /// Retrieves a Screen for the display that contains the specified point in pixels. /// /// A that specifies the location for which to retrieve a Screen. - /// A Screen for the display that contains the point. In multiple display environments where no display contains the point, the display closest to the specified point is returned. + /// + /// A Screen for the display that contains the point in pixels. In multiple display environments where no display contains + /// the point, the display closest to the specified point is returned. + /// public static Screen FromPoint(Point point) { - if (multiMonitorSupport) + if (MultiMonitorSupport) { var pt = new NativeMethods.POINTSTRUCT((int)point.X, (int)point.Y); return new Screen(NativeMethods.MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST)); @@ -171,6 +247,43 @@ public static Screen FromPoint(Point point) return new Screen((IntPtr)PRIMARY_MONITOR); } + /// + /// Retrieves a Screen for the display that contains the largest portion of the specified control. + /// + /// The window for which to retrieve the Screen. + /// + /// A Screen for the display that contains the largest region of the object. In multiple display environments + /// where no display contains any portion of the specified window, the display closest to the object is returned. + /// + public static Screen FromWindow(Window window) + { + return FromHandle(new WindowInteropHelper(window).Handle); + } + + /// + /// Retrieves a Screen for the display that contains the specified point in units. + /// + /// A that specifies the location for which to retrieve a Screen. + /// + /// 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. + /// + 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); + } + /// /// Gets or sets a value indicating whether the specified object is equal to this Screen. /// @@ -178,14 +291,14 @@ public static Screen FromPoint(Point point) /// true if the specified object is equal to this Screen; otherwise, false. public override bool Equals(object obj) { - var monitor = obj as Screen; - if (monitor != null) + if (obj is Screen monitor) { - if (hmonitor == monitor.hmonitor) + if (this.monitorHandle == monitor.monitorHandle) { return true; } } + return false; } @@ -195,18 +308,27 @@ public override bool Equals(object obj) /// A hash code for an object. public override int GetHashCode() { - return (int)hmonitor; + return this.monitorHandle.GetHashCode(); } + /// + /// The monitor enum callback. + /// private class MonitorEnumCallback { - public ArrayList Screens { get; private set; } - + /// + /// Initializes a new instance of the class. + /// public MonitorEnumCallback() { this.Screens = new ArrayList(); } + /// + /// Gets the screens. + /// + public ArrayList Screens { get; } + public bool Callback(IntPtr monitor, IntPtr hdc, IntPtr lprcMonitor, IntPtr lparam) { this.Screens.Add(new Screen(monitor, hdc)); diff --git a/src/WpfScreenHelper/SystemInformation.cs b/src/WpfScreenHelper/SystemInformation.cs index bd79abc..332ba68 100644 --- a/src/WpfScreenHelper/SystemInformation.cs +++ b/src/WpfScreenHelper/SystemInformation.cs @@ -1,38 +1,73 @@ +using System; +using System.Linq; using System.Windows; namespace WpfScreenHelper { + /// + /// Provides information about the current system environment. + /// public static class SystemInformation { /// - /// Gets the bounds of the virtual screen. + /// Gets the bounds of the virtual screen in pixels. /// - /// A that specifies the bounding rectangle of the entire virtual screen. + /// + /// A that specifies the bounding rectangle of the entire virtual screen in pixels. + /// public static Rect VirtualScreen { get { - var size = new Size(NativeMethods.GetSystemMetrics(NativeMethods.SM_CXSCREEN), - NativeMethods.GetSystemMetrics(NativeMethods.SM_CYSCREEN)); - return new Rect(0, 0, size.Width, size.Height); + var size = new Size( + NativeMethods.GetSystemMetrics(NativeMethods.SystemMetric.SM_CXVIRTUALSCREEN), + NativeMethods.GetSystemMetrics(NativeMethods.SystemMetric.SM_CYVIRTUALSCREEN)); + var location = new Point( + NativeMethods.GetSystemMetrics(NativeMethods.SystemMetric.SM_XVIRTUALSCREEN), + NativeMethods.GetSystemMetrics(NativeMethods.SystemMetric.SM_YVIRTUALSCREEN)); + return new Rect(location.X, location.Y, size.Width, size.Height); } } /// - /// Gets the size, in pixels, of the working area of the screen. + /// Gets the bounds of the virtual screen in units. /// - /// A that represents the size, in pixels, of the working area of the screen. - public static Rect WorkingArea + /// + /// A that specifies the bounding rectangle of the entire virtual screen in units. + /// + public static Rect WpfVirtualScreen { get { - NativeMethods.RECT rc = new NativeMethods.RECT(); - NativeMethods.SystemParametersInfo(NativeMethods.SPI_GETWORKAREA, 0, ref rc, 0); - return new Rect(rc.left, - rc.top, - rc.right - rc.left, - rc.bottom - rc.top); + if (NativeMethods.IsProcessDPIAware()) + { + var values = Screen.AllScreens.Aggregate( + new + { + xMin = 0.0, + yMin = 0.0, + xMax = 0.0, + yMax = 0.0 + }, + (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) + }); + + return new Rect(values.xMin, values.yMin, values.xMax - values.xMin, values.yMax - values.yMin); + } + + return VirtualScreen; } } + + /// + /// Gets the size, in pixels, of the working area of the screen. + /// + /// A that represents the size, in pixels, of the working area of the screen. + public static Rect WorkingArea => Screen.PrimaryScreen.WorkingArea; } } \ No newline at end of file diff --git a/src/WpfScreenHelper/WindowHelper.cs b/src/WpfScreenHelper/WindowHelper.cs new file mode 100644 index 0000000..f726064 --- /dev/null +++ b/src/WpfScreenHelper/WindowHelper.cs @@ -0,0 +1,144 @@ +using System; +using System.Windows; +using WpfScreenHelper.Enum; + +namespace WpfScreenHelper +{ + /// + /// Provides helper functions for window class. + /// + public static class WindowHelper + { + public static void SetWindowPosition(this Window window, WindowPositions pos, Rect bounds) + { + var coordinates = CalculateWindowCoordinates(window, pos, bounds); + + // correct resulting coordinates to match bounds + if (bounds.X > coordinates.X) + { + coordinates.X = bounds.X; + } + if (bounds.Y > coordinates.Y) + { + coordinates.Y = bounds.Y; + } + if (bounds.Width < coordinates.Width) + { + coordinates.Width = bounds.Width; + } + if (bounds.Height < coordinates.Height) + { + coordinates.Height = bounds.Height; + } + + // The first move puts it on the correct monitor, which triggers WM_DPICHANGED + // The +1/-1 coerces WPF to update Window.Top/Left/Width/Height in the second move + window.Left = coordinates.X + 1; + window.Top = coordinates.Y; + window.Width = coordinates.Width + 1; + window.Height = coordinates.Height; + + window.Left = coordinates.X; + window.Top = coordinates.Y; + window.Width = coordinates.Width; + window.Height = coordinates.Height; + } + + public static void MaximizeWindowToVirtualScreen(this Window window) + { + var virtualDisplay = SystemInformation.WpfVirtualScreen; + + window.Left = virtualDisplay.Left; + window.Top = virtualDisplay.Top; + window.Width = virtualDisplay.Width; + window.Height = virtualDisplay.Height; + } + + public static Rect GetWindowAbsolutePlacement(this Window window) + { + var screen = Screen.FromWindow(window); + + var left = Math.Abs(screen.Bounds.Left - window.Left) * screen.ScaleFactor; + var top = Math.Abs(screen.Bounds.Top - window.Top) * screen.ScaleFactor; + var width = window.Width * screen.ScaleFactor; + var height = window.Height * screen.ScaleFactor; + + return new Rect(left, top, width, height); + } + + private static Rect CalculateWindowCoordinates(FrameworkElement window, WindowPositions pos, Rect bounds) + { + switch (pos) + { + case WindowPositions.Center: + { + var x = bounds.X + ((bounds.Width - window.Width) / 2.0); + var y = bounds.Y + ((bounds.Height - window.Height) / 2.0); + + return new Rect(x, y, window.Width, window.Height); + } + + case WindowPositions.Left: + { + var y = bounds.Y + ((bounds.Height - window.Height) / 2.0); + + return new Rect(bounds.X, y, window.Width, window.Height); + } + + case WindowPositions.Top: + { + var x = bounds.X + ((bounds.Width - window.Width) / 2.0); + + return new Rect(x, bounds.Y, window.Width, window.Height); + } + + case WindowPositions.Right: + { + var x = bounds.X + (bounds.Width - window.Width); + var y = bounds.Y + ((bounds.Height - window.Height) / 2.0); + + return new Rect(x, y, window.Width, window.Height); + } + + case WindowPositions.Bottom: + { + var x = bounds.X + ((bounds.Width - window.Width) / 2.0); + var y = bounds.Y + (bounds.Height - window.Height); + + return new Rect(x, y, window.Width, window.Height); + } + + case WindowPositions.TopLeft: + return new Rect(bounds.X, bounds.Y, window.Width, window.Height); + + case WindowPositions.TopRight: + { + var x = bounds.X + (bounds.Width - window.Width); + + return new Rect(x, bounds.Y, window.Width, window.Height); + } + + case WindowPositions.BottomRight: + { + var x = bounds.X + (bounds.Width - window.Width); + var y = bounds.Y + (bounds.Height - window.Height); + + return new Rect(x, y, window.Width, window.Height); + } + + case WindowPositions.BottomLeft: + { + var y = bounds.Y + (bounds.Height - window.Height); + + return new Rect(bounds.X, y, window.Width, window.Height); + } + + case WindowPositions.Maximize: + return bounds; + + default: + return Rect.Empty; + } + } + } +} diff --git a/src/WpfScreenHelper/WpfScreenHelper.csproj b/src/WpfScreenHelper/WpfScreenHelper.csproj index d53ffd3..54930dc 100644 --- a/src/WpfScreenHelper/WpfScreenHelper.csproj +++ b/src/WpfScreenHelper/WpfScreenHelper.csproj @@ -1,7 +1,7 @@  - net40;netcoreapp3.0 + net40;netcoreapp3.1 true true WpfScreenHelper @@ -14,7 +14,7 @@ WPF Screen Monitor Display Helper https://github.com/micdenny/WpfScreenHelper MIT - 1.0.0 + 1.1.0 diff --git a/test/WpfScreenHelper.DpiTestWpfApp/App.xaml b/test/WpfScreenHelper.DpiTestWpfApp/App.xaml new file mode 100644 index 0000000..2740bec --- /dev/null +++ b/test/WpfScreenHelper.DpiTestWpfApp/App.xaml @@ -0,0 +1,8 @@ + + + + + diff --git a/test/WpfScreenHelper.DpiTestWpfApp/App.xaml.cs b/test/WpfScreenHelper.DpiTestWpfApp/App.xaml.cs new file mode 100644 index 0000000..91a4a4b --- /dev/null +++ b/test/WpfScreenHelper.DpiTestWpfApp/App.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows; + +namespace WpfScreenHelper.DpiTestWpfApp +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/test/WpfScreenHelper.DpiTestWpfApp/AssemblyInfo.cs b/test/WpfScreenHelper.DpiTestWpfApp/AssemblyInfo.cs new file mode 100644 index 0000000..8b5504e --- /dev/null +++ b/test/WpfScreenHelper.DpiTestWpfApp/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/test/WpfScreenHelper.DpiTestWpfApp/MainWindow.xaml b/test/WpfScreenHelper.DpiTestWpfApp/MainWindow.xaml new file mode 100644 index 0000000..ded0e18 --- /dev/null +++ b/test/WpfScreenHelper.DpiTestWpfApp/MainWindow.xaml @@ -0,0 +1,31 @@ + + +