web-dev-qa-db-ja.com

C#で作業中に進行状況バーを表示しますか?

作業中に進行状況バーを表示したいのですが、UIがハングし、進行状況バーが更新されません。

WinForm ProgressFormにはProgressBarがあり、これはMarqueeの方法で無期限に継続します。

_using(ProgressForm p = new ProgressForm(this))
{
//Do Some Work
}
_

BeginInvokeを使用する、タスクが完了するのを待ってEndInvokeを呼び出すなど、問題を解決する方法は多数あります。または、BackgroundWorkerまたはThreadsを使用します。

EndInvokeにはいくつかの問題がありますが、それは問題ではありません。問題は、そのような状況を処理するために使用する最良かつ最も簡単な方法です。ユーザーにプログラムが機能し、応答しないことをユーザーに示す必要があります。 tリークし、GUIを更新できます。

BackgroundWorkerのように、複数の関数が必要な場合、メンバー変数を宣言する必要があります。また、ProgressBarフォームへの参照を保持して破棄する必要があります。

編集BackgroundWorkerは答えではありません。進行状況の通知を受け取れない可能性があるためです。つまり、ProgressChangedとしてDoWorkが呼び出されない可能性があります。 ] _は外部関数への単一の呼び出しですが、プログレスバーが回転し続けるためにApplication.DoEvents();を呼び出し続ける必要があります。

報奨金は、この問題に最適なコードソリューションです。 Application.DoEvents()を呼び出すだけで、Marqueプログレスバーは機能しますが、ワーカー関数はメインスレッドで機能し、進捗通知は返されません。進行状況を自動的に報告するための.NETマジックコードは必要ありませんでした。

_Action<String, String> exec = DoSomethingLongAndNotReturnAnyNotification;
IAsyncResult result = exec.BeginInvoke(path, parameters, null, null);
while (!result.IsCompleted)
{
  Application.DoEvents();
}
exec.EndInvoke(result);
_

プログレスバーを維持します(フリーズではなく、マーキーを更新します)

26
Priyank Bolia

少なくとも1つの誤った仮定で操作しているように思われます

1. ProgressChangedイベントを発生させてレスポンシブUIを作成する必要はありません。

あなたの質問でこれを言う:

BackgroundWorkerは答えではありません。進行状況の通知を受け取れない可能性があるためです。つまり、DoWorkは外部関数への単一の呼び出しであるため、ProgressChangedの呼び出しはありません。 。 。

実際、ProgressChangedイベントを呼び出すかどうかは関係ありません。そのイベントの全体的な目的は、一時的に制御をGUIスレッドに戻して、BackgroundWorkerによって行われている作業の進行状況を何らかの形で反映する更新を行うことです。 単にマーキープログレスバーを表示する場合、実際にはProgressChangedイベントを発生させることは無意味ですBackgroundWorkerはGUIとは別のスレッドで作業を行っているであるため、進行状況バーは表示されている限り回転し続けます。

(補足として、DoWorkはイベントです。つまり、just「外部関数への単一の呼び出し」ではないことを意味します。ハンドラーはいくつでも追加できます。など;そして、これらの各ハンドラーには、好きなだけ関数呼び出しを含めることができます。)

2.レスポンシブUIを使用するためにApplication.DoEventsを呼び出す必要はありません。

私には、唯一のGUIを更新する方法はApplication.DoEventsを呼び出すことだと信じているように聞こえます:

Application.DoEvents()を呼び出し続ける必要があります。進行状況バーが回転し続けるために。

これはマルチスレッドシナリオでは当てはまりません; BackgroundWorkerを使用する場合、BackgroundWorkerDoWorkイベントにアタッチされたものを実行する間、GUIは(独自のスレッドで)応答し続けます。以下は、これがどのように機能するかの簡単な例です。

private void ShowProgressFormWhileBackgroundWorkerRuns() {
    // this is your presumably long-running method
    Action<string, string> exec = DoSomethingLongAndNotReturnAnyNotification;

    ProgressForm p = new ProgressForm(this);

    BackgroundWorker b = new BackgroundWorker();

    // set the worker to call your long-running method
    b.DoWork += (object sender, DoWorkEventArgs e) => {
        exec.Invoke(path, parameters);
    };

    // set the worker to close your progress form when it's completed
    b.RunWorkerCompleted += (object sender, RunWorkerCompletedEventArgs e) => {
        if (p != null && p.Visible) p.Close();
    };

    // now actually show the form
    p.Show();

    // this only tells your BackgroundWorker to START working;
    // the current (i.e., GUI) thread will immediately continue,
    // which means your progress bar will update, the window
    // will continue firing button click events and all that
    // good stuff
    b.RunWorkerAsync();
}

3.同じスレッドで2つのメソッドを同時に実行することはできません

あなたはこれを言う:

Application.DoEvents()を呼び出すだけで、Marqueプログレスバーが機能し、ワーカー関数がメインスレッドで機能します。 。 。

あなたが求めているのは単に本物ではないです。 Windowsフォームアプリケーションの「メイン」スレッドはGUIスレッドです。GUIスレッドは、長時間実行されるメソッドでビジーな場合、視覚的な更新を提供しません。そうでないと思われる場合は、BeginInvokeの動作を誤解していると思われます。デリゲートを起動します別のスレッドで。実際、Application.DoEventsexec.BeginInvokeの間でexec.EndInvokeを呼び出すために質問に含めたサンプルコードは冗長です。実際にGUIスレッドからApplication.DoEventsを繰り返し呼び出していますとにかく更新されます。 (別の方法で見つかった場合は、exec.EndInvokeをすぐに呼び出し、メソッドが終了するまで現在のスレッドをブロックしたためだと思います。)

そう、あなたが探している答えはBackgroundWorkerを使用することです。

couldBeginInvokeを使用しますが、GUIスレッドからEndInvokeを呼び出す代わりに(メソッドが終了していない場合はブロックします)、AsyncCallbackパラメーターをBeginInvoke呼び出しに渡します(単にnullを渡すのではなく)、コールバックの進捗フォームを閉じます。ただし、それを行う場合は、GUIスレッドから進捗フォームを閉じるメソッドを呼び出す必要があることに注意してください。そうしないと、GUI関数であるフォームを閉じようとするためです。非GUIスレッド。しかし、実際には、BeginInvoke/EndInvokeを使用することのすべての落とし穴は、既に「.NETマジックコード」と思われる場合でも、BackgroundWorkerクラスを使用してforを処理しています。私、それは直感的で便利なツールです)。

43
Dan Tao

私にとって最も簡単な方法は、この種のタスク用に特別に設計されたBackgroundWorkerを使用することです。 ProgressChangedイベントは、スレッド間の呼び出しを心配することなく、進行状況バーを更新するのに最適です。

16
Thomas Levesque

スレッド化に関する情報の負荷 Stackoverflow上の.NET/C#がありますが、Windowsフォームスレッド化をクリアした記事は、常駐のOracleであるJon Skeetの "Windows Formsのスレッド化"

シリーズ全体を読んで、知識を磨くか、ゼロから学ぶ価値があります。

せっかちです、ただコードを見せてください

「コードの表示」に関する限り、C#3.5でどのように実行するかを以下に示します。フォームには4つのコントロールが含まれています。

  • テキストボックス
  • プログレスバー
  • 2つのボタン:「buttonLongTask」および「buttonAnother」

buttonAnotherは、100カウントまでのタスクの実行中にUIがブロックされないことを示すためだけのものです。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void buttonLongTask_Click(object sender, EventArgs e)
    {
        Thread thread = new Thread(LongTask);
        thread.IsBackground = true;
        thread.Start();
    }

    private void buttonAnother_Click(object sender, EventArgs e)
    {
        textBox1.Text = "Have you seen this?";
    }

    private void LongTask()
    {
        for (int i = 0; i < 100; i++)
        {
            Update1(i);
            Thread.Sleep(500);
        }
    }

    public void Update1(int i)
    {
        if (InvokeRequired)
        {
            this.BeginInvoke(new Action<int>(Update1), new object[] { i });
            return;
        }

        progressBar1.Value = i;
    }
}
10
Chris S

そして、BackgroundWorkerがそれを行う正しい方法であるという別の例...

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

namespace SerialSample
{
    public partial class Form1 : Form
    {
        private BackgroundWorker _BackgroundWorker;
        private Random _Random;

        public Form1()
        {
            InitializeComponent();
            _ProgressBar.Style = ProgressBarStyle.Marquee;
            _ProgressBar.Visible = false;
            _Random = new Random();

            InitializeBackgroundWorker();
        }

        private void InitializeBackgroundWorker()
        {
            _BackgroundWorker = new BackgroundWorker();
            _BackgroundWorker.WorkerReportsProgress = true;

            _BackgroundWorker.DoWork += (sender, e) => ((MethodInvoker)e.Argument).Invoke();
            _BackgroundWorker.ProgressChanged += (sender, e) =>
                {
                    _ProgressBar.Style = ProgressBarStyle.Continuous;
                    _ProgressBar.Value = e.ProgressPercentage;
                };
            _BackgroundWorker.RunWorkerCompleted += (sender, e) =>
            {
                if (_ProgressBar.Style == ProgressBarStyle.Marquee)
                {
                    _ProgressBar.Visible = false;
                }
            };
        }

        private void buttonStart_Click(object sender, EventArgs e)
        {
            _BackgroundWorker.RunWorkerAsync(new MethodInvoker(() =>
                {
                    _ProgressBar.BeginInvoke(new MethodInvoker(() => _ProgressBar.Visible = true));
                    for (int i = 0; i < 1000; i++)
                    {
                        Thread.Sleep(10);
                        _BackgroundWorker.ReportProgress(i / 10);
                    }
                }));
        }
    }
}
9
Oliver

確かにあなたは正しい軌道に乗っています。別のスレッドを使用する必要があり、そのための最良の方法を特定しました。残りは進行状況バーを更新するだけです。他の人が提案しているようにBackgroundWorkerを使用したくない場合は、留意すべきトリックが1つあります。コツは、UIはUIスレッドからしか操作できないため、ワーカースレッドからプログレスバーを更新できないことです。したがって、Invokeメソッドを使用します。それは次のようになります(構文エラーを自分で修正してください、私は簡単な例を書いています):

class MyForm: Form
{
    private void delegate UpdateDelegate(int Progress);

    private void UpdateProgress(int Progress)
    {
        if ( this.InvokeRequired )
            this.Invoke((UpdateDelegate)UpdateProgress, Progress);
        else
            this.MyProgressBar.Progress = Progress;
    }
}

InvokeRequiredプロパティは、フォームを所有するスレッドを除くすべてのスレッドでtrueを返します。 Invokeメソッドは、UIスレッドでメソッドを呼び出し、完了するまでブロックします。ブロックしたくない場合は、代わりにBeginInvokeを呼び出すことができます。

3
Vilx-

BackgroundWorkerは答えではありません。進行状況の通知を受け取れない可能性があるためです...

BackgroundWorkerの使用と、進捗通知を受け取っていないという事実は一体どういう関係にありますか?長時間実行されるタスクに、進行状況を報告するための信頼できるメカニズムがない場合、その進行状況を確実に報告する方法はありません。

長時間実行されるメソッドの進行状況を報告する最も簡単な方法は、UIスレッドでメソッドを実行し、進行状況バーを更新してからApplication.DoEvents()を呼び出して進行状況を報告させることです。これは、技術的には機能します。ただし、UIはApplication.DoEvents()の呼び出し間で応答しなくなります。これは迅速で汚れた解決策であり、スティーブ・マッコネルが観察するように、迅速で汚れた解決策の問題は、汚れの苦味が、迅速な甘さを忘れた後も長く残ることです。

次の最も簡単な方法は、別のポスターが示すように、BackgroundWorkerを使用して長時間実行されるメソッドを実行するモーダルフォームを実装することです。これにより、一般的にユーザーエクスペリエンスが向上し、長時間実行されるタスクの実行中にUIのどの部分が機能するかという潜在的に複雑な問題を解決する必要がなくなります。モーダルフォームが開いている間は、 UIはユーザーアクションに応答します。これは迅速でクリーンなソリューションです。

しかし、それでもユーザーはかなり敵対的です。長時間実行されるタスクの実行中にUIをロックします。それはきれいな方法でそれを行います。ユーザーフレンドリーなソリューションを作成するには、別のスレッドでタスクを実行する必要があります。これを行う最も簡単な方法は、BackgroundWorkerを使用することです。

このアプローチは、多くの問題への扉を開きます。それが意味することになっているものは何でも「漏れ」ません。しかし、実行時間の長いメソッドが何をしているにせよ、実行中に有効のままになっているUIの各部分から完全に分離して行う必要があります。そして完全とは、完全を意味します。ユーザーがマウスで任意の場所をクリックして、長時間実行するメソッドがこれまで見たオブジェクトに何らかの更新を行うことができる場合、問題が発生します。イベントを発生させる可能性のある、長時間実行されるメソッドが使用するオブジェクトは、悲惨さへの潜在的な道です。

それは、BackgroundWorkerが適切に動作しないことであり、それがすべての痛みの原因になります。

2
Robert Rossney

私はそこに最も簡単な答えを投げ出さなければなりません。常に進行状況バーを実装するだけで、実際の進行状況とは何の関係もありません。バーの塗りつぶしを開始すると、1秒に1%、または1秒に10%で、アクションに似ていると思われるものが何であれ、それがいっぱいになってから再び開始されます。

これにより、少なくともユーザーに処理の外観が与えられ、ボタンをクリックするだけで何も起こらずにクリックするのではなく、待つことを理解できるようになります。

1
Chris Marisic

BackgroundWorkerを使用してProgressBarを更新し、BackgroundWorkerProgressbarをメインフォームに追加して以下のコードを使用する別のサンプルコードを次に示します。

public partial class Form1 : Form
{
    public Form1()
    {
      InitializeComponent();
      Shown += new EventHandler(Form1_Shown);

    // To report progress from the background worker we need to set this property
    backgroundWorker1.WorkerReportsProgress = true;
    // This event will be raised on the worker thread when the worker starts
    backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
    // This event will be raised when we call ReportProgress
    backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
}
void Form1_Shown(object sender, EventArgs e)
{
    // Start the background worker
    backgroundWorker1.RunWorkerAsync();
}
// On worker thread so do our thing!
void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    // Your background task goes here
    for (int i = 0; i <= 100; i++)
    {
        // Report progress to 'UI' thread
        backgroundWorker1.ReportProgress(i);
        // Simulate long task
        System.Threading.Thread.Sleep(100);
    }
}
// Back on the 'UI' thread so we can update the progress bar
void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    // The progress percentage is a property of e
    progressBar1.Value = e.ProgressPercentage;
}
}

refrence: codeprojectから

1
Hamid

要件を最も簡単に読み取るには、モードレスフォームを表示し、標準のSystem.Windows.Formsタイマーを使用してモードレスフォームの進捗を更新します。スレッドなし、メモリリークの可能性なし。

これは1つのUIスレッドのみを使用するため、進行状況バーが視覚的に更新されることを保証するために、メイン処理中の特定の時点でApplication.DoEvents()を呼び出す必要もあります。

0
Ash

「回転」プログレスバーが必要な場合は、プログレスバーのスタイルを「マーキー」に設定し、BackgroundWorkerを使用してUIの応答性を維持してください。 「マーキー」スタイルを使用するよりも簡単に回転プログレスバーを達成することはできません...

0

まさにこのシナリオ用に設計されたBackgroundWorkerコンポーネントを使用してください。

進行状況更新イベントにフックして、進行状況バーを更新できます。 BackgroundWorkerクラスは、コールバックがUIスレッドにマーシャリングされることを保証するため、その詳細についても心配する必要はありません。

0
Paolo

このようなことのために、BackgroundWorkerでモーダルフォームを使用しています。

ここに簡単な解決策があります:

  public class ProgressWorker<TArgument> : BackgroundWorker where TArgument : class 
    {
        public Action<TArgument> Action { get; set; }

        protected override void OnDoWork(DoWorkEventArgs e)
        {
            if (Action!=null)
            {
                Action(e.Argument as TArgument);
            }
        }
    }


public sealed partial class ProgressDlg<TArgument> : Form where TArgument : class
{
    private readonly Action<TArgument> action;

    public Exception Error { get; set; }

    public ProgressDlg(Action<TArgument> action)
    {
        if (action == null) throw new ArgumentNullException("action");
        this.action = action;
        //InitializeComponent();
        //MaximumSize = Size;
        MaximizeBox = false;
        Closing += new System.ComponentModel.CancelEventHandler(ProgressDlg_Closing);
    }
    public string NotificationText
    {
        set
        {
            if (value!=null)
            {
                Invoke(new Action<string>(s => Text = value));  
            }

        }
    }
    void ProgressDlg_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        FormClosingEventArgs args = (FormClosingEventArgs)e;
        if (args.CloseReason == CloseReason.UserClosing)
        {
            e.Cancel = true;
        }
    }



    private void ProgressDlg_Load(object sender, EventArgs e)
    {

    }

    public void RunWorker(TArgument argument)
    {
        System.Windows.Forms.Application.DoEvents();
        using (var worker = new ProgressWorker<TArgument> {Action = action})
        {
            worker.RunWorkerAsync();
            worker.RunWorkerCompleted += worker_RunWorkerCompleted;                
            ShowDialog();
        }
    }

    void worker_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
    {
        if (e.Error != null)
        {
            Error = e.Error;
            DialogResult = DialogResult.Abort;
            return;
        }

        DialogResult = DialogResult.OK;
    }
}

そして、それをどう使うか:

var dlg = new ProgressDlg<string>(obj =>
                                  {
                                     //DoWork()
                                     Thread.Sleep(10000);
                                     MessageBox.Show("Background task completed "obj);
                                   });
dlg.RunWorker("SampleValue");
if (dlg.Error != null)
{
  MessageBox.Show(dlg.Error.Message, "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
dlg.Dispose();
0
Sergey Mirvoda

再:あなたの編集。作業を行うにはBackgroundWorkerまたはThreadが必要ですが、ReportProgress()を定期的に呼び出して、UIスレッドに実行内容を通知する必要があります。 DotNetは、あなたが行った作業の量を魔法のように解決することはできません。そのため、(a)到達する最大進捗量は何であるか、そして(b)プロセス中に約100回程度、それはあなたがどれだけの量かです。 (進行状況を100回未満報告すると、進行状況バーは大きなステップでジャンプします。100回以上報告すると、進行状況バーが表示されるよりも詳細な情報を報告しようとして時間を浪費することになります)

バックグラウンドワーカーの実行中にUIスレッドが問題なく続行できる場合、作業は完了です。

ただし、現実的には、進行状況の表示を実行する必要があるほとんどの状況では、再入可能な呼び出しを回避するためにUIを非常に慎重にする必要があります。例えばデータのエクスポート中に進行状況表示を実行している場合、エクスポートの進行中にユーザーがデータのエクスポートを再開できるようにしたくありません。

これは次の2つの方法で処理できます。

  • エクスポート操作は、バックグラウンドワーカーが実行されているかどうかを確認し、既にインポートしている間はエクスポートオプションを無効にします。これにより、ユーザーはエクスポート以外のプログラムで何でもできるようになります。これは、ユーザーが(たとえば)エクスポートされるデータを編集できる場合、依然として危険です。

  • プログレスバーを「モーダル」表示として実行し、エクスポート中にプログラムが「生き残る」ようにしますが、エクスポートが完了するまでユーザーは実際には何もできません(キャンセル以外)。 DotNetは、これが最も一般的なアプローチであるにもかかわらず、これをサポートするのが面倒です。この場合、UIスレッドをApplication.DoEvents()を呼び出すビジー待機ループに入れてメッセージ処理を実行し続ける必要があります(したがって、進行状況バーが機能します)が、アプリケーションのみを許可するMessageFilterを追加する必要があります「安全な」イベントに応答する(たとえば、ペイントイベントを許可してアプリケーションウィンドウを再描画し続けるが、マウスとキーボードメッセージを除外して、エクスポートの進行中にユーザーが実際にプログラムで何もできないようにするまた、ウィンドウを通常どおりに動作させるために通過する必要がある卑劣なメッセージがいくつかあり、これらを理解するには数分かかります-作業中のリストがありますが、持っていませんNCHITTESTのような明らかなものに加えて、この動作を実現するために不可欠な(.WM_USERの範囲内の)卑劣な.netのものもあります)。

ひどいdotNet進行状況バーの最後の「落とし穴」は、操作を終了して進行状況バーを閉じると、「80%」などの値を報告すると通常終了することです。 100%に強制してから約0.5秒待機しても、100%に到達しない場合があります。あら!解決策は、進行状況を100%に設定してから99%に設定してから、100%に戻すことです。進行状況バーが前方に移動するように指示されると、目標値に向かってゆっくりアニメーションします。しかし、「後方」に進むように指示すると、すぐにその位置にジャンプします。そのため、最後に一時的に逆にすることで、表示するように要求した値を実際に表示することができます。

0
Jason Williams