"Warten"-Prozeduren pausieren nicht da, wo sie sollen

Für Fragen von Einsteigern und Programmieranfängern...
Antworten
otterosch
Beiträge: 2
Registriert: Di 7. Mai 2024, 22:43

"Warten"-Prozeduren pausieren nicht da, wo sie sollen

Beitrag von otterosch »

Moin zusammen,

ich habe ein Verständnisproblem. Ich habe hier im Forum eine Prozedur gefunden, die eine andere Prozedur mit einer kurzen Wartezeit unterbrechen soll:

Code: Alles auswählen

procedure TForm1.Delay(dt: DWORD);
var
  tc : DWORD;
begin
  tc := GetTickCount64;
  while (GetTickCount64 < tc + dt) and (not Application.Terminated) do
    Application.Process
    Messages;
end; 
Ich rufe diese Prozedur in der FormCreate-Prozedur auf - die Zeile im Memo soll erst nach 2 Sekunden auftauchen (minimalisiertes Beispiel):

Code: Alles auswählen

procedure TForm1.FormCreate(Sender: TObject);
begin
	Delay(4000);
     	Memo1.Lines.Add('Hallo');
end;
Leider zeigt sich nun aber das komplette Fenster erst nach der Wartezeit - eigentlich soll sich das Fenster zeigen und dann nach 4 Sekunden soll die Zeile im Memo entstehen. Stattdessen zeigt sich das Fenster nach 4 Sekunden direkt mit 'Hallo'.

1. Warum ist das so? Wird das Formular erst angezeigt, nachdem FormCreate komplett fertig ist?
2. Wie schaffe ich es, dass wirklich erst nach der Anzeige des Fensters gewartet wird?

Danke euch!

PascalDragon
Beiträge: 963
Registriert: Mi 3. Jun 2020, 07:18
OS, Lazarus, FPC: L 2.0.8, FPC Trunk, OS Win/Linux
CPU-Target: Aarch64 bis Z80 ;)
Wohnort: München

Re: "Warten"-Prozeduren pausieren nicht da, wo sie sollen

Beitrag von PascalDragon »

otterosch hat geschrieben: Di 7. Mai 2024, 22:50 1. Warum ist das so? Wird das Formular erst angezeigt, nachdem FormCreate komplett fertig ist?
Korrekt.
otterosch hat geschrieben: Di 7. Mai 2024, 22:50 2. Wie schaffe ich es, dass wirklich erst nach der Anzeige des Fensters gewartet wird?
Ich würde eher einen TTimer mit einem entsprechendem Interval nutzen und im OnTimer-Ereignis dann ausschalten.

Ansonsten wäre OnActivate glaube ich eher das Ereignis nach dem du suchst.
FPC Compiler Entwickler

Ekkehard
Beiträge: 67
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 3.6, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: "Warten"-Prozeduren pausieren nicht da, wo sie sollen

Beitrag von Ekkehard »

Hallo
und viel Freude mit Lazarus und fpc.

Code: Alles auswählen

  Application.ProcessMessages;
Ich weiß immer nicht warum Application.ProcessMessages gerade bei Anfängern so hoch im Kurs steht, bei mir war das eigentlich nicht viel anders. Ich vermute, weil man sich von der linearen Vorstellung dessen was das Programm machen soll nicht lösen kann und bei der Realisation "1. Das", "warten", "2. Jenes" und dann "warten", "3. Welches" in Pascal denkt

Code: Alles auswählen

program;

procedure Das;
begin
//...
end;
procedure Jenes;
begin
//...
end;
procedure Welches;
begin
//...
end;
procedure Warten;
begin
//...
end;

begin
  Das;
  Warten;
  Jenes;
  Warten;
  Welches;
end.
Aber eine nachrichtengesteuerte Anwendung, wie die eines Desktop-Fenster-Formulars funktioniert ganz anders.
Hier schläft die Anwendung die ganze Zeit und wenn jemand etwas von ihr will, dann erledigt sie das ganz schnell und schläft sofort weiter.
Deshalb ist die Idee, man könne das o.a. Beispiel einfach in das Formular werfen, falsch und falls es doch unbedingt nötig ist, ist es auch noch wesentlich komplexer als es zunächst aussieht.

Also zurück auf Los.
Erste Frage, warum willst Du dieses zeitgesteuerte Verhalten?
Zweite Frage, wann soll das Verhalten gezeigt werden.

Grundsätzlich gibt es für Zeitabläufe zwei Varianten.
1.) Timer
2.) Threads

Zu 1.)
Timer bieten sich an, wenn man regelmäßig etwas tun mächte, was auch ein Benutzer der Anwendung tun würde, also aufwecken, etwas erledigen, weiter schlafen.
Beispiele: Etwas soll blinken, eine Eingabe soll erst nach einer kurzen Zeit überprüft werden und nicht nach jedem eingegeben Zeichen.
Ein Timer löst letztendlich ein Ereignis aus, wie ein Klick auf einen Button.
Zu 2.)
Threads bieten sich an, wenn etwas sehr zeitintensiv ist und deshalb im Hintergrund erledigt werden muss, weil sonst die ganze Anwendung ruckelt oder stillsteht und nur hin und wieder mal ein Update an den Benutzer geschickt wird, wie bspw. eine Aktualisierung eines Fortschrittsbalkens.
Ein Thread muss erzeugt werden, man muss in der Klasse die Execute Methode überschreiben und man sorgfältig Sorge dafür tragen, dass der Thread sauber beendet wird. Und natürlich kann er nicht direkt auf die Oberfläche zugreifen, diese Zugriffe müssen synchronisiert werden. Das hört sich wesentlich komplizierter an als es ist. Man stelle sich einfach vor, statt eines Zuges fahren jetzt zwei nebeneinander her, klar, dass man eine Nachricht nur übergeben kann, wenn beide gleich schnell fahren und beide eine Tür an gleicher Stelle auf haben :-).

Um Dein Beispiel mit Leben zu erfüllen einige konkrete Hinweise.
FormCreate wird bei der Erzeugung des Forms aufgerufen, zu diesem Zeitpunkt wird es aber noch nicht gezeigt. Das ist der richtige Moment um z.B. weitere Elemente zur Laufzeit auf das Form zu bringen, bspw. 100 Checkboxen in einem 10 x 10 Feld, deren Anlage im Formulareditor sehr mühsam wären.
Dagegen wird FormActivate immer dann aufgerufen, wenn das Form aktiviert wird. Sehr häufig findet man deshalb folgendes

Code: Alles auswählen

procedure TForm1.FormActivate(Sender: TObject);
begin
  OnActivate := Nil; // FormActivate nur einmal durchlaufen
  Memo1.Lines.Add('Hallo, Datum und Uhrzeit: '+FormatDateTime('YYYY-MM-DD HH:NN:SS',Now)+'.');
  Timer1.Enabled := True; // Erklärung folgt im Text
end;
Mittels der Zeile OnActivate := Nil; schaltet man das Event ab, indem man die Referenz auf diese Methode auf Nil setzt. Sehr praktisch. Danach kann man in einem Aufwasch alles aktualisieren, was jetzt wo das Form angezeigt wird, relevant ist, bspw die Häckchen der o.a. 100 Checkboxen setzen.
Um das Beispiel oben zum Laufen zu bringen, packst Du einen Timer auf Dein Form und trägst bei der Eigenschaft Interval die Zeit von 4000 Millisekunden ein und entfernst das Häckchen bei der Eigenschaft enabled, dies verhindert, dass der Timer automatisch losläuft.
In FormActivate wird dies in der Zeile Timer1.Enabled := True; nachgeholt.
In den Ereignissen des Timers klickst Du doppelt auf OnTimer und schreibst

Code: Alles auswählen

procedure TForm1.Timer1Timer(Sender: TObject);
begin
  Memo1.Lines.Add('Hallo, Datum und Uhrzeit: '+FormatDateTime('YYYY-MM-DD HH:NN:SS',Now)+'.'); //Zeit eintragen
  Timer1.Enabled := False; //Timer aus
end;
Wenn Du das Programm startest wird unmittelbar nach dem Start eine Zeile in das Memo eingetragen und dann nach 4 Sekunden eine weitere hinzugefügt.
Natürlich verhält sich das Programm aus der Sicht des Benutzers immer noch etwas seltsam, denn wenn Du sofort beginnst im Memo zu schreiben, wird Dein Schreibfluss im Memo unterbrochen! Aber es ist ja nur ein Beispiel.

Der Vollständigkeithalber noch das gleiche Beispiel als Thread-Lösung

Code: Alles auswählen

  { TTestThread }

  // Eine abgeleitete Klasse eines Thread.
  TTestThread = class(TThread)
  private
    procedure SynchedWrite; // Eine private Methode zum Sunchronen Aufruf
  protected
    procedure Execute;override; // Die überschriebene Methode für das "Programm" des Threads
  end;

  { TForm1 }

  TForm1 = class(TForm)
// Diverses Zeug hier
  private
    FTestThread : TTestThread; // Eine Instanzvariable des Threads
  public 
  end;

{ TTestThread }

procedure TTestThread.SynchedWrite;
begin
  // Die synchrone Ausgabe in das Formular.
  // Ganz wichtig: Diese Methode darf niemals direkt von Execute auferufen werden!!! Warum? Ausprobieren ;-)
  Form1.Memo1.Lines.Add('Hallo from Thread, Datum und Uhrzeit: '+FormatDateTime('YYYY-MM-DD HH:NN:SS',Now)+'.');
end;

// Execute stellt im Grunde ein eigenes Programm dar, es werden einfach die Dinge abgearbeitet
// und wenn man fertig ist, verlässt man die Methode.
// Wichtig: Wenn der Thread von außen beendet werden soll, also die Variable Terminated True ist,
// lässt man alles stehen und liegen, räumt noch das Nötigste auf und verschwindet!
procedure TTestThread.Execute;
var
  t0, t1 : DWORD;
begin
  t0 := GetTickCount64; // Unser Startzeitpunkt
  // Das Folgende ist etwas übertrieben für die gestellte Aufgabe macht es aber leichter
  // dieses Beispiel aufzubohren
  // Ich mache das immer so:
  // Außenrum ein Try..Finally, welches sicherstellt, dass wenn diese Methode verlassen wird
  // auch Terminate aufgerufen wird. Macht man das nicht passieren seltsame Dinge
  // Innen drin eine Schleife while not Terminated, hier überflüssig, weil ja nur einmal was passieren soll.
  try
    while not Terminated do
    begin
      // Eine Warteschleife für 4 Sekunden
      repeat
        if Terminated then Exit; // Abbruch? Dann nichts wie raus hier
        Sleep(10); // Wir schlafen hier 10ms. Lässt man das weg geht die CPU Auslastung für einen Kern auf fast 100%
        t1 := GetTickCount64; // Zeit holen
      until t1-t0 >= 4000; // Prüfen ob vorbei
      if Terminated then Exit; // Abbruch? Dann nichts wie raus hier
      Synchronize(Self,@SynchedWrite); //Jetzt sind 4s rum, mit der Methode Synchronize wird die eigene Methode zur Ausgabe aufgerufen
      // Das funktioniert so, dass im Hintergrund eine Nachricht an das Hauptfenster geschickt wird und SynchedWrite wird wie ein beliebiges
      // OnABC-Ereignis ausgeführt
      Break; // Wir sind fertig also raus
    end;
  finally
    if not Terminated then // Sicherstellen, dass der Thread auch terminiert wird
      Terminate;
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  FTestThread := TTestThread.Create(True); // Den Thread erzeugen, aber noch angehalten lassen
end;

procedure TForm1.FormActivate(Sender: TObject);
begin
  OnActivate := Nil; // FormActivate nur einmal durchlaufen
  Memo1.Lines.Add('Hallo, Datum und Uhrzeit: '+FormatDateTime('YYYY-MM-DD HH:NN:SS',Now)+'.');
  FTestThread.Suspended := False; // Den Thread loslaufen lassen
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  // Auch komplizierter als nötig, aber für zukünftige Erweiterungen gut
  // Haben wir eine erzeugte Thread-Instanz?
  if Assigned(FTestThread) then
  begin
    if not FTestThread.Terminated then // Wurde der Thread noch nicht terminiert?
    begin
      FTestThread.Suspended := False; // Böse Falle, wenn der Thread nicht läuft, kann er nicht beendet werden!! Also laufen lassen.
      FTestThread.Terminate; // Den Wunsch des Beendens dem laufenden Thread mitteilen
    end;
    FTestThread.WaitFor; //Warten bis der Thread wirklich fertig ist
    FreeAndNil(FTestThread); // Freigeben und Nil, hier überflüssig, aber der Versuch einen Thread zweimal freizugeben führt ins Chaos
  end;
end;


Das soweit.
Viel Erfolg.
Zuletzt geändert von Ekkehard am So 12. Mai 2024, 18:50, insgesamt 1-mal geändert.

otterosch
Beiträge: 2
Registriert: Di 7. Mai 2024, 22:43

Re: "Warten"-Prozeduren pausieren nicht da, wo sie sollen

Beitrag von otterosch »

Ekkehard hat geschrieben: Fr 10. Mai 2024, 16:13 Dagegen wird FormActivate immer dann aufgerufen, wenn das Form aktiviert wird. Sehr häufig findet man deshalb folgendes
Vielen Dank für die sehr ausführliche Antwort, werde ich alles ausprobieren. "FormActivate" war mir neu, das wird bestimmt die Lösung sein.

Ekkehard
Beiträge: 67
Registriert: So 12. Feb 2023, 12:42
OS, Lazarus, FPC: Windows Lazarus 3.6, FPC 3.2.2
CPU-Target: 64-Bit
Wohnort: Hildesheim

Re: "Warten"-Prozeduren pausieren nicht da, wo sie sollen

Beitrag von Ekkehard »

Code: Alles auswählen

Vielen Dank für die sehr ausführliche Antwort
Gern geschehen.
Bei der Durchsicht ist mir im Beispiel mit dem Thread noch ein Fehler aufgefallen, den ich dort korrigiert habe.
Ich schrieb, dass man den Thread nach dem Abbruchwunsch bitte schnell verlassen soll, habe das aber nicht realisiert.
Man muss nämlich häufiger die Zeile

Code: Alles auswählen

        if Terminated then Exit; // Abbruch? Dann nichts wie raus hier
einstreuen, insbesondere in Schleifen und vor längeren Aktionen. Dabei darf man, wenn es dann deutlich komplexer wird, auch die Unterfunktionen und Methoden nicht vergessen.
Manchmal baut man Funktionen, die sowohl alleine, als auch von einem Thread auferufen werden sollen, dann gebe ich meistens einen Parameter ACallingThread : TThread = Nil mit und kann dann in der Funktion abfragen ob der Thread, wenn nicht Nil, abgebrochen werden soll.

Antworten