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);
        }
    }
}