Abbrechen Button Event behandeln

Hallo allseits,

es gibt Dinge, die schiebt man immer wieder auf, und sie holen einen immer wieder ein :smile: 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?

Armin.

Hallo,

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.

Hi Nimral

Der Vorschlag von „Jo…“ ist auch eine gute Lösung. Eine weitere Möglichkeit ist folgende:

In den Hauptfenster-Definizionen folgenden Methode und Variabel hinzufügen:

private
{ Private-Deklarationen }
breakoper:boolean;
procedure Multitask;

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;

until a=2;
end;

Gruss
Sam

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.

Gruss Reinhard

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.

Armin.

Hi Armin

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.

Gruss
Sam