26

作業中にプログレスバーを表示したいのですが、UIがハングし、プログレスバーが更新されません。

マーキーProgressBar方式で無期限に継続するWinFormProgressFormがあります。

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

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

EndInvokeで問題が発生していますが、それは問題ではありません。問題は、そのような状況を処理するために使用する最良かつ最も簡単な方法であり、プログラムが機能していて応答しないことをユーザーに示さなければならない場合、そして効率的で効果のない可能な限り最も単純なコードでそれをどのように処理するかです。リークし、GUIを更新できます。

同様BackgroundWorkerに、複数の関数を持っている必要があり、メンバー変数を宣言する必要があります。また、ProgressBarフォームへの参照を保持し、それを破棄する必要があります。

編集BackgroundWorker進行状況の通知が届かない可能性があるため、答えではありません。これは、外部関数への単一の呼び出しであるため、への呼び出しがないことを意味しますが、進行状況バーの呼び出しを続ける必要がProgressChangedあります回転し続けるために。DoWorkApplication.DoEvents();

恩恵は、この問題に対する最良のコードソリューションです。ワーカー関数がメインスレッドで機能している間、Marqueプログレスバーが機能するように呼び出す必要がありApplication.DoEvents()ますが、進行状況の通知は返されません。進行状況を自動的に報告するために.NETマジックコードは必要ありませんでした。次よりも優れたソリューションが必要でした。

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

プログレスバーを維持します(フリーズしないことを意味しますが、マークを更新します)

4

13 に答える 13

44

あなたは少なくとも1つの誤った仮定に基づいて行動しているように私には思えます。

1.レスポンシブUIを使用するために、ProgressChangedイベントを発生させる必要はありません。

あなたの質問であなたはこれを言います:

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

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

(ちなみにDoWork、これはイベントです。これは、単なる「外部関数への単一の呼び出し」ではないことを意味します。必要な数のハンドラーを追加できます。これらの各ハンドラーには、必要な数の関数呼び出しを含めることができます。好きです。)

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

私には、GUIを更新する唯一のApplication.DoEvents方法は、次のコマンドを呼び出すことであるとあなたが信じているように思えます。

Application.DoEvents();を呼び出し続ける必要があります。プログレスバーが回転し続けるようにします。

これは、マルチスレッドシナリオでは当てはまりません。を使用すると、GUIは、そのイベントに関連付けられているすべての処理を実行しているBackgroundWorker間、(独自のスレッドで)応答し続けます。以下は、これがどのように機能するかを示す簡単な例です。BackgroundWorkerDoWork

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つのメソッドを同時に実行することはできません

あなたはこれを言います:

ワーカー関数がメインスレッドで機能している間にMarqueプログレスバーが機能するように、Application.DoEvents()を呼び出す必要があります。。。

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

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

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

于 2009-12-30T02:31:53.013 に答える
16

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

于 2009-12-23T11:27:52.120 に答える
11

Stackoverflowで.NET/C#を使用したスレッド化に関する情報はたくさんありますが、Windowsフォームのスレッド化をクリアした記事は、常駐のオラクルであるJonSkeetの「Windowsフォームでのスレッド化」でした。

シリーズ全体を読んで、知識を磨いたり、最初から学んだりする価値があります。

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

「コードを見せて」に関する限り、以下はC#3.5でそれを行う方法です。フォームには4つのコントロールが含まれています。

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

buttonAnother100までカウントするタスクの実行中に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;
    }
}
于 2009-12-23T12:09:52.460 に答える
9

そして、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);
                    }
                }));
        }
    }
}
于 2009-12-28T15:17:02.547 に答える
3

確かにあなたは正しい軌道に乗っています。別のスレッドを使用する必要があり、それを行うための最良の方法を特定しました。残りはプログレスバーを更新するだけです。他の人が提案しているように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;
    }
}

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

于 2009-12-23T11:32:02.037 に答える
2

BackgroundWorker進捗通知が届かない可能性があるので、答えではありません...

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

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

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

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

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

それは、BackgroundWorker適切に機能しなくなることであり、それがすべての苦痛の原因となるでしょう。

于 2009-12-28T19:33:41.203 に答える
1

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

これにより、少なくともユーザーに処理の外観を与え、ボタンをクリックして何も起こらないことを確認してからさらにクリックするのではなく、待つことをユーザーに理解させることができます。

于 2009-12-28T19:04:03.590 に答える
1

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

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から

于 2016-07-27T16:18:44.917 に答える
0

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

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

于 2009-12-23T11:27:05.400 に答える
0

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();
于 2009-12-23T11:30:20.027 に答える
0

要件を読み取る最も簡単な方法は、モードのないフォームを表示し、標準のSystem.Windows.Formsタイマーを使用して、モードのないフォームの進行状況を更新することです。スレッドやメモリリークの可能性はありません。

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

于 2009-12-23T11:35:17.877 に答える
0

Re:あなたの編集。作業を行うにはBackgroundWorkerまたはThreadが必要ですが、定期的にReportProgress()を呼び出して、UIスレッドに何を行っているかを通知する必要があります。DotNetは、あなたが行った作業の量を魔法のように計算することはできないので、(a)到達する最大進行量を伝え、(b)プロセス中に約100回ほど伝える必要があります。それはあなたがしている金額です。(進行状況を100回未満で報告すると、進行状況バーが大きくジャンプします。100回を超えて報告すると、進行状況バーが表示するよりも細かい詳細を報告しようとして時間を無駄にするだけです)

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

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

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

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

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

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

于 2009-12-28T18:50:04.863 に答える
0

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

于 2009-12-30T12:31:24.640 に答える