Подтвердить что ты не робот

Как вызвать метод пользовательского интерфейса из другого потока

Игра с таймерами. Контекст: winforms с двумя метками.

Я хотел бы посмотреть, как работает System.Timers.Timer, поэтому я не использовал таймер форм. Я понимаю, что форма и 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(); }

    }
  }
}
4b9b3361

Ответ 1

Я предполагаю, что ваш код - это просто тест, поэтому я не буду обсуждать, что вы делаете с вашим таймером. Проблема здесь в том, как сделать что-то с контролем пользовательского интерфейса внутри вашего обратного вызова по таймеру.

Большинство методов и свойств Control можно получить только из потока пользовательского интерфейса (в действительности к ним можно получить доступ только из потока, в котором вы их создали, но это еще одна история). Это связано с тем, что каждый поток должен иметь свой собственный цикл сообщений (GetMessage() отфильтровывает сообщения по потоку), а затем что-то делать с Control вам нужно отправить сообщение из потока в основной поток. В .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 для списка методов, которые вы можете вызывать из любого потока, так же, как ссылка, вы всегда можете вызвать методы Invalidate, BeginInvoke, EndInvoke, Invoke и прочитать свойство InvokeRequired. В общем, это общий шаблон использования (предполагается, что this - это объект, полученный из Control):

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
    }
}

Обратите внимание, что текущий поток будет блокироваться до завершения выполнения потока пользовательского интерфейса. Это может быть проблемой, если важна синхронизация потоков (не забывайте, что поток пользовательского интерфейса может быть занят или немного висел). Если вам не нужно возвращать значение метода, вы можете просто заменить Invoke на BeginInvoke, для 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 и т.д.).

Теперь представьте, что у вас есть рабочий поток, как вы можете назвать свой основной поток? Самый простой способ - использовать эту базовую структуру, чтобы сделать трюк. Я упрощаю задачу, но это следующие шаги:

  • Создайте (потокобезопасную) очередь функций для вызова (некоторые примеры здесь, на SO).
  • Поместить пользовательское сообщение в оконную процедуру. Если вы сделаете эту очередь очередью приоритетов, вы можете даже решить приоритет для этих вызовов (например, уведомление о ходе работы из рабочего потока может иметь более низкий приоритет, чем уведомление о тревоге).
  • В процедуре окна (внутри оператора switch/case) вы понимаете это сообщение, тогда вы можете просмотреть функцию для вызова из очереди и вызвать ее.

И WPF, и WinForms используют этот метод для доставки (отправки) сообщения из потока в поток пользовательского интерфейса. Взгляните на эту статью в MSDN для получения более подробной информации о нескольких потоках и пользовательском интерфейсе, WinForms скрывает много этих деталей, и вам не нужно позаботьтесь о них, но вы можете взглянуть на то, как он работает под капотом.

Ответ 2

Лично, когда я работаю в приложении, которое работает с потоками из пользовательского интерфейса, я обычно пишу этот небольшой фрагмент.

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

Когда я выполняю асинхронный вызов в другом потоке, я всегда могу использовать обратный вызов.

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

Простой и чистый.

Ответ 3

Во-первых, в Windows Forms (и большинстве фреймворков) доступ к элементу управления может быть доступен только (если не задокументирован как "потокобезопасный" ) в потоке пользовательского интерфейса.

Так что 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();
  });
}

Ответ 4

Как я уже сказал, вот мой ответ, который проверяет вызовы перекрестных потоков, синхронизирует обновления переменных, не останавливает и не запускает таймер и не использует таймер для подсчета прошедшего времени.

ИЗМЕНИТЬ фиксированный вызов BeginInvoke. Я сделал перекрестный поток invoke, используя общий Action. Это позволяет передавать отправителю и eventargs. Если они не используются (как они есть здесь), более эффективно использовать 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;
            }
        }
    }
}

Ответ 5

Закончено с использованием следующего. Это комбинация предлагаемых предложений:

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();}
      }
  }
}