Der
erste Teil der AppState-FAQ ist hier zu erreichen!
Ich habe in meiner Anwendung einen (oder auch mehrere) Threads, die die eigentliche Arbeit erledigen, um die GUI nicht zu blockieren.
Wie kann ich in meiner Formular-Anwendung auf Threads warten, ohne die GUI einzufrieren?
Lösung: Anwendungszustand und Thread-Zähler verwenden
Beispiel: Wir wollen eine kleine Anwendung entwickeln, die die Vorgehensweise demonstriert. Als Thread-Beispiel verwenden wir einen threadbasierten Timer:
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: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67:
| type TTimerThread = class(TThread) private FID: Integer; FTimeout: Integer; FStep: Integer; FLog: TStrings; FLogLine: String; protected procedure SyncLogLineAdd; procedure LogIt(const S: String); public constructor Create(const AID, ATimeout, AStep: Integer; const ALog: TStrings; const AOnTerminate: TNotifyEvent); procedure Execute; override; end;
implementation
constructor TTimerThread.Create(const AID, ATimeout, AStep: Integer; const ALog: TStrings; const AOnTerminate: TNotifyEvent); begin inherited Create(TRUE); FID := AID; FTimeout := ATimeout; FStep := AStep; FLog := ALog; OnTerminate := AOnTerminate; FreeOnTerminate := TRUE; if Assigned(FLog) then FLog.Add('+++Thread(ID:'+IntToStr(FID)+', Timeout:'+IntToStr(FTimeout)+'ms, Step:'+IntToStr(FStep)+'ms) erzeugt.'); Resume; end;
procedure TTimerThread.Execute; begin while ( (FTimeout >= 0) and (NOT Terminated) ) do begin LogIt(' Thread '+IntToStr(FID)+': noch '+IntToStr(FTimeout)+'ms...'); Sleep(FStep); Dec(FTimeout,FStep); end; LogIt('---Thread '+IntToStr(FID)+': beendet.'); end;
procedure TTimerThread.LogIt(const S: String); begin FLogLine := S; Synchronize(SyncLogLineAdd); end;
procedure TTimerThread.SyncLogLineAdd; begin if Assigned(FLog) then FLog.Add(FLogLine); end; |
So ein Timer-Thread tut weiter nichts, als in regelmäßigen Abständen (
FStep in Millisekunden) eine Ausgabe in eine
TStrings-Nachfolgerklasse (z.B. in
TMemo.Lines, das zu diesem Zweck auf dem Formular liegt) zu schreiben, bis die Auszeit (
FTimeout in Millisekunden) erreicht ist.
Die Hauptanwendung soll aus einem Formular mit folgenden Elementen bestehen:
Delphi-Quelltext
1: 2: 3: 4: 5:
| type TfrmMain = class(TForm) seThreadCount: TSpinEdit; BtnStart: TButton; Log: TMemo; |
und auf Buttonklick die über das SpinEdit eingestellt Anzahl Timer-Threads erzeugen, Ausgaben sollen im Memo gesammelt werden. Dabei sollen das SpinEdit und der Button deaktiviert sein, solange noch ein Timer-Thread läuft. Es soll also auf das Ende aller Timer-Threads gewartet werden.
Dazu benötigen wir zunächst einen Anwendungszustand:
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:
| type TAppState = ( asUndef, asIdle, asWorking );
TfrmMain = class(TForm) seThreadCount: TSpinEdit; BtnStart: TButton; Log: TMemo; procedure FormCreate(Sender: TObject); private FAppState: TAppState; procedure SetAppState(const Value: TAppState); public ThreadsRunning: Integer; property AppState: TAppState read FAppState write SetAppState; end;
implementation
procedure TfrmMain.SetAppState(const Value: TAppState); begin if (FAppState <> Value) then begin FAppState := Value; seThreadCount.Enabled := (FAppState = asIdle); BtnStart.Enabled := (FAppState = asIdle); end; end;
procedure TfrmMain.FormCreate(Sender: TObject); begin Randomize; ThreadsRunning := 0; AppState := asIdle; end; |
Wir haben in das Grundgerüst auch gleich schon den Zähler für aktuell laufende Timer-Threads eingebaut, damit wir uns etwas Schreibarbeit sparen, um den Beitrag kurz zu halten. Wer bis hier hin das Projekt nachgebaut hat, kann jetzt bereits die Anwendung ein erstes Mal testen, es sollte allerdings genau nichts passieren (also zumindest die Syntax sollte dann bis hier korrekt sein
).
Bei einem Klick auf den Button sollen nun so viele Timer-Threads erstellt werden, wie durch das SpinEdit vorgegeben:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17:
| procedure TfrmMain.BtnStartClick(Sender: TObject); var i: Integer; begin AppState := asWorking; Log.Clear; Log.Lines.Add('Demo gestartet.'); for i := 1 to seThreadCount.Value do begin TTimerThread.Create(i, (Random(10)+1)*1000, (Random(10)+1)*100, Log.Lines, ThreadEnded); Inc(ThreadsRunning); end; end; |
Der Code sollte selbsterklärend sein, lediglich die markierte Stelle wird der Compiler natürlich nicht akzeptieren, da die angegebene Methode (noch) nicht existiert. Also schnell nachgepflegt:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8: 9:
| procedure TfrmMain.ThreadEnded(Sender: TObject); begin Dec(ThreadsRunning); if (ThreadsRunning = 0) then begin Log.Lines.Add('Demo beendet.'); AppState := asIdle; end; end; |
Hinweis: Es handelt sich um eine Methode der Formularklasse, nicht der Thread-Klasse!
Was passiert hier im Detail: Bei einem Klick auf den Button wird zunächst der Anwendungszustand
asWorking gesetzt, wofür die private Methode
TfrmMain.SetAppState aufgerufen wird. Hier werden die Bedienelemente des Formulars (SpinEdit und Button) deaktiviert, so dass der Button nicht noch einmal angeklickt werden kann. Dann werden so viele Timer-Threads erstellt, wie durch das SpinEdit vorgegeben. Wir brauchen uns keine Referenzen zu den Thread-Objekten merken, da wir später nicht mehr darauf zugreifen müssen (selbst wenn, über den Parameter
Sender in der OnTerminate-Methode hätten wir ja doch wieder eine Referenz). Beim Erzeugen der Threads zählen wir diese in
ThreadsRunning.
Wenn nun ein Timer-Thread fertig ist, ruft er (im Kontext des Haupt-Threads, also der Formularanwendung) die Methode
ThreadEnded auf, in der wir den Thread-Zähler wieder herunterzählen. Wenn der Zähler bei 0 angekommen ist, war das gerade eben der letzte Thread, der beendet wurde, so dass wir die Bedienelemente wieder freigeben können, indem wir den Anwendungszustand
asIdle setzen.
Jetzt müssen wir nur noch dafür sorgen, dass man die Anwendung nicht beenden kann, solange noch ein Thread läuft. Dazu legen wir einen Handler für das FormClose-Ereignis des Formulars an:
Delphi-Quelltext
1: 2: 3: 4: 5: 6: 7: 8:
| procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction); begin if (ThreadsRunning > 0) then begin MessageDlg('Die Anwendung ist beschäftigt!',mtWarning,[mbCancel],0); Action := caNone; end; end; |
Falls noch ein Thread läuft, verweigern wir einfach das Schließen des Formulars, fertig.
Damit ist die Anwendung für einen Test bereit, starten, Threadanzahl einstellen (irgendwas im Bereich von 1..10 z.B.) und auf den Button klicken.
Viel Erfolg!
cu
Narses
There are 10 types of people - those who understand binary and those who don´t.