あらかじめ日記

2chまとめサイト「ソノウソホント?」の雑記と、ブログで使えるスクリプトとかテクニックとか。その他、音楽やアニメ、漫画の話題とかも。

まとめサイト「ソノウソホント?」と「ムードもりあげ楽団」はこちら。

【C#】後付けでも使えるプログレスバーウィンドウを作る(Formの別スレッド化)

f:id:allthewayfrom:20160714220202p:plain


さて、よくアプリケーションで見かける処理の経過を示すプログレスバーの作成方法はこちらが参考になります。

進行状況ダイアログを表示する: .NET Tips: C#, VB.NET


今回は「やっぱりこの処理、プログレスバー出すようにしたい…」と言う後付けで対応したいケースがありまして、思いつきで考えた方法の備忘録です。

 
上記サンプルの、最初の方法なら後付でも簡単ですぐに対応できるかと思います。

ただ、同じスレッド上で処理をしているのでサンプルのような1秒に1メモリくらいであれば気になりませんが、それよりも短い時間になるとカクついて滑らかには表示されません。
また、ボタン処理などは処理されないのでキャンセルボタンの対応も出来ません。

手っ取り早く解決するのは、最後の方に書かれているDoEventを使用する場合ですが、表示は確かに滑らかにはなりますが、DoEventはあくまで表示更新を含めたイベントを強制的に実行しているので、そのデメリットがリンク先本文にも書かれています。ですので、これは「奥の手」と言うところでしょうか。

そうなると、マルチスレッド化な訳ですが、サンプルでよく見かける方法は既に作成している処理を別スレッドで処理させていますので、既存の処理を別スレッドにしないといけません。
単純処理なら簡単に終わるかもしれませんが、結構処理が大きかったり、途中下手にコントロールにアクセスしていたらその部分をメインスレッド側で処理するように改修しないといけない。。
(別のスレッドで作られたコントロールに、異なるスレッド上でアクセスするのはNG)

まぁ時間の掛かる処理なので、別スレッド化するのはまぁ正しいので、、普通は処理を作成する前に正しく設計しておくことが良い訳ですが、そうは言っても、趣味で作っていると勢いで作ってしまうことがあるので、そう言うところがお座なりになってしまいがちで。苦笑

そこで考えたのが、処理自体はそのままで逆にプログレスバー(のウィンドウ)自体を別スレッドで作ってしまう、と言うモノ。


まず、インスタンスを内部的に管理できるようにウェイトダイアログのフォームをシングルトン化します。
入口となるメソッドで別スレッドを作成し、そのスレッド内でインスタンスを作って表示させます。

そうすれば処理はメインスレッドでさせたままプログレスバーを持ったウェイトダイアログが出せる、と言う算段です。

あとは表示、クローズ、プログレスバーの最大値の設定と現在値の設定を呼び出しができるように、staticで作成しておけば準備完了です。

この時、何れもメインスレッド側で呼ばれるため、別スレッドで作成されたウェイトダイアログのフォームに対してリソースにアクセスするような処理を実行する場合はそのスレッドで処理するように要求を投げる必要があります。
詳細は、こちらなどを参考に。

.NET TIPS:Windowsフォームで別スレッドからコントロールを操作するには? - @IT



FormのクラスをWaitDialogとして作成します。

//外からインスタンスを生成できないようにコンストラクタはprivate
static WaitDialog mInstance = null;
private WaitDialog()
{
   InitializeComponent();
}

// Showと言うメソッド名を使いたかったので、newで上書きしていますが別にこれは任意の名前で構いません
public static new void Show()
{
   // BeginInvokeで実処理を別スレッド実行
   Action showProc = new Action(ShowProcess);
   IAsyncResultasync = showProc.BeginInvoke(null, null);

   // そのままメインスレッドは処理が流れるため、別スレッドでインスタンスが生成されるまで待つ
   while(true){
       if(mInstance != null){
           break;
       }
   }
   return;
}

private static void ShowProcess()
{
   if(mInstance == null || mInstance.IsDisposed == true){
       mInstance=newWaitDialog();
   }

   // Showだと処理が流れてスレッドが終了してしまうので、ShowDialogで表示して
   // 別スレッド側は処理を待つ
   ((Form)mInstance).ShowDialog();
}

// クローズ処理
public static new void Close()
{
   if(mInstance==null || mInstance.IsDisposed==true) return;

   if(mInstance.InvokeRequired == true) {
       mInstance.Invoke(new Action(Close));
   }
   else{
       ((Form)mInstance).Close();
   }
}

// プログレスバー最大値の設定
public static void SetProgressBar(intiMax)
{
   if(mInstance==null||mInstance.IsDisposed==true)return;

   if(mInstance.InvokeRequired == true){
       mInstance.Invoke(newAction(SetProgressBar),newobject[]{iMax});
   }
   else{
       mInstance.progressBar1.Maximum = iMax;
   }
}

// プログレスバー現在値の設定
public static void SetProgressBarValue(intiVal)
{
   if(mInstance == null || mInstance.IsDisposed == true) return;

   if(mInstance.InvokeRequired == true){
       mInstance.Invoke(newAction(SetProgressBarValue),newobject[]{iVal});
   }
   else{
       mInstance.progressBar1.Value = iVal;
   }
}


キャンセル処理は、コールバック用のメソッドをWaitDialogに渡して、×ボタンで閉じた場合も考慮に入れてFormClosingイベントで、そのメソッドを実行すれば良いでしょうか。


自分自身も思いつきで作っているところがあるので、おかしい箇所やその他考慮すべき事がありましたらご指摘頂けたら幸いです。