0

I'm trying to make a smooth progress bar that "loads" from 0 to 100 based on a time value (in seconds).

So far, I've got a method that sets up the progress bar's maximum value to 100 * loadTime. Additionally, I've got a timer in my form, and its increment is 10 milliseconds. The idea is that the progress bar's maximum, divided by the timer's increment, will equal the number of seconds the progress bar needs to load.

Unfortunately, there are some inconsistencies with my timer. For instance, if I set my timer's increment to 1000 milliseconds and the bar's maximum to loadTime, it will be somewhat consistent, but it doesn't do anything for the first second. It's also very jittery. At 100 milliseconds and 10 * loadTime, it's slightly more consistent, but still very jittery. 10 milliseconds seems to be the sweet spot in terms of smoothness, however, if for instance, loadTime is equal to 5, it will load the progress bar in approximately 7 or 8 seconds.

I have also tried setting the timer's increment to 1, and my bar's maximum to 1000 * loadTime, however, this just makes it slower, and results in times of 10-13 seconds, when it should be 5 for instance.

Why's this the case? Can anything be done about it?

DisplayTimeProgressBar.cs (credit to Crispy's answer)

[DesignerCategory("code")]
public class DisplayTimeProgressBar : ProgressBar
{
    public DisplayTimeProgressBar()
    {
        this.SetStyle(ControlStyles.UserPaint, true);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        Rectangle rec = e.ClipRectangle;

        rec.Width = (int)(rec.Width * ((double)Value / Maximum));
        rec.Height = rec.Height;

        e.Graphics.FillRectangle(Brushes.Aquamarine, 0, 0, rec.Width, rec.Height);
    }
}

Form.Designer.cs

private System.Windows.Forms.Timer progressBarTimer = new System.Windows.Forms.Timer(this.components);
private DisplayTimeProgressBar displayTimeProgressBar = new DisplayTimeProgressBar();

Form.cs

private void loadBar(int timeToDisplay)
{
    this.displayTimeProgressBar.Visible = true;
    this.displayTimeProgressBar.Maximum = timeToDisplay * 100;
    this.progressBarTimer.Start();
}

private void progressBarTimer_Tick(object sender, EventArgs e)
{
    if (this.displayTimeProgressBar.Value >= this.displayTimeProgressBar.Maximum)
    {
        this.displayTimeProgressBar.Value = 0;
        this.displayTimeProgressBar.Visible = false;
        this.progressBarTimer.Stop();
    }
    else
    {
        this.displayTimeProgressBar.Value++;
    }
}

The following is a visual demonstration of my issue. The parameter loadTime has been set to 5 (5 seconds). In this demo, the timer's increment is 10, and the bar's maximum is 500. Demo of the progress bar. loadTime is 5 seconds

yeho
  • 132
  • 2
  • 13
  • The problem maybe is the calculus `rec.Width = (int)(rec.Width * ((double)Value / Maximum));`, in the conversion, maybe you are painting a 0 width rectangle. Put a conditional breakpoint in the next line with `rec.Width==0` as condition. – Frank Jun 23 '21 at 18:29
  • I'm not sure what you mean by that. The width is clearly correct. Look at the attached GIF. The progress bar is being painted just fine, but it's too slow for the provided time. – yeho Jun 23 '21 at 18:33
  • I was talking about the disappearing progress bar. You are using `System.Windows.Forms.Timer`, this timer share process with the UI thread. That means it priority is responsiveness, if you need more accuracy, then use `System.Threading`, just take care of deadlocks. – Frank Jun 23 '21 at 19:21

1 Answers1

0

If you want a continous animation over a specifc duration, wouldn't you have to base it on frames per second (i.e. 40 fps), as well as taking into consideration the maximum width in pixels of the monitor, because the Value corresponds to the number of pixels filled. In other words, using 100 for the Maximum value doesn't make sense. So then sleep 25 millis, calculate the true elapsed time (current time minus start time), divide the elapsed time by the total duration. Now you have a percentage. Multiply that percentage by the Maximum value, and that is the current Value. Also, setting pb.Style = ProgressBarStyle.Continuous; seems to look better, but it may not apply to your situation since you are using a custom paint. Example:

    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    Form f5 = new Form();
    ProgressBar pb = new ProgressBar { Dock = DockStyle.Top, Minimum = 0, Maximum = 2000 };
    pb.Style = ProgressBarStyle.Continuous;

    double durationSeconds = 5;
    Button btnStart = new Button { Text = "Start", Dock = DockStyle.Bottom };
    double fps = 40; // refreshes per second

    btnStart.Click += delegate {
        int maxValue = pb.Maximum;
        Thread t = new Thread(() => {
            DateTime utcNow = DateTime.UtcNow;
            int sleepMillis = (int) (1000 / fps);
            int progress = 0;
            while (true) {
                Thread.Sleep(sleepMillis);
                DateTime utcNow2 = DateTime.UtcNow;
                double elapsedSeconds = (utcNow2 - utcNow).TotalSeconds;
                int newProgress = (int) Math.Round(maxValue * elapsedSeconds / durationSeconds);
                if (newProgress >= maxValue) {
                    pb.BeginInvoke((Action) delegate {
                        pb.Value = maxValue;
                    });
                    break;
                }
                else if (newProgress > progress) {
                    pb.BeginInvoke((Action) delegate {
                        pb.Value = newProgress;
                    });
                }
                progress = newProgress;
            }

        });
        t.IsBackground = true;
        t.Start();
    };
    f5.Controls.Add(pb);
    f5.Controls.Add(btnStart);

    Application.Run(f5);
Loathing
  • 5,109
  • 3
  • 24
  • 35