Skip to content

Instantly share code, notes, and snippets.

@emoacht
Last active June 10, 2020 11:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emoacht/41adbb738d3f06dcdb65 to your computer and use it in GitHub Desktop.
Save emoacht/41adbb738d3f06dcdb65 to your computer and use it in GitHub Desktop.
Behavior to make a WPF Window Per-Monitor DPI aware.
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