Windows Form と スレッド

C# のライブラリっていろいろと便利なものがそろってると思うんですが,どうも資料が不足してるような気がする...前回のエントリでも Bitmap を扱ったけど,いま Image とか Bitmap とかを扱うものを作ってて,一枚の画像を複数のスレッドからいじる必要があったんだけど,「別のスレッドがつかんでる」とか「Invalid Operation」とかの例外がたくさん上がってくるのでちょっと調べたのでメモ.

事前にちょっとググった結果,下記のようなコメント発見.

Windows フォームと複数スレッド
1. Windows フォームでスレッドを作成した場合,フォームやフォーム上のコントロールに対しては,そのスレッドからの操作は動作が保証されない.<- つまり,基本的には UI スレッドからしかいじるなということらしい.

ということで実験.下記,メインのコードです.
下記のコードではまず写真を GUI に表示して, 1 秒毎に写真を反転させます.

public partial class Form1 : Form
{
  private System.Timers.Timer myTimer;

  private void SetupTimer()
  {
    myTimer = new System.Timers.Timer();
    myTimer.Enabled = true;
    myTimer.AutoReset = true;
    myTimer.Interval = 1000;
    myTimer.Elapsed += new ElapsedEventHandler(OnTimerEvent);
  }

  delegate void FlipDelegate();

  internal void FlipImageByThread()
  {
    Image img = pictureBox1.Image;
    img.RotateFlip(RotateFlipType.Rotate180FlipX);
    pictureBox1.Refresh();
  }

  private void OnTimerEvent(object source, ElapsedEventArgs e)
  {
    FlipImageByThread();
    //Console.WriteLine("OnTimerEvent");
    //Invoke(new FlipDelegate(FlipImageByThread));        
  }

  private void DisplayBitmap()
  {
    Bitmap canvas = new Bitmap(pictureBox1.Width, pictureBox1.Height);
    Graphics g = Graphics.FromImage(canvas);
    g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
    Bitmap pic = new Bitmap(@"C:\\Users\\Koichi\\Documents\\Visual Studio 2013\\Projects\\ConsoleApplication1\\OpenCVSharpSample\\img\\boeing777.jpg");
    g.DrawImage(pic, 0, 0, 500, 500);
    pic.Dispose();
    g.Dispose();
    pictureBox1.Image = canvas;           
  }

  public Form1()
  {
    InitializeComponent();
    DisplayBitmap();
    SetupTimer();            
  }
}

結果的に,上記のコードは例外がはかれてしまってうまく動きません.理由は最初に述べたとおり, UI スレッド以外から UI をいじろうとしたからみたいです.

f:id:rkoichi2001:20160528233945p:plain

で,それじゃ別のスレッドから UI をいじりたいとき(例えば,ユーザのマウスイベントを拾ってとか,上記の例みたいにタイマイベントをひろってとか...)にどうすればええんか?って話が出てきますが,Invoke というメソッドを使えば UI スレッドに処理を移譲できるみたい.

該当するコードは下記の部分です.

delegate void FlipDelegate();

internal void FlipImageByThread()
{
  Image img = pictureBox1.Image;
  img.RotateFlip(RotateFlipType.Rotate180FlipX);
  pictureBox1.Refresh();
}

private void OnTimerEvent(object source, ElapsedEventArgs e)
{
  // このラインをコメントアウト.
  // FlipImageByThread();

  // 下記の 2 行を有効化.
  Console.WriteLine("OnTimerEvent");
  Invoke(new FlipDelegate(FlipImageByThread));        
}

結果は,うまく写真が反転して出力されました.
まとめると,Windows フォームは UI スレッド以外からコントロールしてはいけない.コントロールが必要な場合は Invoke メソッドを用いて処理を UI スレッドに移譲する.


f:id:rkoichi2001:20160528234850p:plain

f:id:rkoichi2001:20160528234757p:plain