Autor Beitrag
Narses
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starhalf ontopic star
Administrator
Beiträge: 10181
Erhaltene Danke: 1254

W10ent
TP3 .. D7pro .. D10.2CE
BeitragVerfasst: Di 25.09.07 00:24 
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:
ausblenden volle Höhe 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:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
type
  // die Demo-Thread-Timer-Klasse
  TTimerThread = class(TThread)
  private
    FID: Integer;      // ID des Threads
    FTimeout: Integer; // Laufzeit in Millisekunden
    FStep: Integer;    // Schrittweite in Millisekunden
    FLog: TStrings;    // Ausgaben hier anfügen
    FLogLine: String;  // Zeile, die synchronisiert angefügt werden soll
  protected
    procedure SyncLogLineAdd; // synchronisierte Anfügemethode
    procedure LogIt(const S: String); // interne Logausgabe
  public
    // neues Objekt anlegen, beschreibende Werte direkt übergeben
    constructor Create(const AID, ATimeout, AStep: Integer; // s.o.
      const ALog: TStrings; // s.o.
      const AOnTerminate: TNotifyEvent); // beim Ende aufrufen (synchronisiert)
    procedure Execute; override// hier wird gearbeitet -> also gewartet... ;)
  end;

implementation

// -----------------------------------------------------------------------------
// TTimerThread

constructor TTimerThread.Create(const AID, ATimeout, AStep: Integer;
  const ALog: TStrings; const AOnTerminate: TNotifyEvent);
begin
  inherited Create(TRUE); // gestoppt erzeugen
  // Werte übernehmen
  FID := AID;
  FTimeout := ATimeout;
  FStep := AStep;
  FLog := ALog;
  OnTerminate := AOnTerminate;
  FreeOnTerminate := TRUE;
  if Assigned(FLog) then // der Konstruktor läuft im Hauptthread, direkt ausgeben
    FLog.Add('+++Thread(ID:'+IntToStr(FID)+', Timeout:'+IntToStr(FTimeout)+'ms, Step:'+IntToStr(FStep)+'ms) erzeugt.');
  Resume; // los gehts!
end;

procedure TTimerThread.Execute;
begin
  while ( (FTimeout >= 0)  // solange der Timer noch nicht abgelaufen ist
          and              // und
          (NOT Terminated) // der Thread noch laufen soll
        ) do begin
    LogIt('   Thread '+IntToStr(FID)+': noch '+IntToStr(FTimeout)+'ms...');
    Sleep(FStep); // nur den Thread schlafen legen - die Anwendung blockiert nicht!
    Dec(FTimeout,FStep); // Auszeit anpassen
  end;
  LogIt('---Thread '+IntToStr(FID)+': beendet.');
end;

// interne Logausgabe (synchronisiert) machen
procedure TTimerThread.LogIt(const S: String);
begin
  FLogLine := S; // die Ausgabe puffern
  Synchronize(SyncLogLineAdd); // und synchronisiert in Log schreiben
end;

// Methode, um synchronisiert eine Logzeile zu schreiben
procedure TTimerThread.SyncLogLineAdd;
begin
  if Assigned(FLog) then
    FLog.Add(FLogLine); // gepufferte Zeile ausgeben (jetzt im Hauptthread!)
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:
ausblenden 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:
ausblenden volle Höhe 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:
type
  // Anwendungszustände aufzählen
  TAppState = (
    asUndef,   // Trick, um Aktionen zur Initialisierung im FormCreate zu erzwingen
    asIdle,    // Anwendung wartet auf Eingaben
    asWorking  // Anwendung arbeitet
    );

  TfrmMain = class(TForm)
    seThreadCount: TSpinEdit;
    BtnStart: TButton;
    Log: TMemo;
    procedure FormCreate(Sender: TObject);
  private
    FAppState: TAppState;
    procedure SetAppState(const Value: TAppState);
  public
    ThreadsRunning: Integer; // Zähler für aktuell laufende Threads
    property AppState: TAppState read FAppState write SetAppState; // Anwendungszustand
  end;

implementation

// -----------------------------------------------------------------------------
// TfrmMain

// neuen Anwendungszustand übernehmen und Controls daran anpassen
procedure TfrmMain.SetAppState(const Value: TAppState);
begin
  if (FAppState <> Value) then begin
    FAppState := Value;
    seThreadCount.Enabled := (FAppState = asIdle);
    BtnStart.Enabled      := (FAppState = asIdle);
  end;
end;

// beim Programmstart ausführen...
procedure TfrmMain.FormCreate(Sender: TObject);
begin
  Randomize; // zufälliger Zufall
  ThreadsRunning := 0// es läuft kein Demo-Timer-Thread
  AppState := asIdle; // die Anwendung langweilt sich
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:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
// Demo starten, zufällige Timer-Threads erzeugen
procedure TfrmMain.BtnStartClick(Sender: TObject);
  var
    i: Integer;
begin
  AppState := asWorking; // die Anwendung ist beschäftigt
  Log.Clear;
  Log.Lines.Add('Demo gestartet.');
  for i := 1 to seThreadCount.Value do begin // gewünschte Threadanzahl erzeugen
    TTimerThread.Create(i,                   // ID
                        (Random(10)+1)*1000// Timeout 1..10 Sekunden
                        (Random(10)+1)*100,  // Step 100..1000 Millisekunden
                        Log.Lines,           // Log-Ziel
                        ThreadEnded);        // am Ende hier Bescheid sagen
    Inc(ThreadsRunning); // einer mehr
  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:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
// Methode, die beim Threadende vom Thread (im Hauptthreadkontext) aufgerufen wird
procedure TfrmMain.ThreadEnded(Sender: TObject);
begin
  Dec(ThreadsRunning); // einer weniger
  if (ThreadsRunning = 0then begin // war´s der Letzte?
    Log.Lines.Add('Demo beendet.'); // dann...
    AppState := asIdle; // ...die Anwendung wieder freigeben
  end;
end;

Hinweis: Es handelt sich um eine Methode der Formularklasse, nicht der Thread-Klasse! :mahn:

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:
ausblenden Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
// Schließen des Fensters nur zulassen, wenn kein Thread läuft
procedure TfrmMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  if (ThreadsRunning > 0then begin // läuft noch ein Timer-Thread?
    MessageDlg('Die Anwendung ist beschäftigt!',mtWarning,[mbCancel],0);
    Action := caNone; // Schließen verhindern!
  end;
end;

Falls noch ein Thread läuft, verweigern wir einfach das Schließen des Formulars, fertig. :D

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! :zustimm:

cu
Narses


AppStateMitThreads.zip  (2.97 KB) Download (Rev 0)
 (1302x, 1302x gesamt)
Beschreibung: Quelltext des Demo-Projekts
AppStateMitThreads.exe  (169.5 KB) Download (Rev 0)
 (1465x, 1465x gesamt)
Beschreibung: fertige Demo-Anwendung zum direkten Ausprobieren
_________________
There are 10 types of people - those who understand binary and those who don´t.