ウインドウを最小化した状態としてタスクバーから右クリックして閉じるってやると、次回ウインドウを起動したときに画面外に出てしまって、操作不能になるアプリがある。そんなときは、タスクバーの上にカーソルを置いて、その上に出てくるプレビューウインドウの上で右クリックして出たメニューから「移動」として、上下矢印キーのうちのどれかを押して、その後マウスを動かせばマウスにウインドウがくっついてきて画面内でクリックすると正常化することは知っていた。
そんな利用者側の小技の話ではなく、そもそもプログラムがバグってるんじゃねぇのという厳しい言葉にこたえるべく、あれこれと解説してみようじゃないか。
以下は、ウインドウの位置と最大化の状態をレジストリに保存する処理。この部分には何ら問題はなくて、問題はこのRECT情報をどうやって取得&設定しているのかということである。
static void _WritePosition(CRect* pRect, bool maximized)
{
CPosKeepApp* pApp = (CPosKeepApp*)AfxGetApp();
pApp->WriteProfileInt(_T("DlgPos"), _T("cx"), pRect->left);
pApp->WriteProfileInt(_T("DlgPos"), _T("cy"), pRect->top);
pApp->WriteProfileInt(_T("DlgPos"), _T("Width"), pRect->Width());
pApp->WriteProfileInt(_T("DlgPos"), _T("height"), pRect->Height());
pApp->WriteProfileInt(_T("DlgPos"), _T("maximized"), maximized);
}
static void _ReadPosition(CRect* pRect, bool& maximized)
{
CPosKeepApp* pApp = (CPosKeepApp*)AfxGetApp();
pRect->left = pApp->GetProfileInt(_T("DlgPos"), _T("cx"), 0);
pRect->top = pApp->GetProfileInt(_T("DlgPos"), _T("cy"), 0);
pRect->right = pApp->GetProfileInt(_T("DlgPos"), _T("Width"), 0) + pRect->left;
pRect->bottom = pApp->GetProfileInt(_T("DlgPos"), _T("height"), 0) + pRect->top;
maximized = pApp->GetProfileInt(_T("DlgPos"), _T("maximized"), 0);
}
OnDestroyをオーバーライドして以下のようにGetWindowRectして、アプリ終了時のウインドウのレクトを取得しているならヤバい。最小化されている場合は、ウインドウ座標にマイナス値が入っているので復元時に画面外に飛んでしまう。
void CPosKeepDlg::OnDestroy()
{
CRect rect;
GetWindowRect(&rect);
_WritePosition(&rect, IsZoomed() );
CDialogEx::OnDestroy();
}
ちなみにアプリ起動時の読み込み処理はこんな感じ。
BOOL CPosKeepDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
...
CRect rect;
bool maximized = 0;
_ReadPosition(&rect, maximized);
MoveWindow(rect.left, rect.top, rect.Width(), rect.Height());
if(maximized)
ShowWindow(SW_MAXIMIZE);
return TRUE;
}
【初級編】
手っ取り早く対処するなら以下のようにOnCloseをオーバーライドして、2行足せばよい。最小化されている状態を判定した上で、ウインドウを元に戻すわけで、その際画面上の無駄な動きをなくすためにSW_HIDEモードにもしている。最小化問題はこれでクリア。
void CPosKeepDlg::OnClose()
{
if (IsIconic()) // 最小化されてたら、元に戻す(非表示にするのは画面上の無駄な動きをなくすため)
ShowWindow(SW_HIDE|SW_RESTORE);
CDialogEx::OnClose();
}
【中級編】
最大化して閉じた場合の問題もあって、最大化から元に戻した時のウインドウサイズが最大化状態に近い(8ドットずれあり)が、本来なら最大化した前の状態に戻ってほしいのですよ。ということで、アプリ終了時の処理を以下のように変更。GetWindowRectがGetWindowPlacementに変わっている。その時点のレクト情報を保存するんじゃなくて、通常状態(最小化でも最大化でもない)のレクトを保存しているのが肝要。なお、初級編でやったことは不要になる。
void CPosKeepDlg::OnDestroy()
{
// ウィンドウ位置・状態取得
WINDOWPLACEMENT wndPlace;
wndPlace.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(&wndPlace);
// ウィンドウ位置と最大化状態の保存
CRect rect(wndPlace.rcNormalPosition);
_WritePosition(&rect, wndPlace.showCmd==SW_SHOWMAXIMIZED);
CDialogEx::OnDestroy();
}
【上級編】
最小化とか最大化の問題はこの上までの対応で済む中、おまけとしてアプリ起動時の処理も改良。急にややこしくなっているのは、モニタの解像度変更でウインドウが外側にでちゃったときにウインドウ内におさめる処理であり、またマルチモニターにも対応している。ここまでやる必要あんのかってことだけど、プロのプログラマーならやんなきゃいけないのかね。。
BOOL CPosKeepDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
...
CRect rect;
bool maximized = 0;
_ReadPosition(&rect, maximized);
// 解像度変更によって、ウインドウが画面の外にでてしまった場合の調整)
// 対象モニタの情報を取得(マルチモニタ対応、どのモニタに表示されているかを判定した上で情報取得)
HMONITOR hMonitor = MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST);
MONITORINFO mi;
mi.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(hMonitor, &mi);
// 位置補正(画面から外れていた場合に画面内に収める)
if (rect.right > mi.rcMonitor.right)
{
rect.left -= rect.right - mi.rcMonitor.right;
rect.right = mi.rcMonitor.right;
}
if (rect.left < mi.rcMonitor.left)
{
rect.right += mi.rcMonitor.left - rect.left;
rect.left = mi.rcMonitor.left;
}
if (rect.bottom > mi.rcMonitor.bottom)
{
rect.top -= rect.bottom - mi.rcMonitor.bottom;
rect.bottom = mi.rcMonitor.bottom;
}
if (rect.top < mi.rcMonitor.top)
{
rect.bottom += mi.rcMonitor.top - rect.top;
rect.top = mi.rcMonitor.top;
}
// ウインドウ位置と最大化状態の復元
MoveWindow(rect.left, rect.top, rect.Width(), rect.Height());
if(maximized)
ShowWindow(SW_MAXIMIZE);
return TRUE; // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}