Why does the text on my asynchronous ProgressBar f

2019-06-02 10:37发布

问题:

I want to display text on a ProgressBar, (without all the nonsense of a custom progress bar). This is not hard to do, and does not involve the OnPaint method -- as demonstrated with button1 of the following code. However, this method blocks the UI thread, which is evil. Unfortunately, my best stab at an asynchronous approach causes the text to flicker, which is very annoying.

Can somebody show me how to update the text asynchronously without the flicker?

(To run the following code, just paste it into a new project containing a Form with 3 Buttons and 3 ProgressBars).

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading.Tasks;
using System.Threading;

namespace  ProgBarTest //change namespace in Program.cs accordingly
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //This method will block the main UI thread
            // (thus freezing the other buttons)
            StartUpdate(this.progressBar1); 
        }
        private void button2_Click(object sender, EventArgs e)
        {
            //this method (and the next) don't block the UI, and can be used concurrently,
            // but the text flickers
            Task t = new Task(() => StartUpdate(this.progressBar2));
            t.Start();
        }
        private void button3_Click(object sender, EventArgs e)
        {
            Task t = new Task(() => StartUpdate(this.progressBar3));
            t.Start();
        }

        private void StartUpdate( ProgressBar progBar)
        {
            for (int i = 1; i <= 100; i++)
            {
                UpdateProgressBar(i, progBar);
                Thread.Sleep(100);
            }
        }

        private void UpdateProgressBar(int i, ProgressBar progBar)
        {
            if (progBar.InvokeRequired) //for use with background Task
            {
                progBar.Invoke(new Action<int, ProgressBar>(UpdateProgressBar), 
                                  new Object[] { i, progBar });
            }
            else
            {
                //set progress bar:
                progBar.Value = i;
                progBar.Refresh();

                //set status label:
                string percentStr = i.ToString() + "%";
                PointF location = new PointF(progBar.Width / 2 - 10, progBar.Height / 2 - 7);
                using (Graphics gr = progBar.CreateGraphics())
                {
                    gr.DrawString(percentStr, new Font("Arial",10), Brushes.Red, location );
                }
            }
        }

    }
}

回答1:

In all honesty, a custom ProgressBar would be your best bet.. if you set it up correctly. I know I'm not answering your question so please don't downvote, just offering a different solution. I haven't tested this yet, but it would in theory draw the percentage every time the progress bar had to repaint, which would happen everytime the value was changed.

Your current problem is that the bar is repainting itself everytime you change the value, thus the flickering of the text. If it didn't repaint, you would see percentage you draw starting to overlay on top of eachother which is not good either.

This would be better to encapsulate everything within one control from a design standpoint as well.

class CustomProgressBar : ProgressBar {
    public CustomProgressBar() : base() {}

    protected override void OnPaint(PaintEventArgs e) {
        // Call the OnPaint method of the base class.
        base.OnPaint(e);
        string percentStr = this.Value.ToString() + "%";
        PointF location = new PointF(this.Width / 2 - 10, this.Height / 2 - 7);
        // Call methods of the System.Drawing.Graphics object.
        e.Graphics.DrawString(percentStr, new Font("Arial",10), Brushes.Red, location );
    } 
}


回答2:

It's probably because you're drawing the string from another thread. If you can use a Control based text element instead you can use BeginInvoke in the same way as the ProgressBar:

// Label progressLbl

private void UpdateProgressFromAnotherThread(int completed, int total, string progressText)
{
    this.progBar.BeginInvoke(new Action(() =>
    {
        this.progBar.Maximum = total;
        this.progBar.Value = completed;
    }));

    this.progressLbl.BeginInvoke(new Action(() =>
    {
        this.progressLbl.Text = progressText;
    }));
}