Windows 7 Aero Theme Progress Bar Bug?

2019-01-12 03:16发布

问题:

I have ran into what I consider to be a progress bar bug on Windows 7. To demonstrate the bug I created a WinForm application with a button and a progress bar. In the button's 'on-click' handle I have the following code.

private void buttonGo_Click(object sender, EventArgs e)
{
  this.progressBar.Minimum = 0;
  this.progressBar.Maximum = 100;

  this.buttonGo.Text = "Busy";
  this.buttonGo.Update();

  for (int i = 0; i <= 100; ++i)
  {
    this.progressBar.Value = i;
    this.Update();

    System.Threading.Thread.Sleep(10);
  }

  this.buttonGo.Text = "Ready";
}

The expected behavior is for the progress bar to advance to 100% and then the button text to change to 'Ready'. However, when developing this code on Windows 7, I noticed that the progress bar would rise to about 75% and then the button text would change to 'Ready'. Assuming the code is synchronous, this should not happen!

On further testing I found that the exact same code running on Windows Server 2003 produced the expected results. Furthermore, choosing a non aero theme on Windows 7 produces the expected results.

In my mind, this seems like a bug. Often it is very hard to make a progress bar accurate when the long operation involves complex code but in my particular case it was very straight forward and so I was little disappointed when I found the progress control did not accurately represent the progress.

Has anybody else noticed this behavior? Has anybody found a workaround?

回答1:

It has to do with the animation of the progress bar. If your progress bar is at 0% and you set it to 100% then it will not jump there, but animate the progress bar smoothly filling up. If this is too slow, you will be done before the progress bar finished animating. So even though you have already set it to 80, 90 and 100%, the animation still lags behind.

I never found a way to turn this off, however I have a workaround. The animation is only being done if you increment the progress bar. If you move it backwards, it will immediately jump to that position. So if I want the progress bar to be at x% (x != 100) then I move it to x+1 and then to x. If I want it at 100% I move it to 100, 99 and 100%. (Or whatever values you use, you get the idea.) This works fast enough to not to be visible, and you can leave this code in for previous Windows versions as well (though I don't).



回答2:

I had the same problem. Fozi's tipp was helping me. Before setting a new value I have set the value + 1. To make this work also for 100% the maximum must be increased before. The following worked fine for me.

if (NewValue < progressBar.Maximum)
{
  progressBar.Value = NewValue + 1;
  progressBar.Value--;
}
else
{
  progressBar.Maximum++;
  progressBar.Value = progressBar.Maximum;
  progressBar.Value--;
  progressBar.Maximum--;
}


回答3:

I think the original problem is related to timing and Win7's (or Aero's) animation mechanism for the progress bar.

This Sub is on the form that contains the progress bar (pBar).

It varies the bar's .Maximum and keeps .Value fixed at 10 for percent completes of 1 to 99. The bar's .Minimum is set to 0 at design time.

This sorted out the problem for me.

Public Sub UpdateStatusPC(ByVal pc As Integer)

    Try

        If pc < 0 Then
            pBar.Maximum = 100
            pBar.Value = 0
        ElseIf pc > 100 Then
            pBar.Maximum = 100
            pBar.Value = 100
        ElseIf pc = 0 Then
            pBar.Maximum = 10
            pBar.Value = 0
        Else
            pBar.Value = 10
            pBar.Maximum = 10 / CDbl(pc / 100.0)
        End If

        pBar.Update()

    Catch ex As Exception

        MsgBox("UpdateStatusPC: " & ex.Message)

    End Try

End Sub


回答4:

To Delphi users facing the same problem: Below is a unit called ProgressBarFix that you can use to automatically patch the problem without worrying about changing your progress bar code -- just include ProgressBarFix in your form's interface "uses" clause after the ComCtrls uses and you'll get the workaround automatically:

unit ProgressBarFix;
(* The standard progress bar fails under Windows theming -- it fails to animate
   all the way to the right side. C.f.,
   http://stackoverflow.com/questions/2217688/windows-7-aero-theme-progress-bar-bug

   To work around the problem, include ProgressBarFix in the interface section's
   "uses" clause *after* ComCtrls (this replaces the TProgressBar definition in
   ConCtrls with the one here, effectively allowing the control defined on the
   form to be replaced with the patch version.

   c.f., http://www.deltics.co.nz/blog/?p=222and http://melander.dk/articles/splitter *)

interface
uses ComCtrls ;

type TProgressBar = class(ComCtrls.TProgressBar)
private
    procedure SetPosition(Value: Integer);
    function GetPosition: Integer;
published
    property Position: Integer read GetPosition write SetPosition default 0;
end ;

implementation

{ TProgressBar }

function TProgressBar.GetPosition: Integer;
begin
    result := inherited Position
end;

procedure TProgressBar.SetPosition(Value: Integer);
begin
    if Value=inherited Position then
        exit ;
    if value<Max then begin
        inherited Position := value+1 ;
        inherited Position := value
    end else begin
        Max := Max+1 ;
        inherited Position := Max ;
        inherited Position := value ;
        Max := Max-1
    end            
end;

end.


回答5:

Disable visual effect option "Animate controls and elements inside windows" in "Performance options". Then the progressbars won't be animated any longer.



回答6:

I have seen similar issues with progress bars on Vista and Windows 7.

The key problem in my case was the blocking of the UI thread. (Like you do in your sample).

Windows does not like applications that don't respond to new messages in the message queue. If you spend too much time on one message, windows will mark your application as "not responsive". In Vista/Win7, windows also decides to stop updating your application window.

As a workaround, you could put the actual work on a background worker, or call Application.DoEvents() every once in a while. You do need to make sure that your progress bar window is modal, or else the DoEvents() may enable new commands to start executing halfway through your background processing.

If that feels to kludgy, the more proper way is to do your background work on a BackgroundWorker thread. It comes with support for sending events to the UI thread to update the progress bar.



回答7:

(09/2015) I just jumped from D6 to XE8. Having a number of issues. Including this TProgressBar thing. Tabled it for a while. Came across this (Erik Knowles) fix tonight. Fantastic. Except: the first scenario I ran through had a Max value of 9,770,880. And it (Erik Knowles' "original" fix) REALLY added to the time this process took (with all the extra actual updating of the ProgressBar).

So I expanded his class to reduce the amount of times the ProgressBar actually redraws itself. But ONLY IF the "original" Max value is greater than MIN_TO_REWORK_PCTS (I settled on 5000 here).

If so, the ProgressBar only updates itself HUNDO times (here I started with and pretty much settled on 100, hence the "HUNDO" name).

I accounted for some quirkiness at the Max value as well:

if Abs(FOriginalMax - value) <= 1 then
  pct := HUNDO

I tested this against my original 9.8m Max. And, with this standalone test app:

:
uses
  :
  ProgressBarFix;

const
  PROGRESS_PTS = 500001;

type
  TForm1 = class(TForm)
    Label1: TLabel;
    PB: TProgressBar;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  x: integer;
begin
PB.Min := 0;
PB.Max := PROGRESS_PTS;
PB.Position := 0;

for x := 1 to PROGRESS_PTS do
  begin
  //let's do something
  //
  Label1.Caption := Format('%d of %d',[x,PROGRESS_PTS]);
  Update;

  PB.Position := x;
  end;

PB.Position := 0;
end;

end.

with PROGRESS_PTS values of: 10 100 1,000 10,000 100,000 1,000,000

It's smooth and "accurate" for all of these values - without really slowing anything down.

In testing, I was able to toggle my compiler directive DEF_USE_MY_PROGRESS_BAR to test both ways (this TProgressBar replacement vs the original).

Note that you might want to uncomment the call to Application.ProcessMessages.

Here is the (my "enhanced") ProgressBarFix source:

unit ProgressBarFix;

interface

uses
  Vcl.ComCtrls;

type
  TProgressBar = class(Vcl.ComCtrls.TProgressBar)
  const
    HUNDO = 100;
    MIN_TO_REWORK_PCTS = 5000;
  private
    function  GetMax: integer;
    procedure SetMax(value: integer);
    function  GetPosition: integer;
    procedure SetPosition(value: integer);
  published
    property Max: integer read GetMax write SetMax default 100;
    property Position: integer read GetPosition write SetPosition default 0;

  private
    FReworkingPcts: boolean;
    FOriginalMax:   integer;
    FLastPct:       integer;
  end;

implementation

function TProgressBar.GetMax: integer;
begin
result := inherited Max;
end;

procedure TProgressBar.SetMax(value: integer);
begin
FOriginalMax := value;
FLastPct := 0;

FReworkingPcts := FOriginalMax > MIN_TO_REWORK_PCTS;

if FReworkingPcts then
  inherited Max := HUNDO
else
  inherited Max := value;
end;

function TProgressBar.GetPosition: integer;
begin
result := inherited Position;
end;

procedure TProgressBar.SetPosition(value: integer);
var
  pct: integer;
begin
//Application.ProcessMessages;

if value = inherited Position then
  exit;

if FReworkingPcts then
  begin
  if Abs(FOriginalMax - value) <= 1 then
    pct := HUNDO
  else
    pct := Trunc((value / FOriginalMax) * HUNDO);

  if pct = FLastPct then
    exit;

  FLastPct := pct;

  value := pct;
  end;

if value < Max then
  begin
  inherited Position := Succ(value);
  inherited Position := value;
  end
else
  begin
  Max := Succ(Max);
  inherited Position := Max;
  inherited Position := value;
  Max := Pred(Max);
  end;
end;

end.