連載
» 2004年03月19日 05時00分 公開

.NET TIPS:時間がかかる処理での「応答なし」を回避するには?

Windowsアプリケーションでタイトル・バーに「(応答なし)」と表示されるのを回避するには、Applicationクラス(System.Windows.Forms名前空間)のDoEventsメソッドを活用すればよい。C#およびVB.NETで使う方法を解説する。

[一色政彦,デジタルアドバンテージ]
「.NET TIPS」のインデックス

連載目次

 Windowsアプリケーションで約10秒以上の時間がかかる処理を行う場合、その処理実行中にユーザー・インターフェイスの描画が更新されず、真っ白な画面になってしまったり、タイトル・バーに「(応答なし)」と表示されてしまったりする場合がある。

描画が更新されていないWindowsアプリケーション 描画が更新されていないWindowsアプリケーション
10秒以上の長い処理を行う場合、描画が更新されず、タイトル・バーに「(応答なし)」と表示されてしまう場合がある。

 このような場合、その時間のかかる処理を別のスレッドにするのが最良の対応策の1つだが、もっと手軽に解決する方法もある。その方法は、時間のかかる処理を実行している途中でApplicationクラス(System.Windows.Forms名前空間)のDoEventsメソッドを呼び出す方法だ。

DoEventsメソッドを使ったWindowsメッセージ処理

 DoEventsメソッドは、アプリケーションのメッセージ・キューにたまったすべてのWindowsメッセージを処理するためのものだ。

 Windowsフォームを使ったアプリケーションでは、例えばフォームの再描画が必要なことを知らせるWM_PAINTメッセージなど、さまざまなWindowsメッセージがメッセージ・キューにたまっていく。通常は、アプリケーションを開始するためのApplicationクラスのRunメソッドにより、このキュー内のメッセージが順次処理されるため、適切に画面を再描画できる。しかし、処理に時間がかかるような場合、その処理が完了するまでメッセージ処理が中断されるため、画面の描画が行われなかったり、タイトル・バーに「応答なし」と表示されたりしてしまう。そのような場合、時間のかかる処理を実行している合間にDoEvensメソッドを呼び出せば、たまったWindowsメッセージの処理をすべて片づけてくれる。これによりアプリケーションがフリーズすることはなくなる。

 このDoEventsメソッドを使ったサンプル・プログラムを次に示す。

private void button1_Click(object sender, System.EventArgs e)
{
  // フォームを無効にしてボタンを押せないようにする
  this.Enabled = false;

  // 時間のかかる処理
  for (int i = 0; i < 200; i++)
  {
    // 何らかの処理
    System.Threading.Thread.Sleep(100);

    // メッセージ・キューにあるWindowsメッセージをすべて処理する
    Application.DoEvents();
  }

  // フォームを有効に戻す
  this.Enabled = true;
}

DoEventsメソッドを使ってWindowsメッセージを処理するサンプル・プログラム
サンプル・プログラム(C#:doevents1.cs)のダウンロード

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

  ' フォームを無効にしてボタンを押せないようにする
  Me.Enabled = False

  ' 時間のかかる処理
  Dim i As Integer
  For i = 0 To 199
    ' 何らかの処理
    System.Threading.Thread.Sleep(100)

    ' メッセージ・キューにあるWindowsメッセージをすべて処理する
    Application.DoEvents()
  }

  ' フォームを有効に戻す
  Me.Enabled = True
}

DoEventsメソッドを使ってWindowsメッセージを処理するサンプル・プログラム
サンプル・プログラム(VB.NET:doevents1.vb)のダウンロード

 このサンプル・プログラムを実行すると、先ほどの「応答なし」と表示されていたWindowsアプリケーションの画面は適切に描画されるようになる。

適切に描画されたWindowsアプリケーション 適切に描画されたWindowsアプリケーション
10秒以上の長い処理の間でも、画面が適切に描画され、タイトル・バーに「(応答なし)」は表示されない。

 なお、DoEventsメソッドはすべてのWindowsメッセージを処理してしまうため、ボタンなどが有効になっていると、そのボタンの処理が実行されてしまう可能性がある。例えば、データの削除処理を行っている間に、データの編集などの別のボタンの処理が実行されてしまうと、削除されたデータを編集しようとしてエラーになるなど、アプリケーションの動作に不整合が発生する可能性がある。よって、DoEventsメソッドを呼び出す前に、ボタンを無効化するなど、何らかの対処を行っておく必要がある。

Win32 APIを使ったWindowsメッセージ処理

 DoEventsメソッドはすべてのWindowsメッセージを処理する。つまり、Windowsメッセージを選択的に処理することはできない。例えば、WM_PAINTメッセージ(描画のWindowsメッセージ)のみを処理したいような場合、DoEventsメソッドは使えない。選択的なWindowsメッセージの処理など、より自由なメッセージ処理を実現するには、Win32 APIのPeekMessage関数TranslateMessage関数DispatchMessage関数などを呼び出す必要がある。

 このWin32 APIを使ったWindowsメッセージ処理の詳細な内容は、.NET Frameworkではなく、Win32の実装の解説になってしまうので、割愛させていただくが、参考のため以下にサンプル・コードを示しておくことにする。

 このサンプル・コードでは、 WM_PAINTメッセージのみを処理するようになっているので、画面の再描画だけが行われる。

private void button1_Click(object sender, System.EventArgs e)
{
  // フォームを無効にしてボタンを押せないようにする
  this.Enabled = false;

  // 時間のかかる処理
  for (int i = 0; i < 200; i++)
  {
    // 何らかの処理
    System.Threading.Thread.Sleep(100);

    // Win32 APIを使ってWindowsメッセージをすべて処理する場合のサンプル・コード
    MSG msg = new MSG();
    if (PeekMessage(ref msg, 0, WM_PAINT, WM_PAINT, PeekMsgOption.PM_REMOVE))
    {
      DispatchMessage(ref msg);
    }
  }

  // フォームを有効に戻す
  this.Enabled = true;
}

// Win32 APIのインポート
[ DllImport( "user32.dll", SetLastError = true ) ]
private static extern bool PeekMessage(
  ref MSG lpMsg,
  Int32 hwnd,
  Int32 wMsgFilterMin,
  Int32 wMsgFilterMax,
  PeekMsgOption wRemoveMsg);
[ DllImport( "user32.dll", SetLastError = true ) ]
private static extern bool TranslateMessage(ref MSG lpMsg);
[ DllImport( "user32.dll", SetLastError = true ) ]
private static extern Int32 DispatchMessage(ref MSG lpMsg);

// メッセージの処理方法オプション
private enum PeekMsgOption
{
  PM_NOREMOVE = 0,  // 処理後、メッセージをキューから削除しない
  PM_REMOVE      // 処理後、メッセージをキューから削除する 
}

// Windowsメッセージの定義
private static Int32 WM_PAINT= 0x000F;

// メッセージ構造体
[StructLayout(LayoutKind.Sequential)]
struct MSG
{
  public Int32 HWnd;    // ウィンドウ・ハンドル
  public Int32 Msg;    // メッセージID
  public Int32 WParam;  // WParamフィールド(メッセージIDごとに違う)
  public Int32 LParam;  // LParamフィールド(メッセージIDごとに違う)
  public Int32 Time;    // 時間
  public POINTAPI Pt;    // カーソル位置(スクリーン座標)
}
[StructLayout(LayoutKind.Sequential)]
struct POINTAPI
{
  public Int32 x;      // x座標
  public Int32 y;      // y座標
}

Win32 APIを使ってWindowsメッセージを処理するサンプル・プログラム
サンプル・プログラム(C#:doevents2.cs)のダウンロード

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

  ' フォームを無効にしてボタンを押せないようにする
  Me.Enabled = False

  ' 時間のかかる処理
  Dim i As Integer
  For i = 0 To 199
    ' 何らかの処理
    System.Threading.Thread.Sleep(100)

    ' WIN32APIを使ってWM_PAINTメッセージのみを処理する
    Dim msg As New MSG
    If PeekMessage(msg, 0, WM_PAINT, WM_PAINT, PeekMsgOption.PM_REMOVE) Then
      DispatchMessage(msg)
    End If
  Next i

  ' フォームを有効に戻す
  Me.Enabled = True

End Sub

' WIN32APIのインポート
<DllImport("user32.dll", SetLastError:=True)> _
Private Shared Function _
  PeekMessage( _
    ByRef lpMsg As MSG, _
    ByVal hwnd As Int32, _
    ByVal wMsgFilterMin As Int32, _
    ByVal wMsgFilterMax As Int32, _
    ByVal wRemoveMsg As PeekMsgOption _
  ) As Boolean
End Function
<DllImport("user32.dll", SetLastError:=True)> _
Private Shared Function _
  TranslateMessage(ByRef lpMsg As MSG) As Boolean
End Function
<DllImport("user32.dll", SetLastError:=True)> _
Private Shared Function _
  DispatchMessage(ByRef lpMsg As MSG) As Int32
End Function

' メッセージの処理方法
Private Enum PeekMsgOption
  PM_NOREMOVE = 0 ' 処理後、メッセージをキューから削除しない
  PM_REMOVE     ' 処理後、メッセージをキューから削除する 
End Enum 'PeekMsgOption

' Windowsメッセージの定義
Private Shared WM_PAINT As Int32 = &HF

' メッセージ構造体
<StructLayout(LayoutKind.Sequential)> _
Structure MSG
  Public HWnd As Int32  ' ウィンドウ・ハンドル
  Public Msg As Int32   ' メッセージID
  Public WParam As Int32  ' WParamフィールド(メッセージIDごとに異なる)
  Public LParam As Int32  ' LParamフィールド(メッセージIDごとに異なる)
  Public Time As Int32  ' 時間
  Public Pt As POINTAPI   ' カーソル位置(スクリーン座標)
End Structure 'MSG
<StructLayout(LayoutKind.Sequential)> _
Structure POINTAPI
  Public x As Int32   ' x座標
  Public y As Int32   ' y座標
End Structure 'POINTAPI

Win32 APIを使ってWindowsメッセージを処理するサンプル・プログラム
サンプル・プログラム(VB.NET:doevents2.vb)のダウンロード

 .NETプログラムからWin32 APIを呼び出す方法については、別稿「TIPS:Win32 APIやDLL関数を呼び出すには?」や「TIPS:Win32 APIやDLL関数に構造体を渡すには?」を参照していただきたい。Windowsメッセージの実際の定数値については、MSDNで入手できるプラットフォームSDK(コアSDKセクション)を参照していただきたい。

 なおWin32 APIを使った場合も、DoEventsメソッドと同じようにボタンの無効化などが必要になるので注意していただきたい。

カテゴリ:Windowsフォーム 処理対象:スレッド
使用ライブラリ:Applicationクラス(System.Windows.Forms名前空間)
関連TIPS:Win32 APIやDLL関数を呼び出すには?
関連TIPS:Win32 APIやDLL関数に構造体を渡すには?


「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。