【问题标题】:C# Form Controls Won't Update with MultithreadingC# 表单控件不会随多线程更新
【发布时间】:2016-11-30 01:01:56
【问题描述】:

我一直在玩多线程并阅读这里的一些问题,但我还没有找到直接解决我所关心的问题的答案。

我有一个在单个线程上运行的应用程序,除了单独窗口中的进度条。根据我的研究,我需要为该表单创建一个新线程,该线程将在其属性更改时重新绘制表单的控件。我已将问题简化为以下简单示例:

这是“主”程序:

class Program
{
    static MyForm form;

    static void Main(string[] args)
    {
        form = new MyForm();
        form.Show();

        doWork();

        form.Close();
    }

    //arbitrary example of processing that takes some period of time
    static void doWork()
    {
        while (form.Value < 100000) 
        {
            form.ChangeVal();
            Thread.Sleep(1);
        }
        return;
    }
}

...这是表格。我不包括 VS 自动生成的东西。

public partial class MyForm : Form
{
    private int val;
    public int Value
    {
        get { return val; }
        set { val = value; }
    }

    public Thread GUIupdater;


    public MyForm()
    {
        InitializeComponent();
        this.Refresh();
    }

    private void MyForm_Load(object sender, EventArgs e)
    {
        GUIupdater = new Thread(new ThreadStart(GUIupdaterThread));
        GUIupdater.Start();

        this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(killThreadOnClose);
    }

    public void ChangeVal()
    {
        val++;
    }

    private void changeLabel(string s)
    {
        label.Text = s;
        label.Refresh();
    }
    private delegate void labelChanger(string s);
    private void GUIupdaterThread()
    {
        while (true)
        {
            Invoke(new labelChanger(changeLabel), new object[]{val.ToString()} );
            Thread.Sleep(100); //100 ms
        }
    }
    private void killThreadOnClose(object sender, FormClosingEventArgs e)
    {
        GUIupdater.Abort();
    }
}

所以,我的目的是让计算持续运行,窗口的图形更新得相当快。然而,当我运行程序时,invoke 函数只被调用一次,并且标签实际上从未更新!

感谢任何和所有反馈。如果你想在 IDE 中查看代码,可以从Here下载我的项目

编辑:

  • 当我添加 Console.WriteLine 语句时,我发现 GUIupdaterThread(用于更新 GUI 的东西)循环总是在 Invoke 语句上“中断”,从未到达“Thread.Sleep”。我将其更改为“BeginInvoke”,这会导致循环正常运行,但这并没有改变 GUI 不更新的事实。

澄清:

关于我的“实际”项目:

  • “程序”中的主线程模拟我的软件,这是一个实现接口的插件。我决定在该线程中更改 val / value,而不是在窗口创建的线程中,是经过深思熟虑的。
  • 我只能使用 .NET 4.0 。任何更新的功能都帮不了我

【问题讨论】:

  • 看来你是在倒退。您的计算应该在线程上运行,而不是更新表单。
  • 由于我正在使用的系统,计算必须绝对在主线程上运行,我的目标是在 GUI 窗口中显示结果。必须有某种方法来完成这项工作,对吧?
  • 我会摆脱所有那些while循环,只使用一个计时器。我在您的代码中看不到任何需要单独线程的内容。如果这只是一个练习,请遵循 BackgroundWorker 教程。

标签: c# multithreading winforms user-interface


【解决方案1】:

由于在您的应用程序中您有 GUI 线程(主线程) - 所有 UI 控件都只能从该线程访问。

有几种方法可以从其他线程更新控件。 我想建议您使用基于Progress < T > class 的现代原生方法之一(它是.Net 平台的原生方法)。

【讨论】:

  • 你是说我的主线程是我的GUI线程?我在表单中创建的那个呢?
  • 另外,我被困在使用 .Net 4.0 上。看起来这只是4.5+。我一定会把它添加到我的问题中
  • @BrentAllard 4.0 已经有一段时间不受支持了。您可能已经在您的机器上运行 4.5.2 而没有意识到这一点,您的客户也是如此。在任何情况下,您可以使用 BCL.Async 包将 async/await 和 Progress 添加到 .NET 4.0。只是没有理由了,因为不支持.NET 4
  • @BrentAllard 还注意到 TLS 1.2 是在 .NET 4.5.2 中添加的。您必须在某个时候进行升级,或者失去使用 HTTPS 的能力。每个人至少需要 TLS1.1,许多服务需要 TLS1.2
【解决方案2】:

我建议覆盖表单的OnPaint 方法。然后在ChangeVal 中,在您更新了需要更新的任何变量/数据后,调用this.Invalidate 应该会触发表单重新绘制自身。

或者,如果您只是更新单个标签,请在 ChangeVal 方法中调用 label.Refresh。表单应正确更新。这是一个对我有用的例子:

此表单上有一个标签。

public partial class ProgressForm : Form
{

    private int currentValue = 0;

    public ProgressForm()
    {
        InitializeComponent();
    }

    public void ChangeValue(int newValue)
    {
        currentValue = newValue;
        lblValue.Text = string.Format("Current value: {0}", currentValue);
        lblValue.Refresh();   //Call Refresh to make the label update itself
    }
}

static class Program
{
    private static ProgressForm progressForm = null;

    [STAThread]
    static void Main()
    {
        //Application.EnableVisualStyles();
        //Application.SetCompatibleTextRenderingDefault(false);
        //Application.Run(new Form1());

        progressForm = new ProgressForm();
        progressForm.Show();

        doWork();

        progressForm.Close();
    }


    //arbitrary example of processing that takes some period of time
    static void doWork()
    {
        int i = 0;
        while (i < 100000)
        {
            progressForm.ChangeValue(i);
            Thread.Sleep(1);
            i++;
        }
        return;
    }
}

【讨论】:

  • 这段代码非常适合我!根据给出的答案,对于我的特定情况(计算在父窗口中),单线程似乎是最佳选择。
【解决方案3】:

当您尝试访问主线程以外的 UI 控件(从中创建它)时,您可以改用以下内容。

while ( true )
{
    Invoke ( ( Action ) (() =>
    {
       label.Text = val.ToString();
       label.Refresh()
       Application.DoEvents();
    }));

    Thread.Sleep( 100 );
}

【讨论】:

  • 感谢您的回复!我实现了这一点,并最终得到了相同的结果,其中 GUI 标签没有更新。它保持空白。
  • 我已经编辑了答案,忘记了 Application.DoEvents();这将强制更新。
  • 我将它添加到我的代码中,但我仍然没有从 GUI 获得任何结果。查看我在问题底部所做的编辑—— Invoke 似乎正在破坏 while 循环的执行,而实际上从未使该操作发生。您能否尝试运行代码(您可以从我的问题中的链接下载项目)?这是我第一次使用 GUI 元素进行多线程,所以我可能犯了一些简单的错误。
  • 使用Application.DoEvents 是个坏主意。网上有很多文章讨论这个。
【解决方案4】:

我建议你使用“backgroundworker”。

首先将CheckForIllegalCrossThreadCalls = false;添加到初始化部分,否则会出现InvalidOperationException

private void btnDoIt_Click(object sender, EventArgs e)
{
    backgroundWorker.RunWorkerAsync();
}

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    Foo();
}

int total = 0;
private void Foo()
{
    for (int i = 0; i <= 100000; i++)
    {
        total += i;
        this.Text = i.ToString();
    }
}

private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    // Run next process
}

【讨论】:

  • 感谢您的回复!由于我无法控制的设计限制,我必须更改我的“主”线程中的值(在您的代码中,“总计”),这是类程序中的一个。在我的实际项目中,我正在编写一个实现接口的插件,因此所有信息都必须经过主线程。你会建议一种不同的方法吗?
  • 这是个坏主意。 CheckForIllegalCrossThreadCalls 只是隐藏了潜在的问题。从另一个线程更新 UI 时,必须将调用编组到 UI 线程。对于BackgroundWorker,这是通过处理ProgressChanged 事件并在DoWork 方法中调用ReportProgress 来完成的。
  • 没有理由使用 BackgroundWorker,Task 和 Progress 提供了相同的功能(以及更多功能)。不要介意禁用跨线程调用检查的非常非常糟糕的想法,这可能会导致 UI 乱码
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-08-26
  • 1970-01-01
  • 1970-01-01
  • 2012-05-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多