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

ある一定以上のプログラミングスキルがついてくると、スレッドを使いたくなる状況が多々でてきます。

そのとき、スレッド内部のコードで Windows のコントロールにアクセスしたいときに

pictureBox1.Invalidate();

のように安易にコードを記述してしまうと、たちまち実行時に

有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'pictureBox1' がアクセスされました

という悲しい例外が発生してしまいます。

これを避けるためには、Invoke メソッドをつかって、スレッド内部から間接的に Windows コントロールをアクセスするという方法を用います。

ネットで散見されるサンプルソースは、スレッドの動作コードを Form1.cs の中に記述しているものがほとんどです。

しかし、実際の現場では、スレッドの動作は別のファイルで記述してソースの管理や見通しをよくしたいという要求があります。

したがって、本解説では Form1.cs と、スレッドの動作コードを別のファイルで定義した ThisProject.cs、という二つのファイルを使ってスレッドを動作させるサンプルを例示します。

作成するアプリケーション

Fig. 1 にしめすアプリケーションを作成します。

Form1 というフォームに button1, button2 という名前のボタンを配置してください。
ボタン1には button1_Click というクリックイベントの準備をしてください。
ボタン2には button2_Click というクリックイベントの準備をしてください。

button1_Click するとスレッドを起動します。
button2_Click するとスレッドを停止します。

スレッドは内部的に準備している Bitmap に黒から赤に徐々に変化するデータを書き込みます。スレッドの内部からピクチャボックスの再描画要求を Invoke メソッド経由で行います。

Fig. 1 作成するアプリケーションの外観

Windowsコントロールのファイル Form1.cs

下記のコードが、フォームのコードです。

重要なのは 107~111行目のコードが public で公開されているということです。よくウェブで解説がされている記事ではスレッドのコードが Form1 の中で閉じている場合が多いため、ここが private になっています。

デリゲートを public で定義することで、別のファイルにある別のクラスから Form1 のコントロールに対して制御を要求することができます。

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

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.

namespace abc
{
	public partial class Form1 : Form
	{

		const int REQ_W = 320;
		const int REQ_H = 240;

		Thread TheThread = null;
		ParametersForThread Prm;

		public Form1()
		{

			InitializeComponent();

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

			pictureBox1.Width = REQ_W;
			pictureBox1.Height = REQ_H;

		}

		private void Form1_Load( object sender, EventArgs e )
		{

		}

		private void button1_Click( object sender, EventArgs e )
		{

			// スレッドがすでに生成されているかどうか.
			if ( TheThread == null )
			{
				// パラメータを渡してスレッドを起動する場合のクラスを生成する.
				ParameterizedThreadStart pts = new ParameterizedThreadStart( ThisProject.Worker );

				// スレッドを生成する(まだ開始はしない).
				TheThread = new Thread( pts );
			}

			// スレッドが活動している場合はスレッドを起動しない.
			if ( TheThread.IsAlive )
			{
				MessageBox.Show( "すでにスレッドが起動されています." );
				return;
			}
			else
			{
				// 念のためスレッド内で使う変数を初期化する.
				Prm.FlagReqThreadExit = false;
				Prm.Counter = 0;

				// パラメータを引数で渡してスレッド開始.
				object p = (object)( Prm );
				TheThread.Start( p );
			}

		}

		private void button2_Click( object sender, EventArgs e )
		{

			if ( TheThread == null )
			{
				MessageBox.Show( "スレッドが起動していません." );
				return;
			}
			else
			{

				// スレッドが活動している場合はスレッドを止める.
				if ( TheThread.IsAlive )
				{

					// スレッドを停止する要請をする.
					Prm.FlagReqThreadExit = true;

					// たまっている描画メッセージをすべて実施する要請をする.
					Application.DoEvents();

					// スレッドの終了を待つ.
					TheThread.Join();

					// スレッドを破棄する.
					TheThread = null;

				}

			}

		}

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

		private void pictureBox1_Paint( object sender, PaintEventArgs e )
		{

			// スレッド内のデータ操作と排他する.
			lock( Prm.Key )
			{
				// ビットマップを描画する.
				int w = Prm.Bmp.Width;
				int h = Prm.Bmp.Height;
				e.Graphics.DrawImage(( Prm.Bmp ), 0, 0, w, h );
			}

			// タイトルバーに現在のスレッド内でカウントアップしている値を表示する.
			int tmp = ( Prm.Counter ) % 256;
			this.Text = String.Format( "{0} ({1})", ( Prm.Counter ), tmp );

		}
	}
}

スレッド動作を定義したファイル ThisProject.cs

スレッドに渡すパラメータのクラスと、スレッドの実際の動作の定義を記述したファイルです。

13行目から40行目までが、スレッドに渡すパラメータのクラスを定義しています。

パラメータのメンバに Form1 への参照があるのがファイルを分離するコツです。このメンバはパラメータクラスのコンストラクタで必ず設定するようにします。そうしないと Form1 でせっかく public にしたデリゲートにアクセスできません。

51行目以降が、スレッド動作の定義です。

95行目が本記事の核心になります。Invoke 経由で Form1 で public 定義されているデリゲートの実行要求をします。

using System.Drawing; の追加をお忘れなく!
using System.Windows.Forms; の追加をお忘れなく!

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

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

namespace abc
{

	// スレッドに渡すパラメータのクラス.
	public class ParametersForThread
	{

		// 排他ロック用のキー.
		public object Key;

		// スレッド停止要請用のフラグ.
		public bool FlagReqThreadExit;

		public Form1 TheForm;
		public Bitmap Bmp;
		public int Counter;

		// パラメータクラスのコンストラクタ.
		public ParametersForThread( Form1 form, int width, int height )
		{

			Key = new object();

			FlagReqThreadExit = false;

			TheForm = form;

			Bmp = new Bitmap( width, height );
		}

	}

	public class ThisProject
	{

		public ThisProject()
		{
		
		}

		// スレッドの動作を定義する.
		public static void Worker( object parameter )
		{

			// パラメータは object 型で渡されるのでキャストして使う.
			ParametersForThread prm = (ParametersForThread)( parameter );

			int w = prm.Bmp.Width;
			int h = prm.Bmp.Height;

			prm.Counter = 0;

			// 無限ループ.
			for (;;)
			{
			
				// スレッドの停止要請がきたら脱出する.
				if ( prm.FlagReqThreadExit )
				{
					prm.FlagReqThreadExit = false;
					return;
				}

				// ビットマップのデータ操作とウインドウ表示を排他する.
				lock ( prm.Key )
				{

					// level は必ず 0 ~ 255 になる.
					int level = ( prm.Counter % 256 );

					for ( int j = 0; j < h; j++ )
					{
						for ( int i = 0; i < w; i++ )
						{
							// 赤のレベルだけ変化させる.
							Color c = Color.FromArgb( level, 0, 0 );

							// ピクセルにデータを仕込む.
							prm.Bmp.SetPixel( i, j, c );
						}
					}

				}

				// Form1 にある pictureBox1 に対して再描画要請をする.
				prm.TheForm.Invoke( new Form1.DelegatePictureBoxInvalidate( prm.TheForm.PictureBoxInvalidate ) );

				( prm.Counter )++;

			}

		}
	}
}

上記のふたつのファイルをいちいち作成しつつコピペしてうまく動かない場合のために、下記にサンプルソースをご用意しました、よろしければご利用ください。