スレッドで進捗ウインドウを表示する

大量のファイルを保存したりする場合や、ネットから大きなファイルをダウンロードしたりする場合は、現在の進捗を表示しないとユーザは不安でたまりません。

何も考えずにプログラムすると、そういった重い処理をしている間はユーザはそれをずっと待っている必要があります。これではマルチタスクなOSを使う意義が感じられません。

Fig. 1 時間がかかる処理のときに進捗ウインドウを表示する

本記事では、スレッドを使って重い処理を実施しつつ、その重い処理の進捗をユーザに知らせる方法のベストプラクティスを紹介します。

例として、Bitmap クラスに SetPixel メソッドを使ってデータを仕込み、そのビットマップ画像ファイルを所定のディレクトリに大量に保存するというケースを考えます。

皆さんご存知のとおり SetPixel メソッドは、非常に重い処理です。プロの現場で SetPixel を使うことは多くありませんが、本記事では例として逆に好都合です。

複数のファイルを使って解説をしているので、コピペで動作確認をするのは厳しいかもしれません。本記事の最後でサンプルソースをダウンロードできるようにしておきました。

メインウインドウのコード

フォームに button1, button2 というコントロールを配置してください。それぞれにクリックイベントを準備してください。

24行目で指定したディレクトリを、ソフト起動時に作成します。みなさんの環境にあわせて適当に変更してください。実際に作成するのは55行目の CreateDirectory() です。

51行目は画像のリクエスト保存枚数を指定しています。自信のPCの性能にあわせて適宜増減してください。高性能なPCで、この数が少ないと動作の意味がわからないと思います。

button1 を押すとファイル保存のスレッドが起動します。それと同時に90行目の Process.Start() によって保存するディレクトリが開きます。ファイルがどんどん増えていくことがわかると思います。もし、動作の確認の妨げになるようでしたらこの行をコメントアウトしてください。

using System.Threading; の追加をお忘れなく.
using System.IO; の追加をお忘れなく.
using System.Diagnostics; の追加をお忘れなく.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.Threading; // user add.
using System.IO; // user add.
using System.Diagnostics; // user add.

namespace aaa
{
	public partial class Form1 : Form
	{

		Thread TheThread;
		ParametersForThread Prm;

		// 画像を保存するディレクトリ.
		const String DIR_SAVE = "c:/tmp/record";

		// 保存する画像の縦横ピクセル数.
		const int REQ_W = 640;
		const int REQ_H = 480;

		// 保存する画像ファイルのリクエスト個数の変数.
		int ReqCount = 0;

		// 進捗表示ウインドウ.
		FormProgress FormPgs;

		public Form1()
		{

			InitializeComponent();

			// スレッドに渡すパラメータを生成する.
			Prm = new ParametersForThread(); 

			// 進捗表示ウインドウを作成する,スレッドに渡すパラメータ情報を共有する.
			FormPgs = new FormProgress( Prm );

			// 進捗表示ウインドウのオーナはこのウインドウ.
			FormPgs.Owner = this;

			// スレッドに渡すパラメータに所望の値をセットする.
			ReqCount = 256;
			Prm.SetParameters( DIR_SAVE, ReqCount, REQ_W, REQ_H, FormPgs );

			// ディレクトリが存在しなければ作成する.
			Directory.CreateDirectory( DIR_SAVE );

		}

		private void Form1_Load( object sender, EventArgs e )
		{
			button1.Text = "スレッド開始";
			button2.Text = "スレッド中断";
		}

		private void button1_Click( object sender, EventArgs e )
		{

			// すでにスレッドが実行されていたら何もしない.
			if ( TheThread != null )
			{
				if ( TheThread.IsAlive )
				{
					MessageBox.Show( "スレッドはすでに開始しています." );
					return; // warning.
				}
			}

			// 引数つきのスレッドを作成する.
			ParameterizedThreadStart pts = new ParameterizedThreadStart( ThisProject.Worker );
			TheThread = new Thread( pts );

			// 念のためスレッド停止要求を false にしてからスレッドを開始する.
			Prm.FlagReqThreadExit = false;
			TheThread.Start( Prm );

			// 進捗表示ウインドウを表示する.
			FormPgs.Visible = true;

			// 保存対象ディレクトリをひらく.
			Process.Start( DIR_SAVE );

		}

		private void button2_Click( object sender, EventArgs e )
		{
			// スレッドを中断するフラグをたてる.
			Prm.FlagReqThreadExit = true;
		}
	}
}

画像を保存するスレッドのコード

15行目から57行目は、スレッドに渡されるパラメータ用のクラスです。

63行目から155行目が、スレッドに渡されたパラメータによるリクエスト保存枚数ぶんループをして画像を保存しているコードです。

その中でも107行目から119行目が実際にデータをビットマップに仕込んでいるところです。このサンプルのために、あえて SetPixel という重いメソッドを使っています。

131行目から136行目が、進捗表示ウインドウの再描画の要請を Invoke を経由して行っているところです。これがないと進捗表示がされません。別のスレッドからコントロールのメソッドには直接アクセスできませんので Invoke 経由にしています。これについては下記の記事を参考になさってください。

スレッドからコントロールを操作する方法

スレッド内部で Windows のコントロールにアクセスしたいときにいつもと同じようにコードを書いてしまうと実行時エラーが発生します。これを回避する方法をご紹介します。

144行目から148行目は、処理が完了したので進捗ウインドウを隠しているところです。これも 131~136行目のコードと同様に直接アクセスできませんので Invoke 経由で進捗ウインドウを隠しています。

進捗ウインドウを閉じるのではなく隠しているというところにご注目ください。

using System.Windows.Forms; をお忘れなく.
using System.Drawing; をお忘れなく.
using System.Drawing.Imaging; をお忘れなく.
using System.IO; をお忘れなく.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Windows.Forms; // user add.
using System.Drawing; // user add.
using System.Drawing.Imaging; // user add.
using System.IO; // user add.

namespace aaa
{

	public class ParametersForThread
	{

		// スレッド中断要求のフラグ.
		public bool FlagReqThreadExit = false;

		public String DirSave = "";
		public int ReqSaveCount = 0;
		public int DataWidth;
		public int DataHeight;

		public int SavedCount = 0;

		public FormProgress TheFormProgress = null;

		public ParametersForThread( )
		{
			FlagReqThreadExit = false;
		}

		public bool SetParameters( String dir_save, int req_save_count, int data_width, int data_height, FormProgress form_progress )
		{

			DirSave = dir_save;
			ReqSaveCount = req_save_count;
			DataWidth = data_width;
			DataHeight = data_height;

			SavedCount = 0;

			if ( form_progress != null )
			{
				TheFormProgress = form_progress;
				return true;
			}
			else
			{
				return false;
			}

		}

	}


	public class ThisProject
	{

		public static void Worker( object parameter )
		{

			// パラメータをキャストして取得する.
			ParametersForThread prm = ( ParametersForThread )( parameter );

			// 画像の幅と高さをパラメータから取得してビットマップを生成する.
			int w = prm.DataWidth;
			int h = prm.DataHeight;
			Bitmap bmp = new Bitmap( w, h );

			// 保存済みファイルカウンタをゼロ初期化する.
			prm.SavedCount = 0;

			// ループ回数.
			int loop = prm.ReqSaveCount;

			// ループして画像保存する.
			for ( int k = 0; k < loop; k++ )
			{

				// スレッド中断フラグがはいったかどうか.
				if ( prm.FlagReqThreadExit )
				{
					// とりあえずフラグをもどす.
					prm.FlagReqThreadExit = false;

					// ビットマップを破棄する
					bmp.Dispose();
					bmp = null;

					// 進捗表示ウインドウがセットされていたら、進捗表示ウインドウを隠す.
					if ( prm.TheFormProgress != null )
					{
						prm.TheFormProgress.Invoke( new FormProgress.DelegateXxx( prm.TheFormProgress.FormHide ));
					}

					// メッセージボックスを表示する.
					MessageBox.Show( "スレッドの中断要求を検知しました." );

					// スレッドを脱出する.
					return;
				}

				// だんだん黒から赤になる画像データを作成する.
				double rate = (double)( k )/(double)( loop - 1 ); 
				byte level = (byte)( Math.Round( rate * 255.0 ));
				Color c = Color.FromArgb( level, 0, 0 );

				// ビットマップに画像データを仕込む.
				for ( int j = 0; j < h; j++ )
				{
					for ( int i = 0; i < w; i++ )
					{
						bmp.SetPixel( i, j, c );
					}
				}

				// 保存パスを生成する.
				String filename = String.Format( "{0:d8}.bmp", k );
				String filepath = Path.Combine(( prm.DirSave ), filename );

				// ビットマップ画像を保存する.
				bmp.Save( filepath, ImageFormat.Bmp );

				// 保存カウンタを増やす.
				prm.SavedCount++;

				// 進捗表示ウインドウがパラメータに設定されていたら再描画を要請する.
				if ( prm.TheFormProgress != null )
				{
					// 進捗表示ウインドウの再描画を要請する.
					prm.TheFormProgress.Invoke( new FormProgress.DelegateXxx( prm.TheFormProgress.PictureBoxInvalidate ));
				}

			}

			// ビットマップを破棄する.
			bmp.Dispose();
			bmp = null;

			// 進捗表示ウインドウがセットされていたら、進捗表示ウインドウを隠す.
			if ( prm.TheFormProgress != null )
			{
				prm.TheFormProgress.Invoke( new FormProgress.DelegateXxx( prm.TheFormProgress.FormHide ));
			}

			// 終了メッセージボックスを表示する.
			MessageBox.Show( "保存スレッドが安全に終了しました." );

			return;

		}

	}
}

進捗ウインドウのコード

下記が進捗ウインドウのコードです。button1 と pictureBox1 を配置してください。

button1 にはクリックイベントを準備してください。pictureBox1 はペイントイベントを準備してください。

54行目からが pictureBox1 のペイントイベントです、この中で具体的な進捗バーを塗りつぶすコードを記述しています。進捗バーの表現が没個性になってもいいのならば ProgressControl をそのまま使ってもいいです。

42行目から52行目が、このウインドウを閉じるのではなく隠すというコードです。スレッドからこのウインドウにアクセスする場合に、いちいち閉じていると管理がめんどうなので隠すようにしています。

ユーザから見ると、閉じていても、隠していても、その差はありません。

90行目から100行目が、外部のスレッドからこのウインドウに対して Invoke で制御をおこなう場合に用いるデリゲートの宣言と実際の動作コードです。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace aaa
{
	public partial class FormProgress : Form
	{

		ParametersForThread Pft;

		public FormProgress( ParametersForThread parameters_for_thread )
		{

			InitializeComponent();

			Pft = parameters_for_thread;

		}

		private void FormProgress_Load( object sender, EventArgs e )
		{

			// 固定サイズツールウインドウとする.
			FormBorderStyle = FormBorderStyle.FixedToolWindow;

			// 進捗ウインドウをタスクバーに表示しない.
			ShowInTaskbar = false;

			// タイトルバーのダブルクリックで最大化させない.
			MaximizeBox = false;

			button1.Text = "スレッド中断";
		}

		private void FormProgress_FormClosing( object sender, FormClosingEventArgs e )
		{
			if ( e.CloseReason == CloseReason.UserClosing )
			{
				// 閉じるのをやめる.
				e.Cancel = true;

				// 閉じるのではなく隠す.
				this.Visible = false;
			}
		}

		private void pictureBox1_Paint( object sender, PaintEventArgs e )
		{

			// 進捗率.
			double rate = (double)( Pft.SavedCount )/(double)( Pft.ReqSaveCount );

			// 独自プログレスバーの色を決める.
			SolidBrush sb0 = new SolidBrush( Color.Black );
			SolidBrush sb1 = new SolidBrush( Color.Lime );

			// 独自プログレスバーの全領域.
			int pw0 = pictureBox1.Width;
			int ph0 = pictureBox1.Height;
			Rectangle rect0 = new Rectangle( 0, 0, pw0, ph0 );

			// 進捗に応じてプログレスバーの領域を決める.
			int pw1 = (int)( Math.Round( pw0 * rate ));
			int ph1 = ph0;
			Rectangle rect1 = new Rectangle( 0, 0, pw1, ph1 );

			// 矩形を塗りつぶし描画する.
			e.Graphics.FillRectangle( sb0, rect0 );
			e.Graphics.FillRectangle( sb1, rect1 );

			// ブラシを破棄する.
			sb0.Dispose();
			sb1.Dispose();

		}

		private void button1_Click( object sender, EventArgs e )
		{
			// スレッドを中断するフラグをたてる.
			Pft.FlagReqThreadExit = true;
		}

		public delegate void DelegateXxx();

		public void FormHide()
		{
			this.Visible = false;
		}

		public void PictureBoxInvalidate()
		{
			pictureBox1.Invalidate( true );
		}
	}
}

下記にサンプルソースを用意しました、よろしければご利用ください。