初めに
「終了時のウィンドウ位置を覚えておいて、次回起動時に復元したい」ただそれだけのことを実現するのにだいぶハマったので記録しておきます。
なお、同じアプリを複数起動する場合に、いい具合に整列するとかそんなことは考えていません。
Windows 10 Creators Update (1703)以降のWPFアプリを対象とします。
想定環境
ディスプレイ1: 4K(3840x2160) 拡大率 100%
ディスプレイ2: FHD(1920x1080) 拡大率 150%
プロジェクトの基本設定
プロジェクトにアプリケーションマニフェストファイルapp.manifestを追加し、下記の設定を記入します。
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
単にファイルを追加しただけだと認識してくれません。
プロジェクトファイル *.csproj に記述しておきます。
<Project Sdk="Microsoft.NET.Sdk">
:
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
:
</Project>
このように設定しておくと、ディスプレイ1からディスプレイ2にウィンドウを移動したときも自動的に150%に拡大されます。文字もビットマップ補間ではなく相当のフォントサイズで再描画されるのでぼやけたりしません。
ウィンドウ位置・サイズの復元
Windowに位置を設定してもだめ
WPFなので System.Windows.Windowに値を設定しよう、と考えるところなのですが、だめです。
Windowには論理座標、論理サイズが格納されています。
ディスプレイ1にウィンドウがあって、{Left=100,Top=150,Width=600, Height=400}だったとします。
ディスプレイ2に移動すると{Left=2752.666,Top=148.666, Width=600, Height=400}のようになったりします。
実座標でいうと{Left=4129,Top=223}なのですが、ディスプレイ2の拡大率が150%のためこのような値が返ってきます。これではWindowの座標を見てもディスプレイ1にあるのかディスプレイ2にあるのかわかりません。
Win32APIを使う
WPFだけでは解決できないため、Win32APIの SetWindowPlacement()ではなくてSetWindowPos()を使います。これは実座標を使って指定します。GetWindowPlacement()も後で使います。
Win32APIだけでもだめ
前回終了時にGetWindowPlacement()で取得した実座標を覚えておいてSetWindowPlacement()で復元すればよい……と考えがちですが、ここでハマります。ウィンドウ位置を復元するとき、最初のウィンドウ位置はディスプレイ1にあるかと思います。前回終了時の位置がウィンドウ2にあって、{Left=4129, Top=223, Right=5029, Bottom=823}をSetWindowPlacement()でセットすると、ウィンドウ位置がディスプレイ1→ディスプレイ2に移動したと思うのか、ウィンドウサイズを150%拡大してくれるのです。結果右下座標が指定した位置になりません。余計なお世話。
位置はWin32API, サイズはWPF
そこで、SetWindowPos()で位置だけ復元してから、Winfow.WidthとWinow.Heightに論理サイズを設定します。
サブディスプレイはいつも有るとは限らない
前回終了時、サブディスプレイに表示していたものが、今回起動時にはサブディスプレイをつないでいなかった場合、そのままの位置で表示しようとするとウィンドウが見えないわけです。このような場合に備えて、ウィンドウが画面外にある場合は位置を移動しないで、Windowsが勝手に決めた適当な位置のまま表示することにします。
復元はSourceInitializedイベントで
復元はSourceInitializedイベントで行います。ウィンドウのWin32ハンドル生成後で、それでいて表示前なのでちらつくこともありません。
ウィンドウ位置・サイズの取得
終了前に現在のウィンドウ位置・サイズを取得して保存しておく必要があります。
位置はWin32API, サイズはWPF
復元時と同様に位置は実位置を、サイズは論理サイズを取得しておく必要があります。
ただし、SetWindowPos()に対応するGetWindowRect()は最大化状態でも最大化状態のウィンドウ座標を返すため、GetWindowPlacement()で最大化前の位置を取ります。
最大化前のサイズはWPFの Window.RestoreBounds.Width/Height に入っているのでこれを保存します。
サンプルコード
サンプルアプリケーションを作ったので、公開しておきます。
復元・取得のコードはそのまま利用できるように、1ファイルにまとめています。
位置とサイズを復元したい場合はSet()を、位置だけ復元したい場合はSetPos()を使います。
サンプルではパラメータをコードでそのまま記述していますが、実際には設定ファイルから読み込んだり設定ファイルに保存したりの処理が必要になります。
