如何使用后台工作人员更新 GUI?

Posted

技术标签:

【中文标题】如何使用后台工作人员更新 GUI?【英文标题】:How to update GUI with backgroundworker? 【发布时间】:2010-12-24 04:06:57 【问题描述】:

我花了一整天的时间试图让我的应用程序使用线程,但没有运气。我已经阅读了很多关于它的文档,但我仍然遇到很多错误,所以我希望你能帮助我。

我有一个非常耗时的方法,它调用数据库并更新 GUI。这必须一直发生(或大约每 30 秒)。

public class UpdateController

    private UserController _userController;

    public UpdateController(LoginController loginController, UserController userController)
    
        _userController = userController;
        loginController.LoginEvent += Update;
    

    public void Update()
    
        BackgroundWorker backgroundWorker = new BackgroundWorker();
        while(true)
        
            backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
            backgroundWorker.RunWorkerAsync();
             
    

    public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    
        _userController.UpdateUsersOnMap();
    

使用这种方法,我得到一个异常,因为后台工作线程不是 STA 线程(但据我所知,这是我应该使用的)。我尝试过使用 STA 线程,但出现了其他错误。

我认为问题在于我在执行数据库调用时(在后台线程中)尝试更新 GUI。我应该只进行数据库调用,然后以某种方式切换回主线程。主线程执行后,它应该回到后台线程,依此类推。但我不知道该怎么做。

应用程序应在数据库调用后立即更新 GUI。射击事件似乎不起作用。 backgroundthread 只是进入它们。

编辑:

一些非常棒的答案 :) 这是新代码:

public class UpdateController
private UserController _userController;
private BackgroundWorker _backgroundWorker;

public UpdateController(LoginController loginController, UserController userController)

    _userController = userController;
    loginController.LoginEvent += Update;
    _backgroundWorker = new BackgroundWorker();
    _backgroundWorker.DoWork += backgroundWorker_DoWork;
    _backgroundWorker.RunWorkerCompleted += backgroundWorker_RunWorkerCompleted;


public void _backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)

    _userController.UpdateUsersOnMap();


public void Update()
   
    _backgroundWorker.RunWorkerAsync();


void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)

    //UI update
    System.Threading.Thread.Sleep(10000);
    Update();


public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)

    // Big database task

但是我怎样才能让它每 10 秒运行一次呢? System.Threading.Thread.Sleep(10000) 只会让我的 GUI 冻结,而 Update() 中的 while(true) 循环按照建议给出异常(线程太忙)。

【问题讨论】:

你不能在 UI 线程上调用 Sleep - 你需要使用一个计时器,特别是 DispatcherTimer 当有 BackgroundWorker 时,为什么要戳 UI 线程? :) 我很高兴你很努力地问了这个问题,因为它今天对我/我们有好处。 我遇到了报告进度问题。它对“_backgroundWorker.ReportProgress(p, param);”很重要在 DoWork() 中 【参考方案1】:

这是一个基于一些 WinForms 示例代码的源代码模式,您可以使用它,但您也可以非常轻松地将其应用于 WPF。在此示例中,我将输出重定向到控制台,然后我使用该控制台让后台工作人员在处理时将一些消息写入文本框。

它包括:

帮助类 TextBoxStreamWriter 用于将控制台输出重定向到文本框 后台工作人员写入重定向控制台 后台工作完成后需要重置的进度条 一些文本框(txtPath 和 txtResult),以及一个“开始”按钮

换句话说,有一些后台任务需要与 UI 交互。现在我将展示它是如何完成的。

后台任务的上下文中,您需要使用 Invoke 来访问任何 UI 元素。我相信最简单的方法是使用 lambda 表达式语法,例如

progressBar1.Invoke((Action) (() =>
       // inside this context, you can safely access the control
        progressBar1.Style = ProgressBarStyle.Continuous;
    ));

要更新 ProgressBar,一个本地方法,比如

private void UpdateProgress(int value)

    progressBar1.Invoke((Action)(() =>  progressBar1.Value  = value; ));

帮助。它将value 参数作为闭包传递给进度条。


这是用于重定向控制台输出的辅助类TextBoxStreamWriter,

public class TextBoxStreamWriter : TextWriter


    TextBox _output = null;

    public TextBoxStreamWriter(TextBox output)
    
        _output = output;
    

    public override void WriteLine(string value)
    
        // When character data is written, append it to the text box.
        // using Invoke so it works in a different thread as well
        _output.Invoke((Action)(() => _output.AppendText(value+"\r\n")));
    


        

您需要在表单加载事件中使用它,如下所示(其中txtResult是一个文本框,输出将被重定向到该文本框):

private void Form1_Load(object sender, EventArgs e)

    // Instantiate the writer and redirect the console out
    var _writer = new TextBoxStreamWriter(txtResult);
    Console.SetOut(_writer);

表单上还有一个按钮启动后台工作程序,它传递了一个路径:

private void btnStart_Click(object sender, EventArgs e)

    backgroundWorker1.RunWorkerAsync(txtPath.Text);

这是后台工作人员的工作量,请注意它如何使用控制台将消息输出到文本框(因为之前设置的重定向):

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)

    var selectedPath = e.Argument as string;
    Console.Out.WriteLine("Processing Path:"+selectedPath);
    // ...

变量selectedPath 包含之前通过参数txtPath.Text 传递给backgroundWorker1 的路径,它正在通过e.Argument 访问。

如果您之后需要重置某些控件,请按以下方式进行(如上所述):

private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)

    progressBar1.Invoke((Action) (() =>
        
            progressBar1.MarqueeAnimationSpeed = 0;
            progressBar1.Style = ProgressBarStyle.Continuous;
        ));

在此示例中,完成后,正在重置进度条。


重要提示:每当您访问 GUI 控件时,使用Invoke,就像我在上面的示例中所做的那样。 正如您在代码中看到的那样,使用 Lambda 让事情变得简单。


这是在 LinqPad 6 中运行的完整示例(只需将其复制并粘贴到一个空的 C# 程序查询中)- 这次我决定使用 LinqPad,以便您可以学习新的东西,因为你们都知道如何创建一个Visual Studio 中的新 Windows 窗体项目(如果您仍想这样做,只需复制下面的事件并将控件拖放到窗体中):

// see: https://***.com/a/27566468/1016343

using System.ComponentModel;
using System.Windows.Forms;

BackgroundWorker backgroundWorker1 = new System.ComponentModel.BackgroundWorker();
ProgressBar progressBar1 = new ProgressBar()  Text = "Progress", Width = 250, Height=20, Top=10, Left=0 ;
TextBox txtPath = new TextBox()  Text =@"C:\temp\", Width = 100, Height=20, Top=30, Left=0 ;
TextBox txtResult = new TextBox()  Text = "", Width = 200, Height=250, Top=70, Left=0, Multiline=true, Enabled=false ;
Button btnStart = new Button()  Text = "Start", Width = 100, Height=30, Top=320, Left=0 ;

void Main()

    // see: https://www.linqpad.net/CustomVisualizers.aspx

    // Instantiate the writer and redirect the console out
    var _writer = new TextBoxStreamWriter(txtResult);
    Console.SetOut(_writer);
    
    // wire up events
    btnStart.Click += (object sender, EventArgs e) => btnStart_Click(sender, e);
    backgroundWorker1.DoWork += (object sender, DoWorkEventArgs e) => backgroundWorker1_DoWork(sender, e);
    backgroundWorker1.RunWorkerCompleted += (object sender, RunWorkerCompletedEventArgs e)
                                  => backgroundWorker1_RunWorkerCompleted(sender, e);
    using var frm = new Form() Text="Form", Width = 300, Height=400, Top=0, Left=0;
    frm.Controls.Add(progressBar1);
    frm.Controls.Add(txtPath);
    frm.Controls.Add(txtResult);
    frm.Controls.Add(btnStart);
    
    // display controls
    frm.ShowDialog();


private void btnStart_Click(object sender, EventArgs e)

    backgroundWorker1.RunWorkerAsync(txtPath.Text);


private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)

    InitProgress();
    var selectedPath = e.Argument as string;
    Console.Out.WriteLine("Processing Path: " + selectedPath);
    UpdateProgress(0); Thread.Sleep(300); UpdateProgress(30); Thread.Sleep(300); 
    UpdateProgress(50); Thread.Sleep(300); 
    Console.Out.WriteLine("Done.");
    
    // ...


private void UpdateProgress(int value)

    progressBar1.Invoke((Action)(() =>
       
           progressBar1.Value  = value;
       ));


private void InitProgress()

    progressBar1.Invoke((Action)(() =>
       
           progressBar1.MarqueeAnimationSpeed = 0;
           progressBar1.Style = ProgressBarStyle.Continuous;
       ));


private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)

    UpdateProgress(100); // always show 100% when done


// You can define other methods, fields, classes and namespaces here
public class TextBoxStreamWriter : TextWriter


    TextBox _output = null;

    public TextBoxStreamWriter(TextBox output)
    
        _output = output;
    

    public override Encoding Encoding => throw new NotImplementedException();

    public override void WriteLine(string value)
    
        // When character data is written, append it to the text box.
        // using Invoke so it works in a different thread as well
        _output.Invoke((Action)(() => _output.AppendText(value + "\r\n")));
    


【讨论】:

谢谢,简单有效。【参考方案2】:

除了以前的 cmets,请查看 www.albahari.com/threading - 您将找到的关于线程的最佳文档。它将教你如何正确使用 BackgroundWorker。

您应该在 BackgroundWorker 触发 Completed 事件时更新 GUI(在 UI 线程上调用它以方便您,因此您不必自己执行 Control.Invoke)。

【讨论】:

【参考方案3】:

您需要声明和配置一次 BackgroundWorker - 然后在循环中调用 RunWorkerAsync 方法...

public class UpdateController

    private UserController _userController;
    private BackgroundWorker _backgroundWorker;

    public UpdateController(LoginController loginController, UserController userController)
    
        _userController = userController;
        loginController.LoginEvent += Update;
        _backgroundWorker = new BackgroundWorker();
        _backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
        _backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker_ProgressChanged);
        _backgroundWorker.WorkerReportsProgress= true;
    

    public void Update()
    
         _backgroundWorker.RunWorkerAsync();    
    

    public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    
        while (true)
        
        // Do the long-duration work here, and optionally
        // send the update back to the UI thread...
        int p = 0;// set your progress if appropriate
        object param = "something"; // use this to pass any additional parameter back to the UI
        _backgroundWorker.ReportProgress(p, param);
        
    

    // This event handler updates the UI
    private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    
        // Update the UI here
//        _userController.UpdateUsersOnMap();
    

【讨论】:

这会引发异常。 “此 BackgroundWorker 当前正忙,无法同时运行多个任务。” :( 对不起 - 是的,对不起!请参阅更新 - 我已将 while 循环转换为“DoWork”方法的单个实例;这将允许您通过“ReportProgress”推送 UI 通知,然后如果您愿意,如果您想跳出无限循环,可以为“Completed”添加方法处理程序......干杯:) ...您可以将此工作线程发送到 Sleep() 10 秒,而不会影响 UI 线程! 这似乎有效!不幸的是,我看不到/尝试一下。所有这些线程现在都是我在屏幕之间的动画无法正常工作的原因。我不知道为什么 :( 但那是一个全新的问题。标记为已接受的答案。谢谢 :) worker 似乎在第一次执行后退出了循环。不知道为什么。【参考方案4】:

您可以使用 backgroundWorker 类上的 RunWorkerCompleted 事件来定义后台任务完成后应该做什么。所以你应该在 DoWork 处理程序中进行数据库调用,然后在 RunWorkerCompleted 处理程序中更新接口,如下所示:

BackgroundWorker bgw = new BackgroundWorker();
bgw.DoWork += (o, e) =>  longRunningTask(); 

bgw.RunWorkerCompleted += (o, e) => 
    if(e.Error == null && !e.Cancelled)
    
        _userController.UpdateUsersOnMap();
    


bgw.RunWorkerAsync();

【讨论】:

【参考方案5】:

@Lee 的答案中的 if 语句应如下所示:

bgw.RunWorkerCompleted += (o, e) => 
    if(e.Error == null && !e.Cancelled)
    
        _userController.UpdateUsersOnMap();
    

...如果您想在没有错误且 BgWorker 未被取消的情况下调用 UpdateUsersOnMap();

【讨论】:

【参考方案6】:

您必须使用 Control.InvokeRequired 属性来确定您是否在后台线程上。然后,您需要通过 Control.Invoke 方法调用修改 UI 的逻辑,以强制您的 UI 操作发生在主线程上。为此,您可以创建一个委托并将其传递给 Control.Invoke 方法。这里的问题是您需要一些从 Control 派生的对象来调用这些方法。

编辑:正如另一位用户发布的那样,如果您可以等待 BackgroundWorker.Completed 事件来更新您的 UI,那么您可以订阅该事件并调用您的 UI直接打码。在主应用程序线程上调用 BackgroundWorker_Completed。我的代码假设您想在操作期间进行更新。我的方法的一种替代方法是订阅 BwackgroundWorker.ProgressChanged 事件,但我相信在这种情况下您仍需要调用 Invoke 来更新您的 UI。 p>

例如

public class UpdateController

    private UserController _userController;        
    BackgroundWorker backgroundWorker = new BackgroundWorker();

    public UpdateController(LoginController loginController, UserController userController)
    
        _userController = userController;
        loginController.LoginEvent += Update;
    

    public void Update()
                            
         // The while loop was unecessary here
         backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork);
         backgroundWorker.RunWorkerAsync();                 
    

    public delegate void DoUIWorkHandler();


    public void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    
       // You must check here if your are executing on a background thread.
       // UI operations are only allowed on the main application thread
       if (someControlOnMyForm.InvokeRequired)
       
           // This is how you force your logic to be called on the main
           // application thread
           someControlOnMyForm.Invoke(new             
                      DoUIWorkHandler(_userController.UpdateUsersOnMap);
       
       else
       
           _userController.UpdateUsersOnMap()
       
    

【讨论】:

我觉得我做这个应用程序很笨拙。在我的控制器对象中有 UI 对象。所以我不确定这是否适合我的程序。如果我要这样做,我认为也会有很多变化。 鉴于原始问题被标记为wpf,我发现这个答案完全不适用。【参考方案7】:

您应该删除 while(true),您正在添加无限事件处理程序并无限次调用它们。

【讨论】:

以上是关于如何使用后台工作人员更新 GUI?的主要内容,如果未能解决你的问题,请参考以下文章

Swing GUI 不更新

后台 iCloud 处理减慢 gui 和损坏视图控制器堆栈-如何停止多次触摸

如何在带有 Swing gui 的后台线程中使用 jdbc

如何避免从工作线程到 GUI 线程的主体传播

Qt:如何创建一个不最小化且不阻塞后台GUI的窗口

Java:具有后台线程的 GUI 应用程序