简体   繁体   English

从另一个线程更新列表框后,如何强制主GUI线程更新列表框?

[英]How do I force the main GUI thread to update a listbox after it is updated from a different thread?

I'm tinkering away on a multithreaded downloader, using a producer/consumer queue construct; 我正在使用生产者/消费者队列构造来修补多线程下载器。 the downloading parts works fine, but I'm running into a problem keeping the GUI updated. 下载部分工作正常,但是在更新GUI时遇到了问题。

For now I'm using a listbox control on the form to display status messages and updates on downloading progress, eventually I hope to replace that with progressbars. 现在,我在窗体上使用列表框控件来显示状态消息和下载进度的更新,最终我希望将其替换为进度条。

First the Form1 code behind; 首先是Form1代码; the form contains nothing but a button and the listbox: 表单只包含一个按钮和一个列表框:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    public void SetProgressMessage(string message) 
    { 
        if (listboxProgressMessages.InvokeRequired) 
        {
            listboxProgressMessages.Invoke(new MethodInvoker(delegate()
            { SetProgressMessage(message); })); 
        } 
        else 
        {
            listboxProgressMessages.Items.Add(message);
            listboxProgressMessages.Update();
        } 
    }

    private void buttonDownload_Click(object sender, EventArgs e)
    {
        SetProgressMessage("Enqueueing tasks");
        using (TaskQueue q = new TaskQueue(4))
        {
            q.EnqueueTask("url");
            q.EnqueueTask("url");
            q.EnqueueTask("url");
            q.EnqueueTask("url");
            q.EnqueueTask("url");
            q.EnqueueTask("url");
            q.EnqueueTask("url");
            q.EnqueueTask("url");
            q.EnqueueTask("url");
            q.EnqueueTask("url");
        }
        SetProgressMessage("All done!");
    }
}

Now the producer/consumer logic. 现在是生产者/消费者逻辑。 The consumer downloads the files bit by bit, and should tell the listbox living on the GUI thread how the progress is coming along; 使用者一点一点地下载文件,并应该告诉GUI线程上的列表框进度如何; This works, but the listbox doesn't actually update until all is finished, also the messages appear after the 'All done!' 这可以工作,但是列表框实际上不会更新,直到所有步骤完成,并且在“全部完成!”之后还会显示消息。 message, which isn't desirable. 消息,这是不可取的。

TaskQueue.cs: TaskQueue.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO;
using System.Net;
using System.Windows.Forms;
using System.Runtime.Remoting.Messaging;

namespace WinformProducerConsumer
{
    public class TaskQueue : IDisposable
    {
        object queuelocker = new object();
        Thread[] workers;
        Queue<string> taskQ = new Queue<string>();

    public TaskQueue(int workerCount)
    {
        workers = new Thread[workerCount];
        for (int i = 0; i < workerCount; i++)
            (workers[i] = new Thread(Consume)).Start();
    }

    public void Dispose()
    {
        foreach (Thread worker in workers) EnqueueTask(null);
        foreach (Thread worker in workers) worker.Join();
    }

    public void EnqueueTask(string task)
    {
        lock (queuelocker)
        {
            taskQ.Enqueue(task);
            Monitor.PulseAll(queuelocker);
        }
    }

    void Consume()
    {
        while (true)
        {
            string task;
            Random random = new Random(1);
            lock (queuelocker)
            {
                while (taskQ.Count == 0) Monitor.Wait(queuelocker);
                task = taskQ.Dequeue();
            }
            if (task == null) return;

            try
            {
                Uri url = new Uri(task);
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();
                response.Close();
                Int64 iSize = response.ContentLength;
                Int64 iRunningByteTotal = 0;

                using (WebClient client = new System.Net.WebClient())
                {
                    using (Stream streamRemote = client.OpenRead(new Uri(task)))
                    {
                        using (Stream streamLocal = new FileStream(@"images\" + Path.GetFileName(task), FileMode.Create, FileAccess.Write, FileShare.None))
                        {
                            int iByteSize = 0;
                            byte[] byteBuffer = new byte[iSize];
                            while ((iByteSize = streamRemote.Read(byteBuffer, 0, byteBuffer.Length)) > 0)
                            {
                                streamLocal.Write(byteBuffer, 0, iByteSize);
                                iRunningByteTotal += iByteSize;

                                double dIndex = (double)iRunningByteTotal;
                                double dTotal = (double)byteBuffer.Length;
                                double dProgressPercentage = (dIndex / dTotal);
                                int iProgressPercentage = (int)(dProgressPercentage * 100);

                                string message = String.Format("Thread: {0} Done: {1}% File: {2}",
                                    Thread.CurrentThread.ManagedThreadId,
                                    iProgressPercentage,
                                    task);

                                Form1 frm1 = (Form1)FindOpenForm(typeof(Form1));
                                frm1.BeginInvoke(new MethodInvoker(delegate()
                                {
                                    frm1.SetProgressMessage(message);
                                })); 
                            }
                            streamLocal.Close();
                        }
                        streamRemote.Close();
                    }
                }

            }
            catch (Exception ex)
            {
                // Generate message for user
            }
        }
    }

    private static Form FindOpenForm(Type typ) 
    { 
        for (int i1 = 0; i1 < Application.OpenForms.Count; i1++) 
        { 
            if (!Application.OpenForms[i1].IsDisposed && (Application.OpenForms[i1].GetType() == typ))
            { 
                return Application.OpenForms[i1]; 
            } 
        } 
        return null; 
    }
}

} }

Any suggestions, examples? 有什么建议,例子吗? I've looked around for solutions, but couldn't find anything that I could follow or worked. 我一直在寻找解决方案,但是找不到我可以遵循或工作的任何东西。

Replacing the frm1.BeginInvoke(new MethodInvoker(delegate() with a frm1.Invoke(new MethodInvoker(delegate() results in a deadlock. I'm rather stumped here. 将frm1.BeginInvoke(new MethodInvoker(delegate()替换为frm1.Invoke(new MethodInvoker(delegate()会导致死锁)。我在这里很困惑。

Sources: Producer/Consumer example: http://www.albahari.com/threading/part4.aspx 来源:Producer / Consumer示例: http : //www.albahari.com/threading/part4.aspx

Update: I'm going about this the wrong way; 更新:我正在以错误的方式进行操作; instead of invoking back to the GUI from the worker threads, I'll use events that the GUI thread will have to keep an eye on. 而不是从工作线程调用回GUI,我将使用GUI线程必须注意的事件。 A lesson learned. 一个教训。 :) :)

You should eliminate FindOpenForm and add a ProgressChanged event to TaskQueue . 您应该消除 FindOpenForm并将一个ProgressChanged事件添加到TaskQueue It is absolutely not TaskQueue responsibility to make direct calls to the presentation-layer (forms or controls). 直接调用表示层(窗体或控件) 绝对不是 TaskQueue的责任。 It is then the form's responsibility to listen for "progress-changed" events generated by tasks, and then update itself propertly. 然后,表单的职责是侦听任务生成的“进度更改”事件,然后适当地更新自身。

This will easily solve your problem, it will keep simple, follow best-practices, and eliminate timing issues, threading issues, and so on. 这将很容易解决您的问题,将保持简单,遵循最佳实践,并消除时间问题,线程问题等。

My other answer is more appropriate. 我的其他答案更合适。 This answer is more appropriate once you change TaskQueue to raise ProgressChanged event. 一旦更改TaskQueue以引发ProgressChanged事件,此答案就更合适。


Try calling listboxProgressMessages.Refresh() . 尝试调用listboxProgressMessages.Refresh() This forces a paint. 这迫使油漆。 Check Control.Refresh documentation. 检查Control.Refresh文档。 Sometimes, you have to call the refresh method of the form. 有时,您必须调用表单的refresh方法。

Control.Refresh Method 控制刷新方法

Forces the control to invalidate its client area and immediately redraw itself and any child controls. 强制控件使它的客户区无效,并立即重绘自身和所有子控件。

http://msdn.microsoft.com/en-us/library/system.windows.forms.control.refresh.aspx http://msdn.microsoft.com/en-us/library/system.windows.forms.control.refresh.aspx

Also in your code, instead of calling BeginInvoke , call Invoke - the latter is synchronous from the caller. 另外,在你的代码,而不是调用BeginInvoke ,调用Invoke -后者是从调用者同步。 This will mean that you call across to the GUI and wait until the GUI has finished what its doing before returning. 这意味着您将调用GUI,并等到GUI完成其操作后再返回。

Also, the GUI thread could be starved, I think invokes onto the GUI thread for controls are actually marshalled onto the message pump - if the pump doesn't pump, the call never gets through. 而且,GUI线程可能会饿死,我认为对GUI线程的控件调用实际上已编组到消息泵上-如果泵不泵送,则调用将永远无法通过。

Update: alternatively, you can use the BackgroundWorker form component. 更新:或者,您可以使用BackgroundWorker表单组件。 This has been built to work in WinForms, thus working with the GUI. 它已构建为可在WinForms中使用,从而可与GUI一起使用。 It exposes a ProgressChanged event that you can use to pass arbitrary progress reports back to the UI. 它公开一个ProgressChanged事件,您可以使用该事件将任意进度报告传递回UI。 The event itself is marshalled so that it calls on the GUI thread automatically. 事件本身被编组,以便它自动调用GUI线程。 This removes the need for you to do it manually. 这消除了您手动进行操作的需要。

BackgroundWorker threads also use the thread pool. BackgroundWorker线程也使用线程池。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM