Entwickler-Ecke

Grafische Benutzeroberflächen (VCL & FireMonkey) - ProgressBar hängt hinterher


Bergmann89 - So 17.10.10 15:15
Titel: ProgressBar hängt hinterher
Hey,

ich schreib zur Zeit an einem Programm, das ziemlich CPU intensive Berechnungen durchführt. Zur Anzeige des Fortschritts bensutz ich ne ProgressBar. Die kommt aber irgendwie nich ganz hinterher. Wenn ich die Berechnungen starte, fängt sie erst nach ca. einer Sekunde an sich zu bewegen (und das auch ziemlich ruckelnd). Dann sagt mir mein Programm, das die Berechnungen beendet sind, aber die Progressbar bewegt sich immer noch (jetzt wieder flüssig, wie es normalerweiße ist). ProgressMessages hat leider auch nicht geholfen. Weiß jmd wie ich das beheben könnte? Oder sollte ich die Berechnungen lieber gleich in einern extra Thread auslagern?

MfG & Thx Bergmann.


Gausi - So 17.10.10 15:24

Da wär mal interessant, wie die Berechnungen aussehen, und wann und wie und wie oft du die Progressbar aktualisierst. :nixweiss:

Berechnungen in einen Thread und ab und zu eine Message an das Fenster "ich habe jetzt 42% fertig", damit sich die Progressbar verändert, dürfte aber das beste für sowas sein. ;-)


jaenicke - So 17.10.10 15:31

Also ein Thread ist sicher ohnehin die beste Lösung wie schon gesagt, aber ob das an dem Phänomen etwas ändert, bin ich noch nicht so sicher.

Kann es sein, dass du Windows Vista oder 7 einsetzt? Dort versucht Windows die Fortschrittsanzeige zu glätten, was in der Regel auch ganz gut funktioniert. Wenn aber die Fortschrittsanzeige z.B. zuerst relativ langsam voranschreitet und dann plötzlich schnell fertig wird, hängt die Anzeige etwas hinterher. Einfach um nicht so plötzlich die Fortschrittsanzeige springen zu lassen.


Bergmann89 - So 17.10.10 15:34

Hey,


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
  if Assigned(List) then
    List.Clear;
  Finished := False;
  Count := GetCombinationCount(CharSet);
  //Prozesbar initialisieren wenn vorhanden
  if Assigned(ProgBar) then begin
    ProgBar.Min  := 0;
    ProgBar.Step := 1;
    Progbar.Max  := Count;
    ProgBar.Position := 0;
  end;
  //Indices initialisieren
  for i := 0 to 12 do
    IDs[i] := 1;
  Tip := GetString;

  //Schleife zur Berechnung der Kombinationen
  while not Finished do begin
    //tip := GetString;
    if Assigned(List) then
      List.Add(Tip);

    if Assigned(ProgBar) then begin
      ProgBar.StepIt;
    end;

    Finished := incID(0);
  end;

so sieht das ganze aus. Aber egal ob Count 1 ist (Minimum) oder 1.594.323 (Maximum) es hängt immer hinterher. Ich denke auch, das es mit dem Thead hier nicht getan ist. (werd mich jetzt aber trotzdem dran machen das auf Thead umzubauen). Und wie du richtig vermutest hast, nutz ich Win7. Kann man das nich iwie abstellen?!

MfG Bergmann.


kalmi01 - So 17.10.10 15:34

Application.ProcessMessages;
Sleep(0); // oder Sleep(1) wenn andere Programme auch noch zum Zuge kommen sollen

gehört eigentlich in jeden Schleifendurchlauf.
So reagiert das Prg. deutlich flüssiger auf Einflüsse von Aussen oder auch auf Refresh.
Z.B. so:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
  //Schleife zur Berechnung der Kombinationen
  while not Finished do begin
    //tip := GetString;
    if Assigned(List) then
      List.Add(Tip);

    if Assigned(ProgBar) then begin
      ProgBar.StepIt;

      [b]Application.ProcessMessages;
      Sleep(0);[/b]
      end;

    Finished := incID(0);
  end;


jaenicke - So 17.10.10 15:39

user profile iconBergmann89 hat folgendes geschrieben Zum zitierten Posting springen:
Kann man das nich iwie abstellen?!
Nein, das ist nicht möglich, wenn du die Themes benutzt. Die einzige Möglichkeit ist das Theme mit SetWindowTheme [http://msdn.microsoft.com/en-us/library/bb759827(VS.85).aspx] für die ProgressBar selektiv abzuschalten, aber dann sieht die entsprechend Sch*** wie bei Win9x aus.

// EDIT:
user profile iconkalmi01 hat folgendes geschrieben Zum zitierten Posting springen:
gehört eigentlich in jeden Schleifendurchlauf.
So reagiert das Prg. deutlich flüssiger auf Einflüsse von Aussen oder auch auf Refresh.
Dafür dauert der Vorgang an sich extrem viel länger...
// EDIT2:
Und das Sleep ist überflüssig. Ich habe es gerade ausprobiert, Sleep(0) kehrt sofort zurück, bringt also gar nichts.


littleDave - So 17.10.10 15:44

Benutzt du Vista oder Win7 mit Aero? Wenn ja, dann liegt das Hinterherhinken an Aero. Per Design ist es so, dass die ProgressBar "smooth" gezeichnet wird. D.h. beim setzen von Position wird nicht sofort die entsprechende Position gezeichnet, sondern die ProgressBar wandert langsam zu diesem Punkt. Den Effekt zu deaktivieren habe ich noch nicht gefunden (jedenfalls nicht ohne die Themes zu deaktivieren), aber ein einfacher Work-Around schaut wie folgt aus:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
procedure SetProgressBarPosition(aProgressBar: TProgressBar; newPosition: integer);
begin
  if aProgressBar.Position <> newPosition then
  begin
    aProgressBar.Position := aProgressBar.Max; // <- der Trick
    aProgressBar.Position := newPosition;
  end;
end;


kalmi01 - So 17.10.10 15:47

user profile iconjaenicke hat folgendes geschrieben Zum zitierten Posting springen:

Sleep(0) und Sleep(1) gibt (da das Minimum ohnehin 10-15 Millisekunden oder so sind, je nach System).

Sleep(0) lässt Dein Prg. auf eigene Messages reagieren
Sleep(1) lässt auch andere Prozesse zu Wort kommen


jaenicke - So 17.10.10 15:50

user profile iconlittleDave hat folgendes geschrieben Zum zitierten Posting springen:
Den Effekt zu deaktivieren habe ich noch nicht gefunden
Geht auch wie gesagt leider gar nicht.

user profile iconlittleDave hat folgendes geschrieben Zum zitierten Posting springen:
aber ein einfacher Work-Around schaut wie folgt aus:
Gute Idee. :zustimm:
Wichtig ist dann nur noch, dass PBS_SMOOTHREVERSE [http://msdn.microsoft.com/en-us/library/bb760820(VS.85).aspx] nicht gesetzt ist.

user profile iconkalmi01 hat folgendes geschrieben Zum zitierten Posting springen:
Sleep(0) lässt Dein Prg. auf eigene Messages reagieren
Sleep(1) lässt auch andere Prozesse zu Wort kommen
Durch Sleep wird einfach nur der Thread schlafen gelegt. Da kann keine Message-Behandlung stattfinden. ;-)
Dass andere Prozesse / Threads über Zeitscheibe dran kommen, stimmt schon, aber dennoch ist es zumindest ein Tick, der verloren geht, wenn gerade keine CPU im Leerlauf ist.


Bergmann89 - So 17.10.10 16:13

Hey,

danke Dave, funzt 1a^^ Leider is das Programm durch den Thread jetzt nur noch halb so schnell, obwohl ich das Update der ProgressBar erst aufrufe, wenn mind. ein ganzes Prozent dazu gekommen ist. Das heißt ich hab max. 100 Synchronize-Aufrufe. Da es aber bei dem Programm auf die Laufzeit ankommt kann ich mit der Hälfte langsamer nicht leben. Gibt noch ne Möglichkeit das schnell zu machen, oder sollte ich vlt. doch bei der Variante ohne Thread bleiben?!

so sieht das Ganze jetzt aus:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
procedure TCalculateTipsThread.InitProgressBar;
begin
  fProgBar.Min      :=   0;
  fProgBar.Max      := 100;
  fProgBar.Position :=   0;
  fProgBar.Step     :=   1;
end;

procedure TCalculateTipsThread.StepProgressBar;
var
  OldPos: Integer;
begin
  OldPos := fprogbar.Position;
  fProgBar.Position := fProgBar.Max;
  fProgbar.Position := OldPos + 1;
end;

procedure TCalculateTipsThread.Execute;
//[...]
  x := 0;
  StartTime := GetTickCount;

  if Assigned(fTipList) then
    fTipList.Clear;
  Finished := False;

  //Prozesbar initialisieren wenn vorhanden
  if Assigned(fProgBar) then begin
    MaxX := GetCombinationCount(fCharSet)/100;
    Synchronize(InitProgressBar);
  end;
  //Indices initialisieren
  for i := 0 to 12 do
    IDs[i] := 1;
  Tip := GetString;

  //Schleife zur Berechnung der Kombinationen
  while not Finished do begin
    //tip := GetString;
    if Assigned(fTipList) then
      fTipList.Add(Tip);

    if Assigned(fProgBar) then begin
      x := x + 1;
      if x >= MaxX then begin
        x := x - MaxX;
        Synchronize(StepProgressBar);
      end;
    end;

    Finished := incID(0);
  end;

  fCalcTime := GetTickCount - StartTime;
end;


MfG Bergmann


Bergmann89 - So 17.10.10 16:13

user profile iconBergmann89 hat folgendes geschrieben Zum zitierten Posting springen:
Hey,

danke Dave, funzt 1a^^ Leider is das Programm durch den Thread jetzt nur noch halb so schnell, obwohl ich das Update der ProgressBar erst aufrufe, wenn mind. ein ganzes Prozent dazu gekommen ist. Das heißt ich hab max. 100 Synchronize-Aufrufe. Da es aber bei dem Programm auf die Laufzeit ankommt kann ich mit der Hälfte langsamer nicht leben. Gibts noch ne Möglichkeit das schnell zu machen, oder sollte ich vlt. doch bei der Variante ohne Thread bleiben?!

so sieht das Ganze jetzt aus:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
procedure TCalculateTipsThread.InitProgressBar;
begin
  fProgBar.Min      :=   0;
  fProgBar.Max      := 100;
  fProgBar.Position :=   0;
  fProgBar.Step     :=   1;
end;

procedure TCalculateTipsThread.StepProgressBar;
var
  OldPos: Integer;
begin
  OldPos := fprogbar.Position;
  fProgBar.Position := fProgBar.Max;
  fProgbar.Position := OldPos + 1;
end;

procedure TCalculateTipsThread.Execute;
//[...]
  x := 0;
  StartTime := GetTickCount;

  if Assigned(fTipList) then
    fTipList.Clear;
  Finished := False;

  //Prozesbar initialisieren wenn vorhanden
  if Assigned(fProgBar) then begin
    MaxX := GetCombinationCount(fCharSet)/100;
    Synchronize(InitProgressBar);
  end;
  //Indices initialisieren
  for i := 0 to 12 do
    IDs[i] := 1;
  Tip := GetString;

  //Schleife zur Berechnung der Kombinationen
  while not Finished do begin
    //tip := GetString;
    if Assigned(fTipList) then
      fTipList.Add(Tip);

    if Assigned(fProgBar) then begin
      x := x + 1;
      if x >= MaxX then begin
        x := x - MaxX;
        Synchronize(StepProgressBar);
      end;
    end;

    Finished := incID(0);
  end;

  fCalcTime := GetTickCount - StartTime;
end;


MfG Bergmann


€: Shit, ich hab auf Zitieren un nich auf Editieren geklickt ^^


jaenicke - So 17.10.10 16:18

Ganz einfach: Synchronisiere nicht, das ist immer teuer. Und das gilt auch für das Aktualisieren der Oberfläche insgesamt, wenn du Repaint oder Application.ProcessMessages ohne Thread benutzt.

Die Lösung ist viel einfacher: schicke einfach aus dem Thread eine Message mit der Prozentangabe z.B. in LParam an dein Fenster. Die landet dann im Hauptthread und du kannst direkt die Fortschrittsanzeige aktualisieren. Gleichzeitig läuft der Thread aber einfach weiter. ;-)

// EDIT:
Ach ja, nebenbei:
Das Übergeben der ganzen ProgressBar an eine andere Klasse oder hier sogar den Thread ist eher schlechter Programmierstil (Stichwort: objektorientierte Programmierung und Kapselung). Sage besser nur im Hauptfenster per Event Bescheid und lasse sowas da machen (so wie du es hier dann mit der Message machst).


Bergmann89 - So 17.10.10 16:23

Okay, das hab ich noch nie gemacht, sollte aber nich so schwer sein. Die Messages sind ja Cardinal-Werte. Gibts da nen bestimmten Bereich, in dem der User seine Messages selbst definieren kann? Nich das ich dann den gleichen Wert für meine Message nehme, wie eine System-Message.


jaenicke - So 17.10.10 16:24

Innerhalb der Anwendung:
WM_USER + X, aber <= $7FFF
Zwischen Anwendungen:
WM_APP + X, aber <= $BFFF
Siehe:
http://msdn.microsoft.com/en-us/library/ms644931(VS.85).aspx


Bergmann89 - So 17.10.10 16:29

Ah, super! Ab jetzt sollte ich allein klar kommen. Dankeschön!

€: Wow, mit Thread komm ich auf 0,3sec anstatt 2sec. Liegt aber auch daran, das ich die ProgressBar nicht so oft aktualisiere.