🖨️

『応答なし』にならないアプリ開発 Part.1

2022/12/11に公開

『応答なし』と表示される原因

処理開始から終了まで一定時間描画系のWindowsメッセージ(WM_PAINT等)が処理されないと『応答なし』が表示されます。

主にシングルスレッドで重い処理(メソッド内で時間のかかる処理をしたりDBから大量のデータを取得等々)した時におこります。ループ処理で調子よく動いているように見えてもフォームをクリックとかしたら途端に『応答なし』になったりしますよね。

『応答なし』の回避策あれこれ

Application.DoEventsをコール

まず思いつくのがVBユーザ御用達の「DoEvents」。勿論VB.NetだけでなくC#でも使用できます。
便利な反面メッセージキューにある全てのWindowsメッセージが処理されるため、思いがけない動作をする可能性があります。
業務用アプリでは二度押し防止策等で動作中各種ボタンを非活性にする等することが多いですが、やはりすべての副作用的要素が取り除けない為、出来れば最終手段としたいものです。

Win32APIによるWindowsメッセージ処理

次にWin32APIの使用で、描画系のメッセージのみ処理する方法があります。
(DoEventsより副作用は抑えられるはずですが面倒ですね)

BackgroundWorkerにる別スレッド実行

ここからが本題です。
今となっては古い手法になりますが、BackgroundWorkerを使用することで手軽にマルチスレッド処理が実現できます。
注意点としてはワーカースレッドからコントロール等スレッドセーフでないものに直接アクセスしないことでしょうか。処理の中でコントロールへアクセスする必要がある場合、Invokeを用いるか、BackgroundWorkerのProgressChanged・RunWorkerCompletedイベントのようなメインスレッドで実行される部分でする必要があります。

以下簡単なサンプルになります。

  • Form1にボタン・プログレスバー・ラベルを一個ずつ配置
C#サンプルソース
Form1.cs
using System;
using System.Runtime.CompilerServices;
using System.ComponentModel;

public class Form1
{
    private MAX_COUNT = 100;
    private BackgroundWorker _bgWorker = new BackgroundWorker();

    private void Form1_Load(object sender, EventArgs e)
    {
        ProgressBar1.Hide();
        Label1.Hide();
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        ProgressBar1.Maximum = MAX_COUNT;
        ProgressBar1.Value = 0;
        ProgressBar1.Step = 1;
        Label1.Text = string.Empty;

        // 進行状況の報告を有効に
        bgWorker.WorkerReportsProgress = true;
        // 処理実施
        ProgressBar1.Show();
        Label1.Show();
        bgWorker.RunWorkerAsync();
    }

    /// <summary>
    /// BackgroundWorkerの処理部
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void bgWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        for (var i = 1; i <= MAX_COUNT; i++)
        {
            // 重い処理
            System.Threading.Thread.Sleep(500);
            // プログレス処理
            bgWorker.ReportProgress(i);
        }
    }

    /// <summary>
    /// プログレス処理
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        ProgressBar1.PerformStep();
        Label1.Text = $"({ProgressBar1.Value}/{MAX_COUNT})";
    }

    /// <summary>
    /// BackgroundWorker処理完了時
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void bgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        ProgressBar1.Hide();
        Label1.Hide();
    }
}

VB.Netサンプルソース
Form1.vb
Imports System.ComponentModel
Public Class Form1
    Private MAX_COUNT = 100
    Private WithEvents bgWorker As New BackgroundWorker

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
        ProgressBar1.Hide()
        Label1.Hide()
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        ProgressBar1.Maximum = MAX_COUNT
        ProgressBar1.Value = 0
        ProgressBar1.Step = 1
        Label1.Text = String.Empty

        '*** 進行状況の報告を有効に
        bgWorker.WorkerReportsProgress = True
        '*** 処理実施
        ProgressBar1.Show()
        Label1.Show()
        bgWorker.RunWorkerAsync()
    End Sub

    ''' <summary>
    ''' BackgroundWorkerの処理部
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub bgWorker_DoWork(sender As Object, e As DoWorkEventArgs) Handles bgWorker.DoWork
        For i = 1 To MAX_COUNT
            '*** 重い処理
            Threading.Thread.Sleep(500)
            '*** プログレス処理
            bgWorker.ReportProgress(i)
        Next
    End Sub

    ''' <summary>
    ''' プログレス処理
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub bgWorker_ProgressChanged(sender As Object, e As ProgressChangedEventArgs) Handles bgWorker.ProgressChanged
        ProgressBar1.PerformStep()
        Label1.Text = $"({ProgressBar1.Value}/{MAX_COUNT})"
    End Sub

    ''' <summary>
    ''' BackgroundWorker処理完了時
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="e"></param>
    Private Sub bgWorker_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles bgWorker.RunWorkerCompleted
        ProgressBar1.Hide()
        Label1.Hide()
    End Sub
End Class

BackgroundWorker等別スレッドで実行する際もDoEvents同様各ボタンの非活性等で同時実行を制御する必要があることに気を付けてください。

次のステップとして、Task(Async/Await)がありますが、長くなっているので次回に(。-`ω-)b

Discussion