Win32 SDKでウィンドウ位置を正しく保存・復元する方法。
概要 †
Windowsプログラミングにも慣れてきた初心者と中級者の間くらいのプログラマが往々にして実装したがる機能があります。
それが今回取り扱うウィンドウ位置・状態の保存・復元機能です。
アプリケーションの終了時にその時点でのウィンドウの位置と状態(最大化されているか否か)を設定ファイル等に保存し、次回起動時に同じ位置と状態で起動するという、プログラマの自己満足度をかなり補充することのできる機能です。
ですがこの機能、簡単なようで意外と奥深い面があります。
見た目はうまく実装できているようでも、いざ配布してみると思いもよらないバグが報告されたりもします。
特にマルチモニタ環境で使うユーザに捕まったりした日には、まず手元にマルチモニタ環境が無いと現象の再現すらできません。
そこでこのコンテンツでは、誰でも実装できるようで誰でも間違っている可能性のあるこの機能を正しく実装してみたいと思います。
実装する内容 †
今回は、以下の2関数の実装を目的とします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
-
|
|
|
|
|
!
-
|
|
|
|
|
!
| #include <windows.h>
void SaveWindowState(HWND hWnd, LPCTSTR iniFilePath);
void LoadWindowState(HWND hWnd, LPCTSTR iniFilePath);
|
今回はINIファイルへ保存するようにしましたが、別に保存先はレジストリでも構いません。
作成するアプリケーションにとって適切な場所へ保存して下さい。
マルチユーザ環境の考慮も忘れずに。
また、 RECT
構造体の内容を保存、復元する以下の関数を定義します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
-
|
|
|
!
-
|
|
|
|
|
|
|
|
|
|
|
|
|
!
-
|
|
|
|
|
!
-
|
|
|
|
|
|
|
|
!
| #include <windows.h>
#include <tchar.h>
void SaveRect(const RECT* pRect, LPCTSTR iniFilePath)
{
TCHAR strNum[64];
_ltot(pRect->left, strNum, 10);
WritePrivateProfileString(
_T("window"), _T("left"), strNum, iniFilePath);
_ltot(pRect->top, strNum, 10);
WritePrivateProfileString(
_T("window"), _T("top"), strNum, iniFilePath);
_ltot(pRect->right, strNum, 10);
WritePrivateProfileString(
_T("window"), _T("right"), strNum, iniFilePath);
_ltot(pRect->bottom, strNum, 10);
WritePrivateProfileString(
_T("window"), _T("bottom"), strNum, iniFilePath);
}
void LoadRect(RECT* pRect, LPCTSTR iniFilePath)
{
pRect->left = (long)GetPrivateProfileInt(
_T("window"), _T("left"), pRect->left, iniFilePath);
pRect->top = (long)GetPrivateProfileInt(
_T("window"), _T("top"), pRect->top, iniFilePath);
pRect->right = (long)GetPrivateProfileInt(
_T("window"), _T("right"), pRect->right, iniFilePath);
pRect->bottom = (long)GetPrivateProfileInt(
_T("window"), _T("bottom"), pRect->bottom, iniFilePath);
}
|
以下の説明において、ウィンドウ位置の保存と復元にはこれらの関数を用います。
正しい位置取得 †
よくある間違い †
次の実装を見てみて下さい。
なお、 #include
および関数の説明コメントは省略してあります(以下同様)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
-
-
!
|
|
-
!
|
-
!
-
|
|
!
|
-
|
|
!
!
-
-
!
|
|
-
!
|
-
!
|
|
|
|
-
!
|
|
-
|
!
!
| void SaveWindowState(HWND hWnd, LPCTSTR iniFilePath)
{
RECT rcWnd;
GetWindowRect(hWnd, &rcWnd);
SaveRect(&rcWnd, iniFilePath);
if (IsZoomed(hWnd))
{
WritePrivateProfileString(
_T("window"), _T("zoomed"), _T("1"), iniFilePath);
}
else
{
WritePrivateProfileString(
_T("window"), _T("zoomed"), _T("0"), iniFilePath);
}
}
void LoadWindowState(HWND hWnd, LPCTSTR iniFilePath)
{
RECT rcWnd;
GetWindowRect(hWnd, &rcWnd);
LoadRect(&rcWnd, iniFilePath);
SetWindowPos(
hWnd, NULL, rcWnd.left, rcWnd.top,
rcWnd.right - rcWnd.left, rcWnd.bottom - rcWnd.top,
SWP_NOZORDER);
UINT isZoomed = GetPrivateProfileInt(
_T("window"), _T("zoomed"), 0, iniFilePath);
if (isZoomed != 0)
{
ShowWindow(hWnd, SW_MAXIMIZE);
}
}
|
初心者が現在の知識を総動員して実装したウィンドウ状態保存・復元はこんな感じになるんじゃないでしょうか。
実際、このコードの過ちに気付かない方もいるのでは?
このコードを実装したアプリケーションの起動と終了を繰り返すと、確かにウィンドウの位置と最大化状態が保存されていることがわかります。
しかしここで満足してしまうと、ユーザの使い方一つで恐ろしいことになってしまいます。
まず、最大化した状態で終了し、再び起動した後、最大化を解いて(通常状態にして)みて下さい。
やってみればわかりますが、最大化した時のウィンドウサイズが保存されてしまっており、通常状態にしてもウィンドウが小さくなりません。
更に、最小化した状態でタスクバーの右クリックメニューから終了させてみて下さい。
環境にもよりますが、INIファイルになかなか恐ろしい値が書き込まれたのではないでしょうか。
私の環境では -32000
などという値が書き込まれました。
そして次の起動時には、タスクバーに姿はあれどウィンドウは見えなくなってしまいました。
もうわかったと思いますが、 GetWindowRect
関数は現在の状態におけるウィンドウの位置を返します。
最大化されていれば最大化された位置とサイズを返しますし、最小化されていればとてつもなく小さい値を返します。
これでは正しい位置保存とはいえません。
正しい処理 †
ではどうすればいいのかというと、 GetWindowPlacement
関数を使います。
次のコードを見て下さい。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
-
-
!
|
|
|
-
!
|
-
!
-
|
|
!
|
-
|
|
!
!
-
-
!
|
|
|
|
-
!
|
-
!
|
|
|
|
-
!
|
|
-
|
!
!
| void SaveWindowState(HWND hWnd, LPCTSTR iniFilePath)
{
WINDOWPLACEMENT wndPlace;
wndPlace.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(hWnd, &wndPlace);
SaveRect(&wndPlace.rcNormalPosition, iniFilePath);
if (wndPlace.showCmd == SW_SHOWMAXIMIZED)
{
WritePrivateProfileString(
_T("window"), _T("zoomed"), _T("1"), iniFilePath);
}
else
{
WritePrivateProfileString(
_T("window"), _T("zoomed"), _T("0"), iniFilePath);
}
}
void LoadWindowState(HWND hWnd, LPCTSTR iniFilePath)
{
WINDOWPLACEMENT wndPlace;
wndPlace.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(hWnd, &wndPlace);
RECT rcWnd = wndPlace.rcNormalPosition;
LoadRect(&rcWnd, iniFilePath);
SetWindowPos(
hWnd, NULL, rcWnd.left, rcWnd.top,
rcWnd.right - rcWnd.left, rcWnd.bottom - rcWnd.top,
SWP_NOZORDER);
UINT isZoomed = GetPrivateProfileInt(
_T("window"), _T("zoomed"), 0, iniFilePath);
if (isZoomed != 0)
{
ShowWindow(hWnd, SW_MAXIMIZE);
}
}
|
GetWindowPlacement
関数は、 WINDOWPLACEMENT
構造体に値を設定します。
具体的にどんな値が設定されるかについてはMSDNライブラリを参照して下さい。
この構造体の rcNormalPosition
メンバには、現在の状態に関係なく常に通常状態でのウィンドウ位置が格納されます。
これにより、どんな状態で終了したとしても正しいウィンドウ位置を書き出すことができます。
解像度変更への対応 †
よくある間違い †
実は先に述べたコードではまだ問題があります。
それは、アプリケーション終了後に解像度が変更された場合です。
例えば1280x1024の環境でウィンドウを左端へ持っていき終了した後、解像度を1024x768などに下げると、これまたウィンドウは画面の外へ行ってしまいます。
そこで、ウィンドウ位置の復元時に現在の画面領域内に収まっていなかったら位置をずらす必要があります。
それを実装してみたのが次のコードです。
なお、 SaveWindowState
関数は内容が変わらないため省略します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
-
-
!
|
|
|
|
-
!
|
-
!
|
|
-
!
-
|
|
!
|
-
|
|
!
|
-
|
|
!
|
-
|
|
!
|
-
!
|
|
|
|
-
!
|
|
-
|
!
!
| void LoadWindowState(HWND hWnd, LPCTSTR iniFilePath)
{
WINDOWPLACEMENT wndPlace;
wndPlace.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(hWnd, &wndPlace);
RECT rcWnd = wndPlace.rcNormalPosition;
LoadRect(&rcWnd, iniFilePath);
long dispX = GetSystemMetrics(SM_CXSCREEN);
long dispY = GetSystemMetrics(SM_CYSCREEN);
if (rcWnd.right > dispX)
{
rcWnd.left -= rcWnd.right - dispX;
rcWnd.right = dispX;
}
if (rcWnd.left < 0)
{
rcWnd.right -= rcWnd.left;
rcWnd.left = 0;
}
if (rcWnd.bottom > dispY)
{
rcWnd.top -= rcWnd.bottom - dispY;
rcWnd.bottom = dispY;
}
if (rcWnd.top < 0)
{
rcWnd.bottom -= rcWnd.top;
rcWnd.top = 0;
}
SetWindowPos(
hWnd, NULL, rcWnd.left, rcWnd.top,
rcWnd.right - rcWnd.left, rcWnd.bottom - rcWnd.top,
SWP_NOZORDER);
UINT isZoomed = GetPrivateProfileInt(
_T("window"), _T("zoomed"), 0, iniFilePath);
if (isZoomed != 0)
{
ShowWindow(hWnd, SW_MAXIMIZE);
}
}
|
GetSystemMetrics
関数で画面のサイズを取得し、ウィンドウが画面からはみ出ていた場合は画面内に収まるように位置を補正します。
ただし、ウィンドウの方が画面よりも大きい場合はウィンドウを左端及び上端を画面の端に合わせます。
とまぁ、これで完璧に見えるでしょう。
実際、こういった感じで紹介されているサイトも探せばあると思います。
ですが、これでは間違い、というか不十分なのです。
画面が一つの環境では気付かないかもしれません。
そう、このコードはマルチモニタ環境に対応していないのです。
GetSystemMetrics
関数で取得できる画面のサイズは、プライマリモニタの画面サイズです。
よってこのアプリケーションをセカンダリモニタ上で使いたいといった場合、位置補正処理がかえって邪魔をしてしまいます。
上のコードでは、セカンダリモニタ上は画面外と見なされ、プライマリモニタ上へ位置補正されてしまうのです。
これを考慮せずにリリースされているソフトウェアは多く、市販のソフトウェアですら考慮されていないこともあります。
正しい処理 †
マルチモニタ対応の処理方法はいくつか考えられますが、ここでは MonitorFromRect
関数を使ってみたいと思います。
この関数は、指定した長方形領域との交差部分が最も広いモニタのハンドルを返します。
この関数を用いた実装方法を次に示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
-
-
!
|
|
|
|
-
!
|
-
!
|
|
|
|
|
-
!
-
|
|
!
|
-
|
|
!
|
-
|
|
!
|
-
|
|
!
|
-
!
|
|
|
|
-
!
|
|
-
|
!
!
| void LoadWindowState(HWND hWnd, LPCTSTR iniFilePath)
{
WINDOWPLACEMENT wndPlace;
wndPlace.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(hWnd, &wndPlace);
RECT rcWnd = wndPlace.rcNormalPosition;
LoadRect(&rcWnd, iniFilePath);
HMONITOR hMonitor = MonitorFromRect(
&rcWnd, MONITOR_DEFAULTTONEAREST);
MONITORINFO mi;
mi.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(hMonitor, &mi);
if (rcWnd.right > mi.rcMonitor.right)
{
rcWnd.left -= rcWnd.right - mi.rcMonitor.right;
rcWnd.right = mi.rcMonitor.right;
}
if (rcWnd.left < mi.rcMonitor.left)
{
rcWnd.right += mi.rcMonitor.left - rcWnd.left;
rcWnd.left = mi.rcMonitor.left;
}
if (rcWnd.bottom > mi.rcMonitor.bottom)
{
rcWnd.top -= rcWnd.bottom - mi.rcMonitor.bottom;
rcWnd.bottom = mi.rcMonitor.bottom;
}
if (rcWnd.top < mi.rcMonitor.top)
{
rcWnd.bottom += mi.rcMonitor.top - rcWnd.top;
rcWnd.top = mi.rcMonitor.top;
}
SetWindowPos(
hWnd, NULL, rcWnd.left, rcWnd.top,
rcWnd.right - rcWnd.left, rcWnd.bottom - rcWnd.top,
SWP_NOZORDER);
UINT isZoomed = GetPrivateProfileInt(
_T("window"), _T("zoomed"), 0, iniFilePath);
if (isZoomed != 0)
{
ShowWindow(hWnd, SW_MAXIMIZE);
}
}
|
対象となるモニタの範囲内に収まるように位置を補正しています。
マルチモニタ環境の方は実際に動作確認をしてみると良いでしょう。
このコードでは、ウィンドウが複数のモニタにまたがっていた場合、より広く交差しているモニタの方へ移動されます。
複数のモニタをまたぐことを許可する場合は処理を考え直す必要があります。
複数のモニタをまたぐ処理を行う際に注意すべきことは、複数画面全体での左上端座標を(0, 0)としないことです。
座標(0, 0)はあくまでプライマリモニタの左上端座標です。
例えばプライマリモニタの左側にセカンダリモニタを置いている環境の場合、全体での左端のX座標はマイナスになります。
このことを考慮しているプログラムはフリーソフトレベルでは滅多に無いですが、完璧を目指すならば是非考慮してみて下さい。