web-dev-qa-db-ja.com

UIスレッドからのGUI更新を強制する

WinFormsでは、UIスレッドからUIを即座に更新するにはどうすればよいですか?

私がやっていることは大体です:

label.Text = "Please Wait..."
try 
{
    SomewhatLongRunningOperation(); 
}
catch(Exception e)
{
    label.Text = "Error: " + e.Message;
    return;
}
label.Text = "Success!";

ラベルテキストは、操作の前に「お待ちください...」に設定されません。

操作に別のスレッドを使用してこれを解決しましたが、毛並みが悪くなり、コードを単純化したいと思います。

69
dbkk

最初は、なぜOPが回答の1つを回答としてまだマークしていないのか疑問に思いましたが、自分で試してみてもまだうまくいかないので、少し深く掘り下げて、この問題にはもっと多くのことがあると思いました想定。

同様の質問を読んで理解を深めることができます。 なぜプロセス中に更新/更新を制御しないのか

最後に、レコードについては、次の操作を行うことでラベルを更新することができました。

private void SetStatus(string status) 
{
    lblStatus.Text = status;
    lblStatus.Invalidate();
    lblStatus.Update();
    lblStatus.Refresh();
    Application.DoEvents();
}

私が理解していることからすると、これはエレガントで正しいアプローチとはほど遠いものです。これは、スレッドの混雑度に応じて機能する場合と機能しない場合があります。

93
Jagd

label.Invalidateを呼び出してからlabel.Update()-通常、更新は現在の関数を終了した後にのみ行われますが、Updateを呼び出すとコードの特定の場所で更新が強制されます。 [〜#〜] msdn [〜#〜] から:

Invalidateメソッドは、ペイントまたは再ペイントされるものを管理します。 Updateメソッドは、ペイントまたは再描画がいつ発生するかを管理します。 Refreshを呼び出すのではなく、InvalidateメソッドとUpdateメソッドを一緒に使用する場合、再描画される内容は、使用するInvalidateのオーバーロードによって異なります。 Updateメソッドは、コントロールをただちにペイントするように強制しますが、Invalidateメソッドは、Updateメソッドを呼び出したときにペイントされるものを管理します。

14
Dror Helper

ラベルを設定した後にApplication.DoEvents()を呼び出しますが、ユーザーがウィンドウを閉じることができるように、代わりにすべての作業を別のスレッドで行う必要があります。

13
Scoregraphic

私はちょうど同じ問題につまずいて、いくつかの興味深い情報を見つけたので、2セントを入れてここに追加したかったのです。

まず第一に、他の人が既に述べたように、長時間実行される操作はスレッドによって行われるべきです。スレッドはバックグラウンドワーカー、明示的なスレッド、スレッドプールからのスレッド、または(.Net 4.0以降)タスクです: Stackoverflow 570537:UIが応答し続けるように、windows-forms で処理中のラベルを更新します。

しかし、短いタスクの場合、スレッド化は実際には必要ありませんが、もちろん問題はありません。

この問題を分析するために、ボタンとラベルが1つあるwinformを作成しました。

_System::Void button1_Click(System::Object^  sender, System::EventArgs^  e)
{
  label1->Text = "Start 1";
  label1->Update();
  System::Threading::Thread::Sleep(5000); // do other work
}
_

私の分析では、コードをステップオーバーし(F10を使用)、何が起こったのかを確認していました。この記事を読んだ後、 WinFormsのマルチスレッド化 何か面白いことがわかりました。最初のページの最後にある記事では、現在実行されている関数が終了し、しばらくするとWindowsがウィンドウを「応答しない」とマークするまで、UIスレッドはUIを再描画できません。また、上からのテストアプリケーションでは、特定の場合にのみステップスルーすることに気付きました。

(次のテストでは、Visual Studioをフルスクリーンに設定しないことが重要です。その横にある小さなアプリケーションウィンドウを同時に表示できる必要があります。デバッグのためにVisual Studioウィンドウとアプリケーションを起動して、アプリケーションを起動し、_label1->Text ..._にブレークポイントを設定し、VSウィンドウの横にアプリケーションウィンドウを置き、VSウィンドウの上にマウスカーソルを置きます。

  1. アプリの起動後にVSを1回クリックすると(そこにfocuesを配置してステッピングを有効にします)、マウスを動かさずにステップスルーすると、新しいテキストが設定され、更新時にラベルが更新されます()function。これは、UIが明らかに再描画されることを意味します。

  2. 最初の行をステップオーバーした後、マウスをたくさん動かしてどこかをクリックし、さらにステップすると、新しいテキストが設定され、update()関数が呼び出されますが、UIは更新/再描画されず、古いテキストbutton1_click()関数が終了するまでそこに残ります。再描画する代わりに、ウィンドウは「応答しない」とマークされます!また、フォーム全体を更新するためにthis->Update();を追加しても役に立ちません。

  3. Application::DoEvents();を追加すると、UIに更新/再ペイントの機会が与えられます。とにかく、ユーザーがボタンを押したり、許可されていないUIで他の操作を実行できないように注意する必要があります!!したがって: DoEvents()! を避けてください。スレッド化の使用をお勧めします(.Netでは非常に単純だと思います)。
    しかし、(@ Jagd、April 2 '10 at 19:25.refresh().invalidate()

私の説明は次のとおりです:AFAIK winformはまだWINAPI関数を使用しています。 System.Windows.Forms Control.Updateメソッドに関するMSDNの記事 は、WINAPI関数WM_Paintを指します。 MSDNのWM_Paint に関する記事では、最初の文で、WM_Paintコマンドはメッセージキューが空の場合にのみシステムによって送信されると述べています。ただし、メッセージキューは2番目のケースで既にいっぱいになっているため、送信されず、ラベルとアプリケーションフォームは再描画されません。

<> joke>結論:したがって、ユーザーがマウスを使用しないようにする必要があります;-) <>/joke>

4
Tobias Knauss

これを試すことができます

using System.Windows.Forms; // u need this to include.

MethodInvoker updateIt = delegate
                {
                    this.label1.Text = "Started...";
                };
this.label1.BeginInvoke(updateIt);

動作するかどうかを確認してください。

3
Rick2047

UIを更新した後、長時間実行される操作で実行するタスクを開始します。

label.Text = "Please Wait...";

Task<string> task = Task<string>.Factory.StartNew(() =>
{
    try
    {
        SomewhatLongRunningOperation();
        return "Success!";
    }
    catch (Exception e)
    {
        return "Error: " + e.Message;
    }
});
Task UITask = task.ContinueWith((ret) =>
{
    label.Text = ret.Result;
}, TaskScheduler.FromCurrentSynchronizationContext());

これは.NET 3.5以降で機能します。

2
pixelgrease

これを「修正」してUI更新を強制するのは非常に魅力的ですが、最善の修正は、イベントに応答できるように、UIスレッドを拘束せずにバックグラウンドスレッドでこれを行うことです。

1
Mike Hall
1
Chetan

上記と少しの実験から抽出した答えがあると思います。

_progressBar.Value = progressBar.Maximum - 1;
progressBar.Maximum = progressBar.Value;
_

値をデクリメントしようとしましたが、デバッグモードでも画面が更新されましたが、プログレスバーの値を最大値以上に設定できないため、_progressBar.Value_を_progressBar.Maximum_に設定しても機能しません。 _progressBar.Value_を_progressBar.Maximum -_ 1に設定してから、_progressBar.Maxiumum_を_progressBar.Valu_ eと等しく設定します。猫を殺す方法は複数あると言われています。ビル・ゲイツを殺したいときもあれば、今誰でも殺したいときもある:o)。

この結果では、Invalidate()Refresh()Update()、またはプログレスバーやそのPanelコンテナまたは親に対して何もする必要はないようです。形。

1
Graham Bennett

いくつかのコントロールのみを更新する必要がある場合は、.update()で十分です。

btnMyButton.BackColor=Color.Green; // it eventually turned green, after a delay
btnMyButton.Update(); // after I added this, it turned green quickly
1
user3029478

プロパティEnabledで同じ問題が発生し、first chance exceptionが原因で発生しますスレッドセーフではありません。 「C#の別のスレッドからGUIを更新する方法」に関する解決策を見つけました。ここ https://stackoverflow.com/a/661706/1529139 そして、それは動作します!

0
56ka