使用 Timer 和 Thread、股票应用程序更新 Winforms 标签

Posted

技术标签:

【中文标题】使用 Timer 和 Thread、股票应用程序更新 Winforms 标签【英文标题】:Updating Winforms Label with Timer and Thread, stock app 【发布时间】:2021-12-27 18:19:42 【问题描述】:

之前可能有人问过它的要点,但我完全迷路了,所以我正在寻找一些个人指导。一直在尝试使用 WinForms 和 Yahoo API 为有趣的人制作股票跟踪器应用程序。尝试获取它,以便您可以输入跟踪器符号,它将创建一个新的标签,该标签将不时地自我更新。但是,它不断给我有关“跨线程操作无效”的错误消息。我试图做一些谷歌搜索,但是,是的,完全迷路了。大部分代码都写到这里了,希望大家能看懂。

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using YahooFinanceApi;

namespace stockpoging4

    public partial class Form1 : Form
    
        public Form1()
        
            System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo("en-US");
            InitializeComponent();
        

        private void button1_Click(object sender, EventArgs e)
        
            using (Prompt prompt = new Prompt("Enter the ticker symbol", "Add ticker"))
            
                string result = prompt.Result;
                result = result.ToUpper();
                if (!string.IsNullOrEmpty(result))
                
                    do_Things(result);
                
            
        
        public async Task<string> getStockPrices(string symbol)
        
            try
            
                var securities = await Yahoo.Symbols(symbol).Fields(Field.RegularMarketPrice).QueryAsync();
                var aapl = securities[symbol];
                var price = aapl[Field.RegularMarketPrice];
                return symbol + " $" + price;

            
            catch
            
                return "404";
            
        


        public async void do_Things(string result)
        
            string price;
            Label label = null;

            if (label == null)
            
                price = await getStockPrices(result);
                label = new Label()  Name = result, Text = result + " $" + price ;
                flowLayoutPanel2.Controls.Add(label);
            
            else
            
                Thread testThread = new Thread(async delegate ()
                
                    uiLockingTask();
                    price = await getStockPrices(result);
                    label.Text = result + " $" + price;
                    label.Update();
                );
            
            System.Timers.Timer timer = new System.Timers.Timer(10000);
            timer.Start();
            timer.Elapsed += do_Things(results);
        

        private void uiLockingTask() 
            Thread.Sleep(5000);
        
    

【问题讨论】:

我在label == null 中看不到任何关系,并且在使用label 的线程上执行某些操作... 在 UI 线程上使用 async/await 创建一个简单的“定时器/循环” (没有阻塞)很容易。我试着举个例子。 【参考方案1】:

让我指出你的实现中的几件事。

    您订阅 timer.Elapsed after timer.Start 可能在短时间间隔的情况下无效 在后台调用事件处理程序,这就是为什么您不断收到“跨线程操作无效”的原因。 UI 组件应该从后台线程正确调度,例如,通过调用 flowLayoutPanel2.BeginInvoke(new Action(() => flowLayoutPanel2.Controls.Add(label)));和 label.BeginInvoke(new Action(label.Update))。此更改已经可以解决您的异常。 尽管我会以不同的方式实现此功能,但在这里我发布了一些稍作改动的代码,通过一些调整可以完全满足您的需求。
    public partial class Form1 : Form 
    
        Task _runningTask;
        CancellationTokenSource _cancellationToken;
    
        public Form1()
        
            System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo("en-US");
            InitializeComponent();
        
    
        private void button1_Click(object sender, EventArgs e)
        
            using (Prompt prompt = new Prompt("Enter the ticker symbol", "Add ticker"))
            
                string result = prompt.Result;
                result = result.ToUpper();
                if (!string.IsNullOrEmpty(result))
                
                    do_Things(result);
                    _cancellationToken = new CancellationTokenSource();
                    _runningTask = StartTimer(() => do_Things(result), _cancellationToken);
                
            
        
    
        private void onCancelClick()
        
            _cancellationToken.Cancel();
        
    
    
        public async Task<string> getStockPrices(string symbol)
        
            try
            
                var securities = await Yahoo.Symbols(symbol).Fields(Field.RegularMarketPrice).QueryAsync();
                var aapl = securities[symbol];
                var price = aapl[Field.RegularMarketPrice];
                return symbol + " $" + price;
    
            
            catch
            
                return "404";
            
        
    
        private async Task StartTimer(Action action, CancellationTokenSource cancellationTokenSource)
        
            try
            
                while (!cancellationTokenSource.IsCancellationRequested)
                
                    await Task.Delay(1000, cancellationTokenSource.Token);
                    action();
                
            
            catch (OperationCanceledException)  
        
    
    
        public async void do_Things(string result)
        
            var price = await getStockPrices(result);
            var label = new Label()  Name = result, Text = result + " $" + price ;
            flowLayoutPanel2.BeginInvoke(new Action(() => flowLayoutPanel2.Controls.Add(label)));
        
    

【讨论】:

非常感谢!我刚刚复制了代码,它似乎在起作用,除了它不断打印新标签而不是更新现有标签。我将更好地查看代码以尝试理解/添加它,但无论如何都要感谢! 没问题。如果您将我的答案标记为正确,我会很高兴。【参考方案2】:

如今,一种更简单的方法是使用异步。

这是一个每个间隔触发Action 的类:

public class UITimer : IDisposable

    private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();

    // use a private function which returns a task
    private async Task Innerloop(TimeSpan interval, Action<UITimer> action)
    
        try
        
            while (!_cancellationTokenSource.IsCancellationRequested)
            
                await Task.Delay(interval, _cancellationTokenSource.Token);
                action(this);
            
        
        catch (OperationCanceledException)  
    

    // the constructor calls the private StartTimer, (the first part will run synchroniously, until the away delay) 
    public UITimer(TimeSpan interval, Action<UITimer> action) =>
        _ = Innerloop(interval, action);

    // make sure the while loop will stop.
    public void Dispose() =>
        _cancellationTokenSource?.Cancel();

如果您使用 dotnet 3.0 或更高版本,则可以使用 IAsyncDisposable。有了这个,您就可以等待 DisposeAsync 方法,因此您可以等待 _timerTask 完成。

我创建了一个新表单,并以此作为代码:

public partial class Form1 : Form

    private readonly UITimer _uiTimer;
    private int _counter;

    public Form1()
    
        InitializeComponent();

        // setup the time and pass the callback action
        _uiTimer = new UITimer(TimeSpan.FromSeconds(1), Update);
    

    // the orgin timer is passed as parameter.
    private void Update(UITimer timer)
    
        // do your thing on the UI thread.
        _counter++;
        label1.Text= _counter.ToString();
    

    private void Form1_FormClosed(object sender, FormClosedEventArgs e)
    
        // make sure the time (whileloop) is stopped.
        _uiTimer.Dispose();
    

优点是,回调在 UI 线程上运行,但不会阻塞它。 await Task.Delay(..) 在后台使用 Timer,但在 UI 线程上发布其余的方法/状态机(因为 UI 线程有 SynchronizaionContext)

简单但成功;-)

【讨论】:

以上是关于使用 Timer 和 Thread、股票应用程序更新 Winforms 标签的主要内容,如果未能解决你的问题,请参考以下文章

Java两种延时——thread和timer

Thread.Sleep 还是 Thread.Timer 挂机系统?

求 JAVA 使用 Thread 和 Timer 类来做倒计时的程序代码

基于Thread实现自己的定时器Timer

android中asynctask和thread的区别

使用 SIGEV_THREAD 时,如何为所有 timer_create 回调重用单个线程?