Windowsの高解像度対応で嵌った話

# 経緯
C#で、[クリックした位置の`AutomationElement`](https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.automationelement.frompoint?view=netframework-4.8)を取得して情報を表示するプログラムを作っていて、[ディスプレイに描画](https://puarts.com/?pid=1146)するときにずれたので調べてみた。[^1]

[^1]: [`Current`](https://docs.microsoft.com/en-us/dotnet/api/system.windows.automation.automationelement.current?view=netframework-4.8)プロパティで`AutomationElement.AutomationElementInformation`を取得し、`BoundingRectangle`プロパティで得られる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/24dff9cc-889c-7755-e7c0-48b1ecf84e11.png)

画面横幅のピクセル数とタスクバーのWidthの比を調べ、1.5：1になっている。

# 補正後

決め打ちで1.5倍して描画してみる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/173c7a24-8817-87f6-b0e6-884692482752.png)

なおった？
試しに他のエレメントをクリックしてみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/55e02cf2-2a6f-a00a-1a73-d29af2230795.png)
？！
あかんがな。

補正前に戻して確認してみると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/ff237737-a439-e666-c951-ac93f26f6c54.png)
合いました。

ということで、AutomationElementごとにスケーリング単位が違う？？

# 拡大縮小の設定場所

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/2b84e105-f721-10ab-3bda-b95ce471b82a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/4b44cdf4-5998-0adb-26c3-a38fd065fbff.png)

色々調べて下記にたどり着いた。
[関連するレジストリ](https://yaritakunai.hatenablog.com/entry/2016/02/04/181000)
(参照先の公式:[MSDN](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-8.1-and-8/dn528846(v=win.10)?redirectedfrom=MSDN))

混在しているらしく、これが多分原因か？
> WindowsではDPIスケーリング対応状況別にアプリケーションを以下のように分類しています。
・Unaware
・System Aware
・Per-Moniter Aware

# 確認用ソースコード

テキトウなのであしからず

```csharp

using System;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Automation;
using System.Windows.Forms;

public static class NativeMethods
{
    [StructLayout(LayoutKind.Sequential)]
    public struct POINT
    {
        public int x;
        public int y;
    }


    public const int WH_MOUSE_LL = 14;
    public delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern IntPtr SetWindowsHookEx(int idHook, HookProc callback, IntPtr hInstance, int threadId);

    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool UnhookWindowsHookEx(IntPtr hHook);
    
    [DllImport("user32.dll")]
    public static extern IntPtr CallNextHookEx(IntPtr hHook, int nCode, IntPtr wParam, IntPtr lParam);

    public const int  WM_LBUTTONDOWN = 0x0201;

    // [DllImport("user32.dll")]
    // [return: MarshalAs(UnmanagedType.Bool)]
    // public static extern bool GetCursorPos(out POINT lpPoint);

    [DllImport("user32.dll",SetLastError = true)]
    public static extern IntPtr WindowFromPoint(POINT point);

    [DllImport("User32.dll")]
    public static extern IntPtr GetDC(IntPtr hwnd);

    [DllImport("User32.dll")]
    public static extern void ReleaseDC(IntPtr hwnd, IntPtr dc);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool SetProcessDPIAware();
}


public class AutomationTest : Form
{
    IntPtr _hHook;
    NativeMethods.HookProc _handler;
    GCHandle _hookProcHandle;
    NativeMethods.POINT _lastPoint;

    System.Windows.Forms.Timer timer;
    Button btn;
    ListView lsv;

    AutomationTest()
    {
        // NativeMethods.SetProcessDPIAware();

        timer = new System.Windows.Forms.Timer();
        timer.Interval = 10;
        timer.Tick += (s,e)=>{Timer_Tick();};

        btn = new Button(){Text="get"};
        btn.Click += (s,e)=>{Btn_Click();};
        Controls.Add(btn);

        lsv = new ListView(){
            Location = new System.Drawing.Point(0, 40),
            Size = new System.Drawing.Size(400, 400),
            FullRowSelect = true,
            GridLines = true,
            HideSelection = false,
            MultiSelect = false,
            View = View.Details
        };
        lsv.MouseDoubleClick += Lsv_MouseDoubleClick;
        lsv.Columns.AddRange(new ColumnHeader[]{
            new ColumnHeader(){Text="ClassName",Width=100},
            new ColumnHeader(){Text="AutomationId",Width=50},
            new ColumnHeader(){Text="ControlType",Width=100},
            new ColumnHeader(){Text="FrameworkId",Width=50},
            new ColumnHeader(){Text="Name",Width=100},
            new ColumnHeader(){Text="ItemType",Width=50},
            new ColumnHeader(){Text="X",Width=50},
            new ColumnHeader(){Text="Y",Width=50},
            new ColumnHeader(){Text="Width",Width=50},
            new ColumnHeader(){Text="Height",Width=50},
        });
        Controls.Add(lsv);

        FormClosed += (s,e)=>{UnHook();};

        Load += (s,e)=>{MyResize();};
        Resize += (s,e)=>{MyResize();};
        ResizeEnd += (s,e)=>{MyResize();};
    }

    void MyResize()
    {
        int h = ClientSize.Height - lsv.Top;
        if (h<50){h=50;}
        lsv.Size = new System.Drawing.Size(ClientSize.Width, h);
    }

    void Timer_Tick()
    {
        UnHook();
        if ( !timer.Enabled ) {
            return;
        }
        timer.Stop();
        btn.Enabled = true;

        var p = new System.Windows.Point(_lastPoint.x, _lastPoint.y);
        //var elem = AutomationElement.FromHandle(NativeMethods.WindowFromPoint(_lastPoint));
        var elem = AutomationElement.FromPoint(p);
        bool topElemFlag = true;

        lsv.BeginUpdate();
        try {
            while (elem != null) {
                AutomationElement.AutomationElementInformation elemInfo;
                try {
                    elemInfo = elem.Current;
                }
                catch( ElementNotAvailableException ) {
                    return;
                }

                if ( topElemFlag ) {
                    DrawPointAndRectToScreen(p, elemInfo.BoundingRectangle);
                }

                lsv.Items.Add(AeToListItem(elemInfo));
                elem = FindNextElementFromPoint(elem, p);

                topElemFlag = false;
            }
        }
        finally {
            lsv.EndUpdate();
        }
    }

    AutomationElement FindNextElementFromPoint(AutomationElement elem, System.Windows.Point p)
    {
        var childElements = elem.FindAll(TreeScope.Children, Condition.TrueCondition);

        foreach(AutomationElement childElem in childElements) {
            AutomationElement.AutomationElementInformation elemInfo;
            try {
                elemInfo = childElem.Current;
            }
            catch( ElementNotAvailableException ) {
                return null;
            }

            if ( elemInfo.BoundingRectangle.Contains(p) ) {
                return childElem;
            }
        }
        return null;
    }

    ListViewItem AeToListItem(AutomationElement.AutomationElementInformation a)
    {
        System.Windows.Rect r = a.BoundingRectangle;

        return new ListViewItem(new string[]{
            a.ClassName,
            a.AutomationId,
            a.ControlType.ToString(),
            a.FrameworkId,
            a.Name,
            a.ItemType,
            r.X.ToString(),
            r.Y.ToString(),
            r.Width.ToString(),
            r.Height.ToString()
        });
    }

    void Btn_Click()
    {
        try {
            SetHook();
        }
        catch (System.ComponentModel.Win32Exception e) {
            MessageBox.Show(e.ToString());
            return;
        }

        btn.Enabled = false;
        lsv.Items.Clear();
    }

    void Lsv_MouseDoubleClick(object sender, MouseEventArgs e)
    {
        ListViewHitTestInfo info = lsv.HitTest(e.Location);
        if ( info.SubItem != null ) {
            SubForm f = new SubForm(info.SubItem.Text);
            f.ShowDialog();
        }
    }

    void DrawPointAndRectToScreen(System.Windows.Point p, System.Windows.Rect rect)
    {
        IntPtr desktopDC = NativeMethods.GetDC(IntPtr.Zero);
        
        if (desktopDC == IntPtr.Zero) {
            // failed
            return;
        }

        try {
            var pen = new System.Drawing.Pen(System.Drawing.Color.Red, 3.0f);
            // 描画が欠ける Scalingがうまくいっていないっぽい
            using (var g = System.Drawing.Graphics.FromHdc(desktopDC)) {
                g.DrawLine(pen, (float)((p.X-5)*_highDpiScale), (float)((p.Y-5)*_highDpiScale), (float)((p.X+5)*_highDpiScale), (float)((p.Y+5)*_highDpiScale));
                g.DrawLine(pen, (float)((p.X-5)*_highDpiScale), (float)((p.Y+5)*_highDpiScale), (float)((p.X+5)*_highDpiScale), (float)((p.Y-5)*_highDpiScale));
                g.DrawRectangle(pen, (float)(rect.X*_highDpiScale), (float)(rect.Y*_highDpiScale), (float)(rect.Width*_highDpiScale), (float)(rect.Height*_highDpiScale));
            }
        }
        finally {
            NativeMethods.ReleaseDC(IntPtr.Zero, desktopDC);
        }
    }

    void SetHook()
    {
        IntPtr module = IntPtr.Zero;
        _handler = CallbackProc;
        _hookProcHandle = GCHandle.Alloc(_handler);
        _hHook = NativeMethods.SetWindowsHookEx(NativeMethods.WH_MOUSE_LL, _handler, module, 0);

        if ( _hHook == IntPtr.Zero ) {
            // failed
            int errorCode = Marshal.GetLastWin32Error();
            _hookProcHandle.Free();
            _handler = null;
            throw new System.ComponentModel.Win32Exception(errorCode);
        }
    }

    void UnHook()
    {
        if ( _hHook != IntPtr.Zero ) {
            NativeMethods.UnhookWindowsHookEx(_hHook);
            _hHook = IntPtr.Zero;
            _hookProcHandle.Free();
            _handler = null;
        }
    }

    IntPtr CallbackProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
        if ( nCode < 0 ) {
            return NativeMethods.CallNextHookEx(_hHook, nCode, wParam, lParam);
        }
        else {
            if ( (long)wParam == NativeMethods.WM_LBUTTONDOWN ) {
                //NativeMethods.GetCursorPos(out _lastPoint);
                var p = Cursor.Position;
                _lastPoint = new NativeMethods.POINT(){x=p.X, y=p.Y};
                Console.Write(_lastPoint.x);
                Console.Write(", ");
                Console.WriteLine(_lastPoint.y);
                timer.Start();

                // cancel
                return new IntPtr(1);
            }

            return NativeMethods.CallNextHookEx(_hHook, nCode, wParam, lParam);
        }
    }


    static float _highDpiScale = 1.0f;

    [STAThread]
    static void Main(string[] args)
    {
        if (args.Length==1) {
            float scale = (float)(Convert.ToInt32(args[0])/100.0);
            if (scale>=1.0f && scale<=5.0f) {
                _highDpiScale = scale;
            }
        }
        Application.Run(new AutomationTest());
    }
}

internal class SubForm : Form
{
    internal SubForm(string text)
    {
        var txt = new TextBox(){
            Text = text,
            Multiline = true,
            ScrollBars = ScrollBars.Both,
            Dock = DockStyle.Fill
        };
        txt.KeyDown += (sender,e)=>{
            if (e.Control && e.KeyCode == Keys.A) { txt.SelectAll(); }
        };
        Controls.Add(txt);
    }
}
```

# コンパイルバッチ

```text

csc ^
 /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\UIAutomationClient\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationClient.dll ^
 /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\UIAutomationTypes\v4.0_4.0.0.0__31bf3856ad364e35\UIAutomationTypes.dll ^
 /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\WindowsBase\v4.0_4.0.0.0__31bf3856ad364e35\WindowsBase.dll ^
 %*
```

# 対策案
下記3が妥当か。

1. 運用でカバーする。(個人で使う分には、100%に設定すればとりあえず回避できる。)
2. ウィンドウハンドルをつかう[^2]（描画はあきらめる）。
3. <b>`SetProcessDPIAware`を使う。</b>(Formとかのサイズが変わるので注意)

[^2]: `AutomationElement.FromHandle(NativeMethods.WindowFromPoint(GetCursorPos()で得た座標));` ただし、デスクトップ上のアイコンが（良くも悪くも）取れなくなる。

# 参考サイト
- [GetDeviceCapsが常にDPI96を返す問題と解決方法について](https://gist.github.com/retorillo/4e0c4a3cf4c7096e05ac)
- [Windows Formsアプリケーションの高DPI対応](https://qiita.com/felis_silv/items/efee4b1a397b0b95100a)