es gibt Dinge, die schiebt man immer wieder auf, und sie holen einen immer wieder ein Ich suche Vorschläge/Tutorials, wie man folgendes Allerweltsproblem effizient und sauber löst.
Man nehme ein VCL Form, und einen Button. Drückt man den Button, rennt eine OnClick Routine los, und die läuft sehr, sehr, sehr lange. Deshalb wäre es nützlich, einen „Abbrechen“ Button einzublenden, und wenn der Anwender draufdrückt, nach Rückfrage ob er sich sicher ist genau das zu tun, allerdings nicht das ganze programm, sondern nur Zurückkehren zum Hauptformular.
Bisher helfe ich mir damit, dass in den Schleifen der langen Prozedur immer mal wieder ein Application.Processmessages eingestreut ist, und dass die Schleifen, jede für sich, immer wieder mal eine (vom Abbrechen Button gesetzte Variable, nennen wir sie „DoAbort“, testen. Ist sie gesetzt, wird mit Abbruchbedingungen und Sprungbefehlen versucht, sich aus der Tiefe der Aufrufe irgendwie wieder rauszuwursteln. Hässlich und Fehleranfällig ist das.
Irgendwie sagt mir mein Gefühl, müsste es auch eleganter gehen, und irgendwie habe ich das Gefühl, dass eine try Konstruktion mit einem Custom Event der Schlüssel zum Erfolg sein könnte. Aber geht das, und besteht nicht die Gefahr, dass die Speicherverwaltung durcheinandergerät wenn man einfach so wild von irgendeinem Programmteil in irgendeinen anderen Programmteil springt und dort weiter macht?
Kann mir jemand ein paar Leads zukommen lassen wo ich mich einlesen kann?
die Allerweltslösung für dieses Allerweltsproblem ist die Verwendung von Threads. Delphi hat eine TThread-Klasse. Davon leitet man eine eigene Klasse ab und überschreibt die Methode „Excecute“, wo dann all die langen Berechnungen gemacht werden. Über die Zuweisung des OnTerminate-Events kann man den Thread dann wieder eine Routine des hauptprogramms aufrufen lassen, wenn er fertig ist.
Damit der Thread abgebrochen werden kann, muss man in der Execute-Methode regelmäßig prüfen, ob der Flag „Terminated“ gesetzt ist und dann ggf. die Berechnung abbrechen.
Beim Klick auf den Abbruch-Button muss nur Terminated auf TRUE gesetzt werden.
Hier ein volles Beispiel (Das Formular hat schlicht zwei Buttons „btnStart“ und „btnStop“):
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
type
TMeinThread = class(TThread)
public
x: Double;
procedure Execute; override;
end;
TForm1 = class(TForm)
btnStart: TButton;
btnStop: TButton;
procedure btnStartClick(Sender: TObject);
procedure btnStopClick(Sender: TObject);
private
{ Private-Deklarationen }
public
{ Public-Deklarationen }
MeinThread: TMeinThread;
procedure MeinThreadTerminate(Sender: TObject);
end;
var
Form1: TForm1;
implementation
{$R \*.dfm}
procedure TMeinThread.Execute;
begin
While not Terminated do
begin
// mach ganz doll viele lange Berechnungen
x := sin(x - 1/sin(x));
// vielleicht auch zwischendruch nochmal prüfen
if Terminated then break;
// bevor es mit weiteren langen Berechnungen weitergeht
windows.Beep(Trunc(x\*1000),200);
end;
end;
procedure TForm1.MeinThreadTerminate(Sender: TObject);
begin
caption := 'Letztes x: '+FloatToStr(MeinThread.x);
btnStart.Enabled := TRUE;
btnStop.Enabled := FALSE;
end;
procedure TForm1.btnStartClick(Sender: TObject);
begin
// Thread anlegen, aber noch nicht laufen lassen
MeinThread := TMeinThread.Create(TRUE);
// Erst die Thread-Eigenschaften einstellen
MeinThread.OnTerminate := MeinThreadTerminate;
MeinThread.FreeOnTerminate := True;
MeinThread.x := 10;
// und dann laufen lassen
MeinThread.Resume;
btnStart.Enabled := FALSE;
btnStop.Enabled := TRUE;
end;
procedure TForm1.btnStopClick(Sender: TObject);
begin
MeinThread.Terminate;
end;
end.
Dann folgende procedure für das Objekt (TForm1) erstellen:
procedure TForm1.Multitask;
var
Mes:Tmsg;
begin
if PeekMessage(Mes,0,0,0,PM_REMOVE) then
begin
TranslateMessage(mes);
DispatchMessage(mes);
end;
end;
Und dann die Behandlung der OK und des Cancel Buttons als Beispiel:
procedure TForm1.BCancelClick(Sender: TObject);
begin
{Signalisieren, dass die Routine bei der nächsten Möglichkeit
kontrolliert beendet werden soll.}
breakoper:=true;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
a:longint;
begin
{Verarbeitungs-Routine…}
a:=1;
Button2.Enabled:=false; {Damit der OK-Button nicht ein 2.mal
geklickt werden kann, wenn die Routine
bereits läuft. ggf. weitere Elemete im
Fenster für diese Zeit deaktivieren.}
breakoper:=false;
BCancel.Visible:=true;
BCancel.Repaint;
repeat {Als Beispiel eine Schlaufe…}
{Hier Berechnungen…}
Multitask; {Muss regelmässig aufgerufen werden, damit das
Cancel-OnClick-Event verarbeitet werden kann.}
{Hier Berechnungen…}
{Kontrollierter Punkt für den Abbruch…}
if (breakoper=true) then
begin
BCancel.Visible:=false;
MessageDLG(‚Operation abgebrochen!‘,mtInformation,[mbok],0);
Button2.Enabled:=true; {OK-Taste wieder aktiv…}
exit;
end;
Bisher helfe ich mir damit, dass in den Schleifen der langen
Prozedur immer mal wieder ein Application.Processmessages
eingestreut ist, und dass die Schleifen, jede für sich, immer
wieder mal eine (vom Abbrechen Button gesetzte Variable,
nennen wir sie „DoAbort“, testen.
Hallo Armin,
das ist schon die Standardmethode, man kann keine Prozedur, Schleife usw. von „aussen“ abbrechen, ohne das man sowas eingebaut hat. Übrigens muss man ebenso Aufrufe einbauen für eine sinnvolle Fortschrittsanzeige, das kann man gleich in einem Aufwasch erledigen. Ich mache mir da oft einen Dialog mit einem Balken und dem Abbruch-Button.
Einen extra Thread kann man im äussersten Notfall killen, aber das ist nicht „graceful“ und wird auch nicht empfohlen, das Ergebnis ist nicht vorhersagbar. Das entspricht etwa dem Abbruch eines nicht reagierenden Programms per Taskmanager.
Das Dumme ist, bei Fremdsoftware steht man auf dem Schlauch, wenn dort kein Abbruch vorgesehen ist.
An alle: so also nicht?
Irgendwo meine ich mal mit einem System programmiert zu haben, das in etwa folgenden Code zulassen würde (ich schreibe mal wild Pseudo-Code)
Procedure AbortButtonClick
// Der Event-handler eines Buttons
begin
if MsgBox(„You really abort?“) = „Y“ Raise CancelException
end
Procedure ProcessFile(File)
// Eine Datei beackern
begin
FileSize := FileSize + File.Size
Application.ProcessMessages
end
Procedure ProcessDir(Dir)
// Ein Verzeichnis beackern
begin
For each File in Dir.Files
ProcessFile(File)
Next
For each SubDir in Dir.SubDirs
ProcessDir(SubDir)
Next
end
Procedure Main
// Haupt-Acker Routine
begin
Try
FileSize = 0
ProcessDir(„c:“)
MsgBox "Size of all files in c: " + FileSize
Catch CancelException
MsgBox „Aborted“
End Try
end
Klartext: Main würde zur Berechnung der Summe aller Dateigrößen der kompletten Festplatte c: die rekursive Routine ProcessDir bemühen, die wiederum die Größen aller Dateien aufsummiert und ihrerseits per Rekursion ihre eigenen Unterverzsichnisse mitbearbeitet. Die innerste Routine ProcessFile hat einen Aufruf an die ProcessMessages Funktion, und hier würde bemerkt dass der „Abbrechen“ Button gedrückt wurde. Seine Eventroutine AbortButtonClick würde eine Exception „CancelException“ auslösen, die wiederum den Catch Block in der Main Routine abarbeitet. Ich meine, dass das in Dot Net Sprachen so machbsr wäre.
Geht sowas auch in Delphi? Ich habe da sowas wie Try Blöcke gefunden, mich aber noch nicht im Detail damit befasst.
Nein, ich würde dies nicht über das Auslösen einer Exception lösen, sodern so, wie ich das im meinem Beispiel beschrieben habe. Da hast Du die Möglichkeit, je nach Standpunkt der Unterbrechung die entsprechenden Massnahmen zu treffen (Files, DBs schliessen usw), um die Routine sauber zu verlassen bzw. zu canceln. Damit hast Du sicher die bessere Kontrolle über den Programmverlauf.