Per-Monitor-Dpi をAttached Propertyで実現する
WPFのアプリで、Per-Monitor-DPIを実現する方法自体は、
Developing a Per-Monitor DPI-Aware WPF Application - MSDN
で紹介されており、これに基づいて様々なPer-Monitor-Dpi用のヘルパーが用意されています。githubで per-monitor-dpiでググると、それらしいものが沢山出てきます。
それらの中のものでも、例えば、カスタムのWindow クラスだと、他のフレームワークで実装されたWindowを継承している場合には使えません。
また、普通のヘルパークラスとして提供されている場合は、コードビハインドでワンライナーみたいな感じになります。
で、コードビハインドでワンライナーなら別に良いじゃんで終わりなんですが、何となく、何の意味もなく、あれっ、これって、ひょっとしてXAML中でAttatched Propertyで実装しても良いんじゃないのって思ったので実装してみました。動きました。おしまい。
正直なところ、Attached Propertyで実装する意味もないですし、そもそも、これって、UIのコードなのか、ロジックなのか???な感じです。コードビハインドにワンライナーの方がよっぽど分かりやすいかも知れません。その辺は、分かった上でやってみただけなので、本質的な部分でツッコまないでください。
XAMLにわずか2行を足すだけ!
<Window x:Class="MyFirstPerMonitorDpi.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" ... xmlns:pmdh="clr-namespace:PerMonitorDpiHelper;assembly=PerMonitorDpiHelper" pmdh:PerMonitorDpiHelper.IsEnabled="True"> ... </Window>
以下、いろんなところからパクったコードを纏めて、ワンソースにした物:
// PerMonitorDpiHelper.cs using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Data; using System.Windows.Interop; using System.Windows.Media; namespace PerMonitorDpiHelper { /// <summary> /// Handles all the Per-Monitor-DPI related tasks for a window. /// </summary> public class PerMonitorDpiHelper { static PerMonitorDpiHelper() { if (PerMonitorDpiHelperMethods.IsSupported) NativeMethods.SetProcessDpiAwareness(NativeMethods.ProcessDpiAwareness.PerMonitorDpiHelperAware); } /// <summary> /// Setup Per-Monitor-DPI configurations on a window. /// </summary> /// <param name="window">Target window.</param> public PerMonitorDpiHelper(Window window) { this.window = window; this.window.Loaded += window_Loaded; } private void window_Loaded(object sender, RoutedEventArgs e) { this.systemDpi = window.GetSystemDpi(); this.hwndSource = PresentationSource.FromVisual(window) as HwndSource; if (this.hwndSource != null) { this.currentDpi = this.hwndSource.GetDpi(); this.ChangeDpi(this.currentDpi); this.hwndSource.AddHook(this.WndProc); this.window.Closed += window_Closed; } } private void window_Closed(object sender, EventArgs e) { RemoveHook(); } public void RemoveHook() { if (this.hwndSource != null) { this.hwndSource.RemoveHook(this.WndProc); this.hwndSource = null; } if (this.window != null) { this.window.Loaded -= window_Loaded; this.window.Closed -= window_Closed; this.window = null; } } private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == (int)NativeMethods.WindowMessage.WM_DPICHANGED) { var dpiX = wParam.ToHiWord(); var dpiY = wParam.ToLoWord(); this.ChangeDpi(new Dpi(dpiX, dpiY)); handled = true; } return IntPtr.Zero; } private void ChangeDpi(Dpi dpi) { if (!PerMonitorDpiHelperMethods.IsSupported) return; var elem = window.Content as FrameworkElement; if (elem != null) { elem.LayoutTransform = (dpi == this.systemDpi) ? Transform.Identity : new ScaleTransform((double)dpi.X / this.systemDpi.X, (double)dpi.Y / this.systemDpi.Y); } this.window.Width = this.window.Width * dpi.X / this.currentDpi.X; this.window.Height = this.window.Height * dpi.Y / this.currentDpi.Y; Debug.WriteLine(string.Format("DPI Change: {0} -> {1} (System: {2})", this.currentDpi, dpi, this.systemDpi)); this.currentDpi = dpi; } private Dpi systemDpi; private Dpi currentDpi; private Window window; private HwndSource hwndSource; // // Attached property implementation // public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(PerMonitorDpiHelper), new PropertyMetadata(false, SetEnabled)); public static void SetIsEnabled(DependencyObject dp, bool value) { dp.SetValue(IsEnabledProperty, value); } public static bool GetIsEnabled(DependencyObject dp) { return (bool)dp.GetValue(IsEnabledProperty); } private static void SetEnabled(DependencyObject sender, DependencyPropertyChangedEventArgs e) { var window = sender as Window; if (window == null) return; if ((bool)e.OldValue) { GetPerMonitorDpiHelper(window).RemoveHook(); } if ((bool)e.NewValue) { SetPerMonitorDpiHelper(window, new PerMonitorDpiHelper(window)); } } private static readonly DependencyProperty PerMonitorDpiHelperProperty = DependencyProperty.RegisterAttached("PerMonitorDpiHelper", typeof(PerMonitorDpiHelper), typeof(PerMonitorDpiHelper)); private static void SetPerMonitorDpiHelper(DependencyObject dp, PerMonitorDpiHelper value) { dp.SetValue(PerMonitorDpiHelperProperty, value); } private static PerMonitorDpiHelper GetPerMonitorDpiHelper(DependencyObject dp) { return (PerMonitorDpiHelper)dp.GetValue(PerMonitorDpiHelperProperty); } } internal static class PerMonitorDpiHelperMethods { public static bool IsSupported { get { var version = Environment.OSVersion.Version; return version.Major * 1000 + version.Minor >= 6003; // Windows 8.1: 6.3 } } public static Dpi GetSystemDpi(this Visual visual) { var source = PresentationSource.FromVisual(visual); if (source != null && source.CompositionTarget != null) { return new Dpi( (uint)(Dpi.Default.X * source.CompositionTarget.TransformToDevice.M11), (uint)(Dpi.Default.Y * source.CompositionTarget.TransformToDevice.M22)); } return Dpi.Default; } public static Dpi GetDpi(this HwndSource hwndSource, MonitorDpiType dpiType = MonitorDpiType.Default) { if (!IsSupported) return Dpi.Default; var hMonitor = NativeMethods.MonitorFromWindow( hwndSource.Handle, NativeMethods.MonitorDefaultTo.Nearest); uint dpiX = 1, dpiY = 1; NativeMethods.GetDpiForMonitor(hMonitor, dpiType, ref dpiX, ref dpiY); return new Dpi(dpiX, dpiY); } } internal 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 obj) { if (ReferenceEquals(null, obj)) return false; return obj is Dpi && Equals((Dpi)obj); } public override int GetHashCode() { unchecked { return ((int)this.X * 397) ^ (int)this.Y; } } public override string ToString() { return string.Format("[X={0},Y={1}]", X, Y); } } /// <summary> /// Identifies dots per inch (dpi) type. /// </summary> internal enum MonitorDpiType { /// <summary> /// MDT_Effective_DPI /// <para>Effective DPI that incorporates accessibility overrides and matches what Desktop Window Manage (DWM) uses to scale desktop applications.</para> /// </summary> EffectiveDpi = 0, /// <summary> /// MDT_Angular_DPI /// <para>DPI that ensures rendering at a compliant angular resolution on the screen, without incorporating accessibility overrides.</para> /// </summary> AngularDpi = 1, /// <summary> /// MDT_Raw_DPI /// <para>Linear DPI of the screen as measures on the screen itself.</para> /// </summary> RawDpi = 2, /// <summary> /// MDT_Default /// </summary> Default = EffectiveDpi, } internal static class NativeMethods { [DllImport("user32.dll")] public static extern IntPtr MonitorFromWindow(IntPtr hwnd, MonitorDefaultTo dwFlags); [DllImport("shcore.dll")] public static extern void GetDpiForMonitor(IntPtr hmonitor, MonitorDpiType dpiType, ref uint dpiX, ref uint dpiY); [DllImport("shcore.dll")] public static extern int SetProcessDpiAwareness(ProcessDpiAwareness awareness); [DllImport("shcore.dll")] private static extern int GetProcessDpiAwareness(IntPtr handle, ref ProcessDpiAwareness awareness); public static ProcessDpiAwareness GetProcessDpiAwareness(IntPtr handle) { ProcessDpiAwareness pda = ProcessDpiAwareness.Unaware; if (GetProcessDpiAwareness(handle, ref pda) == 0) return pda; return ProcessDpiAwareness.Unaware; } public enum MonitorDefaultTo { Null = 0, Primary = 1, Nearest = 2, } public enum WindowMessage { WM_DPICHANGED = 0x02E0, } public enum ProcessDpiAwareness { Unaware = 0, SystemDpiAware = 1, PerMonitorDpiHelperAware = 2 } } internal static class IntPtrExtensions { public static ushort ToLoWord(this IntPtr dword) { return (ushort)((uint)dword & 0xffff); } public static ushort ToHiWord(this IntPtr dword) { return (ushort)((uint)dword >> 16); } } }