進捗状況画面の表示その2
dobon.net さんの記事で、進行状況ダイアログを表示するという記事があるのですが、.NET Framework 1.1
版のものと、.NET Framework 2.0
版のもので、操作仕様が違っています。Thread と BackgroundWorker の違いもありますが、操作性は、.NET Framework 1.1
版のものの方が分かりやすいかなぁと個人的には思っていました。
というわけで、以下サンプルです。細かい差異はありますが大体の流れは同じです。
やっぱり非同期関連は難しいです。Thread と ManualResetEvent、Async/Await と Task、UI スレッドと別スレッド、フォアスレッドとバッググラウンドスレッド、ShowDialog() しつつ次の行以降の処理を進めるとか、よくわからなくなってきますね。
目次
進捗ダイアログ画面(ProgressForm)
デザイン
ソースコード
using System; using System.Windows.Forms; namespace WindowsFormsApp1 { /// <summary> /// 進行状況ダイアログを表示するためのクラスです。 /// </summary> public partial class ProgressForm : Form { /// <summary> /// コンストラクタです。画面デザイン上でセットしてしまった方がいいかも。 /// </summary> public ProgressForm() { InitializeComponent(); // label1.Text = string.Empty; // button1.Text = "キャンセル"; // FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false; ShowInTaskbar = false; Text = "進捗状況"; } // コントロールを公開したくないので(private から internal に変えてもいいんだけど、なんとなく) // 操作メソッドを準備・公開しておく // public void SetTitle(string value) { Text = value; } public void SetMessage(string value) { label1.Text = value; } // public void SetProgressMaximum(int value) { progressBar1.Maximum = value; } public void SetProgressMinimum(int value) { progressBar1.Minimum = value; } public void SetProgressValue(int value) { progressBar1.Value = value; } // public void AddButtonEvent(EventHandler action) { button1.Click += action; } } }
進捗ダイアログ画面を操作するクラス(ProgressDialog)
ソースコード
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApp1 { /// <summary> /// 進行状況ダイアログを操作するためのクラスです。 /// </summary> public class ProgressDialog : IDisposable { // private volatile bool _IsCanceled = false; private volatile bool _IsShown = false; private volatile bool _IsClosing = false; private volatile ProgressForm _ProgressForm = null; private Form OwnerForm = null; private volatile string _Title = "進捗状況"; private volatile int _Minimum = 0; private volatile int _Maximum = 100; private volatile int _Value = 0; private volatile string _Message = string.Empty; /// <summary> /// ダイアログのタイトルバーに表示する文字列 を取得、または設定します。 /// </summary> public string Title { get { return _Title; } set { _Title = value; InvokeForm(new Action(SetTitle)); } } /// <summary> /// ダイアログに表示するメッセージ を取得、または設定します。 /// </summary> public string Message { get { return _Message; } set { _Message = value; InvokeForm(new Action(SetMessage)); } } /// <summary> /// プログレスバーの最小値 を取得、または設定します。 /// </summary> public int Minimum { get { return _Minimum; } set { _Minimum = value; InvokeForm(new Action(SetProgressMinimum)); } } /// <summary> /// プログレスバーの最大値 を取得、または設定します。 /// </summary> public int Maximum { get { return _Maximum; } set { _Maximum = value; InvokeForm(new Action(SetProgressMaximum)); } } /// <summary> /// プログレスバーの現在値 を取得、または設定します。 /// </summary> public int Value { get { return _Value; } set { _Value = value; InvokeForm(new Action(SetProgressValue)); } } /// <summary> /// キャンセルボタンを押したかどうか を取得します。 /// </summary> public bool IsCanceled { get { return _IsCanceled; } } /// <summary> /// ダイアログを表示します。 /// </summary> /// <returns></returns> public async Task Show() { await Show(null); } /// <summary> /// ダイアログを表示します。 /// </summary> /// <param name="owner"></param> /// <remarks> /// このメソッドは一回しか呼び出せません。 /// </remarks> /// <returns></returns> public async Task Show(Form owner) { if (_IsShown) throw new InvalidOperationException("ダイアログは一度表示されています。"); _IsShown = true; _IsCanceled = false; OwnerForm = owner; await Task.Run(() => Run()); } public void Close() { _IsClosing = true; InvokeForm(new Action(_ProgressForm.Close)); } public void Dispose() { _IsClosing = true; InvokeForm(new Action(_ProgressForm.Dispose)); } // private bool IsAliveProgressForm() { return (_ProgressForm != null && !_ProgressForm.IsDisposed); } private void InvokeForm(Action action) { if (IsAliveProgressForm()) { _ProgressForm.Invoke(action); } } // private void SetTitle() { if (IsAliveProgressForm()) _ProgressForm.SetTitle(_Title); } private void SetMessage() { if (IsAliveProgressForm()) _ProgressForm.SetMessage(_Message); } // private void SetProgressMinimum() { if (IsAliveProgressForm()) _ProgressForm.SetProgressMinimum(_Minimum); } private void SetProgressMaximum() { if (IsAliveProgressForm()) _ProgressForm.SetProgressMaximum(_Maximum); } private void SetProgressValue() { if (IsAliveProgressForm()) _ProgressForm.SetProgressValue(_Value); } // private void Run() { // System.InvalidOperationException: 有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'Form1' がアクセスされました。 var dummy = Application.OpenForms[0]; if (dummy.InvokeRequired) { var action = new Action(Run); //dummy.Invoke(action, new object[] { }); // UI スレッド上で ShowDialog() なので、閉じるまで停止してしまう dummy.BeginInvoke(action, new object[] { }); // UI スレッド上で非同期で実行するから?ShowDialog() でも止まらない。なんでうまくいくんだったか忘れた...orz return; } // _ProgressForm = new ProgressForm(); //_ProgressForm.SetTitle(Title); _ProgressForm.Text = Title; if (OwnerForm != null) { //_ProgressForm.StartPosition = FormStartPosition.Manual; //_ProgressForm.Left = OwnerForm.Left + (OwnerForm.Width - _ProgressForm.Width) / 2; //_ProgressForm.Top = OwnerForm.Top + (OwnerForm.Height - _ProgressForm.Height) / 2; _ProgressForm.Owner = OwnerForm; _ProgressForm.StartPosition = FormStartPosition.CenterParent; } _ProgressForm.FormClosing += (sender, e) => { if (!_IsClosing) { e.Cancel = true; _IsCanceled = true; } }; // _ProgressForm.SetProgressMinimum(Minimum); _ProgressForm.SetProgressMaximum(Maximum); _ProgressForm.SetProgressValue(Value); _ProgressForm.AddButtonEvent(new EventHandler((sender, e) => { _IsCanceled = true; })); _ProgressForm.ShowDialog(); if (!_ProgressForm.IsDisposed) _ProgressForm.Dispose(); } } }
操作する側(Form1)
ソースコード
using System; using System.Threading.Tasks; using System.Windows.Forms; /* * Button を配置しています。 * */ namespace WindowsFormsApp1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private async void button1_Click(object sender, EventArgs e) { using (var dlg = new ProgressDialog()) { dlg.Title = "カウントアップ"; dlg.Minimum = 0; dlg.Maximum = 10; dlg.Value = 0; await dlg.Show(this); for (var i = 0; i < 10; i++) { dlg.Value = i + 1; dlg.Message = $"{i + 1} 番目を処理中..."; if (dlg.IsCanceled) break; await Task.Delay(1000); } if (dlg.IsCanceled) Text = "Canceled."; else Text = "Completed!"; } } } }