Do users of your Delphi application actually see the progress bar moving?
The TProgressBar control provides visual feedback about the progress of some actions within your application. The Position, Max and Min properties determine the current position of the progress bar within its minimum and maximum values.
In most processing situations of some kind, where the progress bar is used, I have code that looks like:
ProgressBar1.Max := NUMBER_OF_ELEMENTS; try for i := 1 to NUMBER_OF_ELEMENTS do begin begin // do some operation end; //update progress bar ProgressBar1.StepIt; //if Step = 1 //OR ProgressBar1.Position := i; end; finally ProgressBar1.Position := 0; end;
I guess the above way of using a progress bar is also what you have in your application. Set the Max value, run some loop, update the progress bar position to notify the user of the progress of the loop.
Note that after the operation is finished the progress bar position is set to zero, to make it ready for the next operation requiring visual feedback where the same progress bar control is used. In fact, I have a single progress bar positioned in the status bar of the main form of my application – this single progress bar is used for all progress-type actions.
Now, would you expect, using the above code, to actually see the progress bar progressing?
The answer to that questions depends on two other questions: how fast will the loop finish and on what version of Windows (themed or not) the application is being run!
The TProgressBar as implemented in Delphi (as the case is with most other controls) is a direct implementation of the Windows common control – and therefore Windows (the operating system) is responsible for painting it.
Progress Animation on Vista, Windows 7, 8 ….
If your application is run under Windows Vista or Windows 7 (and later) with Aero enabled, the operating system draws the progress bar in a way that it smoothly scrolls from the previous position to the new position. Since this painting takes time, it can take more time then needed in your code to step to the next position.
As a result, and depending on the time needed to process your loop, you might notice that the progress bar is sometimes not painted at all, sometimes it will go to 30%, sometimes to 80% (whatever number or position).
Here’s a real code example you can use to see this (should be running on Windows 7, 8 or Vista with Aero):
procedure TProgressForm.Button1Click(Sender: TObject); var dirFiles : TStringDynArray; aFile : string; begin dirFiles := TDirectory.GetFiles('c:\SomeFolderWithPasFiles'); ProgressBar1.Max := Length(dirFiles); ProgressBar1.Position := 0; for aFile in dirFiles do begin if TPath.MatchesPattern(ExtractFileName(aFile),'*.pas',false) then begin //do something with .PAS files that could take time end; ListBox1.Items.Add(aFile); ProgressBar1.Position := 1 + ProgressBar1.Position; end; ProgressBar1.Position := 0; end;
The code loops through all the files in a specified folder and places their names in a list box, a progress bar is used for the visual feedback. If a file is a .PAS file some additional action will be done (whatever). Depending on the number of files and the number of .PAS files (and what you would do to them) it might appear that the progress bar is not being painted at all or it will reach some random position before being reset to position zero (initial position).
Similarly, if the progress bar is at 0% and you set it to 75% then 100% (very fast) – the progress bar does not “jump” to that position – it “slowly” and smoothly fills in its area – possibly too slow for the purpose!.
ProgressBar.ProperlyPaint!
Now, enough with the intro to the problem. The thing is that the way to solve the painting problem is rather simple and involves a trick.
When you set the progress position backwards the (slow) animation does not take place, rather the progress bar jumps quickly to that position.
Having this in mind, I have a simple procedure “ProgressBarStepItOne” implemented as:
procedure TProgressForm.ProgressBarStepItOne; begin ProgressBar1.StepBy(1); ProgressBar1.StepBy(-1); ProgressBar1.StepBy(1); //same as (* ProgressBar1.Position := 1 + ProgressBar1.Position; ProgressBar1.Position := -1 + ProgressBar1.Position; ProgressBar1.Position := 1 + ProgressBar1.Position; *) end;
And my code looking as below, ensures the visibility of the progress bar actually moving even when the time to process something is less than it would be needed for Windows to paint (animate) the progress bar moving from 0 to 100!
procedure TProgressForm.Button1Click(Sender: TObject); var dirFiles : TStringDynArray; aFile : string; begin dirFiles := TDirectory.GetFiles('C:\SomeFolderWithPasFiles'); ProgressBar1.Max := Length(dirFiles); ProgressBar1.Position := 0; for aFile in dirFiles do begin if TPath.MatchesPattern(ExtractFileName(aFile),'*.pas',false) then begin //do something with .PAS files that could take time end; ListBox1.Items.Add(aFile); ProgressBarStepItOne; end; ProgressBar1.Position := 0; end;
If you use the TProgressBar’s StepIt procedure to advance the Position property with the current value of M by some value > 1, let’s say N , make sure you then set it to M+N-1 and back to M+N.
Now, is that a trick you would never think of or?
Yep, stumbled into this just some weeks ago. In the end I decided that as long as what I’m passing is in all senses right, it’s Windows fault for not displaying it properly. Any hacks I implement might turn into something even worse on, say, Windows 9 where progress bar is rendered yet again differently.
I use:
ProgressBar1.Position := aValue;
ProgressBar1.Update ;
and haven’t noticed any weird behaviour. Maybe I’m just lucky ?
Stuart, I guess you are lucky, as this does not work work for me (Win 7, Aero, fast loop, setting position to 0 when “finished”).
I use similar but tell the app to process it’s paint requests:
application.processmessages;
If you refresh that often that the progress bar cannot follow in time, I would think about the concept, not about tricks. And calling Update is just another dirty trick. These unnecessarily delay the whole process, which by its nature (otherwise you wouldn’t need a progress bar) is already taking precious time from the user.
Why waste that much CPU time on the UI, plus let the user wait longer?
Put your processing code into a thread (that’s what they are for!) instead, update a TMultiReadExclusiveWriteSynchronizer (or similar) with the status, and let the UI pull status update once every 100 or 250 ms. That way, your processing won’t take longer than necessary and the UI still gets updated, without too much flicker or CPU just for that.
Thanks for the tip Zarko 🙂 I am glad to see your blogging!
I might stubled on this problem a year ago or so. Personally I hate progress bars that provide false information to the user. (It is common under Windows to notice a progress bar that goes to 99% or even 100% and stays at this point for ages. ) \
My rule of thumb is to not show progress bar when I am not able to divide my process to given amount of parts. In such case it is better to notice unser in different way.
Continuing your topic in new delphi you can use class helpers for changing position property. In this way you can use standard component, but animation will be off:
type
test = class helper for TProgressBar
private
function getpos: integer;
procedure setpos(value: integer);
public
property position: integer read getpos write setpos;
end;
function test.getpos: integer;
begin
Result:= inherited Position;
end;
procedure test.setpos(value: integer);
begin
inherited position:= value;
StepBy(-1);
StepBy(1);
end;
Continue your ‘dirty trick’ you can use class helper (in delphi > 7) to modify position property. In this way you can use standard component, but it will be modified by mentioned trick.
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ComCtrls, Vcl.StdCtrls;
type
test = class helper for TProgressBar
private
function getpos: integer;
procedure setpos(value: integer);
public
property position: integer read getpos write setpos;
end;
TForm1 = class(TForm)
ProgressBar1: TProgressBar;
Button1: TButton;
Edit1: TEdit;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
{ test }
function test.getpos: integer;
begin
Result:= inherited Position;
end;
procedure test.setpos(value: integer);
begin
inherited position:= value+1;
if value max then
begin
StepBy(-1);
StepBy(0);
end else
begin
StepBy(-1);
StepBy(1);
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
licznik: integer;
begin
for licznik := 1 to 10 do
begin
progressbar1.position:= licznik*10;
Edit1.Text:= IntToStr(progressbar1.position);
sleep(100);
end;
end;
end.