パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.7.5.3(3)
本サイトは移転しました。旧アドレスからのリダイレクトは2025年03月31日(月)まで有効です。
🛈
wxWidgetsとシステムシャットダウン

ウィンドウズシャットダウン時のwxWidgetsアプリケーション挙動について考察し、コーディング指針を定める。

シャットダウンはシステム停止してパソコンの電源を落とし、サインアウトはユーザーの使用を終了してサインイン画面に戻る。シャットダウン時あるいはサインアウト時にアプリケーションが起動しているとウィンドウズは共通の方法で終了を指示する。本記事は最初にウィンドウズのシャットダウン/サインアウトにおける挙動を説明し、続いてwxWidgetsライブラリを利用して作成したウィンドウズデスクトップアプリケーション(以下、wxWidgetsアプリケーション)の挙動を説明し、最後にwxWidgetsアプリケーションにおけるコーディングの指針を説明する。

本記事は"シャットダウン"をサインアウトを含めて参照し、サインアウトの場合は適切に読み替えるものとする。サインアウトを除く本来のシャットダウンは"狭義のシャットダウン"として参照する。

ウィンドウズのシャットダウン挙動

概要

ウィンドウズのシャットダウン時に起動しているアプリケーションにはWM_QUERYENDSESSIONとWM_ENDSESSIONが送信される。

アプリケーションがWM_QUERYENDSESSIONにFALSEを返せばシャットダウンをキャンセルできるが、ウィンドウズはユーザーに再確認を求める。フルスクリーン画面にシャットダウンUIを表示してユーザーは[強制的にシャットダウン]あるいは[キャンセル]を選択し、前者ならアプリケーションを"強制終了"してシャットダウンし、後者ならキャンセルしてデスクトップ画面に戻る。WM_QUERYENDSESSIONにTRUEを返せばウィンドウズはWM_ENDSESSIONを送信して、アプリケーションはこれを処理して"正常終了"する。

本記事で"正常終了"とはアプリケーションがWM_ENDSESSIONの処理を完了して(WM_ENDSESSIONハンドラを抜けてメッセージループへ戻ろうとして)終了する事、"強制終了"とは処理を完了せずに終了する事と定義する。"正常終了"はアプリケーションの終了手順によらずウィンドウズが"強制"するものでもあるが、本記事の文脈ではあくまでもWM_ENDSESSIONハンドラから抜ける事である。"強制終了"はWM_ENDSESSIONハンドラから抜ける前にアプリケーションを終了する事で、WM_QUERYENDSESSION/WM_ENDSESSIONメッセージ処理中にアプリケーションが手順に沿って"正常"に終了する場合を含む。アプリケーションが"正常終了"/"強制終了"しても他のアプリケーションがシャットダウンをキャンセルするかもしれない。

ウィンドウズの推奨動作はWM_QUERYENDSESSIONの処理にTRUEで即応して、時間のかかる処理はWM_ENDSESSIONの処理に委ねる。本サイトはWM_QUERYENDSESSIONの処理に5秒以内でTRUEを返し、WM_ENDSESSIONの処理を5秒以内で行う事を標準動作と定義する。

アクティビティ図による検討

テストプログラムで確認した詳細をUMLアクティビティ図に示す。動作はシャットダウンUIへのユーザー入力の他、WM_QUERYENDSESSION/WM_ENDSESSIONハンドラの処理時間などにも依存する。狭義のシャットダウンならPC電源オフとキャンセル後に表示されるかもしれないロック画面は明確に区別できるが、サインアウトはサインイン画面とロック画面が共通で見かけによる区別が難しい。

各動作におけるアプリケーション終了位置をまとめる。WM_ENDSESSIONを処理するアプリケーションは必ず終了する。(*)はハンドラ終了と競合して処理後の結果となる可能性がわずかにあると思う(未確認)。(**)はWM_QUERYENDSESSION受信時に実行中の別メッセージ処理が残る場合である。WM_QUERYENDSESSIONとWM_ENDSESSIONの処理は連続し、つまりWM_ENDSESSION側に別メッセージの処理が残る事はほとんど無い(WM_QUERYENDSESSIONハンドラがメッセージポストすれば競合となる可能性は残る)。(***)でWM_QUERYENDSESSIONの処理はTRUEを返すかもしれないが、それでも強制終了となってWM_ENDSESSIONは送信されない。

条件 UI表示 UI解除理由 ユーザー入力 動作 アプリケーション終了位置
標準動作 - - - 正常終了 WM_ENDSESSION処理後
WM_ENDSESSION
処理が5秒以上
1分以内 ユーザー入力 強制的にシャットダウン 強制終了 WM_ENDSESSION処理中(*)
キャンセル デスクトップ復帰 WM_ENDSESSION処理後
ハンドラ終了 - 正常終了 WM_ENDSESSION処理後
1分以上 - - ロック画面表示 WM_ENDSESSION処理後
WM_QUERYENDSESSION
にFALSEを返した
1分以内 ユーザー入力 強制的にシャットダウン 強制終了 WM_QUERYENDSESSION処理後
キャンセル デスクトップ復帰 -
1分以上 - - ロック画面表示 -
WM_QUERYENDSESSION
処理が5秒以上
1分以内 ユーザー入力 強制的にシャットダウン 強制終了 WM_QUERYENDSESSION処理中(*)/処理前(**)
キャンセル デスクトップ復帰 -
ハンドラ終了 - 強制終了 WM_QUERYENDSESSION処理後(***)
1分以上 - - ロック画面表示 -

"強制終了"のアプリケーション終了位置は確定できない。(**)を排除できるのであれば、シャットダウン時に必要な処理を最も確実に行えるのはWM_QUERYENDSESSION処理の冒頭となる。

wxWidgetsのデフォルト挙動

wxWidgetsアプリケーションのシャットダウン時のデフォルト挙動を説明する。ウィンドウズアプリケーションとしてWM_QUERYENDSESSION/WM_ENDSESSIONを処理するのは同じだが、wxWidgetsライブラリにおける解釈を明確にする。wxWidgetsアプリケーションの通常の(シャットダウン時でない)終了動作を説明してから、WM_QUERYENDSESSION/WM_ENDSESSIONのデフォルト処理方法を説明して挙動としてまとめる。

各実装を説明目的で大きく簡略化するが実際は非常に複雑で、例えばアプリケーション実装クラスはwxAppクラスを継承するが、wxAppはwxApp→wxAppBase→wxAppConsole→wxAppConsoleBaseと継承してメンバ定義は各基底に分散する。説明は必要最低限をwxAppクラス内定義に一括して、そのため宣言順は矛盾するかもしれない。wxApp以外も同様に簡略化する。検討はwxWidgetsバージョン3.2.2.1のソースコードに依った。

通常の終了動作

アプリケーションはwxAppの継承として実装し、そのインスタンスをグローバルなポインタ変数のwxTheAppに保持する。エントリ関数をwxEntryとして簡略を示す。wxTheAppを構築し、OnInit、OnRun、OnExitメンバ関数を順次コールして、終了すればwxTheAppを解体する。wxTheAppの構築/解体はwxEntryStart/wxEntryCleanup自由関数が行う。

wxApp* wxTheApp;
int wxEntry(int& argc, wxChar **argv)
{
int exitCode=-1;
wxEntryStart(argc,argv); // wxWidgetsライブラリの初期化とwxTheAppの構築
if (wxTheApp->OnInit()) // メインTLW構築
{
exitCode=wxTheApp->OnRun(); // メッセージループ実行
wxTheApp->OnExit(); // 終了処理
}
wxEntryCleanup(); // wxTheAppの解体とwxWidgetsライブラリの終了処理
return exitCode;
};

メッセージループ実装クラスをwxEventLoopとして、wxAppとwxEventLoopの簡略を示す。グローバル変数としてwxTopLevelWindowsとwxPendingDeleteを利用する。wxList<T>はヘルプドキュメントのみに現れる仮想のクラステンプレートで、要素を(Tでなく)T*で保持するダブルリンクリストとしてstd::list<T*>に互換するインターフェースを併せ持つ。説明はこの仮想クラステンプレートを用いるが、実際はマクロ定義から生成されるクラスである。wxTopLevelWindowsはトップレベルウィンドウ(以下、TLW)のリストで、wxPendingDeleteはアイドル処理まで解体遅延されたwxObjectである。本記事の範囲で解体遅延されたwxObjectとはwxTopLevelWindow::Destroyメンバ関数をコールしたTLWであるが、その他のwxObjectがそれぞれの理由で解体遅延されるかもしれない。アプリケーションのほとんどで親を持たないTLWはメインウィンドウ一つ(以下、メインTLW)だが、それを複数持つ場合もある。

wxTheAppのwxApp::OnRunメンバ関数はメッセージループを構築してwxApp::m_eventLoopに保持する。メッセージループはウィンドウ標準のWM_QUITポスト(ウィンドウズAPIの::PostQuitMessage関数)でも停止するが、標準手順はwxEventLoop::Exitメンバ関数からのwxEventLoop::m_heventWakeカーネルイベントオブジェクト(以下、m_heventWake)のシグナルによる。m_heventWakeはウェイクアップ一般に用いられ、区別するためwxEventLoop::m_shouldExitフラグを併用する。WM_QUITを用いない理由は他に起動されている可能性のあるメッセージループとの干渉を避けるためとされる。wxApp::ExitMainLoopメンバ関数はwxEventLoop::Exitメンバ関数でwxApp::m_eventLoopを停止し、wxApp::OnRunを抜けてアプリケーションは終了する。

wxTheApp::GetTopWindowメンバ関数から解体遅延されるTLWを除く処理はバージョン3.1.1で追加された(※1)。解体遅延されるTLWを親として生成したダイアログが、親と無関係なTLWとしてアイドル処理で解体されてしまうのを防ぐためとされる。

wxList<wxWindow> wxTopLevelWindows; // TLWリスト、std::list<wxWindow*>同等
wxList<wxObject> wxPendingDelete; // 解体遅延されたwxObjectリスト、std::list<wxObject*>同等
class wxApp:public wxEventHandler
{
private:
wxEventLoop* m_eventLoop; // アプリケーションメッセージループ
public:
virtual ~wxApp() {}
virtual bool OnInit() {...}
virtual int OnRun()
{
m_eventLoop=new wxEventLoop();
int exitcode=m_eventLoop?m_mainLoop->Run():-1;
delete m_eventLoop;
return exitcode;
}
virtual int OnExit()
{
// TLWは全て解体されているが他の解体遅延されたwxObjectが残る
for (wxList<wxObject>::iterator i=wxPendingDelete.begin();i!=wxPendingDelete.end();)
{
wxPendingDelete.erase(i);
delete *i;
i=wxPendingDelete.begin();
}
}
void ExitMainLoop()
{
m_mainLoop->Exit(0);
}
bool ProcessIdle()
{
wxIdleEvent event;
event.SetEventObject(this);
ProcessEvent(event); // 基底wxEventHandler::ProcessEvent、イベント処理
// wxTopLevelWindow::Destroyで解体遅延されたTLWを他の解体遅延されたwxObjectと共に解体
// wxTopLevelWindowデストラクタは自動的に解体遅延されたwxObjectリストから自らを除くが、
// 解体カスケード対応で解体前にリストから除いて、次の解体対象は必ずリスト先頭から取得
for (wxList<wxObject>::iterator i=wxPendingDelete.begin();i!=wxPendingDelete.end();)
{
wxPendingDelete.erase(i);
delete *i;
i=wxPendingDelete.begin();
}
bool needMore=event.MoreRequested();
// 解体遅延されたTLW(先行forループで解体済みでは?)を除く全てのTLWへwxEVT_IDLEイベントを送信
for (wxList<wxWindow>::iterator i=wxTopLevelWindows.begin();i!=wxTopLevelWindows.end();++i)
{
if (std::find(wxPendingDelete.begin(),wxPendingDelete.end(),*i)==wxPendingDelete.end())
{
needMore=((*i)->SendIdleEvents(event))||needMore;
}
}
return needMore;
}
void WakeUpIdle()
{
m_mainloop->WakeUp();
}
// TLWリストから解体遅延されない最初のTLWを返す
// TLWがメインTLWだけならwxTopLevelWindow::Destroyコール以降はNULLを返す
wxWindow* GetTopWindow() const
{
wxWindow* win=NULL;
for (wxList<wxWindow>::iterator i=wxTopLevelWindows.begin();i!=wxTopLevelWindows.end();++i)
{
if (std::find(wxPendingDelete.begin(),wxPendingDelete.end(),*i)==wxPendingDelete.end())
{ // 解体遅延されるTLWを除く、3.1.1で追加(※1)
win=*i;
break;
}
}
return win;
}
};
class wxEventLoop
{
private:
bool m_shouldExit; // ウェイクアップ時メッセージループ終了フラグ
int m_exitcode; // メッセージループ終了コード
HANDLE m_heventWake; // ウェイクアップでシグナルするカーネルイベントオブジェクト
public:
wxEventLoop()
{
m_shouldExit=false;
m_exitcode=0;
m_heventWake=::CreateEvent(NULL,FALSE,FALSE,NULL);
}
~wxEventLoop()
{
::CloseHandle(m_heventWake);
}
bool PreProcessMessage(MSG* msg) {...}
int Run()
{
MSG msg={0};
for (bool exitFlag=false;!exitFlag;)
{
// アイドル処理
wxTheApp->ProcessIdle();
// ::PeekMessageコールはポストメッセージをmsgに得ればTRUE、得なければFALSEを返す
// 他スレッドからの送信メッセージはポストに優先して関数内でディスパッチするが戻り値には影響しない
while (!::PeekMessage(&msg,0,0,0,PM_REMOVE)) // 他スレッドからの送信メッセージはここでディスパッチ
{
// ::MsgWaitForMultipleObjectsコールはm_heventWakeシグナル、新しいポストメッセージ、
// 他スレッドから送信メッセージ、のいずれかまでスリープ
switch (::MsgWaitForMultipleObjects
(1,&m_heventWake,FALSE,INFINITY,QS_ALLINPUT|QS_ALLPOSTMESSAGE))
{
case WAIT_OBJECT_0: // m_heventWakeシグナル
exitFlag=m_shouldExit;
goto label1; // 次のforループへ、アイドル処理を通過する
case WAIT_OBJECT_0+1: // 新しいポストメッセージ/他スレッドからの送信メッセージ
break; // 次のwhileループへ、アイドル処理を通過しない
}
}
// ::PeekMessageコールがTRUE、ポストメッセージの処理
exitFlag=(msg->message==WM_QUIT)
if (!exitFlag)
{
if (!PreProcessMessage(&msg)) // モードレスダイアログ処理、アクセラレータキー処理など
{
::TranslateMessage(&msg);
::DispatchMessage(&msg); // ポストメッセージはここでディスパッチ
}
}
label1:
}
return m_exitcode;
}
// m_heventWakeシグナルによるウェイクアップ、本記事の範囲では
// Runコールからのディスパッチ(同スレッド)がコールするためフラグ立てと表現したほうが適当かも
void WakeUp()
{
::SetEvent(m_heventWake);
}
void Exit(int rc);
{
m_exitcode=rc;
m_shouldExit=true;
WakeUp();
}
};

TLWを実装するwxTopLevelWindowクラスはwxFrame(フレーム)とwxDialog(ダイアログ)の基底でデスクトップに独立表示される。コンストラクタの処理を実装するCreateメンバ関数でwxTopLevelWindowsリストに自らを登録し、デストラクタで登録を解除する。wxEVT_CLOSE_WINDOW(あるいはEVT_CLOSE)イベントのデフォルトハンドラOnCloseWindowメンバ関数はDestroyメンバ関数をコールし、DestroyはwxPendingDeleteリストに登録して解体遅延する。最後のTLW(多くの場合メインTLW)が解体されるとデストラクタはwxApp::ExitMainLoopメンバ関数をコールしてアプリケーションを終了する。

TLWでないウィンドウのDestroyメンバ関数はwxPendingDeleteリストに登録せず即座に解体する。TLWは次のアイドル処理まで解体遅延するが、ある特殊な場合でメッセージループがアイドル処理を通過しないとして、バージョン3.1.4でウェイクアップが追加された(※2)。本記事の範囲ではWM_QUERYENDSESSION/WM_ENDSESSIONの処理がこれに該当する。

wxBEGIN_EVENT_TABLE(wxTopLevelWindowBase, wxWindow)
EVT_CLOSE(wxTopLevelWindowBase::OnCloseWindow)
wxEND_EVENT_TABLE()
class wxTopLevelWindow:public wxWindow
{
public:
wxTopLevelWindow(...):wxWindow(...) {...}
bool Create(...)
{
...
wxTopLevelWindows.push_back(this);
...
}
~wxTopLevelWindow()
{
wxTopLevelWindows.erase(this);
// 子TLWがwxPendingDeleteに残ると解体遅延で親ウィンドウポインタ(this)が不正となるため
// ここで解体する
for (wxList<wxObject>::iterator i=wxPendingDelete.begin();i!=wxPendingDelete.end();)
{
wxWindow* const win=dynamic_cast<wxWindow*>(*i);
if (win&&wxGetTopLevelParent(win->GetParent())==this)
{
wxPendingDelete.erase(i);
delete *i;
i=wxPendingDelete.begin();
}
else
{
++i;
}
}
// 最後のTLWならメッセージループを停止してアプリケーションを終了する
if (wxTopLevelWindows.begin()==wxTopLevelWindows.end())
{
wxTheApp->ExitMainLoop();
}
// 解体遅延されたwxObjectリストから削除、基底クラスで実装
wxPendingDelete.erase(this);
}
// wxPendingDeleteに登録して次のアイドル処理で解体する
// 他スレッドからの送信メッセージ(WM_QUERYENDMESSAGE/WM_ENDMESSAGEを含む)のディスパッチでメッセージループは
// アイドル処理を行わないが、wxTheApp::WakeUpIdleメンバ関数でそれを強制できる(コメントは別の目的を語るが)
// ただしWM_ENDMESSAGEの場合はアイドル処理までたどり着くより早くアプリケーションは終了する
bool Destroy()
{
if (std::find(wxPendingDelete.begin(),wxPendingDelete.end(),this)==wxPendingDeleate.end())
{
wxPendingDelete.push_back(this);
}
wxTheApp->WakeUpIdle(); // メッセージループをウェイクアップ、3.1.4で追加(※2)
return true;
}
// wxEVT_CLOSE_WINDOWイベントを生成して基底wxWindow::GetEventHandlerコールで得るハンドラ(自身とは限らない)に委ねる
// デフォルトはDestroyメンバ関数をコールする
bool Close(bool force)
{
wxCloseEvent event(wxEVT_CLOSE_WINDOW,m_windowId);
event.SetEventObject(this);
event.SetCanVeto(!force);
return GetEventHandler()->ProcessEvent(event)&&!event.GetVeto();
}
// wxEVT_CLOSE_WINDOWのデフォルトハンドラ
void OnCloseWindow(wxCloseEvent& event)
{
Destroy();
}
// wxEVT_IDLEイベント処理を基底wxWindow::GetEventHandlerコールで得るハンドラ(自身とは限らない)に委ねる
// 全ての子ウィンドウへも転送するが、実装はwxWindowなのでTLWでない子ウィンドウも同じ処理を繰り返す
bool SendIdleEvents(wxIdleEvent& event)
{
event.SetEventObject(this);
GetEventHandler()->ProcessEvent(event);
bool needMore=event.MoreRequested();
// 全ての子ウィンドウへwxEVT_IDLEイベントを転送
for (wxList<wxWindow>::iterator i=GetChildren().begin();i!=GetChildren().end();++i)
{
needMore=((*i)->SendIdleEvents(event))||needMore;
}
return needMore;
}
};

まとめればアプリケーションは以下のように終了する。

  1. 最後のTLW(多くの場合はメインTLW)を閉じるとそのインスタンスは解体遅延される
  2. 次のアイドル処理で解体遅延されたTLWを解体する
  3. TLWのデストラクタはwxTheAppメッセージループを停止する
  4. メッセージループから抜けてアプリケーションは終了する

シャットダウンメッセージの処理方法

wxTopLevelWindowにWM_QUERYENDSESSION/WM_ENDSESSION処理を追記する。wxTopLevelWindow::MSWHandleMessageメンバ関数はウィンドウプロシージャからコールされてこれらのメッセージを処理する。実際はwxWindow基底クラスで実装されて他の多数のメッセージと共に処理されるが、WM_QUERYENDSESSION/WM_ENDSESSIONに限ればトップレベルウィンドウへ送信されるのでこの解釈でも問題ない。実際はソースコード(wxWidgetsのメッセージハンドラ実装)を参照のこと。MSWHandleMessageはWM_QUERYENDSESSION/WM_ENDSESSIONをwxEVT_QUERY_END_MESSAGE/wxEVT_END_MESSAGEイベントとしてwxTheAppにイベント処理を委ねる。wxEVT_QUERY_END_MESSAGEで渡されるwxCloseEventをwxCloseEvent::Vetoコールする(以下、Vetoする)とWM_QUERYENDSESSIONのハンドラはFALSEを返してシャットダウンをキャンセルできる。

class wxTopLevelWindow:public wxWindow
{
...
public:
// ウィンドウプロシージャがコール
bool MSWHandleMessage(WXLRESULT *result,WXUINT message,WXWPARAM wParam,WXLPARAM lParam)
{
bool processed = false;
union
{
bool allow;
WXLRESULT result;
WXHBRUSH hBrush;
} rc;
rc.result = 0;
switch (message)
{
...
case WM_QUERYENDSESSION:
processed=HandleQueryEndSession(lParam,&rc.allow);
break;
case WM_ENDSESSION:
processed=HandleEndSession(wParam!=0,lParam);
break;
...
}
if (!processed) {return false;}
*result=rc.result;
return true;
}
// WM_QUERYENDSESSIONをwxEVT_QUERY_END_SESSIONイベントとしてwxTheAppに処理を委ねる
// TLWが複数あると複数回のイベント処理がコールされてしまうのでは?
bool HandleQueryEndSession(long logOff,bool *mayEnd)
{
wxCloseEvent event(wxEVT_QUERY_END_SESSION,wxID_ANY);
event.SetEventObject(wxTheApp);
event.SetCanVeto(true);
event.SetLoggingOff(logOff==(long)ENDSESSION_LOGOFF);
bool rc=wxTheApp->ProcessEvent(event);
if (rc)
{
*mayEnd=!event.GetVeto();
}
return rc;
}
// WM_ENDSESSIONをwxEVT_END_SESSIONイベントとしてwxTheAppに処理を委ねる
// TLWがメインTLWだけでDestroyコールしたが解体遅延されていない場合、
// wxTheApp::GetTopWindowメンバ関数はNULLでwxEVT_END_SESSIONイベントが発生せず、
// メインTLWを残したままWM_ENDSESSIONハンドラを抜けてシステムはアプレケーションを強制終了する
bool HandleEndSession(bool endSession,long logOff)
{
if (!endSession) {return false;}
if (this!=wxTheApp->GetTopWindow()) {return false;} // イベント処理は1回だけ
wxCloseEvent event(wxEVT_END_SESSION,wxID_ANY);
event.SetEventObject(wxTheApp);
event.SetCanVeto(false);
event.SetLoggingOff((logOff&ENDSESSION_LOGOFF)!=0);
return wxTheApp->ProcessEvent(event);
}
};

wxAppにwxEVT_QUERY_END_SESSION/wxEVT_END_SESSIONデフォルトハンドラを追記する。

wxApp::OnQueryEndSessionメンバ関数はwxEVT_QUERY_END_SESSIONのデフォルトハンドラで、wxApp::GetTopWindowコールが返すTLW(通常はメインTLW)のwxTopLevelWindow::Closeメンバ関数を実引数!event.CanVeto()でコールしてfalseが戻る(TLWがwxEVT_CLOSE_WINDOWイベントでVetoする)とVetoする。event.CanVeto()はwxTopLevelWindow::HandleQueryEndSessionコールでtrueとしてwxEVT_CLOSE_EVENTハンドラに渡されるwxCloseEventインスタンスのCanVeto()もtrueを返し、イベントがセッション終了なのか通常のウィンドウを閉じる操作なのか区別できない。

wxApp::OnEndSessionメンバ関数はwxEVT_END_SESSIONのデフォルトハンドラで、TLWを全解体してからwxApp::OnExitコールで解体遅延を実行して、wxEntryCleanup自由関数でwxTheAppを解体して、C言語標準ライブラリexit関数でアプリケーションを終了する。メッセージディスパッチ中に全てのウィンドウハンドル(HWND)を破壊(ウィンドウズAPIの::DestroyWindow関数)するとアプリケーションはそこで終了するため(これを確認するドキュメントは見つからない)、事前にTLW一つをwxWindow::SetHWND(0)してHWNDとの関連付けを破りリーク覚悟でHWNDを残している。exitで終了するのは静的ストレージオブジェクトの解体が保証されるためで、このためメッセージループには戻らない。なおメッセージループに戻ろうとしてもWM_ENDSESSIONの処理完了したとしてシステムは勝手にアプリケーションを終了するし、仮に戻れたとしてもHWNDの関連付けが破られていて正常な処理は期待できない。

TLW全解体はOnExitコール時にTLWが残ると通常の終了動作と異なるため、バージョン3.1.1で加えられた(※3)。

wxBEGIN_EVENT_TABLE(wxApp, wxEvtHandler)
EVT_QUERY_END_SESSION(wxApp::OnQueryEndSession)
EVT_END_SESSION(wxApp::OnEndSession)
wxEND_EVENT_TABLE()
class wxApp:public wxEventHandler
{
...
public:
// wxEVT_QUERY_END_SESSIONのデフォルトハンドラ
void OnQueryEndSession(wxCloseEvent& event)
{
if (GetTopWindow())
{
if (!GetTopWindow()->Close(!event.CanVeto()))
{
event.Veto(true);
}
}
}
// wxEVT_END_SESSIONのデフォルトハンドラ
void OnEndSession(wxCloseEvent&WXUNUSED(event))
{
if (!wxTopLevelWindows.empty())
{
wxTopLevelWindows[0]->SetHWND(0); // TLWの一つ(多くの場合GetTopWindow()と同じ)のHWNDを残す
} // このTLWのデストラクタでGetHWND()は0を返す事になる
while (!wxTopLevelWindows.empty()) // 全てのTLWを解体、3.1.1で追加(※3)
{
delete wxTopLevelWindows[0];
}
const int rc=OnExit(); // TLW以外の解体遅延するwxObjectを解体
wxEntryCleanup(); // wxTheAppの解体とwxWidgetsライブラリの終了処理
exit(rc); // 静的ストレージオブジェクト解体してアプリケーション終了
} // メッセージループに戻らない
};

まとめればwxAppはシャットダウンメッセージを以下にデフォルト処理する。

  1. WM_QUERYENDSESSIONはメインTLWを閉じて、拒否されればFALSEを返す
  2. WM_ENDSESSIONはクリーンアップ後にアプリケーションを終了してメッセージループに戻らない

wxWidgetsのコーディング指針

wxWidgetsアプリケーションのシャットダウン時の挙動をカスタマイズするには、二つの観点から注意を必要とする。

  • デフォルト挙動のバージョン依存性に留意する
  • ウィンドウズが期待する挙動に合わせる

デフォルト挙動はバージョン3.1.1(※1)(※3)および3.1.4(※2)で変更されていて、比較としてバージョン3.0.5、3.1.2、3.2.2.1で確認した。以降、それぞれを3.1.0以前、3.1.1~3.1.3、3.1.4以降として参照するが、その他の変更を見落としているかもしれない。最初にデフォルト挙動のバージョン依存性を確認し、次にバージョン依存性を解決できるWM_ENDSESSIONメッセージ利用の欠点を指摘する。最後に、バージョン依存性を解決しながらウィンドウズが期待に沿った挙動となるコーディングの指針を説明する。

デフォルト挙動のバージョン依存性

何のカスタマイズも加えない挙動を説明する。TLWのwxEVT_CLOSE_WINDOW(あるいはEVT_CLOSE)ハンドラもデフォルトのままとする。説明を単純化するためTLWはメインTLWのみと仮定する。図中でwxTopLevelWindowはwxTLWと略した。本記事の文脈で"正常終了"とはアプリケーションがWM_ENDSESSIONを処理完了して終了する事、"強制終了"とは処理完了せずに終了する事であることを確認しておこう。

wxWidgetsバージョンに依存してメインTLWデストラクタコールの有無が異なる。3.1.0以前はコールされ、3.1.1~3.1.3は(※1)の追加でコールされない。やっかいなのは3.1.4以降で、競合条件が発生してコール有無が一定とならない。本記事の範囲ではシステムが送信するWM_QUERYENDSESSION/WM_ENDSESSIONメッセージとアプリケーションが自らを終了するm_heventWakeがメッセージループを駆動するが、(※2)の追加でWM_ENDSESSIONとm_heventWakeがループ内の::MsgWaitForMultipleObjectsウィンドウズAPI関数で競合条件となる場合がある。WM_ENDSESSIONを先に処理すれば3.1.1~3.1.3同様にコールせず、m_heventWakeを先に処理すれば通常のアプリケーション終了処理となってコールする。後者はwxEventLoop::Exitメンバ関数がm_heventWakeを再シグナルしてもう一度競合条件が発生するが結果に影響しない。

メインTLWのwxEVT_CLOSE_WINDOWハンドラをカスタマイズしてVetoする、あるいはwxTheAppのwxEVT_QUERY_END_SESSIONハンドラをカスタマイズしてVetoすればシャットダウンはキャンセルできる。その場合システムはシャットダウンUIを表示して、ユーザーが[キャンセル]を選択すればデスクトップに復帰してアプリケーションを継続実行する。サンプルコードのように状況依存でメインTLWクローズをダイアログでユーザー確認するアプリケーションは多いが、そういったアプリケーションはシャットダウン時にもダイアログを表示してTLWクローズをキャンセルすればシャットダウンもキャンセルする。ドキュメントはこれら全てを推奨しないが、特にダイアログはシャットダウンUIに重複してユーザー入力を求めることになる。wxEVT_CLOSE_WINDOWハンドラがイベントのCanVeto()を確認しているのなら、wxEVT_QUERY_END_SESSIONハンドラでイベントをSetCanVeto(false)すればダイアログ表示を抑止することはできる。

wxEVT_QUERY_END_SESSIONハンドラをカスタマイズしてDestroyメンバ関数でメインTLWを解体すればwxEVT_CLOSE_WINDOWハンドラを省略できるが、wxWidgetsバージョン依存でデストラクタコールの有無が異なるのは変わらない。delete演算子でメインTLWを即解体してしまえばデストラクタコールは常に保証されるが、残余イベントが解体後のウィンドウに送信される可能性があり避けるべきとされる。

WM_ENDSESSIONでの挙動

WM_ENDSESSIONハンドラでメインTLWをクローズすればバージョン依存性は解決できるが大きな問題を抱える。wxAppを継承するTMyAppクラスで説明する。TMyAppのwxEVT_QUERY_END_SESSIONハンドラ(TMyApp::OnQueryEndSessionメンバ関数)は何もせず、wxEVT_END_SESSIONハンドラ(TMyApp::OnEndSessionメンバ関数)はwxApp::OnQueryEndSession相当の処理(wxTopLevelWindow::Closeコール)を行ってからイベントをwxEvent::SkipコールしてwxAppのハンドラ(wxApp::OnEndSessionメンバ関数)へ渡す。

wxTopLevelWindows::Destroyメンバ関数より先にwxTopLevelWindows::HandleEndSessionメンバ関数をコールするので、(※1)の有無に関係なくwxApp::GetTopWindowメンバ関数はメインTLWを返してTMyApp::OnEndSessionメンバ関数をコールする。(※2)はメッセージループをウェイクアップするが、WM_ENDSESSIONハンドラがメッセージループに戻ることはないので有無は結果に影響しない。(※3)はメインTLWの解体を他の解体遅延されたwxObjectより先に行うことを保証するだけで、その有無もまた挙動を大きく変えない。TLWデストラクタは解体遅延されたwxObjectリストから自らを削除して、リストに残って二重解体される心配も不要だ。このように挙動はwxWidgetsバージョンに依存せず競合条件も発生しない。全てのバージョンでメインTLWデストラクタをコールしてexitコールで強制終了する。なおwx_EVT_END_SESSIONイベントスキップせずwxApp::OnEndSessionメンバ関数をコールしないとすると全てのバージョンでメインTLWは解体遅延されたままWM_ENDSESSIONハンドラを抜けて、正常終了してメインTLWデストラクタはコールされない。

WM_ENDSESSIONでメインTLWをクローズすればwxWidgetsバージョンに依存しない処理ができるわけだが大きな欠点がある。メインTLWがwxEVT_CLOSE_WINDOWハンドラをカスタマイズしてVetoしてもシャットダウンしてしまう。メインTLWが状況依存でクローズをダイアログ確認する場合はさらに悪く、シャットダウンUIを表示して[キャンセル]を選択すればデスクトップに復帰するが、さらにダイアログでクローズをキャンセルしてもアプリケーションは終了してしまう。どちらもWM_ENDSESSIONを処理するアプリケーションは必ず終了するという事を思い出せば当然の結果であるが、特に後者はユーザーから見て直感に反する。つまりWM_ENDSESSIONハンドラはアプリケーション終了をキャンセルできないがシャットダウンはキャンセルできるという、ウィンドウズの不可解な仕様をあぶり出す。

このようにWM_ENDSESSIONでのメインTLWクローズはユーザーの直感に反する挙動となるのでやるべきでない。Destroyメンバ関数ならwxEVT_CLOSE_WINDOWハンドラを省略してそのようなことはないが、それならデフォルトのwxApp::OnEndSessionメンバ関数だけで必要十分だろう。

コーディングの指針

ドキュメントはウィンドウズの期待する挙動を以下とする。

  • アプリケーションはシャットダウンをブロックするべきでない。WM_QUERYENDSESSIONに可能な限り早く応答してクリーンアップはWM_ENDSESSIONに委ねる。
  • それでもシャットダウンをブロックする必要のあるアプリケーションはウィンドウズAPIの::ShutdownBlockReasonCreate関数でその理由をシャットダウンUIに示す。ユーザーはシャットダウンを継続するかキャンセルするかを判断する。
  • アプリケーションはいつでもシャットダウンをブロックできるなどと期待してはいけない。
覚え書き
ドキュメントは::ShutdownBlockReasonCreate関数だけでシャットダウンをブロックできるかの誤解を与えるが、実際はシャットダウンUIへの表示文字列を設定するだけだ。

wxWidgetsアプリケーションのデフォルト挙動はWM_QUERYENDSESSIONでメインTLWクローズしてWM_ENDSESSIONまで解体遅延するが、WM_ENDSESSIONでメインTLWの解体を含むクリーンアップが実行されるかどうかはwxWidgetsバージョンと競合条件発生に依存する。メインTLWがwxEVT_CLOSE_WINDOWハンドラで表示するダイアログはシャットダウン時にも表示されるが、ユーザーはシャットダウンUIで[キャンセル]を選択してから再度ダイアログに応えなければならない。

これに対して本サイトがベストと考えるwxWidtgetsアプリケーションのシャットダウン時処理をまとめる。

WM_QUERYENDSESSIONのメインTLWクローズは応答時間に影響するほどの実行コストは無く問題ないが、WM_ENDSESSIONのバージョン依存/競合条件による挙動差の原因となるため避ける。メインTLWがwxEVT_CLOSE_WINDOWハンドラで表示するダイアログは表示できなくなるが、必要であれば同等の条件判断をwx_QUERY_END_SESSIONハンドラで行い、クローズ/シャットダウンをキャンセルする場合は::ShutdownBlockReasonCreate関数に理由を与えてVetoする。ユーザーがシャットダウンUIで[キャンセル]すればデスクトップに復帰してアプリケーションは実行継続するが、[強制的にシャットダウン]すればtaskkillコマンド同等で終了してクリーンアップは実行されない。つまりバージョン依存/競合条件による挙動差を排除してもクリーンアップが実行されない条件が一つ残る。WM_ENDSESSIONはデフォルト動作wxApp::OnEndSessionメンバ関数で必要十分でカスタマイズせず、つまりメインTLW(および全てのTLW)解体はこのメンバ関数に委ねる。

クリーンアップが実行されない条件で何らかの終了処理を確実に実行するにはwx_QUERY_END_SESSIONハンドラで無条件に事前実行する(強制終了がWM_QUERYENDSESSION処理前(**)となる場合までは面倒見れないが)。もちろん応答時間に影響するほどの実行コストとなる処理は避けなければならない。この終了処理は何らかの状態を変化させてはならない。仮に状態変化を伴うとしてVetoすれば、シャットダウンUIで[強制的にシャットダウン]ならそのまま終了処理後の状態を維持し、[キャンセル]ならアプリケーションを実行継続するため状態変化を戻して終了処理のされていない状態とする。しかしシャットダウンUIはアプリケーションが関与できず、そのように処理を切り替えることが不可能なのだ。こういった理由でライブラリと静的ストレージオブジェクトのクリーンアップは事前実行できず(特に後者はアプリケーション終了による解体なので絶対に事前実行できない)成り行きに任せるしかない。事前実行できるのはあなたがコーディングする終了処理で状態変化を伴わないもので、例えばレジストリデータ保存や編集済みデータ退避などである。そういった終了処理の多くはメインTLWのデストラクタにコーディングされるので、メインTLWがwxTopLevelWindowを継承するTMyTLWクラスであるとして説明する。

wxEVT_QUERY_END_SESSIONでVetoする可能性のないアプリケーションに終了処理の事前実行は不要でwxApp::OnEndSessionに全て委ねても良いが、一つ留意すべき点がある。wxApp::OnEndSessionは最初に一つTLWをwxWindow::SetHWND(0)して次にdelete演算子で全てのTLWを解体するが、SetHWND(0)されたTLW(多くの場合メインTLW)デストラクタでwxWindow::GetHWNDメンバ関数からHWNDを得ることができず、必要なら事前に記憶しておく必要がある。例えば本サイトに掲げるアプリケーション(サンプルコード、自作ツール公開ソフトウェア)のメインTLWはデストラクタの終了処理としてウィンドウのサイズ/位置をレジストリに保存するが、その取得にウィンドウズAPIの::GetWindowPlacement関数を利用してHWNDを必要とする。終了処理を事前実行しておけば、こういったHWNDの記憶は不要になる。