使用不同的线程更新 GUI (WPF)

Posted

技术标签:

【中文标题】使用不同的线程更新 GUI (WPF)【英文标题】:Updating GUI (WPF) using a different thread 【发布时间】:2011-05-14 06:39:17 【问题描述】:

这里有一个问题,我不知道如何解决。我正在做一个涉及 GUI 和串行数据的小项目。 GUI 正在由主线程运行,并且由于保存我传入的串行数据的数据变量需要不断更新,因此这些变量正在第二个线程中更新。问题是当我需要更新 GUI 上的一些文本框时,需要使用来自辅助线程的数据来更新这些文本框,这就是我的问题所在。我不能直接从辅助线程更新它们,我不知道如何从辅助线程传输数据并制定一个从主线程更新它们的系统。我把我的代码放在下面:

任何帮助都会很棒。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;
using System.IO.Ports;
using System.Threading;

namespace GUIBike

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    
        public static string inputdata;
        public static int MaximumSpeed, maximumRiderInput, RiderInput, Time, CurrentSpeed, DistanceTravelled, MaximumMotorOutput, MotorOutput, InputSpeed;
        public static string SaveDataString;
        public Thread Serial;
        public static SerialPort SerialData;
        public static string[] portlist = SerialPort.GetPortNames();
        public static string[] SaveData = new string[4];
        public static string directory = "C:\\";

        public MainWindow()
        
            Serial = new Thread(ReadData);
            InitializeComponent();
            int Count = 0;
            for (Count = 0; Count < portlist.Length; Count++)
            
                ComPortCombo.Items.Add(portlist[Count]);
            
        

        private void StartDataButton_Click(object sender, RoutedEventArgs e)
        
            SerialData = new SerialPort(ComPortCombo.Text, 19200, Parity.None, 8, StopBits.One);
            SerialData.Open();
            SerialData.WriteLine("P");
            Serial.Start();
            StartDataButton.IsEnabled = false;
            EndDataButton.IsEnabled = true;
            ComPortCombo.IsEnabled = false;
            CurrentSpeed = 0;
            MaximumSpeed = 0;
            Time = 0;
            DistanceTravelled = 0;
            MotorOutput = 0;
            RiderInput = 0;
            SaveData[0] = "";
            SaveData[1] = "";
            SaveData[2] = "";
            SaveData[3] = "";
            SaveDataButton.IsEnabled = false;
            if (SerialData.IsOpen)
            
                ComPortStatusLabel.Content = "OPEN";
                SerialData.NewLine = "/n";
                SerialData.WriteLine("0");
                SerialData.WriteLine("/n");
            
        

        private void EndDataButton_Click(object sender, RoutedEventArgs e)
        
            SerialData.Close();
            SaveDataButton.IsEnabled = true;
            SerialData.WriteLine("1");
            SerialData.WriteLine("0");
            if (!SerialData.IsOpen)
            
                ComPortStatusLabel.Content = "CLOSED";
            
            int i = 0;
            for (i = 0; i < 4; i++)
            
                if (i == 0)
                
                    SaveDataString = "MaximumSpeed during the Ride was = " + Convert.ToString(MaximumSpeed) + "m/h";
                    SaveData[i] = SaveDataString;
                
                if (i == 1)
                
                    SaveDataString = "Total Distance Travelled = " + Convert.ToString(DistanceTravelled) + "m";
                    SaveData[i] = SaveDataString;
                
                if (i == 2)
                
                    SaveDataString = "Maximum Rider Input Power = " + Convert.ToString(maximumRiderInput) + "Watts";
                    SaveData[i] = SaveDataString;
                
                if (i == 3)
                
                    SaveDataString = "Maximum Motor Output Power = " + Convert.ToString(MaximumMotorOutput) + "Watts";
                    SaveData[i] = SaveDataString;
                
            
        

        private void SaveDataButton_Click(object sender, RoutedEventArgs e)
        
            //File.WriteAllBytes(directory + "image" + imageNO + ".txt", ); //saves the file to Disk    
            File.WriteAllLines(directory + "BikeData.txt", SaveData);
        

        public void ReadData()
        
            int counter = 0;

            while (SerialData.IsOpen)
            
                if (counter == 0)
                
                    //try
                    //
                        InputSpeed = Convert.ToInt16(SerialData.ReadChar());
                        CurrentSpeed = InputSpeed;
                        if (CurrentSpeed > MaximumSpeed)
                        
                            MaximumSpeed = CurrentSpeed;
                        
                        SpeedTextBox.Text = "Current Wheel Speed = " + Convert.ToString(CurrentSpeed) + "Km/h";
                        DistanceTravelled = DistanceTravelled + (Convert.ToInt16(CurrentSpeed) * Time);
                        DistanceTravelledTextBox.Text = "Total Distance Travelled = " + Convert.ToString(DistanceTravelled) + "Km";
                    //
                    //catch (Exception)  
                
                if (counter == 1)
                
                    try
                    
                        RiderInput = Convert.ToInt16(SerialData.ReadLine());
                        if (RiderInput > maximumRiderInput)
                        
                            maximumRiderInput = RiderInput;
                        
                        RiderInputTextBox.Text = "Current Rider Input Power =" + Convert.ToString(RiderInput) + "Watts";
                    
                    catch (Exception)  
                
                if (counter == 2)
                
                    try
                    
                        MotorOutput = Convert.ToInt16(SerialData.ReadLine());
                        if (MotorOutput > MaximumMotorOutput)
                        
                            MaximumMotorOutput = MotorOutput;
                        

                        MotorOutputTextBox.Text = "Current Motor Output = " + Convert.ToString(MotorOutput) + "Watts";
                    
                    catch (Exception)  
                
                counter++;
                if (counter == 3)
                
                    counter = 0;
                
            
        

        private void ComPortCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
        
            StartDataButton.IsEnabled = true;
        


        private void Window_Closed(object sender, RoutedEventArgs e)
        
            if (SerialData.IsOpen)
            
                SerialData.Close();
            
        

【问题讨论】:

可能是***.com/questions/1684341/…的欺骗 【参考方案1】:

您可以使用 Dispatcher.Invoke 从辅助线程更新您的 GUI。

这是一个例子:

    private void Window_Loaded(object sender, RoutedEventArgs e)
    
        new Thread(DoSomething).Start();
    
    public void DoSomething()
    
        for (int i = 0; i < 100000000; i++)
        
               this.Dispatcher.Invoke(()=>
               textbox.Text=i.ToString();
               );    
         
    

【讨论】:

已使用 Dispatcher 但获取数据后 UI 挂起【参考方案2】:

您可以使用委托来解决此问题。 这是一个显示如何使用不同线程更新文本框的示例

public delegate void UpdateTextCallback(string message);

private void TestThread()

    for (int i = 0; i <= 1000000000; i++)
    
        Thread.Sleep(1000);                
        richTextBox1.Dispatcher.Invoke(
            new UpdateTextCallback(this.UpdateText),
            new object[]  i.ToString() 
        );
    

private void UpdateText(string message)

    richTextBox1.AppendText(message + "\n");


private void button1_Click(object sender, RoutedEventArgs e)

   Thread test = new Thread(new ThreadStart(TestThread));
   test.Start();

TestThread方法被名为test的线程用来更新文本框

【讨论】:

【参考方案3】:

那里。

我也在开发一个使用WPF的串口测试工具, 我想分享一些我的经验。

我认为你应该根据 MVVM 设计模式重构你的源代码。

一开始我遇到了和你遇到的一样的问题,我用这段代码解决了:

new Thread(() => 

    while (...)
    
        SomeTextBox.Dispatcher.BeginInvoke((Action)(() => SomeTextBox.Text = ...));
    
).Start();

这可行,但太丑陋了。 我不知道如何重构它,直到我看到这个: http://www.codeproject.com/Articles/165368/WPF-MVVM-Quick-Start-Tutorial

这是一个非常友好的 MVVM 分步教程,适合初学者。 没有闪亮的 UI,没有复杂的逻辑,只有 MVVM 的基础。

【讨论】:

这个答案中的唯一代码不是您的实际建议,这有点误导。很高兴在这里有一个工作样本。 :) 很难提供 MVVM 范例的工作示例,因为它可能需要重新设计整个程序。【参考方案4】:

使用以下方法更新 GUI。

     Public Void UpdateUI()
     
         //Here update your label, button or any string related object.

         //Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate  ));    
         Application.Current.Dispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate  ));
     

请记住,当您当时使用此方法时不要直接从调度程序线程更新相同的对象,否则您只会得到更新的字符串,并且此方法是无用/无用的。 如果仍然无法正常工作,那么在方法中注释该行和取消注释注释的行都具有几乎相同的效果,只是访问它的方式不同。

【讨论】:

【参考方案5】:

这是一个更新 UI 文本框的完整示例

<Window x:Class="WpfThreading.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfThreading"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="216.84">
<Grid Margin="0,0,2,0">
    <Button Content="Button" HorizontalAlignment="Left" Margin="10,10,0,0"
            VerticalAlignment="Top" Width="75" Click="Button_Click"/>
    <TextBox HorizontalAlignment="Left" Margin="10,35,0,10" TextWrapping="Wrap" Name="mtextBox" Width="87"   VerticalScrollBarVisibility="Auto"/>
    <TextBox HorizontalAlignment="Left" Margin="111,35,0,10" TextWrapping="Wrap" x:Name="mtextBox2" Width="87"   VerticalScrollBarVisibility="Auto"/>
</Grid></Window>

在代码中

public partial class MainWindow : Window

    public MainWindow()
    
        InitializeComponent();
    

    private void Button_Click(object sender, RoutedEventArgs e)
    
        new Thread(DoSomething).Start();
        new Thread(DoSomething2).Start();
    


    public void DoSomething()
    
        for (int i = 0; i < 100; i++)
        
            Dispatcher.BeginInvoke(new Action(() => 
                mtextBox.Text += $"i.ToString()Environment.NewLine";
            ), DispatcherPriority.SystemIdle);

            Thread.Sleep(100);
        

    

    public void DoSomething2()
    
        for (int i = 100; i > 0; i--)
        
            Dispatcher.BeginInvoke(new Action(() => 
                mtextBox2.Text += $"i.ToString()Environment.NewLine";
            ), DispatcherPriority.SystemIdle);

            Thread.Sleep(100);
        

    

【讨论】:

【参考方案6】:

您需要使用Dispatcher.BeginInvoke。我没有对其进行测试,但您可以查看this 链接(这是 Julio G 提供的相同链接),以便更好地了解如何从不同线程更新 UI 控件。我已经修改了你的ReadData() 代码

public void ReadData()

    int counter = 0;

    while (SerialData.IsOpen)
    
        if (counter == 0)
        
            //try
            //
                InputSpeed = Convert.ToInt16(SerialData.ReadChar());
                CurrentSpeed = InputSpeed;
                if (CurrentSpeed > MaximumSpeed)
                
                    MaximumSpeed = CurrentSpeed;
                
    SpeedTextBox.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal,
        new Action(delegate()  SpeedTextBox.Text = "Current Wheel Speed = " + Convert.ToString(CurrentSpeed) + "Km/h"; );//update GUI from this thread


                DistanceTravelled = DistanceTravelled + (Convert.ToInt16(CurrentSpeed) * Time);

    DistanceTravelledTextBox.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal,
        new Action(delegate() DistanceTravelledTextBox.Text = "Total Distance Travelled = " + Convert.ToString(DistanceTravelled) + "Km"; );//update GUI from this thread

            //
            //catch (Exception)  
        
        if (counter == 1)
        
            try
            
                RiderInput = Convert.ToInt16(SerialData.ReadLine());
                if (RiderInput > maximumRiderInput)
                
                    maximumRiderInput = RiderInput;
                                       
    RiderInputTextBox.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, 
        new Action(delegate()  RiderInputTextBox.Text = "Current Rider Input Power =" + Convert.ToString(RiderInput) + "Watts"; );//update GUI from this thread
            
            catch (Exception)  
        
        if (counter == 2)
        
            try
            
                MotorOutput = Convert.ToInt16(SerialData.ReadLine());
                if (MotorOutput > MaximumMotorOutput)
                
                    MaximumMotorOutput = MotorOutput;
                
    MotorOutputTextBox.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, 
        new Action(delegate()  MotorOutputTextBox.Text = "Current Motor Output = " + Convert.ToString(MotorOutput) + "Watts"; );//update GUI from this thread                        
            
            catch (Exception)  
        
        counter++;
        if (counter == 3)
        
            counter = 0;
        
    

【讨论】:

【参考方案7】:

正如 akjoshi 和 Julio 所说,这是关于在与 GUI 项相同的线程上分派一个 Action 来更新 GUI,但从处理后台数据的方法开始。您可以在上面 akjoshi 的回答中以特定形式查看此代码。这是一个通用版本。

myTextBlock.Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal,
                                   new Action(delegate() 
                                      
                                      myTextBlock.Text = Convert.ToString(myDataObject.getMeData());
                                      ));

关键部分是调用 UI 对象的调度程序 - 确保您拥有正确的线程。

从个人经验来看,像这样创建和使用内联动作似乎要容易得多。在类级别声明它给我带来了很多静态/非静态上下文的问题。

【讨论】:

【参考方案8】:

我想你有几个选择。

一种方法是使用 BackgroundWorker。这是应用程序中多线程的常用助手。它公开了在线程池中的后台线程上处理的 DoWork 事件和在后台线程完成时在主线程上调用的 RunWorkerCompleted 事件。它还具有尝试/捕获后台线程上运行的代码的好处,因此未处理的异常不会杀死应用程序。

如果您不想走这条路,您可以使用 WPF 调度程序对象来调用一个操作,以将 GUI 更新回主线程。随机参考:

http://www.switchonthecode.com/tutorials/working-with-the-wpf-dispatcher

还有很多其他选项,但这是最常见的两个。

【讨论】:

以上是关于使用不同的线程更新 GUI (WPF)的主要内容,如果未能解决你的问题,请参考以下文章

“调用线程无法访问此对象,因为不同的线程拥有它”从 WPF 中的不同线程更新 UI 控件时出现错误

无法从使用 c# 在不同线程上运行的另一个页面 User.cs 更新 GUI (MainWindow.xaml) 中的进度条

PyQT和线程

WPF 从其他线程访问 GUI

如何使用 tkinter 事件“继续”或暂停不同的 while 循环?

调用线程无法访问此对象,因为不同的线程拥有它 - WPF [重复]