Errorhandling in AutoLisp ist ein vieldiskutiertes Thema,
und kaum ein User hat ein klares Konzept, wie die Sache
sinnvoll anzugehen ist. Ich möchte nicht wissen, wieviele
unnütze Arbeitsstunden weltweit in den letzten Jahren damit
verbracht wurden, immer und immer wieder ein Errorhandling
in kleine Programme einzubauen. Hier liegt das Dilemma: 99%
der AutoLisp-Programmierer erfinden das Rad täglich neu und
schreiben sich für jede noch so kleine Applikation jeweils
einen neuen Handler.
Was soll ein Errorhandler in AutoLisp leisten? Er hat vor
allem zwei Dinge zu erledigen: Zum Einen sollen alle
AutoCAD-Systemvariablen, die innerhalb des Programms modifiziert
wurden, wieder auf die Ausgangswerte zurücksetzen.
Die andere Aufgabe ist das Behandeln der Undo-Funktionalität:
Mit einem einzigen Aufruf des 'Z'-Befehls muss alles, was das
Programm in der Zeichnung verändert hat, wieder rückgängig zu
machen sein. Man stelle sich ein Programm vor, das z.B. Tausende
von Kreisen zeichnet, aber kurz vor Ende durch den Benutzer
abgebrochen wird oder wegen eines Fehlers aus dem digitalen
Leben scheidet: Ist kein Undo-Handling eingebaut, muss der
Benutzer jetzt Tausende Male den Z-Befehl aufrufen!
Wir müssen uns darüber im Klaren sein, dass AutoLisp keine
großartigen Möglichkeiten zum Error- und Undo-Handling bietet -
wir müssen mit wenig auskommen. Dreh- und Angelpunkt des Ganzen
ist die Funktion
(*error*). Dieser Name ist fest verdrahtet,
also unveränderbar, und diese Funktion wird immer dann aufgerufen,
wenn der Interpreter sich an einem Fehler verschluckt oder das Programm
durch den Benutzer abgebrochen wird.
Die Funkton ist AutoLisp-intern vordefiniert und tut nichts
weiter als das Ausgeben einer Fehermeldung - jedenfalls in den
neuen AutoLisp-Versionen. In den älteren Versionen war sie auch
für das 'Ausschütten' des Codes in immer höheren Verschachtelungsebenen
verantwortlich, was zwar einerseits optisch etwas verwirrend,
andererseits aber durchaus von praktischem Nutzen war. Jedenfalls
haben wir die Möglichkeit, diese Funktion zu überschreiben und
etwas mehr daraus zu machen.
Zwei Überlegungen, um uns das Leben leichter zu machen: Statt
für jede Systemvariable Codezeilen zu schreiben, sollten wir uns
darauf besinnen, was die Stärke von Lisp ist und hier Funktionen
zur Listenbearbeitung einsetzen. Und dann sollte kein Code doppelt
vorhanden sein: Wir ziehen uns aus der Affäre, indem wir das
Zurücksetzen ausschliesslich in der
(*error*)-Funktion
durchführen. Damit auch bei einem normalen (fehler- und
abbruchslosen) Verlauf alles sauber zurückgesetzt wird, rufen wir
einfach am Ende des Programms die
(*error*)-Funktion
selbst auf. Damit diese weiss, dass gar kein Fehler vorliegt,
geben wir aber als
msg-Argument einfach
nil mit.
Für die gesicherten Systemvariablen legen wir nun keine
einzelnen Lispvariablen mehr an, sondern nur eine einzige Liste.
Den notwendigen Code zur Fehlerbehandlung lagern wir in zwei
wiederverwendbare Bibliotheksfunktionen aus:
(defun startErrorHandler(varsToSave / )
(command "_undo" "_begin")
(foreach pair varsToSave
(setq *saveList*
(cons
(cons(car pair)(getvar(car pair)))
saveList
)
)
(setvar(car pair)(cdr pair))
)
(setq *oldError* *error*)
(setq *error* myErrorFunction)
)
(defun endErrorHandling( / )
(*error* nil)
)
(defun myErrorFunction(msg / )
(if msg(print msg))
(command"_undo""_end")
(foreach pair *savedList*
(setvar(car pair)(cadr pair))
)
(setq *savedVars* nil)
(setq *error* *oldError*)
(setq *oldError* nil)
)
; so wird's verwendet:
(defun C:irgendwas( / )
(startErrorHandling
'( ("osmode" . 0)
("cmddia" . 0)
("filedia" . 0)
)
)
...
...
...
(endErrorHandling)
)
Es ist nicht zu übersehen, dass der Code in der Programmfunktion
selbst nun absolut minimal und übersichtlich geworden ist - das
ist schon mal ein voller Erfolg! Die Start- und Ende-Funktionen
sowie den Code von
(myErrorFunction)
können wir abspeichern und von nun an immer wieder verwenden.
Allerdings haben wir die Erleichterungen für den Preis zweier
globaler Variablen erreicht:
*savedVars* und
*oldError*
schwirren im Speicher herum - das muss beim gegenwärtigen Stand
der Dinge so sein, damit die Funktion
(*error*) auf die darin
enthaltenen Daten zugreifen kann.
Eine Überlegung noch zum Ende dieses Kapitels: Warum muss
eigentlich die bisherige Fehlerfunktion abgesichert und
wiederhergestellt werden? Nun ja, das muss eigentlich gar nicht
sein. Tatsächlich reicht es aus, dem Symbol
*error* die Bindung
wegzunehmen, also ein
(setq *error* nil) aufzurufen, damit
wieder die in AutoCAD eingebaute
(*error*)-Funktion verwendet
wird. Allerdings kann es auch vorkommen, dass Lisp-Programme aus
anderen Lisp-Programmen heraus aufgerufen werden. In diesem Fall müsste
also die bisher vorhandene Error-Funktion auch noch ausgeführt
werden - theoretisch wäre sogar ein Stapel von Error-Funktionen
denkbar. Dies verträgt sich allerdings nicht mit den
beiden globalen Variablen, die wir jetzt haben.
Zum Austesten schreiben wir uns eine kleine Funktion, die zwar
ziemlich unsinnig ist, aber zum Testen ausreicht. Sie zeichnet
zwei Linien und fordert den Benutzer dann auf, eine Zahl einzugeben.
Wenn wir an dieser Stelle das Programm mit der ESC-Taste abbrechen,
tritt unser Errorhandler auf den Plan. Er setzt die beiden
Systemvariablen zurück, erzeugt das Ende der Undo-Gruppe
und gibt die Fehlermeldung 'Funktion abgebrochen' aus. Wenn wir
nach dem Abbruch den AutoCAD-Befel 'Z' aufrufen, verschwinden
beide(!) Linien vom Bildschirm (wäre keine Undo-Gruppierung da,
müsste man dazu zweimal 'Z' aufrufen).
(defun c:eh-test1( / )
(startErrorHandler nil
'(("osmode" . 0)("blipmode" . 1))
)
(command "_line" '(0 0)'(100 100)"")
(command "_line" '(100 0)'(0 100)"")
(getint "\nZahl eingeben: ")
(endErrorHandler)
)
Das Ganze testen wir aber nun noch einmal mit einer zweiten
Testfunktion, die zwar ähnlich ist, aber noch ein weiteres
Problem aufwirft:
(defun c:eh-test2( / p1)
(startErrorHandler nil
'(("osmode" . 0)("blipmode" . 1))
)
(command "_line"
(setq p1(getpoint "\nErsten Punkt wählen: "))
(getpoint p1 "\nZweiten Punkt wählen: ")
)
(endErrorHandler)
)
Wenn wir hier während einer der beiden
(getpoint)-Anfragen mit
ESC abbrechen, wird zwar Lisp terminiert und der Error-Handler
aufgerufen, allerdings erzeugt dieser sofort wieder einen
weiteren Fehler, da der
(command)-Aufruf nicht beendet
wurde und noch läuft. Daher fügen wir eine weitere kleine
Ergänzung in unseren Errorhandler ein, der zunächst einmal
prüft, ob noch irgendwelche AutoCAD-Befehle offen sind und
dieses gegebenenfalls abbricht. Damit man die Bestandteile nicht
umständlich suchen muss, hier noch einmal der komplette
Code inkl. der kleinen Änderung:
(defun startErrorHandler(varsToSave / )
(command "_undo" "_begin")
(foreach pair varsToSave
(setq *saveList*
(cons
(cons(car pair)(getvar(car pair)))
saveList
)
)
(setvar(car pair)(cdr pair))
)
(setq *oldError* *error*)
(setq *error* myErrorFunction)
)
(defun endErrorHandling( / )
(*error* nil)
)
(defun myErrorFunction(msg / )
; hinzugefügt:
(while(>(getvar "cmdactive")0)(command))
(if msg(print msg))
(command"_undo""_end")
(foreach pair *savedList*
(setvar(car pair)(cadr pair))
)
(setq *savedVars* nil)
(setq *error* *oldError*)
(setq *oldError* nil)
)
Im nächsten Kapitel werden wir einen Weg kennenlernen, bei dem
wir auf globale Variablen völlig verzichten, noch etwas Komfort
einbauen und die Dinge sogar vollständig stapelbar machen. Sollte
also Programm A das Programm B aufrufen und dieses wiederum
Programm C, dann werden die
(*error*)-Funktionen aller
drei Programme nacheinander ausgeführt (natürlich in der Reihenfolge
C-B-A), damit alle Rücksetzungen der Programme einwandfrei
funktionieren.