coding for absolute dummies

oder auch

Programmieren für absolute ... Laien

Dies ist der Versuch, zu beweisen, dass Programmieren etwas sehr einfaches ist.

Als ein Leser dieses Tutorials scheinst du vermutlich sehr verzweifelt zu sein, immer noch nicht programmieren zu können und gleichzeitig auch wohl deswegen, dass du gerade einen Text wählst, dessen Titel dich bereits beleidigt. Vielleicht bist du auch ein Programmieranfänger und scheiterst immer wieder bei den ersten Gehversuchen, obwohl du bisher nichtmals richtig krabbeln kannst (dies ist im übertragenen Sinne gemeint).

Die größte Überwindungshürde ist häufig die, damit klar zu werden, dass Programmieren wirklich etwas sehr einfaches ist - und wegzugehen von der Einstellung "Hm, das sieht ja alles ganz trivial aus, aber so extrem simpel kann das doch gar nicht sein, also muss das wohl ganz kompliziert sein, denn diesen komplizierten Inhalt sehe ich nicht."

Hinweis: Dieses Tutorial ist zur Zeit noch nicht vollständig und wird stetig nachbearbeitet und erweitert. (Das Endziel ist die komplette Einführung in die objekt-orientierte Programmierung.) Bitte nicht erschrecken, wenn das Ende etwas plötzlich kommt. Auch bin ich für Tipps deinerseits dankbar, falls du etwas nicht verstanden hast, so dass ich in dem Fall auf das entsprechende Detail noch genauer eingehen kann.

Das wird dich erwarten: (Und falls du denkst, bereits etwas schon lange zu wissen und verstanden zu haben, kannst du ruhig Kapitel überfliegen bzw. auslassen.)

  1. Beginn der absoluten Grundlagen des Programmierens
  2. Schnelleinführung in Object Pascal
  3. Schnelleinführung in Lazarus
  4. Praktische Anwendung in Object Pascal

Der Anfang

Beginnen wir mit dem Anfang:
Programmieren, was ist das eigentlich?

Definitionsversuch

Man könnte allgemein sagen, Programmieren ist das Aufschreiben und Überlegen von Algorithmen.

Da das Schreiben hoffentlich keine Schwierigkeit darstellt und die Schwierigkeiten beim Überlegen sich in Grenzen halten, bleibt noch die Frage, was Algorithmen sind.

TODO Liste

Algorithmen sind, grob gesagt, Anleitungen, wie ein bestimmtes Ziel zu einem Problem bzw. einer Aufgabe erreicht  werden kann. Diese Anleitung ist dabei in der Art einer Liste von Anweisungen, die abgearbeitet werden müssen. Je nach Kontext ist es natürlich wichtig, die Anweisungen hinreichend primitiv zu halten - eine Anweisung wie "löse das Problem" ist nicht zugelassen. Man beachte dabei: Bei der allgemeinen Definition ist absolut nicht festgelegt, für wen diese Anweisungen sind, also von wem sie ausgeführt werden sollen. Dies kann sowohl ein Computer als auch ein Mensch oder sonst etwas ganz anderes sein. Auch die Problemstellung und das gewünschte Ziel kann alles beliebige sein. Wichtig ist nur, dass in jedem Fall klar definiert ist, was getan werden soll. Es muss klar sein, welches die erste Anweisung ist. Es muss außerdem klar sein nach dem Ausführen einer beliebiger Anweisung, welche Anweisung als nächstes auszuführen ist. Auch nicht definiert ist, in welcher Sprache oder in welcher Form diese Liste von Anweisungen sein muss.

Beispiele von Algorithmen

Ein paar Beispiele:

Aufgabe:
In meinem Zimmer befindet sich ein Haufen Abfall (sagen wir 1 m³), der in den Müllkontainer im Keller soll.

Lösung 1:

  1. Man nehme meinen Müllkorb, [der 1/3 m³ transportieren kann]
  2. fülle ihn mit 1/3 m³ des Abfalls,
  3. laufe damit nach unten in den Keller,
  4. entleere ihn im Müllkontainer,
  5. laufe mit ihm wieder nach oben in mein Zimmer,
  6. fülle ihn mit 1/3 m³ des Abfalls,
  7. laufe damit nach unten in den Keller,
  8. entleere ihn im Müllkontainer,
  9. laufe mit ihm wieder nach oben in mein Zimmer,
  10. fülle ihn mit 1/3 m³ des Abfalls,
  11. laufe damit nach unten in den Keller,
  12. entleere ihn im Müllkontainer.

Um Schreibarbeit zu sparen, fasst man wiederholende Aktionen häufig zusammen (was vollkommen legitim ist, solange eindeutig bleibt, was genau getan werden soll; man nennt dies eine Schleife):

Nochmal Lösung 1:
(1.) Man nehme meinen Müllkorb

Führe folgende Aktionen 3 Mal aus:
    (2.) fülle ihn mit 1/3 m³ des Abfalls,
    (3.) laufe damit nach unten in den Keller,
    (4.) entleere ihn im Müllkontainer,
    (5.) laufe mit ihm wieder nach oben in mein Zimmer,
Ende der zu wiederholenden Aktionen


Man beachte, dass man, je nach Kontext, evtl. das Füllen des Müllkorbes auch noch weiter spezifizieren muss (man kann natürlich an anderer Stelle definieren, was genau damit gemeint ist).
Da man sich gerne allgemeiner hält (was ist, wenn der Müllberg doch größer ist oder mein Müllkorb noch mehr fassen kann?), könnte man den Algorithmus aus so formalieren:

Lösung 2:

  1. Man nehme meinen Müllkorb,
  2. fülle ihn (den Müllkorb) mit Abfall, bis er voll ist,
  3. laufe damit nach unten in den Keller,
  4. entleere ihn im Müllkontainer,
  5. laufe mit ihm wieder nach oben in mein Zimmer,
  6. wenn noch Müll vorhanden ist, dann (7.) fahre mit Aktion 2 fort

Dies ist nicht exakt der gleiche Algorithmus, denn die Aktionen sind nicht ganz dieselben. Abgesehen von der allgemeineren Fassung ist hier die Anweisung 6 von neuem Charakter. Die Anweisung 7 nennt man eine bedingte Anweisung, die nämlich genau dann ausgeführt wird, wenn die Bedingung der Anweisung 6 erfüllt wird (bzw. sie soll dann ausgeführt werden; ein Algorithmus ist nichts, was getan wird, sondern beschreibt eine Möglichkeit, etwas zu tun). Naiv ist hoffentlich sehr klar, wenn ich z.B. dir diese Anleitung gebe, was du zu tun hast. Entsprechend könnte man bei so einer bedingten Anweisung natürlich noch einen "sonst tue was anderes"-Fall hinzufügen.

"Fahre mit Aktion 2 fort" nennt man einen Sprung und zwar in diesem Fall zu Aktion 2 (deshalb würde man es auch Rücksprung nennen). Auf diese Art wird hier die Schleife realisiert.

In diesem Fall ist die Schleife noch relativ simpel (inklusive dem ganzen Algorithmus). Es könnte aber Fälle geben, in denen das ganze nicht so gut überschaubar ist. Es stellt sich dann die wichtige Frage, ob es bei der Ausführung des Algorithmus' passieren kann, dass man unendlich lange in einer Schleife gefangen ist und nicht heraus kommt. Man nennt dies dann eine Endlosschleife. Insgeheim haben wir bisher angenommen, dass der Ausführer des Algorithmus der einzige ist, der auf den Müll in meinem Zimmer zugreift - wenn beispielsweise aber aus einem unerklärlichen Grund irgendwoher pro Sekunde 1/10 m³ neuer Müll hinzukommt, der Ausführer mit dem Füllen des Mülleimers aber bereits schon eine halbe Minute braucht, sieht man leicht, dass der Ausführer des Algorithmus' in Lösung 2 in einer Endlosschleife ist. In Lösung 1 wäre dies nicht so, allerdings wäre am Ende des Algorithmus das Ziel nicht erreicht.

Lösung 3:

  1. Gehe in den Keller,
  2. nehme den Müllkontainer,
  3. laufe mit ihm nach oben zurück in mein Zimmer,
  4. fülle den im Zimmer liegenden Müll hinein,
  5. laufe mit dem Müllkontainer zurück in den Keller.

Genug zu diesem Beispiel und zu einem Neuen.

Aufgabe:
n hoch kEs soll errechnet werden, was n hoch k ist (auch geschrieben als n^k). n und k sollen dabei nicht negative, ganzen Zahlen (also ohne Nachkommastellen) sein. Dem Ausführer wird als Voraussetzung gegeben, dass er multiplizieren kann.

SpeicherfelderIm Folgenden kommt ein sehr wichtiges neues Element in dem Algorithmus hinzu. Wir müssen uns gewisse Sachen merken. Wie das Merken genau aussieht, spielt dabei wieder keine Rolle. Ein Computer wird dazu seinen Arbeitsspeicher oder evtl. die Festplatte benutzen. Ein Mensch benutzt einfach sein Gedächtnis oder ein Notizblock oder eine sonstige Erinnerungsstütze. Für jede Sache, die zu merken ist, haben wir dabei einen bestimmten Platz (im Kopf, im Arbeitsspeicher oder wo auch immer). Wir wollen die Möglichkeit lassen, nach gewisser Zeit an dieser Stelle neue Daten/Werte zu merken. Das Merken ist dabei nicht (wie man es als Mensch vielleicht ganz gerne hätte) etwas Ergänzendes, sondern es wird immer die alte Information an dieser Stelle vergessen und überschrieben mit der neuen Information. Vielleicht stellt man sich auch einfach eine Schublade vor, in die genau eine Information reinpasst - und diese kann abgefragt werden oder auch durch eine andere ersetzt werden.

Beispiel:
Buchstaben-SpeicherfeldWir wollen uns einen Buchstaben merken. Am Anfang wollen wir uns für den Buchstaben mal das 'A' merken.
Wir erinnern uns nun, dass wir uns für den Buchstaben ein 'A' gemerkt haben.
Nun wollen wir uns für den Buchstaben stattdessen ein 'B' merken. Nun haben wir uns also für den Buchstaben ein 'B' gemerkt und wissen deshalb nichts mehr von dem 'A', denn nun haben wir stattdessen uns ja das 'B' gemerkt. (Bei der Benutzung eines Notizblocks heißt das, dass beim neuen Setzen eines neuen Wertes der alte gestrichen wird. Für eine Sache, in diesem Fall dem Buchstaben, kann immer nur ein Wert gemerkt werden.)

Man beachte, dass man einen Anfangswert auch angeben sollte, denn sonst hat man da ein Speicherfeld ohne Ihnalt am Anfang. Dies kann in gewissen Situationen zu Uneindeutigkeiten kommen. (Was soll z. B. die Anweisung bedeuten "multipliziere 5 mit dem Inhalt des Speicherfeldes", wenn dort bisher kein Inahlt ist?)

Lösung der eigentlichen Aufgabe "n hoch k":
(1.) Man merke sich einen Zähler, der bei k beginnen soll. [Zähler entspricht also dabei der Bezeichnung für Schublade 1]
(2.) Man merke sich das Zwischenergebnis, welches erstmal 1 sein soll. [Zwischenergebnis ist die Schublade 2]
(3.) Wenn der Zähler nicht 0 ist, tue folgendes:
        (4.) Multipliziere das Zwischenergebnis mit n und merke es sich als neues Zwischenergebnis.
        (5.) Zähle den Zähler um eins runter (subtrahiere ihn mit 1) und merke es sich als neuen Zähler-Wert.
        (6.) Gehe zu Befehl 3.
Ende der im Wenn-Fall auszuführenden Befehle.
(7.) Das Ergebnis ist nun der Wert vom Zwischenergebnis. [man könnte das Ergebnis als die Ergebnis-Schublade ansehen]

Um alle bisherigen Prinzipien (die Schleife und die zu merkenden Sachen) noch mal zu verdeutlichen, wollen wir als Mensch den Algorithmus einmal für n=3 und k=2 durchgehen.

  1. Befehl 1: Suche einen Notizblock für den Zähler und schreibe 2 drauf.
  2. B2: Suche einen Notizblock für das Zwischenergebnis und schreibe 1 drauf.
  3. B3: Überprüfe den Notizblock für den Zähler, ob dort eine Zahl ungleich 0 draufsteht. Weil das der Fall ist (es steht im Moment 2 drauf), gehe zu Befehl 4.
  4. B4: Der Wert vom Zwischenergebnis-Block (ist im Moment 1) multipliziert mit 3 ergibt 3. Nun streichen wir den alten Wert vom Zwischenergebnis-Block durch und schreiben stattdessen 3 drauf.
  5. B5: Der Zählerblock ist im Moment 2. Eins weniger ist also 1. Radiere den alten Inhalt vom Zählerblock nun aus und schreibe die 1 drauf.
  6. B6: Wir sollen nun zu Befehl 3 gehen.
  7. B3: Überprüfe den Zählerblockwert, ob dieser ungleich 0 ist. Er ist im Moment 2. Also gehen wir weiter zu Befehl 4.
  8. B4: Zwischenergebnis bekommt den neuen Wert 9.
  9. B5: Der Zähler bekommt den neuen Wert 0.
  10. B6: Und weiter zu Befehl 3.
  11. B3: Zähler ist nun 0, also gehe zum Ende der im Wenn-Fall auszuführenden Befehle und damit zu Befehl 7.
  12. B4: Das Ergebnis ist nun die Zahl, die auf dem Zwischenergebnis-Block draufsteht, also 9.

Man mache sich einmal klar, dass dieser Algorithmus genau dem entspricht, was man auch sonst im Kopf ausgeführt hätte. (Bei diesen niedrigen Zahlen hätte man es hoffentlich noch im Kopf geschafft.)

Direkt zu einem etwas komplexeren Beispiel:

Aufgabe:
Das Ergebnis der Addition 2er Zahlen (nicht negative, ganze Zahlen, also ohne Nachkommastellen), die im Dezimalsystem vorliegen, soll errechnet werden.
Es ist dabei bekannt, wie 2 Zahlen von 0 bis 9 inklusive einer 3. Zahl, die entweder 0 oder 1 ist, zu addieren sind. Diese Addition 2er Ziffern (mit Übertragsziffer) kann also als Operation im Algorithmus verwendet werden.

Lösung (Grundschulmethode aus der 2. Klasse: "schriftlich addieren"):

  1. Man merke sich die aktuelle Ziffernposition und setze diese Ziffernposition auf die 1. Ziffer [wir betrachten die ganz rechteste Ziffer beider Zahlen als die 1. Ziffer]
  2. Man merke sich den aktuellen Übertrag als 0.
  3. Man merke sich die Summe der Ziffern an der aktuellen Ziffernposition der beiden gegebenen Zahlen sowie zusätzlich dem gemerkten Übertrag. [diese können wir nach Voraussetzung berechnen]
  4. Die 1. Ziffer dieser Summe notiere man sich als die Ziffer vom Ergebnis an der aktuellen Ziffernposition.
  5. Der Übertrag soll nun die 2. Ziffer der gemerkten Summe sein (falls diese 2ziffrig ist, also überhaupt ein Übertrag vorhanden ist.)
  6. Erhöhe die aktuelle Ziffernposition um 1.
  7. Wenn der Übertrag nicht 0 ist oder wir nicht mit der aktuellen Ziffernposition hinter dem Ende beider Zahlen sind, dann fahre mit Aktion 3 fort.

(Nur ein Beispiel.)

Im gesamten Algorithmus wird der Ausführer sich diesmal wieder viel merken müssen. Diese bereits im letzten kleinen Beispiel erwähnten Merkplätze oder auch Speicherplätze nennt man Variablen. Der kleine Unterschied zwischen den aus der Mathematik bekannten Variablen ist, dass diese regelmäßig im Algorithmus immer wieder neu gesetzt werden können - in der Mathematik wird eine Variable nur ein einziges mal eindeutig definiert. Variablen in Algorithmen sind eher zu vergleichen mit einer Speicherstelle, z.B. einer Textdatei auf dem Computer oder einem Notizblock. Das Entscheidende ist, dass diese Variablen einen Wert enthalten (den sogenannten Inhalt) und dass dieser Inhalt jederzeit neu geschrieben werden kann (genauso wie der Inhalt einer Textdatei entfernt und neu geschrieben werden kann), wobei natürlich dann der alte Inhalt verloren geht. Man sagt dazu auch, dass man einer Variable einen Wert zuweist oder die Variable setzt.

Man beachte, dass dies eine zusätzliche Voraussetzung an den Ausführer darstellt, nämlich die Fähigkeit, sich Daten zu merken/speichern. Wenn nicht anders angegeben, kann man diese Fähigkeit als gegeben annehmen. In gewissen Fällen steht man aber manchmal vor dem Problem, einen Algorithmus speziell für einen Ausführer zu entwerfen, der diese Möglichkeit nicht hat. Der Algorithmus in obiger Form wäre dann so nicht möglich. So eine seltsame Situation betrachten wir hier aber nicht.

Wenn man das Prinzip der Variablen verstanden hat, kann man den Algorithmus auch etwas lesbarer so schreiben:

Nochmal die Lösung:

  1. Führe neue Variable ein: Ziffernposition
  2. Führe neue Variable ein: Übertrag
  3. Führe neue Variable ein: Summe
  4. Setze Ziffernposition auf 1.
  5. Setze Übertrag auf 0.
  6. Setze Summe auf (Ziffer an Ziffernposition von gegebener Zahl 1) + (Ziffer an Ziffernposition von gegebener Zahl 2) + Übertrag.
  7. Setze die Ziffernposition-te Ziffer vom Ergebnis auf die 1. Ziffer von Summe.
  8. Setze Übertrag auf die 2. Ziffer von Summe.
  9. Setze Ziffernposition auf Ziffernposition + 1.
  10. Wenn (Übertrag nicht 0 ist) oder (Ziffernposition nicht hinter dem Ende beider Zahlen), dann (11.) fahre mit Aktion 6 fort.

Die im Algorithmus benutzten Klammern dienen dabei nur dem Zweck, zu verdeutlichen, was zusammengehört (wie in der Mathematik) und nicht dem Zweck, etwas auszuklammern.

In einem Algorithmus, wo ein spezieller Wert als Ergebnis erwartet wird (in diesem Fall die Summe) und evtl. spezielle Werte vorgegeben werden (in diesem Fall die beiden Zahlen), sieht man diese vorgegebenen Werte (auch häufig Argumente oder Parameter genannt) und das Ergebnis (oder auch Rückgabewert genannt) auch als Variablen an, um so die Notation einheitlicher zu halten.

Wir wollen den Algorithmus noch ein letztes Mal, noch ein wenig lesbarer, formulieren. Hierzu ist es praktisch, eine Kurzschreibweise für die i-te Ziffer von einer Zahl einzufügen. Wir wollen diesen Wert also im Folgenden schreiben als Zahl[i]. Außerdem wollen wir mit ZifferAnzahl(Zahl) die Zifferanzahl der Zahl bezeichnen. Das Setzen einer Variable kürzen wir ab mit einem Definitionszeichen. Zahl1 und Zahl2 seien nun die beiden vorgegebenen Zahlen und Ergebnis soll das Ergebnis sein. Da es kein einfaches Zeichen für Kleiner-oder-gleich gibt, verwenden wir hierfür <= (entsprechend >=) (weil es meistens genau so gemacht wird).

Und nochmal die Lösung:

  1. Neue Variable: Position
  2. Neue Variable: Übertrag
  3. Neue Variable: Summe
  4. Position := 1.
  5. Übertrag := 0.
  6. Summe := Zahl1[Position] + Zahl2[Position] + Übertrag.
  7. Ergebnis[Position] := Summe[1].
  8. Übertrag := Summe[2].
  9. Position := Position + 1.
  10. Wenn (nicht Übertrag = 0 ist) oder (Position <= ZifferAnzahl(Zahl1)) oder (Position <= ZifferAnzahl(Zahl2)), dann (11.) fahre mit Aktion 6 fort.


(Ausführung des Befehls 7 und 8 rot markiert.)

Man bemerke, dass bei allen Angaben des Algorithmus, der Algorithmus selbst sich nie geändert hat, sondern nur die Form, wie er aufgeschrieben wurde. Genauso wie ein "Hallo" sich in die meisten Sprachen dieser und anderer Welten übersetzen lässt und sich auf die verschiedensten Formen hinschreiben oder symbolisieren lässt, aber immer das gleiche bedeutet.

Wir wollen diesen Algorithmus 'Addierer' nennen. Dieser Algorithmus ist von den 2 gegebenen Zahlen abhängig und liefert ein Ergebnis. Alle gegebenen Werte/Informationen nennt man die Parameter. Das Ergebnis nennt man auch den Rückgabewert. Da durch diesen Algorithmus nun bekannt ist, wie man 2 Zahlen addiert, können wir diese Technik natürlich in anderen Algorithmen weiterverwenden. Als Kurzform schreibt man häufig, wenn man 2 Zahlen X und Y addieren will, Addierer(X,Y). Die Parameter werden dabei in den Klammern hintereinander angegeben, bekommen so also eine Reihenfolge. Wir wollen einfach mal den ersten Parameter als Zahl1 und den 2. Parameter als Zahl2 festlegen (wobei es bei der Addition natürlich keine Rolle spielt). Es gilt also z.B.: 5 = Addierer(2,3). Man betrachtet häufig Addierer nun auch als Funktion, denn wie im Mathematischen Sinne einer Funktion bekommt man zu gewissen Parametern einen Rückgabewert.

Betrachten wir zur Verdeutlichung eine neue Aufgabe. In dieser wollen wir diesen Addierer-Algorithmus als bekannt voraussetzen. Außerdem soll bekannt sein, wie man eine Zahl um 1 vermindert.

Aufgabe:
Berechne das Ergebnis der Multiplikation 2er Zahlen (wieder ganze, nicht negative Zahlen) Zahl1 und Zahl2.

Lösung 1 (grob formuliert):
Rechne Zahl2 genau Zahl1-mal aufeinander (also Zahl2 + Zahl2 + Zahl2 ...).

Lösung 1 (exakte Formulierung):
(1.) Neue Variable: Zwischenergebnis
(2.) Neue Variable: Rest
(3.) Zwischenergebnis := 0
(4.) Rest := Zahl1
Wiederhole folgende Befehle, wenn nicht Rest = 0 ist:
    (5.) Zwischenergebnis := Addierer(ZwischenergebnisZahl2)
    (6.) Rest := Rest - 1
Ende der zu wiederholenden Befehle
(7.) Ergebnis := Zwischenergebnis

Die Schleife wurde diesmal wieder ein wenig anders geschrieben. Gemeint ist, dass alle Befehle innerhalb der Schleife immer zusammen einmal hintereinander ausgeführt werden sollen und danach erneut die Überprüfung der Bedingung der Schleife (nicht Rest = 0) durchgeführt werden soll und falls diese Überprüfung negativ ausfällt (wenn Rest = 0 ist), dann sollen die Befehle innerhalb der Schleife nicht noch ein weiteres Mal ausgeführt werden, sondern damit ist die Schleife abgeschlossen (und evtl. Befehle hinter der Schleife sollen nun ausgeführt werden).

Man könnte mit Hilfe von bedingten Sprüngen die Lösung 1 auch äquivalent so formulieren:

Lösung 1:

  1. Neue Variable: Zwischenergebnis
  2. Neue Variable: Rest
  3. Zwischenergebnis := 0
  4. Rest := Zahl1
  5. Wenn Rest = 0 ist, fahre mit Befehl 9 fort.
  6. Zwischenergebnis := Addierer(ZwischenergebnisZahl2)
  7. Rest := Rest - 1
  8. Fahre nun mit Befehl 5 fort.
  9. Ergebnis := Zwischenergebnis

Im Endeffekt ist aber natürlich die bessere Lesbarkeit zu Empfehlen, die natürlich in der 'Wiederhole, wenn ...'-Variante mehr gegeben ist. Allgemein sind Sprünge irgenwelcher Art etwas verpöhnt unter Programmierern, denn wenn man gleich 2 oder mehr Sprünge in einem Algorithmus hat, welche hin und her und durcheinander irgendwo hin springen, ist für einen Leser häufig nur schwer nachzuvollziehen, was genau der Algorithmus eigentlich tun soll.

Noch einmal zu der Nutzung des anderen Algorithmus' innerhalb diesem: Ein Algorithmus ist genau dann klar und eindeutig definiert, wenn ein Ausführer zu jedem Schritt genau weiß, welcher Schritt als nächstes ausgeführt werden soll. An der Stelle, wo nun der andere Algorithmus erwähnt und benutzt wird, was genau hat der Ausführer also verständlicherweise zu tun? Er arbeitet nun natürlich aus dem anderen Algorithmus die einzelnen Schritte ab, bis er das Ergebnis ermittelt hat und kehrt dann wieder zurück (in dem Fall kann dann damit nun die Variable Zwischenergebnis gesetzt werden).
Dies ist nicht ungewöhnlich und entspricht genau dem wirklichen Leben. Wenn ich z.B. eine Maschine baue, die meine TV-Fernbedienung benutzt, um den Fernseher auszuschalten und nun diese Maschiene starte (meine Aktion), ist die Funktion der Fernbedienung exakt die gleiche, wie wenn ich sie direkt benutzt hätte.

Wenn man sich diesen Algorithmus jetzt so mal betrachtet und zum Spaß ihn mal mit 2 Zahlen ausprobiert, sieht man gleich, dass man bei diesem Algorithmus bei etwas größeren Zahlen (vor allem bei einer großen Zahl1, wenn wir die Addierer-Tätigkeit mal als relativ gering einschätzen für alle Zahlen) ziemlich lange braucht. Wenn man so etwas feststellt, ist das ein Hinweis darauf, dass der Algorithmus evtl. nicht optimal ist. Es stellt sich natürlich immer die Frage, ob es überhaupt einen besseren Algorithmus gibt oder ob man bereits den bestmöglichen hat. Diese Frage kann in komplizierten Fällen sehr schwierig zu beantworten sein.

Wenn man diesen Algorithmus genauer auf seine Geschwindigkeit hin untersuchen will, versucht man eine ungefähre Formel für die Anzahl der Aktionen in Abhängigkeit der Parameter (also der Eingaben, d.h. der vorgegebenen Zahlen) zu ermitteln. Hierbei muss man sich natürlich darauf festlegen, was als eine Aktion betrachtet werden soll (also in diesem Fall: soll die Addierer-Benutzung in Befehl 6 die einzelnen Schritte des Addierer-Algorithmus mitzählen oder selbst nur ein Befehl darstellen?). Wir wollen hier alle Befehle, so wie sie sind, nur jeweils als eine Aktion betrachten, also den Aufwand der Addier-Tätigkeit vernachlässigen (wenn man davon ausgeht, dass der Algorithmus von einem Computer ausgeführt wird, kann man auch durchaus davon ausgehen, dass das Addieren auf Prozessorebene nur einen Befehl kostet). Man kommt zu dem Ergebnis, dass die Befehle 5, 6, 7 und 8 genau Zahl1-mal ausgeführt werden. Sonst hat man noch 5 Befehle, die überhaupt nur einmal ausgeführt werden müssen. Insgesamt hat man also 4*Zahl1+5 auszuführende Befehle. Da diese Formel linear von der Eingabe (nur Zahl1 in diesem Fall) abhängt, sagt man auch, der Algorithmus liegt in der linearen Komplexitätsklasse (der höchste Grad der Formel entscheidet darüber, denn der höchste Grad ist natürlich bei größeren Zahlen immer domminierend).

Wie kann man den Algorithmus verbessern? Eine Möglichkeit wäre, falls Zahl1 > Zahl2, diese beiden zu vertauschen, denn so sind dann weniger Befehle auszuführen. Sind aber beide Zahlen groß, hilft das nicht so viel. Eine andere Möglichkeit hat man bereits in der Grundschule in der 3. Klasse unter dem Namen "schriftlich Multiplizieren" gelernt. Hier eine Möglichkeit, diesen Algorithmus zu formulieren:

Lösung 2:
Neue Variablen: Teil[1], Teil[2] ... bis Teil[ZifferAnzahl(Zahl2)]
Neue Variable: Zwischenergebnis
Neue Variable: Position
Für Position := 1 bis ZifferAnzahl(Zahl2), tue:
    (*1)    Teil[Position] := Zahl2[Position] * Zahl1
Zwischenergebnis := 0
Für Position := 1 bis ZifferAnzahl(Zahl2), tue:
    (*2)    Zwischenergebnis := Zwischenergebnis + Teil[Position] * 10^(Position-1)
Ergebnis := Zwischenergebnis

'Für Position := Start bis Ende, tue Aktion' soll bedeuten, dass wir Position als erstes auf Start setzen, dann die Aktion ausführen, danach Position um eins erhöhen, erneut die Aktion ausführen und dies solange wiederholen, bis Position bei Ende angelangt ist, wofür wir dann ein letztes Mal die Aktion ausführen (insgesamt sind das Ziel - Start + 1 Ausführungen von Aktion; falls StartEnde ist, wollen wir die Aktion gar nicht ausführen).

Wir wollen in (*1) die Multiplikation mal als primitiv voraussetzen (man könnte hierfür beispielsweise den in Lösung 1 angegebenen Algorithmus verwenden, denn Zahl2[Position] ist nur eine Ziffer, also eine Zahl von 0-9, so dass auch Lösung 1 hierfür schnell sein sollte). In (*2) soll 10^k für "10 hoch k" stehen. Die Multiplikation mit der 10er-Potenz bedeutet nur das Anhängen von Nullen an der rechten Seite der Zahl, kann also auch als primitiv angesehen werden.

Nachdem hoffentlich klar geworden ist, dass dies exakt dem entspricht, was man bei einer schriftlichen Multiplikation tut (Teil[1], Teil[2] ... bis Teil[ZifferAnzahl(Zahl2)] sind dabei die untereinander hingeschriebenen Teilergebnisse, die man hinterher alle zusammenaddiert), die man aus der Grundschule kennt, wollen wir den Algorithmus etwas verbessern. Störend in ihm ist die ganze Reihe von Variablen (Teil[1], Teil[2] ... bis Teil[ZifferAnzahl(Zahl2)]), denn hierfür muss der Ausführer eine ganze Menge von Informationen sich merken (ein Mensch bei Verwendung eines Blatt Papiers für die Ausführung hat natürlich den Speicherplatz dort zu Verfügung; trotzdem wäre es nicht nötig und wenn er alles im Kopf machen würde, hätte er auch dabei Probleme). Man nennt diese Reihe von Variablen übrigens auch Array. In anderen Fällen von anderen Problemstellungen kommt man nicht drum herum, so ein Array zu benutzen. Hier jedoch geht es einfacher:

bessere Lösung 2:
Neue Variable: Teilmultiplikation
Neue Variable: Zwischenergebnis
Neue Variable: Position
Zwischenergebnis := 0
Für Position := 1 bis ZifferAnzahl(Zahl2), tue:
    Teilmultiplikation := Zahl2[Position] * Zahl1
    Teilmultiplikation := Teilmultiplikation * 10^(Position-1)
    Zwischenergebnis := Zwischenergebnis + Teilmultiplikation
Ergebnis := Zwischenergebnis

Nun noch ein etwas anderes, sehr wichtiges Beispiel.

Aufgabe:
Man bekommt ein Kartenblatt (Anzahl der Karten soll hier nicht weiter festgelegt sein) und soll dieses Sortieren, wobei am Anfang die kleinste Karte stehen soll und am Ende die Größte. Wir wollen dabei keine Farben unterscheiden. Wir wollen folgende Reihenfolge (niedrigste Karte bis höchste Karte): Ass, 2, 3, 4, 5, 6, 7, 8, 9, 10, Bube, Dame, König, Joker. Wir wollen Karte1 < Karte2 schreiben, wenn Karte1 niedriger ist. Wir können zur Vereinfachung dem Ass den Zahlenwert 1 geben und Bube, Dame, König, Joker entsprechend die Werte 11, 12, 13, 14. Wir betrachten also vereinfacht jede Karte als Zahl von 1-14.

Falls euch das verwirrend sollte, warum Ass am niedrigsten ist: Das soll ein Beispiel sein und ist völlig absolut egal und irrelevant hier und jetzt! Außerdem kann jeder selbst entscheiden, wie er gerne seine Karten sortieren will.

Macht euch als erstes einmal bewusst, dass ihr in der Lage sein solltet, diese Aufgabe zu lösen (oder habt ihr noch nie Karten gespielt?). Bei der Überlegung, wie ein Algorithmus aussehen könnte, überlegt einfach, wie ihr selbst denn nun die Karten sortieren würdet. Die Schwierigkeit liegt nun dadrin, das, was ihr sonst immer so total unbewusst und automatisch immer wieder selbst macht, in Worte zu fassen. Ihr solltet an dieser Stelle vielleicht auch wirklich einmal versuchen, es selbst zu überlegen und auch aufzuschreiben (hierbei immer darauf achten: jeder einzelne Schritt muss exakt sein und es muss immer eindeutig sein, welcher Befehl als nächstes kommt).

Hier nun eine der vielen möglichen Möglichkeiten:

Lösung 1 (Insertion-Sort auch genannt) grob formuliert:
Man nehme sich eine Karte und lege sie in einen neuen Stapel. Jede weitere Karte ist an die richtige Stelle im neuen Stapel zu legen, d.h. man geht den neuen Stapel von Anfang an durch und bei der ersten Position, wo die nächste Karte eine größere ist, legt man die ausgewählte Karte hinein. Dies wiederholt man, bis der ursprüngliche Stapel keine Karten mehr enthält. Der neue Stapel enthält nun alle Karten in sortierter Reihenfolge, also das gewünschte Ergebnis.

Lösung 1 exakt:
Neue Variable: Position
Neue Variable: Karte
Erstelle einen neuen (anfangs leeren) Stapel, nenne ihn neuerStapel.
Wiederhole folgendes, solange noch Karten im Kartenblatt sind:
        Nehme die erste Karte aus dem Kartenblatt heraus.
        Für Position := 1 bis (Anzahl Karten in neuerStapel), tue:
                Wenn neuerStapel[Position] > Karte ist, höre mit dieser Schleife vorzeitig auf.
        Ende der "Für"-Schleife.
        Füge Karte in neuerStapel an Position ein (zwischen Position-1 und Position).
Ende der "Wiederholen"-Schleife.
neuerStapel ist nun das Ergebnis.

Wir wollen den Algorithmus noch etwas vereinfacht aufschreiben. Es bietet sich hier an, gleich eine ganze Reihe von Kurzschreibweisen einzuführen. Wir wollen sowohl Kartenblatt als auch neuerStapel als eine Liste auffassen. Jeder Eintrag besitzt daher eine Position in der Liste und eine Liste besitzt gewöhnlich auch eine Länge, d.h. eine Anzahl von Einträgen (wir wollen ersteinmal keine unendlichen Listen (wie z.B. die Liste aller positiven, ganzen Zahlen 1,2,3,...) betrachten).

Wenn wir nun schreiben würden:
    Karte := Kartenblatt[Position]
Dann stellt sich die Frage, ob das nun ein Rausnehmen aus dem Kartenblatt bedeuten soll oder aber, dass die Karte sich noch im Kartenblatt befindet und die Variable Karte nur ein Verweis oder vielleicht auch eine Kopie auf den Eintrag im Kartenblatt. Wir wollen uns darauf festlegen, dass in so einem Fall die Liste niemals geändert wird (es würden an vielen anderen Stellen, wenn man sich auch mal die vorherigen Algorithmen betrachtet, sonst Verwirrungen entstehen).

Es stellt sich immer noch die Frage, ob Karte nun eine Kopie der Karte im Kartenblatt ist oder nur ein Verweis auf diese Karte im Kartenblatt (in diesem Beispiel ist es für einen Mensch als Ausführer natürlich nicht so leicht, die Karte eben mal zu kopieren; wenn es aber um ganz normale Zahlen geht, was wir ja vereinfacht angenommen haben, ist dies natürlich möglich).

Eine Kopie und ein Verweis (manchmal auch Referenz genannt) ist aber natürlich etwas unterschiedliches. Wenn man z.B. auf Karte nun "Hallo" oben links in die Ecke schreibt und es sich um eine Kopie gehandelt hat, dann hat sich bei der Originalkarte im Kartenblatt nichts geändert. Wenn aber die Variable Karte nur ein Verweis auf die Karte im Kartenblatt war und nun auf Karte "Hallo" geschrieben wurde, ist damit die Karte im Kartenblatt beschrieben worden.

In manchen Fällen kann es nun gewünscht sein, dass es sich um einen Verweis handelt, in manchen anderen Fällen ist es eher erwünscht, dass man immer mit Kopien handelt. Man kann dies natürlich immer an jeder Stelle explizit angeben, was gemeint ist. Man hat sich zusätzlich aber darauf geeinigt, dass man bei primitiven Variablenwerten immer implizit eine Kopie meint (wenn nicht anders angegeben) und im Falle von komplexeren Objekten meint man ein Verweis. Primitive Typen sind z.B. Zahlen und kürzere Texte/Buchstabenfolgen. Komplexere Typen sind z.B. ganze Listen oder andere Objekte, die gleich mehrere Sachen enthalten.

Beispiel 1 (implizite Kopie):
Neue Variablen vom Typ Zahl: Zahl1, Zahl2
Zahl1 := 10
Zahl2 := Zahl1
Zahl2 := 5
Ergebnis := Zahl1

Das Ergebnis ist nun 10, da bei der Änderung von Zahl2 die Zahl1 nicht geändert wurde, denn Zahl2 war nur eine Kopie von Zahl1, stand mit ihr also in keinem Zusammenhang nach der Kopie.

Beispiel 2 (impliziter Verweis):
Neue Variablen vom Typ Liste: Liste1, Liste2
Liste1 und Liste2 sollen erstmal leer sein.
Füge die Zahl 5 in Liste1 am Ende hinzu.
Füge die Zahl 3 in Liste1 am Ende hinzu.
Liste2 := Liste1
Füge die Zahl 42 in Liste2 am Ende hinzu.
Ergebnis := Liste1

Das Ergebnis ist nun die Liste, die aus 5, 3 und 42 besteht.

Falls eine Kopie allerdings erwünscht war, könnte man es z.B. folgendermaßen aufschreiben:

Beispiel 3 (explizite Kopie):
Neue Variablen vom Typ Liste: Liste1, Liste2
Liste1 und Liste2 sollen erstmal leer sein.
Füge die Zahl 5 in Liste1 am Ende hinzu.
Füge die Zahl 3 in Liste1 am Ende hinzu.
Liste2 := Copy(Liste1)
Füge die Zahl 42 in Liste2 am Ende hinzu.
Ergebnis := Liste1

Das Ergebnis ist nun die Liste, die aus 5 und 3 besteht.

Zurück zu dem ursprünglichen Beispiel: Da wir uns darauf festgelegt haben, dass Zahlen implizit kopiert werden sollen und wir die Karten als Zahlen darstellen wollten, liegen hier also Kopien vor. Dies soll uns aber nicht weiter stören. Nach dem Befehl:
    Karte := Kartenblatt[1]
Ist es allerdings noch notwendig, die erste Karte aus dem Kartenblatt zu entfernen. Wir wollen hierzu eine Art Funktion einführen (übliche Schreibweise, zusätzliche Methoden als Funktionen anzusehen), die wir Delete nennen wollen, wobei Delete(Liste, i) die i-te Stelle der Liste löschen soll. Wie bereits früher wollen wir mit Length(Liste) die Anzahl der Einträge in der Liste bezeichnen. Wir wollen außerdem mit der Funktion Add(Liste, i, Eintrag) das Hinzufügen von Eintrag an der i-ten Stelle (d.h. zwischen dem (i-1)-ten und dem i-ten Eintrag) in der Liste bezeichnen. Legen wir uns im Folgenden nun auch darauf fest, dass neue Variablen für Zahlen anfangs automatisch mit 0 initialisiert werden sollen und Listen am Anfang nach der Einführung leer sein sollen (um nicht immer wieder schreiben zu müssen, dass die Liste am Anfang leer sein soll).

Wir können den Algorithmus mit diesen neuen Schreibweisen und Festlegungen nun noch ein wenig exakter so formulieren:

Lösung 1 exakt:
Neue Variable vom Typ Zahl: Position
Neue Variable vom Typ Zahl: Karte
Neue Variable vom Typ Liste: neuerStapel

Wiederhole folgendes, solange Length(Kartenblatt) > 0 ist:
        Karte := Kartenblatt[1]
        Delete(Kartenblatt, 1)      

        Für Position := 1 bis Length(neuerStapel), tue:
                Wenn neuerStapel[Position] > Karte ist, höre mit dieser Schleife vorzeitig auf.
        Ende der "Für"-Schleife.
        Add(neuerStapel, Position, Karte)
Ende der "Wiederholen"-Schleife.
Ergebnis := neuerStapel

Etwas stillschweigend haben wir hier und auch schon bereits in den vorherigen kleineren Beispielen für Variablen spezielle Typen mitangegeben. Dies war in den allerersten einfachen Beispielen noch nicht notwendig, da sowieso im ganzen Algorithmus nur ein Typ verwendet wurde. Durch solche neuen Fragen wie die Kopie oder dem Verweis und auch durch solche Funktionen wie Length, Delete und Add und die Schreibweise Liste[i] ist es aber sinnvoll, einen Typ im Vorhinein mit anzugeben, da sonst Verwirrungen und Unklarheiten auftreten könnten (was z.B. soll bedeuten: Delete(Zahl, 1) ?). Die meisten Computer z.B. unterscheiden auch zwischen ganzen Zahlen und Zahlen mit Nachkommastellen.

Betrachten wir noch einen weiteren Algorithmus zum Sortieren:

Lösung 2 (Bubble-Sort):
Neue Variablen vom Typ Zahl: x, i, j
Für i := 1 bis Length(Kartenblatt), tue
        Für j := 1 bis Length(Kartenblatt)-1, tue
                Wenn Kartenblatt[j] > Kartenblatt[j+1], dann tue
                        x := Kartenblatt[j]
                        Kartenblatt[j] := Kartenblatt[j+1]
                        Kartenblatt[j+1] := x
                Ende "Wenn"-Fall
        Ende "Für"-Schleife
Ende "Für"-Schleife
Ergebnis := Kartenblatt

Dies ist interessanterweise der Algorithmus, auf den die meisten Leute als erstes kommen, wenn sie sich einen Algorithmus zum Sortieren überlegen sollen. Man beachte, dass offensichtlich immer 3*n*n Befehle (n sei dabei die Anzahl der Einträge im Kartenblatt) notwendig sind. Lösung 1 ist ein abhängig von der bereits gegebenen Ordnung vom Kartenblatt, aber selbst im extremsten Fall ist Lösung 1 schneller.

Es gibt noch viele weitere Algorithmen zum Sortieren von Listen, die teilweise (vor allem für sehr große Listen) noch deutlich schneller sind. Diese sind aber auch etwas umständlicher, deshalb wollen wir hier erstmal nicht darauf eingehen.

Wir wollen nun zu einem neuen Kapitel übergehen. Das Überlegen von Algorithmen bleibt aber immer beim Programmieren die absolute Grundlage. Es kann daher nützlich sein, sich zu bestimmten Sachen selbst Algorithmen zu überlegen.

Ein paar Denkansätze für weitere Problemstellungen (manche davon leichter, manche schwerer und bei manchen wurde erst vor vergleichsweise kurzer Zeit mathematisch bewiesen, dass eine Lösung nicht möglich ist):

Man beachte auch, dass das Überlegen von Algorithmen etwas sehr altes ist. Jedes Kuchenrezept, jede Bauanleitung und jeder sonstige beschriebene Vorgang ist ein Algorithmus. Wir Menschen haben uns Algorithmen überlegt, seit es uns gibt. Selbst Tiere leben ihr gesamtes Leben nach einem Algorithmus ab, der sich intuitiv in ihnen befindet, programmiert von der Evolution. (Wenn Hunger, dann esse. Wenn Durst, dann trinke. Wenn gewisse sexuellen Bedürftnisse, dann Partnersuche. Wenn müde, dann suche Platz zum Schlafen und schlafe. ... Wiederhole Überprüfungen wieder am Anfang.) Und natürlich ist das Arbeitsprinzip einer Maschiene nichts anderes, als einfach einen festgelegten Algorithmus Schritt für Schritt abzuarbeiten.

Hier noch vom Lehrstuhl 1 der Informatik der RWTH Aachen eine sehr schöne Beschreibung eines Algorithmus' zum Suchen einer CD aus einer geordneten Sammlung von CDs:
www-i1.informatik.rwth-aachen.de/~algorithmus/algo1.php

Einführung in eine Programmiersprache (Object Pascal)

An dieser Stelle sind bereits nun alle Grundlagen zum Programmieren gelegt und wenn ihr alles verstanden habt könnt ihr bereits sagen, dass ihr programmieren könnt, auch wenn das Bisherige vom Wissensumfang wirklich minimal und trivial ist.

Damit sich das Wissen aber auch irgendwie festsetzt und hält, wollen wir nun das Programmieren praktisch anwenden. Wir wollen damit beginnen, geschriebene Algorithmen testen zu lassen und sie in einer einheitlicheren Form aufzuschreiben. Abgesehen von dem Lerneffekt ist es auch ein unbeschreiblich cooles Gefühl zu sehen, wie da der eigene Algorithmus durchläuft und genau das tut, was er soll. Hierfür wurden sogenannte Programmiersprachen entwickelt. In Programmiersprachen sind sehr strikte Regeln vorgegeben, in welcher Form der Algorithmus aufgeschrieben werden muss. Zu dieser Form, die man auch Grammatik nennt, gehören solche Details wie z.B. ein Semikolon am Ende eines Befehls und Ähnliches. Außerdem kann man grundsätzlich in der Programmiersprache nur die von der Programmiersprache bereitsgestellten Befehle benutzen (diese Menge von Befehlen wird häufig auch das Vokabular genannt). Auch wird von der Programmiersprache meistens eine Reihe von Standardvariablentypen bereitgestellt, die meistens Ganzzahlen, Zahlen mit Nachkommastellen, Listen und weiteres enthalten. Der Vorteil dieser sehr strikten Regeln ist, dass die Algorithmen in so einer klarer Form dann geschrieben sind, dass sie ein Computer sehr leicht interpretieren und ausführen kann.

Im Folgenden, vor allem in der Einführung in Object Pascal, werden sehr viele spezielle Details aufgelistet. Es ist nicht notwendig, wirklich alles davon auswendig zu lernen. Sobald man ein wenig dadrin selbst arbeitet, kommt das Lernen automatisch und kurze Zeit später, nachdem man den Anfang hinter sich hat, wo man noch immer wieder alles nachgucken muss und häufig alles nicht auf Anhieb funktioniert, wird es dann ungemein Spaß machen.

Bevor wir die Programmiersprache einführen, ist es allerdings notwendig, zumindest einige wesentliche Grundlagen der Arbeitsweise eines Computers zu kennen und zu verstehen, daher hier eine kurze Zusammenfassung:

Der Kern eines jeden Computers ist sein Prozessor. Dieser führt die eigentlichen Befehle aus. Er kennt dabei eine vorgegebene Menge von relativ primitiven Befehlen, mit denen letztendlich jede Funktion eines Computers realisiert werden muss. Man nennt diese primitiven Befehle die Maschienenbefehle. Diese Befehle sind dabei einfach durchnummeriert, hat einen bestimmten Namen und erwarten jeweils eine bestimmte vorgegebene Anzahl von Parametern. Außerdem besitzt jeder Computer einen begrenzten Arbeitsspeicher, in dem er sich Sachen (Variablen) speichern kann. Dieser Arbeitsspeicher ist kein dauerhafter Speicher, d.h. bei einem Neustart ist alles wieder leer. Für die dauerhafte Speicherung ist die Festplatte gedacht, die genauso eine begrenzte Speichermenge zu Verfügung stellt.

Ein Computerprogramm ist nun nichts anderes, als eine Auflistung von Maschienenbefehlen (d.h. deren zugeordnete Nummer), wobei jedem Maschienenbefehl immer die Parameter folgen. Der Prozessor führt nun einen Befehl nach dem anderen aus.

So und zwar wirklich genau so läuft alles, was man auf dem Monitor erkennen kann, intern in einem Computer auf unterster Ebene ab. (Wozu soll man es auch komplizierter machen, wenn es schon so einfach funktioniert.)

Da diese vom Prozessor unterstützten Maschienenbefehle sehr primitiver Art sind (grob gesagt ist da nicht sehr viel mehr als das Schreiben und Auslesen bestimmter binärer Daten auf die bzw. von der Hardware und dann noch grundlegende mathematische Befehle wie Addieren, Multiplizieren, usw.), hat man sogenannte höhere Programmiersprachen entwickelt, in denen man die Programme in Textform formulieren kann. Man nennt diesen Text Programmcode. Dieser Programmcode kann dann von sogenannten Compiler-Programmen in Maschienencode, d.h. ein vom Prozessor ausführbares Programm, umgewandelt werden. (Diese Umwandlung nennt man auch compilieren.)

Von solchen höheren Programmiersprachen gibt es heutzutage eine fast unendliche Menge, die teilweise sehr verschiedener Art sind. Grundsätzlich hat man sich aber in den meisten Programmiersprachen für Bezeichnungen auf die Sprache Englisch geeinigt (und allgemein auf relativ gut lesbare Konstrukte). Ein erster Unterschied, der auffällt zu den in rein sprachlicher Form aufgeschriebenen Algorithmen, ist die Wichtigkeit der Einhaltung gewisser von der Programmiersprache vorgegebener Details. Diese Details umfassen z.B., dass ein Befehl (je nach Programmiersprache) immer mit einem Semikolon enden muss und Ähnliches.

Wir wollen hier die Programmiersprache Object Pascal für weitere Beispiele verwenden. Object Pascal ist relativ gut lesbar für einen Nicht-Programmierer und auf der anderen Seite aber auch sehr mächtig. Object Pascal ist die objektorientierte Erweiterung von dem ursprünglichen Pascal. Was genau objektorientierte Programmierung ist, wird später behandelt und spielt erstmal keine weitere Rolle. Bekannte Compiler für Object Pascal sind z.B. Delphi oder Lazarus basierend auf Free Pascal. Lazarus bietet den Vorteil, als Open Source Projekt frei verfügbar zu sein und außerdem auf allen gängigen Betriebssystemen und Computerarchitekturen zu laufen. Der unter Windows manuell auszuführende, notwendige Download des Programms kann auf der Homepage www.lazarus.freepascal.org durchgeführt werden.

Wir beginnen direkt mit einem Beispiel, anhand dem wir weitere Details dann erklären wollen. Dieses Beispiel soll den vorhin besprochenen BubbleSort-Algorithmus darstellen:

procedure BubbleSort(var list: array of Integer);
var
	x,i,j: Integer;
begin
	for i := 0 to High(list) do				{1}
	begin
		for j := 0 to High(list)-1 do			{2}
		begin
	  		if list[j] > list[j+1] then		{3}
			begin
  				x := list[j];			{4}
  				list[j] := list[j+1];		{5}
  				list[j+1] := x;			{6}
			end;
		end;
	end;
end;

Beginnen wir, im Detail die Einzelheiten der Sprache festzuhalten.

Der Programmcode beginnt mit dem Schlüsselwort procedure. Wir hatten bereits erwähnt, dass man einen Algorithmus häufig als Funktion ansieht, denn dieser bekommt in der Regel gewisse Parameter (in diesem Fall nur einen, nämlich list; beachte, dass auch wenn die Liste selbst vielleicht einiges an Informationen enthält, die Liste an sich trotzdem nur ein Objekt ist). Außerdem gibt ein Algorithmus in der Regel einen Wert zurück, den sogenannten Rückgabewert. Im Falle der Addition z.B. war dies das Ergebnis. Es gibt aber auch Fälle, wo im Algorithmus irgendetwas aktiv getan wird, wie z.B. das Entfernen des Mülls oder in diesem Fall das Sortieren der Liste. Einen solchen Algorithmus ohne speziellen Rückgabewert nennt man auch häufig Prozedur oder Methode. In anderen Programmiersprachen gibt es kein spezielles Schlüsselwort für eine Prozedur, da man dort einfach eine Funktion ohne Rückgabewert beschreiben kann. Ich werde deshalb im weiteren häufig auch Prozeduren als Funktionen bezeichnen. Von manchen Leuten wird dies nicht so gerne gesehen, da im mathematischen Sinn eine Funktion immer einen Rückgabewert haben muss, da es sonst keine Funktion ist. Ich hoffe, ihr seit hinreichend gestärkt, um trotzdem damit fertig zu werden.

Der gesamte Code in Pascal besteht aus Funktionen und Prozeduren, in welchen die eigentlichen Aktionen ablaufen (bzw. beschrieben sind). Beim Schreiben einer Prozedur oder einer Funktion muss mit dem ersten Wort gekennzeichnet sein, ob es sich um eine Prozedur (in dem Fall procedure) handelt oder um eine Funktion (in dem Fall function).

Als nächstes legt man den Namen der Funktion fest. In diesem Fall ist er passenderweise BubbleSort.

Falls die Funktion Parameter bekommen soll, werden diese nun in Klammern angegeben. (Eine Funktion muss nicht unbedingt Parameter bekommen. Das Müllentfernen beispielsweise ist eine Aktion. Das Herunterfahren des PCs ist auch eine Aktion. In beiden Fällen sind vermutlich zusätzliche Parameter nicht so sinnvoll. Das bleibt aber natürlich dem Author des Algorithmus überlassen.) Alle in den Klammern angegebenen Parameter sind durch ein Semikolon zu trennen (da in diesem Fall nur ein Parameter da ist, ist auch nichts zu trennen).

Das im Beispiel benutze Schlüsselwort var hat eine besondere Bedeutung. Es symbolisiert, dass der Parameter ein Verweis und keine Kopie sein soll (der Unterschied wurde bereits besprochen). Dies bedeutet, dass Manipulationen des Parameters sich direkt auf das Original auswirken sollen. (Ansonsten wäre der oben angegebene Algorithmus auch nicht sinnvoll, da die kopierte sortierte Liste nirgendwo zurückgegeben wird. Es wäre natürlich in dem Fall dann eine gute Idee, diese kopierte Liste auch zurückzugeben. Das Zurückgeben einer Liste wird aber in der Sprache Pascal nicht erlaubt, da eine Liste ein zu komplexes Objekt ist. Mehr hierzu später und auch, wie man es sonst hätte machen können.) Das Schlüsselwort var kann benutzt werden (hat dann die beschriebene Wirkung), muss aber nicht (in dem Fall ist der entsprechende Parameter eine Kopie).

Der Parameter selbst, genauso wie auch Variablen (dazu gleich), besteht dann aus dem Namen (in diesem Fall list) und, getrennt mit einem Doppelpunkt, dem Typen des Parameters bzw. der Variable. Beachte, dass in Pascal (sowie auch in vielen anderen Programmiersprachen) der Typ immer angegeben werden muss. Dies erleichtert es, den Code zu lesen, gewisse Vorhersagen über das Verhalten des Codes bei der Ausführung machen zu können und außerdem für den Compiler, Fehler im Code zu finden. (Man kann sehr froh sein, wenn der Compiler einen Fehler im Code findet, denn dann kann man diesen leicht beheben. Falls der Compiler den Fehler nicht findet und man dann das Programm startet, es aber irgendwie nicht richtig funktioniert, ist es deutlich schwieriger, eigentlich überhaupt die Fehlerursache zu finden.)

Primitive Variablentypen
Typ Bedeutung Beispiel
Integer eine ganze Zahl -2 oder 10
Real eine Zahl mit Nachkommastellen 3.1415
Boolean nur 2 mögliche Werte: true oder false true
String ein Text 'Einen schönen guten Tag.'
Char (genau) ein einzelnes Zeichen 'b' oder '3' oder '+' oder ' '

Komplexere Variablentypen
Typ Bedeutung Beispiel
array of <Typ> ein Array (variabler Länge) vom Variablentyp <Typ> (1, 2)
array[0..n] of <Typ> ein Array (fester Länge, nämlich genau n+1 Einträgen) von <Typ> ('a', 'b', 'c')

Weitere Typen werden später eingeführt.

Wir haben nun nahezu die erste Zeile des Codes komplett erklärt. Was fehlt ist das Semikolon am Ende der Zeile. Mit dem Semikolon schließt man in Pascal Dinge ab, in diesem Fall die Funktionsbeschreibung. Ansonsten wird es auch benutzt (bzw. muss benutzt werden; auf so etwas legt der Compiler Wert), um 2 Befehle voneinander zu trennen.

Nun können, wenn Variablen in der Funktion benötigt werden, diese festgelegt werden. Wir müssen hier zu Beginn alle in der Funktion benutzten Variablen direkt festlegen und können später im eigentlichen Code keine weiteren hinzufügen. Das sollte aber kein Problem darstellen, denn dann schreiben wir einfach alle Variablen nun hier hin. Die Variablenliste leitet man mit dem Schlüsselwort var an dieser Stelle ein. (Man beachte, dass je nach Stelle im Code, gewisse gleiche Schlüsselwörter unterschiedliche Bedeutungen haben können.) Diese werden im gleichen Format niedergeschrieben, wie auch die Parameter. Man beachte auch die von Pascal erlaubte Zusammenfassung von Variablen (bzw. auch Parametern), welche im Beispiel auch benutzt wurde. Man hätte es auch schreiben können:
    var x: Integer; i: Integer; j: Integer;
Falls keine Variablen erwünscht werden, lässt man das Schlüsselwort var einfach weg.

Man beachte, dass Zeilenumbrüche (so wird es genannt, wenn eine neue Zeile beginnt) und Leerzeichen (auch Tabulatoren) keine Rolle in Pascal spielen. Diese können gesetzt werden, wo sie auch immer zur besseren Lesbarkeit erwünscht werden.

Auch können (sollten) Kommentare im Code hinterlassen werden. Diese werden entweder in geschweifte Klammern gesetzt oder beginnen alternativ mit // (2 mal Schrägstrich) und gelten dann bis zum Ende der Zeile (die Kommentare innerhalb der geschweiften Klammern können über mehrere Zeilen gehen). Alle diese Kommentare werden vollkommen vom Compiler ignoriert und dienen damit nur der Lesbarkeit.

Der eigentliche Code wird nun mit dem Schlüsselwort begin eingeleitet, gefolgt von den eigentlichen Befehlen. Generell gehört zu jedem begin auch immer ein end am Ende. So ein begin/end markiert einen Codeblock. Ein Codeblock ist nichts weiter als ein Block (d.h. eine gewisse Anzahl von Befehlen) Code. Solche Codeblöcke können auch ineinander geschachtelt werden, wie es auch im Beispiel gemacht wurde. Einzelne Befehle sind dabei entweder Zuweisungen (variable := irgendwas), Funktions- oder Prozeduraufrufe, ein Schleifenanfang oder eine if-Abfrage.

Der erste Befehl im Beispiel ist direkt eine Schleife. In diesem Fall wurde die for-Schleife verwendet. Die Ausführung der Schleife geschieht dabei exakt genauso, wie es bereits für den äquivalenten Pseudocode beschrieben wurde. Die for-Schleife sieht in Pascal immer folgendermaßen aus:
    for i := start to ende do aktion;
i, start und ende sind dabei beliebige Variable oder Werte vom Typ Integer. aktion ist entweder ein einzelner Befehl oder ein Codeblock.

Beachte, dass in Pascal ein Array immer beginnt mit dem 0-ten Eintrag. Die im Beispiel benutzen Funktion High bekommt als Parameter ein Array und liefert als Rückgabewert den Index des letzten Eintrags (der Index eines Eintrags ist die Position in dem Array bzw. in einer Liste). High(list) gibt als eins weniger als die Anzahl der Einträge in list zurück, da der 0-te Eintrag noch hinzukommt.

Der restliche Code entspricht nach allen Erklärungen nun exakt dem, was auch als Pseudocode bereits präsentiert wurde.

Um zu Vergewissern, dass auch klar ist, was im Detail genau passiert (was sehr wichtig für das Verständnis ist), werden wir mal genau im Beispiel durchgehen, was der Computer bei der Ausführung dieser Prozedur genau in welcher Reihenfolge tut, also wie er diesen Code liest. Sei dazu meine_liste = (2,1) gegeben. Wir beschreiben nun, was genau der Computer macht, wenn an anderer Stelle der Befehl BubbleSort(meine_liste); aufgerufen wird.

  1. Setze den Parameter list als Verweis auf meine_liste.
  2. Erstelle die Variablen x, i und j im Arbeitsspeicher.
  3. Gehe zu Befehl 1.
  4. Rufe die Funktion High auf, mit list als Parameter und speichere den Wert als Endwert der Schleife. (Der Endwert ist damit 1.)
  5. Setze i := 0.
  6. Gehe zu Befehl 2.
  7. Rufe die Funktion High mit list als Parameter auf, subtrahiere den Wert mit 1 und speichere den Wert als Endwert der Schleife. (Der Endwert ist damit 0.)
  8. Setze j := 0.
  9. Gehe zu Befehl 3.
  10. Überprüfe, ob list[j] > list[j+1] gilt. j ist 0, j+1 ist 1, list[0] ist 2 und list[1] ist 1. Weil 2>1 ist, gilt es also. Also gehe zu Befehl 4.
  11. Setze x := list[j]. (x ist damit 2.)
  12. Setze list[j] := list[j+1]. (Beachte, hier wird direkt meine_liste geändert! An der 0-ten Stelle ist nun eine 1. Also ist list = (1,1), denn die 1-te Stelle wurde ja nicht geändert.)
  13. Setze list[j+1] := x. (Damit ist list = (1,2).)
  14. Ende des Codeblocks der if-Abfrage.
  15. Ende des Codeblocks der for-Schleife aus Befehl 2, gehe also zur Stelle 2 und erhöhe j um eins. (j ist dann 1.) Überprüfe, ob j nun größer als der gespeicherte Endwert ist (der Endwert war 0). Dies ist der Fall, also höre auf, die Schleife noch einmal zu durchlaufen und gehe damit zum nächsten Befehl hinter der Schleife.
  16. Ende des Codeblocks der for-Schleife aus Befehl 1, gehe also zur Stelle 1 und erhöhe i um eins. (i ist dann 1.) Überprüfe, ob i nun größer als der gespeicherte Endwert ist (der Endwert war 1). Dies ist nicht der Fall, also fahre mit Befehl 2 fort.
  17. Rufe die Funktion High mit list als Parameter auf, subtrahiere den Wert mit 1 und speichere den Wert als Endwert der Schleife. (Der Endwert ist damit 0.)
  18. Setze j := 0.
  19. Gehe zu Befehl 3.
  20. Überprüfe, ob list[j] > list[j+1] gilt. list[0] ist 1 und list[1] ist 2. Weil nicht 1>2 gilt, überspringen wir den Inhalt der if-Abfrage. Also gehe zum nächsten Befehl dahinter.
  21. Ende des Codeblocks der for-Schleife aus Befehl 2, gehe also zur Stelle 2 und erhöhe j um eins. (j ist dann 1.) Überprüfe, ob j nun größer als der gespeicherte Endwert ist (der Endwert war 0). Dies ist der Fall, also höre auf, die Schleife noch einmal zu durchlaufen und gehe damit zum nächsten Befehl hinter der Schleife.
  22. Ende des Codeblocks der for-Schleife aus Befehl 1, gehe also zur Stelle 1 und erhöhe i um eins. (i ist dann 2.) Überprüfe, ob i nun größer als der gespeicherte Endwert ist (der Endwert war 1). Dies ist der Fall, also höre auf, die Schleife noch einmal zu durchlaufen und gehe zum nächsten Befehl dahinter.
  23. Ende des Codeblocks der Funktion. Damit sind wir fertig.

Ist das alles verstanden, sollte damit klar sein, wie man grundsätzlich Programmcode in Object Pascal schreibt. Falls sich das bei dir im Kopf alles noch nicht so feste gesetzt hat, ist das nicht schlimm und auch nicht weiter verwunderlich. Wirklich festsetzen tut sich sowas erst nach ein wenig praktischer Anwendung - und bei den ersten Gehversuchen (Krabbeln sollte jetzt bei dir jetzt drin sein) und auch überhaupt immer kann man sich ja irgendwelche Referenzen oder Tutorials zur Vorlage nehmen (ist ja völlig egal, wie man an sein Endresultat kommt, nur das Endresultat hinterher zählt wirklich).

Was noch ein wenig unklar ist, ist wie man grundsätzlich nun ein lauffähiges Programm in Object Pascal schreibt. Dazu mal ein einfaches Beispiel:

program hallo_welt_extended;

// BubbleSort aus dem vorherigen Beispiel (nur mit char statt integer)
procedure BubbleSort(var list: array of char);
var
	x: char;
	i,j: integer;
begin
	for i := 0 to High(list) do
	begin
		for j := 0 to High(list)-1 do
		begin
	  		if list[j] > list[j+1] then
			begin
  				x := list[j];
  				list[j] := list[j+1];
  				list[j+1] := x;
			end;
		end;
	end;
end;


// Hauptprozedur
procedure main();
var
	zeichen: array[0..9] of char;
	i: integer;
begin
	// Eingabe
	write('Gib 10 Zeichen ein: ');
	for i := 0 to 9 do
	begin
		// lese Zeichen vom Benutzer ein
		read(zeichen[i]);
	end;


	// Ausgabe
	write('Du hast eingegeben: ');
	for i := 0 to 9 do
	begin
		// gib Zeichen einzeln aus
		write(zeichen[i]);
	end;
	writeln(''); // und noch einen Zeilenumbruch am Ende
	

	// Sortieren
	writeln('Wir sortieren nun...');
	BubbleSort(zeichen);


	// neue Ausgabe
	write('Fertig, das Ergebnis ist: ');
	for i := 0 to 9 do
	begin
		write(zeichen[i]);
	end;
	writeln('');

end;



// Hauptcode unseres Programms
begin
	// starte unsere Hauptprozedur
	main();

	// hier ist bereits nun das Ende des Programms
end.

Dieses Programm ist ein reines Konsolenprogramm. Im Hauptcode wird direkt als einzige Aktion die main-Funktion gestartet. Diese enthält die eigentlichen Aktionen des Programms. Am Anfang liest sie 10 Zeichen vom Benutzer auf der Konsole ein, danach gibt es diese 10 Zeichen wieder aus, danach wird unsere bereits besprochene Prozedur BubbleSort darauf angewendet und letztendlich wird die Liste der 10 Zeichen dann ein weiteres Mal ausgegeben (nun hoffentlich korrekt sortiert).

Die erste richtige Zeile leitet in Object Pascal immer die Datei ein. Das kann dabei sowohl eine Unit (unit name;) oder auch hier die Programm-Deklaration (program name;) selbst sein. Der Name sollte dabei dem Dateinamen (ohne Dateierweiterung) entsprechen, ansonsten könnte es zu Verwirrungen beim Compiler kommen. Diese Einleitung muss ganz am Ende der Datei mit einem end. abgeschlossen werden.

Ein vollständiges Programm besteht immer aus einer solchen Programm-Deklaration und mehreren Units (bei diesem Beispiel war gar keine zusätzliche Unit mehr nötig). Die Units benutzt man, um sein Programm ein wenig aufzuteilen und so eine übersichtlichere Struktur zu bekommen (wir hätten beispielsweise die BubbleSort-Prozedur in eine extra Unit packen können). Die Programm-Deklaration hält man in der Regel auch so klein wie möglich, man will eigentlich immer den gesamten Code in den Units haben.

Programm-Deklarations-Struktur:

// Einleitung der Datei
program my_sample_prog;

// Unit-Abhaengigkeiten
uses
	Unit1,
	Unit2 {...}	; // nicht vergessen am Ende; dies schliesst den uses-Teil ab


{ irgendwelche Funktionen / Prozeduren }

// Variablendeklarationen
var
	zahl: integer;
	noch_ne_zahl: integer;

// Beginn des eigentlichen Hauptcodes
begin
	{ Hauptcode hier }

end. // absolutes Ende

Unit-Struktur:

// Dateieinleitung fuer eine Unit
unit my_sample_unit;

// Beginn des Interface-Teils
interface

	// welche anderen Units brauche ich hier?
	uses
		my_other_unit, some_graphics_stuff;

	// ein paar Konstanten, die ich vielleicht brauche
	const
		PI = 3.1415;
		MIN_NEEDED_CPU_SPEED = 800; // MHz Angabe
		MENSCHEN : array[1..3] of string
			= ('Linus', 'Florian', 'Albert');
		{ ... }

	// ein paar eigene Variablentypen definieren
	type
		{ spaeter mehr zur eigenen Variablentypen ... }

	// hier noch irgendwelche Funktionen, aber ohne Code
	function BubbleSort(list: array of integer);
	{ ... weitere Funktionen hier ... }

	// ein paar globale Variablen
	var
		AnzahlSpieler: integer;
		txt1, txt2, txt3: string;
		{ ... }
		

// Beginn des Implementierungsteils
implementation

	// welche anderen Units brauche ich hier?
	uses
		super_mega_unit;

	// Code fuer die deklarierten Funktionen ...
	function BubbleSort(list: array of integer);
	begin
		{ ... }
	end;

	{ ... weitere Implementierungen der oben eingefuehrten Sachen ... }


// Beginn des Initialisierungscodes
initialization
	AnzahlSpieler := 0;
	txt1 := 'Hallo Welt';
	{ ... }

 
end. // Ende der Unit

Im Prinzip sind viele alle Teile optional, d.h. wenn ein Teil nicht benötigt wird, lässt man ihn einfach weg (nicht immer muss man Konstanten einführen oder benötigt für die ganze Unit globale Variablen oder braucht einen Initialisierungscode usw.). Nur die Grundstruktur, d.h. bei einer Unit der Interfaceteil und der Implementationteil, muss vorhanden sein.

Die Unterteilung übrigens zwischen Interface und Implementierung hat seinen Sinn: Unter einem Interface versteht man die genaue Angabe der Schnittstelle der Unit, d.h. die nach außen hin (für andere Units) zu Verfügung gestellten Sachen. Dies sind in erster Linie eigene Funktionen und eigene Variablentypen, aber auch Konstanten und ähnliches (alles das, was unter interface steht). Nicht alles muss man nach außen hin zu Verfügung stellen - nur das, was man als sinnvoll erachtet. Unter einer Implementierung versteht man einen Programmcode für eine spezielle Sache, z.B. eine im Interface deklarierte Funktion. Der Grund zur Unterteilung zwischen Interface und Implementierung ist, dass man als menschlicher Leser schneller erkennen kann, was einem eine Unit anbietet. Die Implementierung ist in der Regel um ein vielfaches größer und länger als das Interface.

Kurzeinführung in Lazarus

Das letzte Beispiel war ein reines Konsolenprogramm. Angenommen, der Dateiname der Programmdeklaration war hallo_welt_extended.pas. In der Konsole gibt man einfach folgendes ein, um das Programm zu kompilieren:

fpc hallo_welt_extended.pas

Man erhält dann die ausführbare Datei hallo_welt_extended. Die noch gestartet und das ganze sieht dann so aus:
FPC Beispiel

Da es Dank Lazarus aber nicht wirklich schwieriger ist, direkt ein Programm mit grafischer Oberfläche zu programmieren und dies außerdem unter Windows, Linux oder sonst wo auf gleiche Art und Weise ohne weitere Probleme durchführbar ist und man außerdem so direkt, auch als Anfänger, zu viel netteren Ergebnissen kommen kann, wollen wir im weiteren das auch tun.

So sieht Lazarus direkt nach dem (ersten) Start aus:
Lazarus Startbildschirm

Ganz kurz zur Erklärung: Oben sieht man das Standardmenü (Lazarus-Editor ...), wo man so Standardsachen machen kann (Speichern, neue Units anlegen, neues Projekt erstellen, usw.). Dieser grüne Playknopf (Start-Symbol) startet das Programm. Außerdem findet man hier die Liste aller verfügbaren Steuerelemente (z.B. Buttons, Textfelder, usw., die man nach Auswahl auf der Form platzieren kann). Links der Objektinspektor listet die auf der aktuell ausgewählten Form verfügbaren Objekte/Steuerelemente auf. Hier gibt man auch erweiterte Eigenschaften an (wie z.B. die Beschriftung von einem Button, den internen Namen (über den das Objekt im Code angesprochen wird), die Farbe, die Schriftart und sonstiges). In der Mitte im Hintergrund ist der Lazarus-Quelltexteditor, in dem man immer den kompletten Code der gerade ausgewählten Unit sieht und editieren kann. Die Form rechts im Bild ist das Fenster, welches später vom erstelten Programm benutzt wird. Zur Entwicklungszeit kann man hier die Steuerelemente einfügen, wie man gerade Lust hat. Nach einem Doppelklick auf ein Steuerelement (z.B. auf ein dort platzierten Button) gelangt man automatisch zu dem entsprechenden Code des Standardereignisses des Steuerelements (z.B. ist sehr häufig das Standardereignis ein einfacher Mausklick).

Packen wir nun mal ein Button (TButton), ein Label (TLabel) und ein Timer (der findet sich unter der Kategorie 'System': TTimer) auf unsere Form:
Form mit Label, Button und Timer

Ein Button ist dabei eins dieser wohlbekannten Klickfelder. Ein Label tut nichts anderes als einen Text anzuzeigen. Und ein Timer ist dafür da, irgendwelchen Code in regelmäßigen Abständen auszuführen. Nun wählen wir den Button aus und geben ihm ein paar vernünftige Eigenschaften. Es empfiehlt sich, die Caption-Eigenschaft (die Beschriftung) zu ändern. Damit du in etwa das gleiche Ergebnis wie ich im Folgenden bekommst, solltest du beim Timer das Intervall auf 100 setzen. Auch die internen Namen (Button1, Label1, Timer1) habe ich soweit erstmal gelassen.

Der aktuelle Code sieht nun folgendermaßen aus:

unit Unit1; 

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, ExtCtrls,
  Buttons, StdCtrls;

type

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    Label1: TLabel;
    Timer1: TTimer;
  private
    { private declarations }
  public
    { public declarations }
  end; 

var
  Form1: TForm1; 

implementation

initialization
  {$I unit1.lrs}

end.

Lazarus hat für uns, wie man sieht, bereits gute Arbeit geleistet. Diese Unit1 ist die entsprechend zu der Form1 gehörige Unit. Es wurden bereits alle abhängigen Units im uses-Teil aufgelistet. Unsere Form wurde im type-Bereich dabei als neuer Typ (und zwar TForm1) eingeführt, der auf dem Typ der Standardform (TForm) basiert. Wirklich im Detail verstehen, was genau diese Typdefinition aussagt, werden wir später. Es reicht aber vorerst völlig aus, wenn man sich grob vorstellt, dass dieser neue Variablentyp TForm1 mehrere Untervariablen (Button1, Label1 und Timer1 bisher) zu Verfügung stellt. Das public und private sagt nur aus, ob von außerhalb des Typs darauf zugegriffen werden kann - ist also auch erstmal uninteressant, so lange das gesamte Programm sowieso nur innerhalb der Form1 sein wird. Im var-Bereich wurde nun gerade unsere Form1 als Variable deklariert, vom Typ TForm1. Alle weiteren Informationen wie z.B. die Position und sonstige Eigenschaften der 3 bisher platzierten Steuerelemente werden im initialization-Teil gesetzt. Dort findet man den Compilerbefehl {$I unit1.lrs}. Alle Kommentare in geschweiften Klammern, die mit einem Dollar-Zeichen beginnen ({$ ...}), sind spezielle Anweisungen für den Compiler, irgendetwas bestimmtes an dieser Stelle beim Compilieren zu tun. Die Anweisung $I sagt dabei, hier soll die folgende Datei (temporär) eingefügt werden. In der Datei unit1.lrs befinden sich dabei die Zuweisungen der speziellen Eigenschaften für die Objekte. (Andere, evtl. von Lazarus hinzugefügten Compileranweisungen können im Moment ignoriert werden.)

Wenn wir jetzt bereits einmal zum Test auf Start (Start-Symbol) drücken, wird das Programm bereits erfolgreich compiliert und auch gestartet. Man wird allerdings feststellen, dass man nicht wirklich etwas sinnvolles tun kann, denn bisher haben wir auch noch keinen Code geschrieben.

Dies holen wir jetzt nach. Nach einem Doppelklick auf den Button erstellt Lazarus automatisch eine Prozedur Button1Click, welche mit dem Klickereignis des Buttons verknüpft ist. Das bedeutet, wenn wir im eigentlichen Programm hinterher auf den Button klicken, wird die Prozedur Button1Click aufgerufen. Lazarus geht außerdem nach dem automatischen Erstellen der Prozedur auch an die richtige Stelle im Code, so dass wir direkt beginnen können, den eigentlichen Code einzugeben (ist das nicht super einfach?).

Zum Test geben wir mal folgende 2 Zeilen ein:
Label1.Top := Label1.Top - 10;
Label1.Caption := IntToStr(Label1.Top);

Nun testen (d.h. starten) wir erneut das Programm. Nach einem Klick auf den Button können wir nun das erstaunliche Ergebnis bestaunen.

Ein wenig was zum Code: Wer aufmerksam war, hat sicherlich bemerkt, dass die Prozedur Button1Click innerhalb des Typs TForm1 deklariert wurde (im interface-Teil) und im implementation-Teil den Namen TForm1.Button1Click bekommen hat. Dies ist eine dem Typ zugeordnete Funktion. Solche Funktionen/Prozeduren bekommen immer im implementation-Teil den vollen Namen, damit keine Namenskonflikte entstehen können. Der Parameter Sender: TObject informiert beim Aufruf über das sendende Objekt, welches in dem Fall immer Form1 ist. Wir können den Parameter also vollkommen ignorieren (erst viel später wird er sinnvoll werden). Im eigentlichen Code ändern wir gleich 2 Eigenschaften des Objektes Label1. Auf dessen Eigenschaften (allgemein auf die Untervariablen eines selbstdefinierten Typs) greift man immer mit einem Punkt zu, also Objektname.Eigenschaft. In dem Fall vermindern wir Label1.Top um 10. Wie man sicherlich beim Testen des Programms festgestellt hat, gibt die Eigenschaft Top (vom Typ Integer) die Y-Position des Labels an. Etwas verwirrend ist, dass die Y-Koordinate scheinbar umgedreht ist (also ganz oben ist 0 und nach unten hin wird es positiv). Dies hat sich im PC-Bereich durchgesetzt - wohl aus dem Grund, da man auch von oben nach unten liest und so der Punkt (0,0) (links oben) dann auch der Startpunkt ist. Die Caption-Eigenschaft (vom Typ String) entspricht der Beschriftung. IntToStr ist eine Funktion, die einen Wert vom Typ Integer (also eine Zahl) in einen Wert vom Typ String (also einen Text) umwandelt. Man beachte (und halte sich immer im Kopf), dass dies wirklich eine echte Umwandlung ist. Dies merkt man schon daran, dass sich nicht jeder beliebige String in einen Integer umwandeln lässt. Für die Strings, für die dies aber doch möglich ist, gibt es die Funktion StrToInt.

Wir wollen nun das Label auf der anderen Seite aber auch wieder nach unten laufen lassen. Hierzu benutzen wir am besten unseren Timer. Wir tätigen mal einen Doppelklick auf diesen, so dass Lazarus automatisch die Prozedur Timer1Timer erstellt. Diese wird bei jedem Intervall des Timers, falls er aktiviert ist, ausgeführt. Wir fügen nun folgenden Code hinzu:
Label1.Top := Label1.Top + 1;
Label1.Caption := IntToStr(Label1.Top);

Das aktuelle Ergebnis scheint schon ganz lustig zu sein. Etwas störend ist noch, dass das Label nicht am unteren Rand der Form halt macht, sondern ewig weiter wandert. Dies ist schnell korrigiert, Timer1Timer sieht nun folgendermaßen aus:
  if Label1.Top + Label1.Height < Form1.Height then
  begin
    Label1.Top := Label1.Top + 1;
    Label1.Caption := IntToStr(Label1.Top);
  end;

Es wäre ja nun noch ganz lustig, wenn sich dieser Fall nach unten des Labels noch beschleunigt, so wie ein richtiger Fall im realen Leben auch sein würde. Hierfür muss das Label eine aktuelle Geschwindigkeit haben, die sich im Fall immer weiter vergrößert und von der die Positionsänderung abhängt. Für die Geschwindigkeit des Labels fügen wir also eine neue Variable ein, die wir innerhalb der TForm1-Deklaration platzieren (innerhalb die private-Sektion, denn für solche Sachen ist diese da):
Label1Vel: Integer;

Nun passen wir Timer1Timer erneut an. Wir wollen, dass wenn das Label unten angekommen ist, die Geschwindigkeit gerade negiert wird und auch etwas Energie verloren geht, so dass die Geschwindigkeit auch halbiert werden soll. Weil man mit einem Integer keine normale Division durchführen kann (denn dies geht nur mit Kommazahlen problemlos), müssen wir die Ganzzahldivision div benutzen. Dies resultiert im folgenden Code:
  if Label1.Top + Label1Vel + Label1.Height > Form1.Height then
    Label1Vel := -Label1Vel div 2
  else
  begin
    Label1.Top := Label1.Top + Label1Vel;
    Label1.Caption := IntToStr(Label1.Top);
    Label1Vel := Label1Vel + 1;
  end;

Wirklich lehrreich für dich wird das ganze, wenn du aber jetzt selbst mal ein wenig rumspielst und eigene Varianten testest. Eine kleine Erweiterung aber trotzdem vielleicht noch: Wähle das Label aus und setze die Eigenschaft DragMode auf dmAutomatic. Wähle nun im Objektinspektor die Form1 aus und statt den Eigenschaften klicke hier auf den Tab Ereignisse. Es werden die Ereignisse der Form1 aufgelistet. Man mache einen Doppelklick auf das Feld neben OnDragOver. Eine Funktion FormDragOver wird erstellt, in welche man folgenden Code hinzufügen mag:
Label1.Left := X; Label1.Top := Y;

Man bestaune erneut das Ergebnis...

Mal ein paar Gedanken zu unseren bisherigen Beispielen in Lazarus: Als erstes haben wir damit begonnen unser späteres Fenster zu entwerfen im Editor, indem wir die entsprechenden Steuerelemente auf der Form platziert haben. Dann haben wir Code geschrieben. Um konkreter zu sein: Wir haben den Code geschrieben, der bei bestimmten Ereignissen der Steuerelemente ausgeführt wird (wie z.B. das Klickereignis von einem Button, d.h. das Ereignis, wenn man auf den Button draufklickt). Diese Form der Programmierung nennt man die Ereignisgesteuerte Programmierung. Das heißt einfach nur, dass all der Code den man schreibt gewissen Ereignissen zugeordnet ist und dann bei den entsprechenden Ereignissen ausgeführt wird. Z.B. wird dieProzedur  Button1Click im vorherigen Beispiel genau dann ausgeführt, wenn man auf den Button klickt. Das heißt übrigens auch, dass in einem Ereignisgesteuerten Programm gar kein Code ausgeführt wird, so lange kein Ereignis eintritt. In diesem Zustand "schläft" das Programm sozusagen und wartet halt auf neue Ereignisse, bei denen es dann irgendetwas ausführen kann.

Es gibt eine ganze Reihe von Ereignissen, die alle bei den verschiedenen Steuerelementen eintreten können. Bei dem Button ist dies z.B. das Klickereignis, aber das Klickereignis kann natürlich für grundsätzlich alle anderen Steuerelemente auch eintreten. Bei einem Textfeld (für die Texteingabe) gibt es z.B. das KeyPress-Ereignis (wenn man eine Taste drückt) oder auch das Change-Ereignis (wenn der Text geändert wird). Bei gewissen Ereignissen bekommt man ausserdem auch noch ein paar Informationen als Parameter mit. Z.B. bekommt man bei dem KeyPress-Ereignis natürlich auch mit, welche Taste denn nun gedrückt wurde. Bei dem MouseMove-Ereignis bekommt man z.B. die genaue Position der Maus mit.

Sehen wir uns einmal in Lazarus an, welche Ereignisse wir so zu Verfügung haben. Nach dem Starten von Lazarus setzen wir also ein Textfeld (TEdit) auf unsere Form. Danach wählen wir im Objektinspektor die Ereignisliste (das Tab Ereignisse, direkt rechts neben Eigenschaften) aus. Wir sehen nun eine Liste aller für dieses Steuerelement (das Textfeld) verfügbaren Ereignisse. Machen wir ein Doppelklick auf ein Ereignis, erstellt Lazarus uns automatisch eine Funktion für das entsprechende Ereignis. Dies habe ich hier einmal für das Change-Ereignis gemacht. Lazarus hat dann automatisch die Funktion TForm1.Edit1Change erstellt. (Edit1 ist der Name von dem Textfeld gewesen, da ich das nicht weiter geändert hatte.)

Lazarus für OnChange

Zum Test setzen wir mal direkt unter das Textfeld noch ein Label (TLabel). Nun schreiben wir in die Funktion TForm1.Edit1Change folgenden Code:
Label1.Caption := Edit1.Text;

Das Ergebnis ist wie erwartet, dass bei jeder Textänderung in dem Textfeld der angezeigte Text im Label (die Caption) aktuallisiert wird.

Hier fehlen noch weitere Beispiele und eine Einführung in die am häufigsten verwendeten Steuerelemente, sowie eine Einführung in prinzipielle Programmiertechniken.

Nachdem du bereits nun hoffentlich selbst etwas rumgespielt hast mit Lazarus, was notwendig ist, um ein gewisses Verständnis davon zu bekommen, wollen wir auf Basis eines kleines Spiels zu weiteren Erfahrungen gelangen. Hier kannst du zwar hinterher nicht behaupten, alles selbst gemacht zu haben, allerdings wirst du schnell den gesamten Code verstehen und ihn selbst soweit du Lust hast erweitern können. Interessant und praktisch ist, dass das Erweitern des Codes sogar bereits möglich sein wird, bevor du überhaupt alles verstanden bzw. überhaupt angeguckt hast. Der Vorteil an dieser Art des praktischen Lernens ist, dass du viel schneller und effektiver nette Sachen machen kannst. Immer wenn ich eine neue Programmiersprache wirklich für die praktische Anwendung lernen will, nehme ich mir ein einfaches Open Source Programm in der entsprechenden Sprache vor, lese mich ein, versuche nur erstmal so grob den Aufbau zu verstehen und beginne dann damit an der ein und anderen Stelle mal dies und das abzuändern oder zu erweitern. Das Entscheidende ist dann immer, das Ergebnis nach der Änderung zu betrachten und so einmal das Verständnis zu erlangen und gleichzeitig praktische Erfahrungen zu sammeln. Beides, das wirst du bald merken, sind ungemein schöne Gefühle.

An dieser Stelle eignet sich wohl am besten das kleine Spiel Robot, geschrieben mit Lazarus. Dieses habe ich ursprünglich programmiert, um zu demonstrieren, was man bereits mit minimalen Programmiertechniken bereits erschaffen kann. Ich habe dafür ein neues Projekt in Lazarus erstellt und in dieser einen Form das gesamte Spiel untergebracht.

Lade dir ersteinmal am besten den Programmcode (am besten der Version 1.7, damit wir eine einheitliche Basis hier zum besprechen haben) herunter und öffne ihn dann mit Lazarus:
Robot-Projektseite mit Download

Screenshot von Robot nach dem Start:
Robot 1.7 beim Start

In Lazarus sieht das Projekt so aus:
Robot in Lazarus

Man sieht bereits, dass zur Entwicklungszeit (also in Lazarus) die Form noch sehr im Rohformat aussieht, also erst während der Laufzeit (nach dem Start) alles gezeichnet wird. Werfen wir nun mal einen Blick auf den Code dieser Form:

unit uMainForm;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, LResources, Forms, Controls, Graphics, Dialogs, ExtCtrls,
  Buttons, GraphType, Crt, StrUtils, StdCtrls, ComCtrls, Menus
{$IFDEF win32}
  ,MMSystem
{$ENDIF}
  ;
  
const
  PICTURE_SIZE = // picture cache size
{$IFDEF win32}
    65; // windows needs more
{$ELSE}
    30; // i think, it's a good value...
{$ENDIF}

  BACKGROUND_PIC = 'hinter.bmp'; // used for resetting
  PLAYER_PICS: array[1..3] of string
               = ('figur.bmp', 'robot*.bmp', 'konig.bmp');
  ERROR_PIC = 'error.bmp'; // used for error-displaying
  
  WORLD_WIDTH = 5; // room count
  WORLD_HEIGHT = 4;
  ROOM_WIDTH = 20; // place count in a room
  ROOM_HEIGHT = 20;
  KNAPSACK_WIDTH = 10; // place count in the knapsack
  KNAPSACK_HEIGHT = 5;
  KNAPSACK_MAX = 27; // compatibility with Robot1 (9*3)
  
  COMPUTERCONTROL_INTERVAL = 750; // timer-interval for computer player control

type
  TRoomNum = record // world coord
    X: 1..WORLD_WIDTH;
    Y: 1..WORLD_HEIGHT;
  end;

  TPlaceNum = record // room coord
    X: 1..ROOM_WIDTH;
    Y: 1..ROOM_HEIGHT;
  end;

  TPlaceAbsNum = 1..(ROOM_WIDTH*ROOM_HEIGHT); // abs room-index
  TRoomAbsNum = 1..(WORLD_WIDTH*WORLD_HEIGHT); // abs place-index
  TKnapsackAbsNum = 1..(KNAPSACK_WIDTH*KNAPSACK_HEIGHT); // abs knapsack-index
  
  TPlace = record
    PicIndex: Integer; // index of TPictureCache
  end;
  TRoom = array[TPlaceAbsNum] of TPlace; // a hole room
  TWorld = array[TRoomAbsNum] of TRoom; // a hole world

  TPlayer = record
    Pos: TPlaceNum;
    PicIndex: Integer; // index of TPictureCache
  end;
  TPlayerList = array of TPlayer; // dyn array of players in the room
  TWorldPlayers = array[TRoomAbsNum] of TPlayerList; // all players in the world

  TKnapsack = array[TKnapsackAbsNum] of TPlace; // a knapsack

  TPictureCacheItem = record
    FileName: string;
    Picture: TBitmap; // picture cache
    ResizedPicture: TBitmap; // resized picture cache
  end;
  TPictureCache = array of TPictureCacheItem;

  TMoveDirection = (mdLeft, mdRight, mdUp, mdDown);

  TFocus = (fcRoom, fcKnapsack);

  TDiamondSet = record
    DiamondNr: Integer
  end;

  { TMainForm }

  TMainForm = class(TForm)
    GamePanel: TPanel;
    KnapsackPanel: TPanel;
	{ ... viele weitere Objekte auf der Form ... }
    ComputerPlayer: TTimer;
    // event handlers
    procedure ComputerPlayerTimer(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure FormPaint(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure GamePanelClick(Sender: TObject);
    procedure GamePanelMouseDown(Sender: TOBject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure GamePanelMouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure KnapsackPanelClick(Sender: TObject);
    procedure KnapsackPanelMouseDown(Sender: TOBject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure mnuEditorLoadClick(Sender: TObject);
    procedure mnuEditorModeClick(Sender: TObject);
    procedure mnuEditorSaveClick(Sender: TObject);
    procedure mnuGameEndClick(Sender: TObject);
    procedure mnuGameLoadClick(Sender: TObject);
    procedure mnuGameNewClick(Sender: TObject);
    procedure mnuHelpAboutClick(Sender: TObject);
    procedure mnuHelpControlClick(Sender: TObject);
    procedure mnuHelpDescriptionClick(Sender: TObject);
    procedure mnuOptionsPauseClick(Sender: TObject);
    procedure mnuOptionsSoundClick(Sender: TObject);
  private
    { private declarations }
  public
    // gameplay
		// goto next room; return true, if succ
    function MoveToRoom(dir: TMoveDirection): boolean;
    function MoveToRoom(rnum: TRoomNum): boolean; // goto another room
    procedure MoveToPlace(dir: TMoveDirection); // move player
		// searchs the player; returns -1, if not found
    function GetMainPlayerIndex(): Integer;
    procedure KillRobots(); // kill all robots in act room
    procedure UseKnapsackSelection();
		// make 'intelligent' movements of all robots and the king
    procedure ControlComputerPlayers();
    
    // background stuff
    procedure InitGame();
    procedure RestartGame();
    procedure UnInitGame();
    procedure ResetRoomPic();
    procedure ResetKnapsackPic();
    procedure ResetWorld();
    procedure ResetKnapsack();
    procedure DrawRoom(); // updates MyRoomPic and GamePanel
    procedure DrawKnapsack(); // updates MyKnapsackPic and KnapsackPanel
    procedure DrawInfo(); // updates InfoPanel
    procedure ShowMsg(msg: string); // printed on MessageBar
    procedure ShowMsg(msgs: array of string); // like ShowMsg; select
											  // randomly a msg
    procedure LoadWorld(fname: string); // loads a hole world (sce-file)
    procedure SaveWorld(fname: string); // saves the hole world
    procedure LoadGame(fname: string); // loads a saved game (included world)
    procedure SaveGame(fname: string); // saves a game
    function ShowLoadGameDialog(): boolean; // returns true, if succ
    function ShowSaveGameDialog(): boolean; // returns true, if succ
    function GetPicture(fname: string): TBitmap; // load picture from cache/disk
    function GetPicture(index: Integer): TBitmap;
    function GetPictureName(index: Integer): string; // returns filename
    function GetPictureCacheIndex(fname: string): Integer;
    procedure ResetPictureResizedCache();
    procedure PlaySound(fname: string); // plays wave-file
		// get viewed place (with players)
    function GetPlace(room: TRoomAbsNum; pos: TPlaceNum): TPlace;
		// get viewed place (with players)
    function GetPlace(pos: TPlaceNum): TPlace;
		// returns picture filename
    function GetPlacePicName(pos: TPlaceNum): string;
    procedure SetPlace(pos: TPlaceNum; p: TPlace); // set room place
		// sets picture filename
    procedure SetPlacePicName(pos: TPlaceNum; pname: string);
    procedure ResetPlace(pos: TPlaceNum);
    function AddPlayer(room: TRoomAbsNum; pos: TPlaceNum;
		picindex: Integer): Integer; // returns index
    function AddPlayer(room: TRoomAbsNum; pos: TPlaceNum;
		picname: string): Integer; // returns index
    procedure RemovePlayer(room: TRoomAbsNum; index: Integer);
    procedure RemovePlayer(room: TRoomAbsNum; pos: TPlaceNum);
    function MovePlayer(oldroom: TRoomAbsNum; oldindex: Integer;
		newroom: TRoomAbsNum; newpos: TPlaceNum): Integer; // returns new index
    function IsPlayerInRoom(picname: string): boolean;
    procedure ResetPlayerList();
    function IsPosInsideRoom(x,y: Integer): boolean;
    function AddToKnapsack(picindex: Integer): boolean; // returns true, if succ
    function AddToKnapsack(picname: string): boolean; // returns true, if succ
    function IsInKnapsack(picname: string): boolean;
    procedure ChangeKnapsackSelection(dir: TMoveDirection);
    procedure AddScores(num: Integer);
    procedure AddLife();
    function RemoveLife(): boolean; // returns true, if still alive
    procedure SetFocus(f: TFocus);
    procedure ChangeFocus();
    procedure SetPauseState(s: boolean);
    Procedure CopyRect(DstCanvas: TCanvas; const Dest: TRect;
		SrcCanvas: TCanvas; const Source: TRect);
                       
    MyWorld: TWorld; // the world
    MyWorldPlayers: TWorldPlayers; // all players
    MyRoomNum: TRoomNum; // selected room of my world
    MyRoomPic: record // user view
                 Room: TRoom; // room actually viewed
                 Picture: TBitmap; // paint cache
               end;
    MyKnapsack: TKnapsack; // the knapsack
    MyEditorKnapsack: TKnapsack; // the knapsack used in editor mode
    MyKnapsackPic: record // user view
                     Knapsack: TKnapsack; // knapsack actually viewed
                     Selection: TKnapsackAbsNum; // selection act viewed
                     Picture: TBitmap; // paint cache
                   end;
    MyKnapsackSelection: TKnapsackAbsNum; // selected item in the knapsack
    MyFocus: TFocus;
    MyPictureCache: TPictureCache; // cache of all graphics in the game
    MyLife: Integer; // lifes
    MyScores: Integer; // scores
    MyDiamonds: array of TDiamondSet; // set diamoonds
    MyPauseState: boolean; // true -> pause
    MySoundState: boolean; // false -> mute
    MyEditorMode: boolean; // true -> editmodus on
  end;

  function RoomNum(X,Y: Integer): TRoomNum;
  function PlaceNum(X,Y: Integer): TPlaceNum;
  function Place(picindex: Integer): TPlace;
  function Player(picindex: Integer; pos: TPlaceNum): TPlayer;
  function GetAbs(rnum: TRoomNum): TRoomAbsNum; // coord -> abs index
  function GetAbs(pnum: TPlaceNum): TPlaceAbsNum; // coord -> abs index
  function GetNumR(absnum: TRoomAbsNum): TRoomNum; // abs index -> coord
  function GetNumP(absnum: TPlaceAbsNum): TPlaceNum; // abs index -> coord

var
  MainForm: TMainForm;

implementation

function RoomNum(X,Y: Integer): TRoomNum;
begin
  RoomNum.X := X;
  RoomNum.Y := Y;
end;

function PlaceNum(X,Y: Integer): TPlaceNum;
begin
  PlaceNum.X := X;
  PlaceNum.Y := Y;
end;

{ ... und die ganze restliche, eigentlich wichtige und entscheidende Implementierung ... }

initialization
  {$I umainform.lrs}

end.

Was ist nun wirklich neu zu unserem bisherigen Wissen? Gehen wir diese Code-Übersicht einmal Schrittweise durch. 

Im uses-Teil befinden sich noch ein paar mehr Units als sonst. Diese wurden automatisch dort im Code hinzugefügt, als ich entsprechende Steuerelemente auf der Form platziert hatte. Dann ist da noch dieses {$IFDEF win32} ... {$ENDIF}. Dies ist, wie wir bereits gesagt hatten, eine Compileranweisung. IFDEF ist dabei die Abfrage, ob ein spezieller Wert des Compilers definiert ist oder nicht. win32 ist dabei genau dann definiert, wenn der Code für ein normales aktuelles Windows kompiliert wird. In dem Fall wird also noch die Unit MMSystem hinzugefügt. Diese stellt unter Windows spezielle Funktionen zum Soundabspielen bereit (mehr dazu später an entsprechender Stelle).

Im const-Teil werden nun für den eigentlichen Code einige Konstanten definiert. Die konkreten Konstanten zu erklären ist vermutlich aber erst sinnvoller, wenn sie im entsprechenden Kontext (im späteren implementation-Teil) auch wirklich verwendet werden, wobei dann die Bedeutung sofort klar wird.

Nun im type-Teil ist die erste wirkliche Neuerung und im Prinzip auch die einzige wirkliche Neuerung im gesamten Code (ausgenommen sind jetzt mal konkrete Funktionen oder ähnliches, denn das ist eigentlich nichts wirklich Neues, sondern etwas, was man halt, wenn man es braucht, in einem Buch oder im Internet sich raussucht). Es wurden in diesem Teil für das Spiel einige neue Typen definiert, so z.B. TRoomNum, TPlaceAbsNum, TRoom und weitere.

Was ist ein record?

unit uRecordBeispiel;

interface
type

TMeinNeuerTyp = record
	Caption: string;
	Top, Left: integer;
	Width, Height: integer;
	Value: real;
	MeineForm: TForm1;
	{ ... }
end;

{ ... }

implementation

procedure TuWasMitNeuemTyp();
var
	stueck: TMeinNeuerTyp;
	wert: real;
begin
	wert := 42;

	stueck.Caption := 'Wilfried';
	stueck.Top := 0;
	stueck.Value := wert;

	// wir gehen davon aus, dass Form1 eine Variable vom Typ TForm1 ist
	stueck.MeineForm := Form1;

	stueck.MeineForm.Caption := 'Form von Wilfried';
	stueck.MeineForm.Top := stueck.Top;

	// wir gehen davon aus, dass TForm1 ein Label1 hat
	stueck.MeineForm.Label1.Caption := 'Hallo Welt';
end;

{ ... }

end.

Ein record ist im Prinzip nichts anderes als eine Zusammenfassung mehrerer einzelner anderer Variablen eines vorhandenen Variablentyps.

Die nächste Neuerung ist ein Variablentyp in der Form a..b. Dies ist nichts weiter als eine eingeschränkte Integer-Typ, der nur Zahlen von a bis b zulässt (beide eingeschlossen). Manchmal ist es sinnvoll den Zahlenbereich im Vorhinein einzuschränken um spätere Fehler frühzeitig zu entdecken. Auch ist es sinnvoll, um dem Leser des Codes einen Hinweis zu geben, was in dieser Variable gespeichert werden soll. Man behalte sich immer im Hinterkopf, dass man den Code nicht nur programmiert, damit er funktioniert, sondern auch, dass er später auch gut gelesen werden kann. Denn vielleicht wollen wir später unseren längst vergessenen alten Code noch einmal ansehen, wiederverwenden und vielleicht erweitern. Dies ist aber nur möglich, wenn wir dann auch verstehen, wieso unser alter Code eigentlich so ist wie er ist.

Ansonsten ist da noch der bisher unbekannte Typ TBitmap. Hierbei handelt es sich wieder um einen Typ der bereits von Lazarus bereitgestellt wird, ähnlich wie TForm. TBitmap enthält alle Daten für ein komplettes Bitmap (also ein Bild) im Speicher. Wir werden in Variablen von diesem Typ letztendlich die Bilder von den Spielobjekten speichern.

Als letzte Typdefinition haben wir die Definition von TMainForm als class(TForm). Diese Definition in der Form wurde komplett von Lazarus selbst vorgenommen, d.h. im Prinzip müssen wir uns hierum nicht weiter kümmern. (Zum Vergleich: das leere neue Lazarus-Projekt.) Wie man aus dem Code erkennen kann, scheint diese Typdefinition wohl ähnlich wie ein record zu sein, wobei es zusätzlich noch Funktionen und Methoden besitzt. Dies nennt man eine Klasse. Im Endeffekt ist es der Typ unseres späteren Fensters, welches selbst auch einfach eine Variable ist (man nennt Variablen vom Typ einer Klasse auch Klasseninstanzen oder auch Objekte). An dieser Stelle ist es unwichtig die genaueren Details zu verstehen (das wird später nachgeholt). Es reicht aus sich TMainForm als ein besonderes record vorzustellen. Ausserdem basiert es auf dem Typ TForm (das wurde in der Klammer angegeben). Das heißt, dass TMainForm so wie TForm ist, nur durch ein paar Sachen erweitert wurde. Auch das ist an dieser Stelle aber nicht weiter von Bedeutung für das eigentliche Verständnis des Spiels. Fast der gesamte Code von der Typdefinition von TMainForm wurde automatisch von Lazarus erstellt, während ich die einzelnen Elemente wie Labels, Picture-Boxen (Boxen für Bildern), Panels (einfach nur Boxen für irgendwas) und Timer auf das Fenster gesetzt habe. Die ganzen Prozeduren wie z.B. GamePanelClick wurden auch alle von Lazarus automatisch angelegt, als ich die entsprechenden Ereignisse bearbeitete.

Der Code in der Typdefinition von TMainForm ab "gameplay" ist der, den ich selbst hinzugefügt habe. Es sind weitere eigene Prozeduren für das eigentliche Spiel. Nach den Prozeduren kommen noch ein paar Variablen für das eigentliche Spiel. Z.B. enthält die Variable MyWorld die komplette Spielwelt.

Ganz am Ende bei den Typdefinitionen, also wieder ausserhalb von TMainForm, sind noch ein paar weitere Funktionen. Diese habe ich angelegt, um leichter bestimmte Variablen umzuwandeln und umzurechnen.

Im var-Teil befinden sich globale Variablen für das gesamte Programm. Hier ist nur die Variable für unser Fenster (vom Typ TMainForm). Alle weiteren Daten hatten wir ja innerhalb von TMainForm gespeichert, d.h. sie sind in der Variable MainForm gespeichert.

Im implementation-Teil befindet sich dann die genauere Deklaration aller vorher definierten Funktionen und Prozeduren.

Der initialisation-Teil ist auch für uns ersteinmal uninteressant (er wurde von Lazarus automatisch angelegt).

Das Spiel Robot

Ein Screenshot haben wir bereits gesehen. Den Programmcode haben wir nun auch auf unserer Festplatte entpackt. Nach der bereits groben Einführung in den Code, wo wir ein paar gröbere Unklarheiten (vor allem wegen neuen Variablentypen) geklärt haben, wollen wir nun auf ein paar Stellen mal einen genaueren Blick werfen.

Sehen wir uns mal die Prozedur TMainForm.FormKeyDown im implementation-Teil an. Dies ist die Prozedur der Hauptform, die bei dem KeyDown-Ereignis der Hauptform ausgeführt wird, d.h. genau dann, wenn der Spieler eine Taste herunterdrückt.

procedure TMainForm.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if Key = Ord('P') then
    SetPauseState(not MyPauseState)
  else
    SetPauseState(false);
    
  if not (ssCtrl in Shift) then // TODO: change to: nothing in Shift
  case Key of
  37: // left
  begin
    if MyFocus = fcRoom then MoveToPlace(mdLeft);
    if MyFocus = fcKnapsack then ChangeKnapsackSelection(mdLeft);
  end;
  39: // right
  begin
    if MyFocus = fcRoom then MoveToPlace(mdRight);
    if MyFocus = fcKnapsack then ChangeKnapsackSelection(mdRight);
  end;
  38: // up
  begin
    if MyFocus = fcRoom then MoveToPlace(mdUp);
    if MyFocus = fcKnapsack then ChangeKnapsackSelection(mdUp);
  end;
  40: // down
  begin
    if MyFocus = fcRoom then MoveToPlace(mdDown);
    if MyFocus = fcKnapsack then ChangeKnapsackSelection(mdDown);
  end;
  Ord(' '), 9: // space, tab
  begin
    ChangeFocus();
    DrawRoom();
    DrawKnapsack();
  end;
  13: // enter
  begin
    UseKnapsackSelection();
    SetFocus(fcRoom);
  end;
//  8, 46: // backspace, del
//  begin
//    MyKnapsack[MyKnapsackSelection].PicIndex := GetPictureCacheIndex(BACKGROUND_PIC);
//    DrawKnapsack();
//  end;
  else
//    WriteLn('pressed key: ' + IntToStr(Key));
  end;

  // only allow the following in editor mode
  if MyEditorMode then
  begin
    if ssCtrl in Shift then
    case Key of
    37: // left
      MoveToRoom(mdLeft);
    39: // right
      MoveToRoom(mdRight);
    38: // up
      MoveToRoom(mdUp);
    40: // down
      MoveToRoom(mdDown);
    end;
  end;
end;

Vorzeitiges Ende ...



Author: Albert Zeyer
Mail: ich AT admin DOT de
andere Projekte

Document made with Nvu