使用BackgroundWorker进程更新和追加C#文本框的值

15

我有一个用C#编写的窗体应用程序,非常简单:

输入:

  • 文本字符串
  • 源文件夹路径
  • 目标文件夹路径
  • 整数计数

该应用程序在源文件夹中搜索包含输入的文本字符串的文本文件;如果找到该字符串,则将该文件和同名的图像文件复制到目标文件夹中。这将根据整数输入重复执行多少次。

所以我有一个按钮,在按钮点击事件中调用:

ProcessImages(tbDID.Text, tbSource.Text, tbDest.Text, comboBoxNumberImages.SelectedItem.ToString());

这是:

private void ProcessImages(string DID, string SourceFolder, string DestFolder, string strNumImages)
        {         
            int ImageCounter = 0;
            int MaxImages = Convert.ToInt32(strNumImages);

            DirectoryInfo di = new DirectoryInfo(SourceFolder);

            foreach (FileInfo fi in di.GetFiles("*.txt"))
            {
                if (fi.OpenText().ReadToEnd().Contains(DID))
                {
                    //found one!
                    FileInfo fi2 = new FileInfo(fi.FullName.Replace(".txt", ".tif"));
                    if (fi2.Exists)
                    {
                        try
                        {
                            tbOutput.Text += "Copying " + fi2.FullName + " to " + tbDest.Text + "\r\n";
                            fi2.CopyTo(tbDest.Text + @"\" + fi2.Name, true);
                            tbOutput.Text += "Copying " + fi.FullName + " to " + tbDest.Text + "\r\n";
                            fi.CopyTo(tbDest.Text + @"\" + fi.Name, true);

                            ImageCounter++;
                        }
                        catch (Exception ex)
                        {
                            MessageBox.Show(ex.Message);
                        }
                    }
                }

                if (ImageCounter >= MaxImages)
                    break;

            }

        }

我的程序运行良好,但我想在文件复制时更新表单中的文本框以显示进度。基本上,在运行过程中窗体会变成空白,完成后输出会出现在文本框中。我希望实现一个BackgroundWorker来更新运行时的UI。

我已经查看了一些示例,但是并没有真正理解它们。我没有完成百分比的值,我只想在每次迭代时更新.Text并将其显示出来。我甚至认为我不需要将实际的复制操作放在不同的线程中,只是听起来它需要从主UI线程中单独运行。也许我总体上过于复杂化了这个问题...能否有人指导我正确的方向?谢谢!

5个回答

16

使用后台工作者是正确的做法。这是一个我自己编写的示例,展示了如何实现此功能。创建一个具有Form1的新Windows应用程序。将4个控件添加到该表单中:label1、backgroundWorker1、button1和button2。然后使用以下代码。您可以使用ReportProgress userState向主线程报告任何您想要的内容。在此示例中,我传递了一个字符串。ProgressChanged事件处理程序位于UI线程上,并更新文本框。

    public partial class Form1 : Form
{
    int backgroundInt;
    public Form1()
    {
        InitializeComponent();
        backgroundWorker1.WorkerReportsProgress = true;
    }

    private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        label1.Text = e.UserState as string;
    }

    private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
    {
        backgroundInt = 1;
        while (backgroundWorker1.CancellationPending == false)
        {
            System.Threading.Thread.Sleep(500);
            backgroundWorker1.ReportProgress(0, 
                String.Format("I found file # {0}!", backgroundInt));
            backgroundInt++;
        }
    }


    private void button1_Click(object sender, EventArgs e)
    {
        backgroundWorker1.RunWorkerAsync();
    }

    private void button2_Click(object sender, EventArgs e)
    {
        backgroundWorker1.CancelAsync();
    }
}

4
这里的重点是,就OP最初提出的“我没有百分比”抱怨而言,ReportProgress不仅可以接受百分比,还可以接受通用对象——就像在这个例子中一样——可以是一个字符串。 - Will Dean
将 e.UserState 转换为字符串并加1。这对我来说显然不是那么容易的事情。 - James John McGuire 'Jahmic'

7
如果您使用后台工作者,可以使用ReportProgress方法返回任何整数,例如已处理的记录数。它不必是百分比。然后,在ProgressChanged处理程序中,您可以更新文本框。例如:
int count = e.ProgressPercentage;
textBox1.Text = string.Format("{0} images processed.", count);

如果您不想使用后台工作器,可以在循环内调用Application.DoEvents()。这将为UI提供刷新自身和响应用户操作的机会。但要注意 - 这会大大减慢程序运行速度,因此您可能只希望在每100次迭代时调用它。


2

只需创建一个(1)委托来包装您的ProcessImages方法调用,(2)使用委托触发调用,(3)当您希望从ProcessImages方法更新文本框时,请检查跨线程操作,并确保您从主线程执行更新:

delegate void ProcessImagesDelegate(string did, string sourceFolder, string destFolder, string strNumImages);

private void ProcessImages(string DID, string SourceFolder, string DestFolder, string strNumImages)
{
    // do work

    // update textbox in form
    if (this.textBox1.InvokeRequired)
    {
        this.textBox1.Invoke(new MethodInvoker(delegate() { this.textBox1.Text = "delegate update"; }));
    }
    else
    {
        this.textBox1.Text = "regular update";
    }

    // do some more work
}

public void MyMethod()
{
    new ProcessImagesDelegate(ProcessImages).BeginInvoke(tbDID.Text, tbSource.Text, tbDest.Text, comboBoxNumberImages.SelectedItem.ToString(), null, null);
}

这比使用BackgroundWorker更好在哪里? - Will Dean
由于它们都使用线程池,并且没有一个比另一个表现更好,我倾向于更喜欢委托,因为它们通常给我提供更干净/更简单的代码(在我看来),而且这只是实现他/她想要的另一种方式。 - Jason

2

UI没有更新是因为您没有允许在长时间的文件处理循环中处理任何窗口消息。WinForms应用程序响应WM_PAINT消息进行重新绘制,并且在主线程的消息队列中进行处理。

最简单的解决方法是强制更新UI:在修改循环内部的文本框后,尝试在您的表单上调用Update()。

您的应用程序仍然会被UI冻结(无法响应鼠标点击等),但这至少可以在屏幕上绘制进度消息。如果更新显示是您真正需要的所有内容,则在此处停止。

下一级别的解决方案是允许应用程序在文件处理循环中处理待处理的窗口消息。在您的循环中调用Application.DoEvents()(而不是form.Update)。这将允许表单使用文本输出更新自身,并消除UI冻结-应用程序可以响应鼠标和键盘活动。

请注意,在此处要小心-用户可能会在当前活动正在进行时单击启动当前活动的按钮-可重入性。您应该至少禁用菜单或按钮,以防止重新进入长时间运行的文件处理。

第三个级别的解决方案是使用后台线程进行文件处理。这引入了许多新问题,您需要了解这些问题,并且在许多情况下,线程是不必要的。如果您不允许用户在文件处理过程中执行其他操作,则将文件处理推送到后台线程中没有太多意义。


OP已经走在了正确的轨道上,即使用BackgroundWorker,它既创建了后台线程(实际上是使用了线程池),也处理了你在最后一段中提到的一些问题。 - Will Dean
OP没有表明用户在文件处理过程中是否有其他UI任务需要完成,因此在我看来,后台线程可能有些过度。 - dthorpe

0

我不需要创建后台工作程序。我只需调用.Update(),然后跟随System.Threading.Thread.Sleep(100),它就开始工作了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接