反応できないことを明示的に意思表示する
アプリケーションが処理に反応できない場合、待機カーソル(WaitCursor:XPまでは砂時計、Vista/Windows7では回転する緑のドーナツ状のマウスカーソル)にするという決まりがあります。まずは、先ほどのループにこれを挟み込んで、応答なしが表示される場合でも頑張って作業していることをユーザーに伝えるコードを追加しましょう。
待機カーソルにするには、LoadCursor APIでシステム定義のIDC_WAITカーソルをロードし、それをSetCursorで一時的にセットします。マウスカーソルは、マウスメッセージが処理されるタイミングでリセットされてしまうため、メッセージループが回るともとのカーソルに戻されてしまうので、注意が必要です。
void BusyLoop( DWORD dwTime )
{
HCURSOR hWait = LoadCursor( NULL, IDC_WAIT );
HCURSOR hPrev = SetCursor( hWait );
DWORD dwStart = GetTickCount();
while( (GetTickCount()-dwStart) < dwTime );
SetCursor( hPrev );
}
これで、ループ中はカーソルが待機カーソルになります。このサンプルは例外が発生したり、途中でリターンしてしまうなどはありませんが、プログラムが大きくなると、例外処理やリターン処理が入ることがあります。そのような場合、マウスカーソルを戻すためだけに例外をキャッチするのはあまり効率がよいとは言えません。C++ではこういう場合RAII(Resource Acquisition Is Initialization)と呼ばれる想定外の事態にも安全に対応できる記述をする手法があります。そちらで記述したものが下記のものです。
void BusyLoop( DWORD dwTime )
{
CSimpleWaitCursor waitCursor;
DWORD dwStart = GetTickCount();
while( (GetTickCount()-dwStart) < dwTime );
}
class CSimpleWaitCursor
{
public:
CSimpleWaitCursor()
{
HCURSOR hWait = ::LoadCursor( NULL, IDC_WAIT );
if( hWait != NULL ){
m_hPrev = ::SetCursor( hWait );
}
else{
m_hPrev = NULL;
}
}
~CSimpleWaitCursor()
{
if( m_hPrev != NULL ){
::SetCursor( m_hPrev );
}
m_hPrev = NULL;
}
private:
HCURSOR m_hPrev;
};
単純なカーソル設定ですが、たったこれだけで、どのタイミングでBusyLoopから抜けても元のカーソルに戻ります。実際の処理時間がせいぜい2秒程度で終わるようなものであれば、これでも十分と言えます。
作業中にメッセージを処理する
待機カーソルはマウスカーソルの見た目を変えることで、ユーザーに時間がかかる作業をしているというフィードバックを与えます。しかし、プログラムがしばらくメッセージを処理できないことをシステムに通知するものではありません。そのため、待機カーソルに変えただけでは一定時間ののちに「(応答なし)」状態になってしまうという状況に変化はありません(サンプルプログラムでは実際にマウスカーソルを変えています)。
冒頭でも書いていたように「(応答なし)」が出る条件は一定時間メッセージを処理しなかった場合です。従って、時間のかかる処理中も何らかの形でメッセージを処理するようにすれば、基本的には問題が解決できるはずです。
では、そのメッセージを処理するとはいったいどういうことなのでしょうか。答えはいたって簡単で、処理中にメッセージポンプと呼ばれる一時的なメッセージ処理ループを回すことで解決します。具体的には、次のようなごく単純なメッセージループを用意しておき、必要に応じて呼び出すだけです。
void PumpMessage()
{
MSG msg;
while( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) ){
TranslateMessage( &msg );
DispatchMessage( &msg );
}
}
さっそく最初のビジーループでも、応答なしが出ないように1秒ごとにメッセージを処理するように修正したものを用意、実行してみましょう。
void BusyLoop2( DWORD dwTime )
{
while( dwTime > 0 ){
PumpMessage();
// メッセージを処理してから待機カーソルにする
CSimpleWaitCursor waitCursor;
DWORD dwWait = ( dwTime > 1000 ) ? 1000 : dwTime ;
DWORD dwStart = GetTickCount();
while( (GetTickCount()-dwStart) < dwWait );
dwTime -= dwWait;
}
}
サンプルプログラムの[体験メニュー]-[ビジーループV2]で体験できます。これで、応答なしが出なくなりました。しかも、嬉しい副作用として、リサイズなどのウィンドウ操作も行うことができます。まずはいろいろ試してみてください。V2では環境によらず30秒間の処理となっています(実際には1秒×30のループ)。
