DisplayPortの切断によるウィンドウサイズの変更を対処するDisplayQort覚書

# はじめに

DisplayPort接続でウィンドウ位置やアイコン位置が変わることに対して文句を述べる輩がいる。
それ自体は別に構わないが、思考停止して解決策を考えなかったり、
**Displayport**をこき下ろして**映像コンテンツ協会がゴリ押し**する**HDMI**に魂を売り渡したりするのはよろしくない。
Displayportは正しく使えばとっても便利なんだ。
あと(無償な)ソフトウェアじゃなくて(有償な)ハードウェアで解決するってのもプログラマとしては納得いかないことだろう。

**解決策?**
簡単さ。解決しない。
位置がずれたら戻せばいいのさ。
これは**コロンブスの卵**でも何でもない。

ソフトウェアの名前はDisplayPortをもじってDisplayQortにした。
全てのコードは[github](https://github.com/tibigame/Displayqort)で見てもらうとして、
自分で解読できなくなる前にコードの抜粋とメモを残しておく。

# 低レイヤーなんてやってられるか

とりあえずC#で書いてみたのだが、普段ライトに書くC#と違って仰々しい構文が続く。

```c#
    [Serializable]
    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;

        public int Area() => (Right - Left) * (Top - Bottom);

        public RECT(int left, int top, int right, int bottom)
        {
            Left = left;
            Top = top;
            Right = right;
            Bottom = bottom;
        }
        public override String ToString() => $"[({Left}, {Top}), ({Right}, {Bottom})]";
    }
    [Serializable]
    public struct RECTSIZE
    {
        public int Width;
        public int Height;
        public int Area() => Width * Height;

        public RECTSIZE(int w, int h)
        {
            Width = w;
            Height = h;
        }

        public static explicit operator RECTSIZE(RECT rect) => new RECTSIZE(rect.Right - rect.Left, rect.Bottom - rect.Top);
        public override String ToString() => $"w={Width}, h={Height}";
    }
    [Serializable]
    public enum SW
    {
        HIDE = 0,
        SHOWNORMAL = 1,
        SHOWMINIMIZED = 2,
        SHOWMAXIMIZED = 3,
        SHOWNOACTIVATE = 4,
        SHOW = 5,
        MINIMIZE = 6,
        SHOWMINNOACTIVE = 7,
        SHOWNA = 8,
        RESTORE = 9,
        SHOWDEFAULT = 10,
    }
    [Serializable]
    [StructLayout(LayoutKind.Sequential)]
    public struct WINDOWPLACEMENT
    {
        public int length;
        public int flags;
        public SW showCmd;
        public POINT minPosition;
        public POINT maxPosition;
        public RECT normalPosition;
        public override String ToString() =>
            $"SW{showCmd}Min{minPosition}Max{maxPosition}N{normalPosition}";
    }
```

**enum SW**は明らかにWindows APIに与える引数っぽい。
初心者が言うところのおまじないとして使ってる**[StructLayout(LayoutKind.Sequential)]**文字列。
おそらく構造体の中でのメモリ配置の順番を書き順通りに強制するみたいな意味だろうが(仕様を調べる気はないので間違ってるかもしれん)
そんなことまで考慮しないといけないとか頭が痛くなるばかりだ。

# Windows APIとの接続

ここが今回の一番の深淵。C++で書けばここへのアクセスは楽なんだろうが、
その先のGUIまでが果てしなく遠いのでC#のが無難かと。

```c#
    // ウィンドウ座標を取得
    [DllImport("user32.dll")]
    public static extern bool GetWindowRect(IntPtr HWND, out RECT rect);
    public bool Getwindowrect(IntPtr hwnd, out RECT rc)
    {
        bool tf = GetWindowRect(hwnd, out rc);
        return tf;
    }
    // ウインドウ情報を取得する
    [DllImport("user32.dll")]
    public static extern bool GetWindowPlacement(IntPtr hWnd, out WINDOWPLACEMENT lpwndpl);
    // ウインドウ情報をセットする
    [DllImport("user32.dll")]
    public static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref WINDOWPLACEMENT lpwndpl);
    // ウインドウ情報を取得する
    [DllImport("user32.dll")]
    public static extern long GetWindowLong(IntPtr hWnd, int nIndex);
    public static long GetWindowLongStyle(IntPtr hWnd) => GetWindowLong(hWnd, GWL_STYLE);
    public static long GetWindowLongExStyle(IntPtr hWnd) => GetWindowLong(hWnd, GWL_EXSTYLE);
    // ウィンドウと指定された関係（ またはオーナー）にあるウィンドウのハンドルを返します
    [DllImport("user32.dll")]
    public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
    public const int GWL_STYLE = -16; // ウインドウスタイルを取得
    public const int GWL_EXSTYLE = -20; // 拡張ウインドウスタイルを取得
    public const long WS_VISIBLE = 0x10000000L;
    public const long WS_EX_NOREDIRECTIONBITMAP = 0x00200000L;
    public const long WS_EX_TOOLWINDOW = 0x00000080L;
    public const uint GW_OWNER = 4;
```

hwnd恐怖症になりそうです。
c++11なんてなかった頃のWindowsアプリのプログラミングはこんな感じだったけど、
これを見て初心者にプログラミングするならc++はいいぞなんて言えるだろうか。
私は無理だ。なんだこのクソ言語はと思う。
(c++はASCII以外の文字列を扱うのも壊滅的なので実用的なアプリなんて無理だ)
pythonはいいぞ。(当時pythonはなかったけど)

# モニターの概念をラップしよう

先のAPIを直接叩くのは筋が悪いのでC#で使いやすい形にラップしよう。

```c#
    /// <summary>
    /// 各モニタの情報を表す構造体
    /// </summary>
    [Serializable]
    public class ScreenObj
    {
        public String DeviceName;
        public int BitsPerPixel;
        public bool Primary;
        public RECT Bounds; // Include TaskBar
        public RECT WorkingArea; // Exclude TaskBar

        public int ScreenNum;

        public ScreenObj(String deviceName, int bitsPerPixel, bool primary, RECT bounds, RECT workingArea, int screenNum)
        {
            DeviceName = deviceName;
            BitsPerPixel = bitsPerPixel;
            Primary = primary;
            Bounds = bounds;
            WorkingArea = workingArea;
            ScreenNum = screenNum;
        }
        public override String ToString() =>
            $"DeviceName: {DeviceName}\nBounds: {Bounds} WorkingArea: {WorkingArea}\nIsPrimary: {Primary} BitsPerPixel: {BitsPerPixel}\n";
    }
```

これは難しくない。コンストラクタはただ値を代入してるだけだし。

```c#
    /// <summary>
    /// マルチモニターを管理するクラス
    /// </summary>
    [Serializable]
    public class MultiMonitor
    {
        public int MonitorNum;
        public List<ScreenObj>ScreenList;

        public MultiMonitor()
        {
            MonitorNum = Screen.AllScreens.Length;
            ScreenList = new List<ScreenObj> { };

            int i = 0;
            foreach (var s in Screen.AllScreens)
            {
                ScreenList.Add(new ScreenObj(
                    s.DeviceName, s.BitsPerPixel, s.Primary,
                    new RECT(s.Bounds.Left, s.Bounds.Top, s.Bounds.Right, s.Bounds.Bottom),
                    new RECT(s.WorkingArea.Left, s.WorkingArea.Top, s.WorkingArea.Right, s.WorkingArea.Bottom),
                    i));
                ++i;
            }

        }
```

**Screen**は確か**using System;**で使えたはず。
**Screen.AllScreens**で全てのモニタのリストになるから**s**は一つのモニタを表す。
それを**foreach**で先程の**ScreenObj**構造体
(classで書いているけどデータを入れるという概念としての構造体)に入れてリスト化している。
つまりコンストラクタを呼べばその時点での全モニタの状態を取得できるコード。

```c#
        public static bool operator ==(MultiMonitor c1, MultiMonitor c2)
        {
            //nullの確認（構造体のようにNULLにならない型では不要）
            //両方nullか（参照元が同じか）
            //(c1 == c2)とすると、無限ループ
            if (object.ReferenceEquals(c1, c2))
            {
                return true;
            }
            //どちらかがnullか
            //(c1 == null)とすると、無限ループ
            if (((object)c1 == null) || ((object)c2 == null))
            {
                return false;
            }
            // モニタの数が異なる
            if (c1.MonitorNum != c2.MonitorNum)
            {
                return false;
            }
            // モニタの座標が異なる
            for (int i = 0; i < c1.MonitorNum; i++)
            {
                var s1 = c1.ScreenList[i];
                var s2 = c2.ScreenList[i];
                if (
                    s1.Bounds.Bottom != s2.Bounds.Bottom
                    || s1.Bounds.Top != s2.Bounds.Top
                    || s1.Bounds.Left != s2.Bounds.Left
                    || s1.Bounds.Right != s2.Bounds.Right
                    )
                {
                    return false;
                }
            }

            return true;
        }

        public static bool operator !=(MultiMonitor c1, MultiMonitor c2)
        {
            return !(c1 == c2);
        }

        public override int GetHashCode()
        {
            var hash = 0;
            for (int i = 0; i < MonitorNum; i++)
            {
                hash += (16 * i + 1) * ((RECTSIZE)ScreenList[i].Bounds).Area();
            }
            return hash;
        }
```

これは等号演算子のオーバーロードだ。
全モニタの状態が等しいとはどういう状態を意味しているのか。
それを考えると、まずモニタの数が等しいことを確定させてから
各モニタの座標をチェックする。全て等しい時のみ等しいとする。
こんな感じのロジックとなるだろう。
GetHashCode()はよくわからんけど必要だったような。

# ウィンドウの状態を取得する

```c#
    /// <summary>
    /// 個々のウィンドウの情報を表す構造体
    /// </summary>
    [Serializable]
    public class WindowObj
    {
        // ウィンドウの情報

        public String ClassName;
        public String TitleName;
        public RECTSIZE Size;
        public RECT Rect;
        public WINDOWPLACEMENT WindowPlacement;
        public IntPtr hWnd;

        // モニターとの関係

        /// <summary>
        /// MultiMonitorのインデックスに対応する
        /// ウィンドウが主に所属するモニターを表す
        /// 無所属の場合はOutOfRangeMonitorを指定すること
        /// </summary>
        public int BelongMonitor;
        /// <summary>
        /// ウィンドウが範囲外などでモニターに所属していない時に設定するモニター番号
        /// </summary>
        public const int OutOfRangeMonitor = System.Int32.MaxValue;
        /// <summary>
        /// モニターに再配置の必要があるかのフラグ
        /// </summary>
        public bool NeedRelocate;

        public WindowObj(IntPtr hWnd_)
        {
            hWnd = hWnd_;
        }
        public override String ToString() =>
            $"{TitleName}, {ClassName} {Rect}[Belong]{BelongMonitor}[Relocate]{NeedRelocate}{WindowPlacement}";
    }
```

これもただの構造体ですね。
型は怪しげなものが入っているけど。

```c#
    [Serializable]
    public class WindowManager
    {
        public List<WindowObj> WindowList;
        private MultiMonitor Monitor;
        public void GetWindowList()
        {
            WindowList.Clear();
            //すべてのウィンドウを列挙する
            Common.EnumWindows(EnumWindowCallBack, (IntPtr)null);
        }

        private bool EnumWindowCallBack(IntPtr hWnd, IntPtr lparam)
        {
            // 可視状態でないものを除く
            if ((Common.GetWindowLongStyle(hWnd) & Common.WS_VISIBLE) == 0)
            {
                return true;
            }
            if ((Common.GetWindowLongExStyle(hWnd) & Common.WS_EX_NOREDIRECTIONBITMAP) != 0)
            {
                return true;
            }
            // タスクバーに表示されているものを除く
            if ((Common.GetWindowLongExStyle(hWnd) & Common.WS_EX_TOOLWINDOW) != 0)
            {
                return true;
            }

            //ウィンドウのタイトルの長さを取得する
            int textLen = Common.GetWindowTextLength(hWnd);
            if (0 < textLen)
            {
                WindowObj TempWindow = new WindowObj(hWnd);
                //ウィンドウのタイトルを取得する
                StringBuilder tsb = new StringBuilder(textLen + 1);
                Common.GetWindowText(hWnd, tsb, tsb.Capacity);
                TempWindow.TitleName = tsb.ToString();

                //ウィンドウのクラス名を取得する
                StringBuilder csb = new StringBuilder(256);
                Common.GetClassName(hWnd, csb, csb.Capacity);
                TempWindow.ClassName = csb.ToString();

                // ウィンドウ位置を取得する
                var rc = new RECT();
                Common.GetWindowRect(hWnd, out rc);
                RECTSIZE rsize = (RECTSIZE)rc;
                TempWindow.Rect = rc;
                TempWindow.Size = rsize;

                // ウィンドウ情報を取得する
                WINDOWPLACEMENT wp = new WINDOWPLACEMENT();
                Common.GetWindowPlacement(hWnd, out wp);
                TempWindow.WindowPlacement = wp;

                if (rsize.Height != 0 || rsize.Width != 0)
                {
                    WindowList.Add(TempWindow);
                }
            }
            return true;
        }
```

ラップしているとはいえ、モロWindows APIなコード。
どこかのコードをコピーしてきてうまく動いたものを採用している。
コールバックだからモニターの変更に対して関数が呼ばれる機構を作っているのだろうが、
このコードの意味は正直わからん。

# メインロジック

本来やりたかったことはこの部分のはずだ。
なのにそれを実現するためにどれだけの下準備コードが必要になったのか。
OSが絡んだりc, c++でハードウェアをいじろうとすると大体こうなる罠。

```c#
        /// <summary>
        /// MultiMonitorを受け取り
        /// BelongMonitorとNeedRelocateの値を設定する
        /// </summary>
        public void SetBelongMonitor(MultiMonitor multiMonitor)
        {
            Monitor = multiMonitor;
            foreach (var window in WindowList)
            {
                var tempBelong = WindowObj.OutOfRangeMonitor;
                foreach (var screen in multiMonitor.ScreenList)
                {
                    // 完全に内包するモニタを見つけた
                    if (Common.IsInclude(screen.Bounds, window.Rect))
                    {
                        window.BelongMonitor = screen.ScreenNum;
                        window.NeedRelocate = false;
                        goto ENDLOOP;
                    }
                    // 一部内包するモニターだった (一部のみのモニターが複数存在するときはどっちの所属でもいい)
                    if (Common.ExtraInclude(screen.Bounds, window.Rect) == Relation.OtherInclude)
                    {
                        tempBelong = screen.ScreenNum;
                    }
                }
                // 範囲外モニターならノータッチ
                if (tempBelong != WindowObj.OutOfRangeMonitor)
                {
                    window.BelongMonitor = tempBelong;
                    window.NeedRelocate = true;
                }
                ENDLOOP:;
            }
        }
        /// <summary>
        /// WindowListの値をファイルに保存する
        /// </summary>
        public void Save()
        {
            using (Stream stream = File.OpenWrite(Filename))
            {
                BinaryFormatter formatter = new BinaryFormatter();
                formatter.Serialize(stream, WindowList);
            }
        }
        /// <summary>
        /// WindowListを保存された値に戻す
        /// </summary>
        public void Reload()
        {
            if (File.Exists(Filename))
            {
                using (Stream stream = File.OpenRead(Filename))
                {
                    BinaryFormatter formatter = new BinaryFormatter();
                    WindowList = (List<WindowObj>)formatter.Deserialize(stream);
                }
            }
        }
        /// <summary>
        /// 全てのWindowを保存された値に戻す
        /// </summary>
        public void Renew()
        {
            // モニタの構造が保存時と異なるときは警告を出す
            if (!IsMonitorEqual())
            {
                var r = System.Windows.Forms.MessageBox.Show(
                    Properties.Resources.MonitorDiffer, Properties.Resources.Warning,
                    MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation,
                    MessageBoxDefaultButton.Button2);
                if (r == DialogResult.No) { return; }
            }
            foreach (var window in WindowList)
            {
                Common.SetWindowPlacement(window.hWnd, ref window.WindowPlacement);
            }
        }
        /// <summary>
        /// 全てのWindowをpointだけ動かす
        /// </summary>
        public void MoveWindows(POINT point)
        {
            foreach (var window in WindowList)
            {
                window.WindowPlacement.normalPosition.Left += point.X;
                window.WindowPlacement.normalPosition.Right += point.X;
                window.WindowPlacement.normalPosition.Top += point.Y;
                window.WindowPlacement.normalPosition.Bottom += point.Y;
                Common.SetWindowPlacement(window.hWnd, ref window.WindowPlacement);
            }
        }
```

モニターとウィンドウの状態を定期的に保存してモニターの変更を検知すればダイアログを出し、
モニターの状態を元に戻した上でウィンドウの復元をすればよいはずという考え。

# 新機能をつけるなら

私自身がDisplayportでのトリプルモニタ環境で不自由しない状態になったので
これ以上このソフトに手を加えるつもりはないが、
複数のモニタの着脱や物理的な位置移動を頻繁にするならば、
ウィンドウセットの概念を入れてウィンドウ集団のモニタ移動を容易にしたい。
GUIをもう少しリッチにとか。

# 他のOSについて

* MACはよくわからんがHDMIみたいな下賤なものは使わずにThunderbolt3でMACの世界にいる限りは大丈夫なんだろう。
* Linuxはずれてもコンソール中心だからどうでもいいだろ。