Last active
June 10, 2020 11:18
-
-
Save emoacht/41adbb738d3f06dcdb65 to your computer and use it in GitHub Desktop.
Behavior to make a WPF Window Per-Monitor DPI aware.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Runtime.InteropServices; | |
using System.Windows; | |
using System.Windows.Interactivity; | |
using System.Windows.Interop; | |
using System.Windows.Media; | |
public class PerMonitorDpiBehavior : Behavior<Window> | |
{ | |
protected override void OnAttached() | |
{ | |
base.OnAttached(); | |
this.AssociatedObject.Loaded += OnWindowLoaded; | |
this.AssociatedObject.Closed += OnWindowClosed; | |
} | |
protected override void OnDetaching() | |
{ | |
base.OnDetaching(); | |
if (this.AssociatedObject == null) | |
return; | |
this.AssociatedObject.Loaded -= OnWindowLoaded; | |
this.AssociatedObject.Closed -= OnWindowClosed; | |
} | |
/// <summary> | |
/// System DPI | |
/// </summary> | |
public Dpi SystemDpi | |
{ | |
get { return _systemDpi; } | |
} | |
private readonly Dpi _systemDpi = GetSystemDpi(); | |
/// <summary> | |
/// Per-Monitor DPI of target Window | |
/// </summary> | |
public Dpi WindowDpi | |
{ | |
get { return (Dpi)GetValue(WindowDpiProperty); } | |
private set { SetValue(WindowDpiPropertyKey, value); } | |
} | |
private static readonly DependencyPropertyKey WindowDpiPropertyKey = | |
DependencyProperty.RegisterReadOnly( | |
"WindowDpi", | |
typeof(Dpi), | |
typeof(PerMonitorDpiBehavior), | |
new FrameworkPropertyMetadata(Dpi.Default)); | |
public static readonly DependencyProperty WindowDpiProperty = WindowDpiPropertyKey.DependencyProperty; | |
private Window targetWindow; | |
private HwndSource targetSource; | |
private void OnWindowLoaded(object sender, RoutedEventArgs e) | |
{ | |
if (!IsPerMonitorDpiAware()) | |
return; | |
targetWindow = this.AssociatedObject; | |
targetSource = PresentationSource.FromVisual(targetWindow) as HwndSource; | |
if (targetSource == null) | |
return; | |
WindowDpi = GetDpiFromHwndSource(targetSource); | |
if (WindowDpi != SystemDpi) | |
{ | |
var rect = new Rect( | |
targetWindow.Left, | |
targetWindow.Top, | |
targetWindow.Width * (double)WindowDpi.X / SystemDpi.X, | |
targetWindow.Height * (double)WindowDpi.Y / SystemDpi.Y); | |
ChangeDpi(WindowDpi, rect); | |
} | |
targetSource.AddHook(WndProc); | |
} | |
private void OnWindowClosed(object sender, EventArgs e) | |
{ | |
if (targetSource != null) | |
targetSource.RemoveHook(WndProc); | |
} | |
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) | |
{ | |
switch (msg) | |
{ | |
case (int)WindowMessage.WM_DPICHANGED: | |
WindowDpi = new Dpi( | |
GetLoWord((uint)wParam), | |
GetHiWord((uint)wParam)); | |
var nativeRect = (NativeRect)Marshal.PtrToStructure(lParam, typeof(NativeRect)); | |
var rect = new Rect( | |
nativeRect.left, | |
nativeRect.top, | |
nativeRect.right - nativeRect.left, | |
nativeRect.bottom - nativeRect.top); | |
ChangeDpi(WindowDpi, rect); | |
handled = true; | |
break; | |
} | |
return IntPtr.Zero; | |
} | |
private void ChangeDpi(Dpi dpi, Rect rect) | |
{ | |
var content = targetWindow.Content as FrameworkElement; | |
if (content != null) | |
{ | |
content.LayoutTransform = (dpi == SystemDpi) | |
? Transform.Identity | |
: new ScaleTransform( | |
(double)dpi.X / SystemDpi.X, | |
(double)dpi.Y / SystemDpi.Y); | |
} | |
targetWindow.Left = rect.Left; | |
targetWindow.Top = rect.Top; | |
targetWindow.Width = rect.Width; | |
targetWindow.Height = rect.Height; | |
} | |
#region Helper | |
/// <summary> | |
/// DPI information | |
/// </summary> | |
/// <remarks> | |
/// This struct is based on the same struct of https://github.com/Grabacr07/XamClaudia | |
/// </remarks> | |
public struct Dpi | |
{ | |
public static readonly Dpi Default = new Dpi(96, 96); | |
public uint X { get; set; } | |
public uint Y { get; set; } | |
public Dpi(uint x, uint y) | |
: this() | |
{ | |
this.X = x; | |
this.Y = y; | |
} | |
public static bool operator ==(Dpi dpi1, Dpi dpi2) | |
{ | |
return (dpi1.X == dpi2.X) && (dpi1.Y == dpi2.Y); | |
} | |
public static bool operator !=(Dpi dpi1, Dpi dpi2) | |
{ | |
return !(dpi1 == dpi2); | |
} | |
public bool Equals(Dpi other) | |
{ | |
return (this.X == other.X) && (this.Y == other.Y); | |
} | |
public override bool Equals(object other) | |
{ | |
if (ReferenceEquals(null, other)) | |
return false; | |
return (other is Dpi) && Equals((Dpi)other); | |
} | |
public override int GetHashCode() | |
{ | |
return this.X.GetHashCode() ^ this.Y.GetHashCode(); | |
} | |
public override string ToString() | |
{ | |
return String.Format("{0}-{1}", this.X, this.Y); | |
} | |
} | |
public static bool IsEightOneOrNewer | |
{ | |
get | |
{ | |
if (!_isEightOneOrNewer.HasValue) | |
{ | |
var ver = Environment.OSVersion.Version; | |
_isEightOneOrNewer = ((6 == ver.Major) && (3 <= ver.Minor)) || (7 <= ver.Major); | |
} | |
return _isEightOneOrNewer.Value; | |
} | |
} | |
private static bool? _isEightOneOrNewer; | |
public static bool IsPerMonitorDpiAware() | |
{ | |
if (!IsEightOneOrNewer) | |
return false; | |
PROCESS_DPI_AWARENESS value; | |
var result = GetProcessDpiAwareness( | |
IntPtr.Zero, // Current process | |
out value); | |
if (result != 0) // 0 means S_OK. | |
return false; | |
return (value == PROCESS_DPI_AWARENESS.Process_Per_Monitor_DPI_Aware); | |
} | |
public static Dpi GetSystemDpi() | |
{ | |
var screen = IntPtr.Zero; | |
try | |
{ | |
screen = GetDC(IntPtr.Zero); | |
return new Dpi( | |
(uint)GetDeviceCaps(screen, LOGPIXELSX), | |
(uint)GetDeviceCaps(screen, LOGPIXELSY)); | |
} | |
finally | |
{ | |
if (screen != IntPtr.Zero) | |
ReleaseDC(IntPtr.Zero, screen); | |
} | |
} | |
public static Dpi GetDpiFromHwndSource(HwndSource targetSource) | |
{ | |
if (targetSource == null) | |
throw new ArgumentNullException("targetSource"); | |
if (!IsEightOneOrNewer) | |
return Dpi.Default; | |
var handleMonitor = MonitorFromWindow( | |
targetSource.Handle, | |
MONITOR_DEFAULTTO.MONITOR_DEFAULTTONEAREST); | |
if (handleMonitor == IntPtr.Zero) | |
return Dpi.Default; | |
uint dpiX = 1; | |
uint dpiY = 1; | |
GetDpiForMonitor( | |
handleMonitor, | |
MONITOR_DPI_TYPE.MDT_Default, | |
ref dpiX, | |
ref dpiY); | |
return new Dpi(dpiX, dpiY); | |
} | |
private static ushort GetLoWord(uint dword) | |
{ | |
return (ushort)(dword & 0xffff); | |
} | |
private static ushort GetHiWord(uint dword) | |
{ | |
return (ushort)(dword >> 16); | |
} | |
#endregion | |
#region Win32 | |
private enum WindowMessage : int | |
{ | |
WM_DPICHANGED = 0x02E0, | |
} | |
[StructLayout(LayoutKind.Sequential)] | |
private struct NativeRect | |
{ | |
public int left; | |
public int top; | |
public int right; | |
public int bottom; | |
} | |
[DllImport("Gdi32.dll", SetLastError = true)] | |
private static extern int GetDeviceCaps( | |
IntPtr hdc, | |
int nIndex); | |
private const int LOGPIXELSX = 88; | |
private const int LOGPIXELSY = 90; | |
[DllImport("User32.dll", SetLastError = true)] | |
private static extern IntPtr GetDC(IntPtr hWnd); | |
[DllImport("User32.dll", SetLastError = true)] | |
[return: MarshalAs(UnmanagedType.Bool)] | |
private static extern bool ReleaseDC( | |
IntPtr hWnd, | |
IntPtr hDC); | |
[DllImport("User32.dll", SetLastError = true)] | |
private static extern IntPtr MonitorFromWindow( | |
IntPtr hwnd, | |
MONITOR_DEFAULTTO dwFlags); | |
private enum MONITOR_DEFAULTTO : uint | |
{ | |
MONITOR_DEFAULTTONULL = 0x00000000, | |
MONITOR_DEFAULTTOPRIMARY = 0x00000001, | |
MONITOR_DEFAULTTONEAREST = 0x00000002, | |
} | |
[DllImport("Shcore.dll", SetLastError = true)] | |
private static extern int GetProcessDpiAwareness( | |
IntPtr hprocess, | |
out PROCESS_DPI_AWARENESS value); | |
private enum PROCESS_DPI_AWARENESS | |
{ | |
Process_DPI_Unaware = 0, | |
Process_System_DPI_Aware = 1, | |
Process_Per_Monitor_DPI_Aware = 2 | |
} | |
[DllImport("Shcore.dll", SetLastError = true)] | |
private static extern void GetDpiForMonitor( | |
IntPtr hmonitor, | |
MONITOR_DPI_TYPE dpiType, | |
ref uint dpiX, | |
ref uint dpiY); | |
private enum MONITOR_DPI_TYPE | |
{ | |
MDT_Effective_DPI = 0, | |
MDT_Angular_DPI = 1, | |
MDT_Raw_DPI = 2, | |
MDT_Default = MDT_Effective_DPI | |
} | |
#endregion | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment