web-dev-qa-db-ja.com

別のスレッドからUIメソッドを呼び出す方法

タイマーで遊んでいます。コンテキスト:2つのラベルを持つwinforms。

System.Timers.Timerは機能するため、Formsタイマーは使用していません。フォームとmyTimerが異なるスレッドで実行されることを理解しています。 lblValueの経過時間を次の形式で表す簡単な方法はありますか?

私はここで [〜#〜] msdn [〜#〜] を見ましたが、もっと簡単な方法があります!

Winformsのコードは次のとおりです。

using System.Timers;

namespace Ariport_Parking
{
  public partial class AirportParking : Form
  {
    //instance variables of the form
    System.Timers.Timer myTimer;
    int ElapsedCounter = 0;

    int MaxTime = 5000;
    int elapsedTime = 0;
    static int tickLength = 100;

    public AirportParking()
    {
        InitializeComponent();
        keepingTime();
        lblValue.Text = "hello";
    }

    //method for keeping time
    public void keepingTime() {

        myTimer = new System.Timers.Timer(tickLength); 
        myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);

        myTimer.AutoReset = true;
        myTimer.Enabled = true;

        myTimer.Start();
    }


    void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){

        myTimer.Stop();
        ElapsedCounter += 1;
        elapsedTime += tickLength; 

        if (elapsedTime < MaxTime)
        {
            this.lblElapsedTime.Text = elapsedTime.ToString();

            if (ElapsedCounter % 2 == 0)
                this.lblValue.Text = "hello world";
            else
                this.lblValue.Text = "hello";

            myTimer.Start(); 

        }
        else
        { myTimer.Start(); }

    }
  }
}
21
whytheq

コードは単なるテストであるため、タイマーで何をするかについては説明しません。ここでの問題は、タイマーコールバック内のユーザーインターフェイスコントロールで何かを行う方法です。

Controlのほとんどのメソッドとプロパティは、UIスレッドからのみアクセスできます(実際には、それらを作成したスレッドからのみアクセスできますが、これは別の話です)。これは、各スレッドが独自のメッセージループ(GetMessage()がスレッドごとにメッセージをフィルタリングする)を持たなければならず、Controlを使用して何かを行うには、スレッドからmainスレッド。 .NETでは、すべてのControlがこの目的のためにいくつかのメソッドを継承するため、簡単です:Invoke/BeginInvoke/EndInvoke。実行中のスレッドがそれらのメソッドを呼び出す必要があるかどうかを知るには、プロパティInvokeRequiredがあります。これを使用してコードを変更するだけで機能します:

if (elapsedTime < MaxTime)
{
    this.BeginInvoke(new MethodInvoker(delegate 
    {
        this.lblElapsedTime.Text = elapsedTime.ToString();

        if (ElapsedCounter % 2 == 0)
            this.lblValue.Text = "hello world";
        else
            this.lblValue.Text = "hello";
    }));
}

どのスレッドからも呼び出すことができるメソッドのリストについては、MSDNを確認してください。参照として常にInvalidateBeginInvokeEndInvokeInvokeメソッドを呼び出すことができますInvokeRequiredプロパティを読み取ります。一般的に、これは一般的な使用パターンです(thisControlから派生したオブジェクトであると仮定):

void DoStuff() {
    // Has been called from a "wrong" thread?
    if (InvokeRequired) {
        // Dispatch to correct thread, use BeginInvoke if you don't need
        // caller thread until operation completes
        Invoke(new MethodInvoker(DoStuff));
    } else {
        // Do things
    }
}

UIスレッドがメソッドの実行を完了するまで、現在のスレッドはブロックされることに注意してください。これは、スレッドのタイミングが重要な場合に問題になる可能性があります(UIスレッドがビジーであるか、少しハングする可能性があることを忘れないでください)。メソッドの戻り値が必要ない場合は、単にInvokeBeginInvokeに置き換えることができます。WinFormsの場合、EndInvokeへの後続の呼び出しさえ必要ありません。

void DoStuff() {
    if (InvokeRequired) {
        BeginInvoke(new MethodInvoker(DoStuff));
    } else {
        // Do things
    }
}

戻り値が必要な場合は、通常のIAsyncResultインターフェイスを処理する必要があります。

使い方?

GUI Windowsアプリケーションは、メッセージループを持つウィンドウプロシージャに基づいています。プレーンCでアプリケーションを作成する場合、次のようなものがあります。

MSG message;
while (GetMessage(&message, NULL, 0, 0))
{
    TranslateMessage(&message);
    DispatchMessage(&message);
}

これらの数行のコードで、アプリケーションはメッセージを待ってからウィンドウプロシージャにメッセージを配信します。ウィンドウプロシージャは、メッセージを確認する大きなswitch/caseステートメントです(WM_)知っていて、何らかの方法で処理します(WM_Paint、あなたはWM_QUIT 等々)。

作業スレッドがあると想像してください。どのようにメインスレッドをcallできますか?最も簡単な方法は、この基本構造を使用してトリックを行うことです。タスクを単純化しますが、次の手順を実行します。

  • 呼び出す関数の(スレッドセーフな)キューを作成します(いくつかの例 ここでSO )。
  • ウィンドウプロシージャにカスタムメッセージを投稿します。このキューを優先度キューにすると、これらの呼び出しの優先度を決定することもできます(たとえば、動作中のスレッドからの進行状況通知は、アラーム通知よりも優先度が低い場合があります)。
  • (switch/caseステートメント内の)ウィンドウプロシージャで、そのメッセージをunderstandしてから、キューから呼び出して呼び出す関数をピークできます。

WPFとWinFormsはどちらもこのメソッドを使用して、スレッドからUIスレッドにメッセージを配信(ディスパッチ)します。複数のスレッドとユーザーインターフェイスの詳細については MSDNのこの記事 を参照してください。WinFormsはこれらの詳細の多くを隠しており、それらの世話をする必要はありませんが、ボンネットの下で動作します。

35
Adriano Repetti

個人的には、UIのスレッド外で動作するアプリケーションで作業するとき、私は通常この小さな断片を書きます。

private void InvokeUI(Action a)
{
    this.BeginInvoke(new MethodInvoker(a));
}

異なるスレッドで非同期呼び出しを行うと、常にコールバックを使用できます。

InvokeUI(() => { 
   Label1.Text = "Super Cool";
});

シンプルできれい。

7

質問通り、クロススレッドコールをチェックし、変数の更新を同期し、タイマーを停止および開始せず、経過時間をカウントするためにタイマーを使用しない私の答えがあります。

[〜#〜] edit [〜#〜] fixed BeginInvoke呼び出し。汎用のActionを使用してクロススレッド呼び出しを実行しました。これにより、送信者とイベント引数を渡すことができます。これらが未使用の場合(ここにあるように)MethodInvokerを使用する方が効率的ですが、処理をパラメーターのないメソッドに移動する必要があると思います。

public partial class AirportParking : Form
{
    private Timer myTimer = new Timer(100);
    private int elapsedCounter = 0;
    private readonly DateTime startTime = DateTime.Now;

    private const string EvenText = "hello";
    private const string OddText = "hello world";

    public AirportParking()
    {
        lblValue.Text = EvenText;
        myTimer.Elapsed += MyTimerElapsed;
        myTimer.AutoReset = true;
        myTimer.Enabled = true;
        myTimer.Start();
    }

    private void MyTimerElapsed(object sender,EventArgs myEventArgs)
    {
        If (lblValue.InvokeRequired)
        {
            var self = new Action<object, EventArgs>(MyTimerElapsed);
            this.BeginInvoke(self, new [] {sender, myEventArgs});
            return;   
        }

        lock (this)
        {
            lblElapsedTime.Text = DateTime.Now.SubTract(startTime).ToString();
            elapesedCounter++;
            if(elapsedCounter % 2 == 0)
            {
                lblValue.Text = EvenText;
            }
            else
            {
                lblValue.Text = OddText;
            }
        }
    }
}
2
Jodrell

まず、Windowsフォーム(およびほとんどのフレームワーク)では、UIスレッドからのみコントロールにアクセスできます(「スレッドセーフ」と記載されている場合を除く)。

そう this.lblElapsedTime.Text = ...コールバックの間違いです。 Control.BeginInvoke をご覧ください。

次に、時間の計算に System.DateTime および System.TimeSpan を使用する必要があります。

未テスト:

DateTime startTime = DateTime.Now;

void myTimer_Elapsed(...) {
  TimeSpan elapsed = DateTime.Now - startTime;
  this.lblElapsedTime.BeginInvoke(delegate() {
    this.lblElapsedTime.Text = elapsed.ToString();
  });
}
1

次を使用して終了しました。それは与えられた提案の組み合わせです:

using System.Timers;

namespace Ariport_Parking
{
  public partial class AirportParking : Form
  {

    //>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    //instance variables of the form
    System.Timers.Timer myTimer;

    private const string EvenText = "hello";
    private const string OddText = "hello world";

    static int tickLength = 100; 
    static int elapsedCounter;
    private int MaxTime = 5000;
    private TimeSpan elapsedTime; 
    private readonly DateTime startTime = DateTime.Now; 
    //<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<


    public AirportParking()
    {
        InitializeComponent();
        lblValue.Text = EvenText;
        keepingTime();
    }

    //method for keeping time
    public void keepingTime() {

    using (System.Timers.Timer myTimer = new System.Timers.Timer(tickLength))
    {  
           myTimer.Elapsed += new ElapsedEventHandler(myTimer_Elapsed);
           myTimer.AutoReset = true;
           myTimer.Enabled = true;
           myTimer.Start(); 
    }  

    }

    private void myTimer_Elapsed(Object myObject,EventArgs myEventArgs){

        elapsedCounter++;
        elapsedTime = DateTime.Now.Subtract(startTime);

        if (elapsedTime.TotalMilliseconds < MaxTime) 
        {
            this.BeginInvoke(new MethodInvoker(delegate
            {
                this.lblElapsedTime.Text = elapsedTime.ToString();

                if (elapsedCounter % 2 == 0)
                    this.lblValue.Text = EvenText;
                else
                    this.lblValue.Text = OddText;
            })); 
        } 
        else {myTimer.Stop();}
      }
  }
}
0
whytheq