Zum Inhalt springen

Programmierkurs: Delphi: Druckversion

Aus Wikibooks
Druckversion des Buches Programmierkurs: Delphi
  • Dieses Buch umfasst derzeit etwa 114 DIN-A4-Seiten einschließlich Bilder (Stand: 02.01.09 20:53).
  • Wenn Sie dieses Buch drucken oder die Druckvorschau Ihres Browsers verwenden, ist diese Notiz nicht sichtbar.
  • Zum Drucken klicken Sie in der linken Menüleiste im Abschnitt „Drucken/exportieren“ auf Als PDF herunterladen.
  • Mehr Informationen über Druckversionen siehe Hilfe:Fertigstellen/ PDF-Versionen.
  • Hinweise:
    • Für einen reinen Text-Ausdruck kann man die Bilder-Darstellung im Browser deaktivieren:
      • Internet-Explorer: Extras > Internetoptionen > Erweitert > Bilder anzeigen (Häkchen entfernen und mit OK bestätigen)
      • Mozilla Firefox: Extras > Einstellungen > Inhalt > Grafiken laden (Häkchen entfernen und mit OK bestätigen)
      • Opera: Ansicht > Bilder > Keine Bilder
    • Texte, die in Klappboxen stehen, werden nicht immer ausgedruckt (abhängig von der Definition). Auf jeden Fall müssen sie ausgeklappt sein, wenn sie gedruckt werden sollen.
    • Die Funktion „Als PDF herunterladen“ kann zu Darstellungsfehlern führen.


Inhaltsverzeichnis

1 Vorwort
2 Pascal

2.1 Grundlagen
2.1.1 Aufbau eines Delphi-Programms
2.1.2 Das erste Programm
2.1.3 Datentypen
2.1.3.1 Grundlegende Datentypen
2.1.3.2 Erweiterte Datentypen
2.1.4 Variablen und Konstanten
2.1.5 Operatoren
2.1.6 Eingabe und Ausgabe
2.1.7 Verzweigungen
2.1.8 Schachtelungen
2.1.9 Schleifen
2.1.10 Prozeduren und Funktionen
2.2 Fortgeschritten
2.2.1 Typdefinition
2.2.2 Typumwandlung
2.2.3 Prozedurale Typen
2.2.4 Rekursion
2.2.5 Threads
2.3 Objektorientierung
2.3.1 Klassen
2.3.2 Konstruktoren und Destruktoren
2.3.3 Eigenschaften
2.3.4 Vererbung
2.3.5 Zugriff auf Klassen
2.3.6 Interfaces
2.3.7 Exceptions

3 Schnelleinstieg

3.1 Einstieg
3.2 Datentypen
3.3 Pointer
3.4 Dynamische Datenstrukturen
3.5 DLL-Programmierung
3.6 Assembler und Delphi

4 RAD-Umgebung

4.1 Warum eine RAD-Umgebung
4.2 Der Debugger
4.3 Grundsätze
4.3.2 Weitergabe
4.3.3 Mehrere Formulare erstellen
4.3.4 Komponenten dynamisch erstellen
4.4 Fortgeschrittene Themen
4.4.1 Grafiken
4.4.2 Menüs
4.4.3 Toolbars
4.4.4 Statuszeilen
4.4.5 Vorlagen
4.4.6 Dialoge
4.4.7 Formatierter Text
4.5 Weitere Themen
4.5.1 Drucken
4.5.2 Tabellen
4.5.3 Aufteilung des Formulars
4.5.4 Trackbar und Scrollbar
4.5.5 GroupBox, RadioGroup und Checkbox
4.5.6 Fortschritt anzeigen
4.5.7 Anwendungsoptionen
4.5.8 WinXP-Themes
4.5.9 Arbeit mit Tasten
4.5.10 Fremdkomponenten installieren
4.6 Komponentenentwicklung

5 Multimediafunktionen
6 Anhänge

6.1 Der (richtige) Programmierstil
6.2 Befehlsregister
6.3 Glossar
6.4 Autoren
6.5 Lizenz

Vorwort

[Bearbeiten]

Delphi, was ist das?

[Bearbeiten]

Gut, so wird wohl keiner fragen, der sich bis hierher durchgeklickt hat, aber trotzdem sollen hier einige Begrifflichkeiten geklärt werden:

Delphi, oder noch korrekter "Object Pascal" bzw. "Delphi Language" (seit Erscheinen von Delphi 7), ist eine Programmiersprache, die auf der in den 70ern entwickelten Sprache "Pascal" basiert. Pascal wurde dabei, wie der Name "Object Pascal" schon vermuten lässt, um die objektorientierte Programmierung (OOP) erweitert.

Compiler

[Bearbeiten]

Für Object Pascal gibt es mehrere Compiler mit integrierter Entwicklungsumgebung. Die meisten Programmierer benutzen die Compiler von Embarcadero "Delphi". Es gibt diesen für Windows. Ein weiterer Compiler stammt vom Open Source-Project Free Pascal, die Entwicklungsumgebung dazu ist Lazarus. Diesen gibt es für Windows, MacOS X, Android, iOS und auch für Linux.

Free Pascal

[Bearbeiten]

Falls Sie den Free Pascal Compiler einsetzen, beachten Sie folgendes:

  • Schreiben Sie in den Quelltext Ihrer Programme {$MODE DELPHI}, um den Compiler in den Delphi-Modus zu schalten. Dieser Modus wird u. A. für Casts, Klassen und die Pseudovariable Result benötigt. Alternativ können Sie den Compiler auch mit fpc -Mdelphi aufrufen.

Aufbau des Buches

[Bearbeiten]

Dieses Buch soll sowohl Anfängern den Einstieg in die Programmierung mit Pascal, als auch Umsteigern von anderen Sprachen (z.B.: C) einen schnellen Einstieg in Pascal ermöglichen. Um dies zu gewährleisten, teilt sich das Buch in 3 Bereiche auf:

Die Grundlagen

[Bearbeiten]

Dieser Abschnitt richtet sich an totale Programmieranfänger und führt schrittweise und anhand von Beispielen in die Pascal-Programmierung ein. Neben der Einführung in die Sprache Pascal werden grundlegende Prinzipien des allgemeinen Programmierens anhand von praktischen Beispielen vermittelt.

Der Schnelleinstieg

[Bearbeiten]

Wer schon eine andere Programmiersprache beherrscht und/oder seine Pascal-Kenntnisse ein bisschen auffrischen will, sollte mit diesem Abschnitt beginnen. Er erläutert alle Features der Sprache und ihrer Syntax, setzt dabei aber grundlegendes Verständnis voraus.

Die RAD Umgebung

[Bearbeiten]

In diesem Abschnitt geht es um die Programmierumgebungen Delphi und Lazarus. Es soll ein Einstieg in die Entwicklung graphischer Oberflächen und die damit verbundenen Programmiertechniken gegeben werden, die allerdings das Verständnis von Pascal voraussetzen. Weiterhin werden der Aufbau und die dahinter liegenden Prinzipien der graphischen Klassenbibliotheken (VCL,LCL) erläutert.


Warum Pascal?

[Bearbeiten]

Pascal wurde speziell zu Lernzwecken entwickelt. Insofern kann Pascal für Programmieranfänger empfohlen werden. Eine der hervorstehensten Eigenschaften von Pascal ist die gute Lesbarkeit des Quellcodes, verglichen mit Programmiersprachen wie z.B. C oder C++. Durch die mit Pascal in moderne Sprachen eingeführte strukturierte Programmierung und durch starke Typisierung (stärker als C++) wird direkt von Beginn an fundamental auf stabilere weniger fehlergeneigte Code-Konstruktion abgezielt.

Von der Geschwindigkeit der ausführbaren Programme her zieht Pascal fast mit C++ gleich. Obwohl bereits 1970 der erste Pascal-Compiler verfügbar war, gelang Pascal erst mit Borlands Turbo Pascal Mitte der 80er Jahre der Durchbruch. Seit Turbo Pascal 5.5 gibt es auch Möglichkeiten für die objektorientierte Programmierung. Der bekannteste aktuelle, kostenlose Pascalcompiler FreePascal ist in vielen Punkten nicht nur zu Turbo Pascal 7, sondern sogar zu Delphi kompatibel und wird ständig weiterentwickelt.

FreePascal ist auf allen aktuellen Umgebungen kompilierfähig und kann leicht auf neue Umgebungen angepasst werden. Eine Delphi-ähnliche, selber in FreePascal geschriebene IDE für FreePascal liegt z.B. mit der ständig weiterentwickelten (Stand 2013) IDE Lazarus vor.

FreePascal wird auch von Universitäten erweitert, so z.B. 2012 durch ein Standard-Template-Library [1].

Pascal ist also keine tote Sprache, obwohl dies oft behauptet wird.

[1] http://www.mii.lt/olympiads_in_informatics/pdf/INFOL101.pdf


Warum Delphi?

[Bearbeiten]

Es gibt viele Gründe, Delphi zu benutzen. Es gibt aber wahrscheinlich auch genauso viele dagegen. Es ist also mehr Geschmackssache, ob man Delphi lernen will oder nicht. Wenn man allerdings Gründe für Delphi sucht, so fällt sicher zuerst auf, dass Delphi einfach zu erlernen ist, vielleicht nicht einfacher als Basic aber doch viel einfacher als C/C++. Für professionelle Programmierer ist es sicher auch wichtig zu wissen, dass die Entwicklung von eigenen Komponenten unter Delphi einfach zu handhaben ist. Diese Komponenten sind dann z.B. auch im C++- Builder verwendbar, somit in der Verwendbarkeit nicht auf Delphi beschränkt. Durch die große Delphi-Community mangelt es auch nicht an Funktionen und Komponenten.

Ein besonderer Vorteil von Delphi ist hohe Typsicherheit. Viele Fehler werden also schon beim Kompilieren bemerkt und müssen nicht durch langwieriges Debuggen entdeckt werden.

Erstellt man größere Projekte mit Borlands Delphi Compiler, so ist die Geschwindigkeit beim Kompilieren sicher ein entscheidender Vorteil. Auch die einfache Modularisierung durch Units, Functions und Procedures ist sicherlich ein Vorteil der Sprache gegenüber einfachen Sprachen wie Basic. Auch die Trennung der Units selbst in einen Interface/Implementation-Teil trägt wesentlich zur Übersichtlichkeit und Wartbarkeit des Codes bei, wobei gerade dieses grundlegende Feature ein Kern-Unterschied zu den C-esken Sprachen ( C++, Java ) ist.

Mit Delphi lässt sich zudem so ziemlich alles entwickeln, auch sehr hardwarenahe Programme. Es ist sogar die Möglichkeit gegeben, mittels eines Inline-Assemblers z.B. zu Geschwindigkeits-Optimierungszwecken Assembler-Code direkt in Methoden / Prozeduren einzubinden. Dies ermöglicht zwar extreme Flexibilität, sollte jedoch wegen der dem Assembler inhärenten schwierigen Lesbarkeit nur in wohldurchdachten Fällen genutzt werden.

Durch die ab den DelphiXE-Versionen mitgelieferte visuelle Komponentenbibliothek FireMonkey ist auch, wenn gewünscht, plattformübergreifendes einheitliches Oberflächendesign möglich. Die Möglichkeit der Entwicklung mit den nativen Betriebssystem-Controls bleibt dazu parallel erhalten.

Eine große Anzahl verfügbarer Komponentensammlungen, insbesondere für Datenbankanbindungen, ermöglichen den bequemen Zugriff auf alle modernen verfügbaren Datenbanken. Eine solche Komponentensammlung für die wichtigsten Datenbanksysteme wird in den größeren Versionen von Delphi direkt mitgeliefert.

Zuguterletzt liefert der Delphi-Compiler echtes Compilat (Binärcode) im Gegensatz zum Bytecode von .Net-Sprachen bzw. Java. Binärcode hat den Vorteil der wesentlich schwereren Decompilierbarkeit, so dass hier Know-How besser geschützt ist. Das oft angeführte Argument des für Bytecode-Sprachen anwendbaren Obfuskators greift hier nur oberflächlich, da nur Namen substituiert werden und der Zugriff auf die CLR nicht verschleiert werden kann, was relativ leichte Rückanalyse zuläßt.


Pascal

[Bearbeiten]

Grundlagen

[Bearbeiten]

Aufbau eines Delphi-Programms

[Bearbeiten]

Delphi ist, anders als andere Programmiersprachen, strenger in Abschnitte eingeteilt. So dürfen zum Beispiel Variablen nicht an fast beliebigen Stellen deklariert werden, sondern nur an dafür vorgesehenen.

Aufbau des Hauptprogramms

[Bearbeiten]

Ein einfaches Konsolenprogramm (ein Textprogramm, das auf der Eingabeaufforderung läuft), besteht aus einem Programmkopf und einem Programmrumpf. Der Kopf besteht dabei aus dem Schlüsselwort program gefolgt von dem Programmnamen und einem Semikolon. Im Rumpf werden alle Variablen, Konstanten, Typen und Funktionen deklariert, danach folgt das Hauptprogramm zwischen den Schlüsselwörtern begin und end. Nach dem abschließenden end. dürfen keinerlei Anweisungen mehr folgen, „das Programm ist beendet, Punktum.“


Die Deklaration von Variablen wird durch das Schlüsselwort var eingeleitet, wovon mehrere Abschnitte möglich sind. Nach diesem Schlüsselwort folgt der Name der Variablen (bei mehreren durch Kommata getrennt), gefolgt von einem Doppelpunkt, dem Datentyp und einem Semikolon:

var
  i: Integer;
  r, d: Double;


Konstanten und Typen werden ähnlich deklariert:

const
  NAME = 'Dieter Meier';
  VERSION = 1.0;

type
  TDateiname = string;


Prozeduren und Funktionen bestehen ebenfalls aus einem Prozedurkopf und einem Prozedurrumpf. Beim Kopf wird hierbei unterschieden, ob es sich um eine Prozedur (procedure, gibt keinen Wert zurück) oder um eine Funktion (function, besitzt einen Rückgabewert) handelt. In abstrakter Form sieht dies so aus:

procedure Name;
procedure Name(Parameter: Typ);
procedure Name(Parameter1: Typ; Parameter2: Typ);
function Name: Typ;
function Name(Parameter: Typ): Typ;

Funktionen enden immer auf : Typ;, wobei hiermit der Datentyp des Rückgabewerts angegeben wird.

Direkt nach dem Prozedurkopf können lokale, also nur innerhalb der Funktion verwendbare Datentypen, Variablen und Konstanten deklariert werden. Dann folgt, eingeschlossen in einen begin...end;-Block der Prozedurrumpf mit den entsprechenden Anweisungen, die in der Prozedur/Funktion ausgeführt werden sollen. Beispiel:


Mehr zur Verwendung von Prozeduren und Funktionen erfahren Sie später.

Aufbau von Units

[Bearbeiten]

Neben dem Hauptprogramm können weitere Programmteile in so genannte Units ausgelagert werden. Units sind ähnlich wie das Hauptprogramm aufgebaut, haben jedoch einen signifikanten Unterschied: Es gibt einen öffentlichen und einen nicht-öffentlichen Programmteil.

Eine Unit wird mithilfe des Schlüsselworts unit gefolgt vom Unitnamen und einem Semikolon eingeleitet. Direkt darauf wird der öffentliche Abschnitt mit dem Wort interface eingeleitet. Hierin erfolgen alle Deklarationen von Programmteilen, auf die wiederum von anderen Programmteilen (z.B. Hauptprogramm oder anderen Units) aus zugegriffen werden soll. Prozeduren werden in diesem Abschnitt jedoch nicht vollständig deklariert, sondern nur der Prozedurkopf. Die Programmierung des Prozedurrumpfs erfolgt im nicht-öffentlichen Teil der Unit.

Der nicht-öffentliche Teil beginnt direkt nach den erfolgten Deklarationen durch das Schlüsselwort implementation. Hier müssen alle im Interface-Abschnitt angegebenen Prozeduren und Funktionen ausgearbeitet werden. Es muss also der Prozedurkopf wiederholt und anschließend der Prozedurrumpf programmiert werden. Im Implementation-Abschnitt können wieder, wie in den anderen Abschnitten, Typen, Variablen und Konstanten deklariert werden, auf die dann nur in diesem Abschnitt zugegriffen werden kann. Ähnlich wie im Hauptprogramm können auch neue Prozeduren erstellt werden, die dann nur von den nachfolgenden Prozeduren dieses Implementation-Abschnitts aufgerufen werden können. Der Implementation-Abschnitt wird mit einem end. abgeschlossen und beendet damit auch die gesamte Unit. Hier einmal eine Beispiel-Unit; über den verwendeten Datentyp erfahren Sie später im Kapitel Records mehr:

unit Beispiel;

interface

{ öffentlicher Teil }

type
  TPerson = record    // ein öffentlicher Datentyp
    Vorname: string;
    Nachname: string;
  end;

procedure SetzeName(Vorname, Nachname: string);
procedure SchreibeName;

implementation

{ nicht-öffentlicher Teil }

var
  Name: TPerson;    // eine nur in diesem Abschnitt verwendbare Variable

procedure SetzeName(Vorname, Nachname: string);
begin
  Name.Vorname := Vorname;    // den Vornamen in der nicht-öffentlichen Variable speichern
  Name.Nachname := Nachname;  // den Nachnamen speichern
end;

procedure SchreibeName;
begin
  WriteLn(Name.Vorname + ' ' + Name.Nachname);  // die gespeicherten Werte auslesen und auf den Bildschirm schreiben
end;

end.


Um eine Unit verwenden zu können, muss diese im entsprechenden Programmteil eingebunden werden. Dies geschieht durch die Uses-Klausel, direkt nach program, interface oder implementation. Die darin öffentlich gemachten Programmteile können anschließend verwendet werden, als wären Sie im Hauptprogramm oder in der anderen Unit selbst deklariert worden:

program Testprogramm;
uses Beispiel;

{ eventuelle Deklarationen }

begin
  SetzeName('Dieter', 'Meier');
  SchreibeName;
end.


Reihenfolge der Deklarationen

[Bearbeiten]

Bei der Reihenfolge ist darauf zu achten, dass die Variablen, Konstanten, Datentypen und Prozeduren immer nur „in Leserichtung“ bekannt sind. Dies bedeutet, alles was weiter oben im Programmtext steht, ist weiter unten auch bekannt. Anders herum gilt, dass z.B. weiter unten deklarierte Programminhalte nicht in den Prozeduren darüber verwendet werden können. Ganz oben, direkt nach program ... angegebene Variablen sind in allen darauffolgenden Prozeduren, sowie im Hauptprogramm „sichtbar“.

Sie brauchen sich hierüber jedoch keine großen Sorgen zu machen. Wenn mal etwas an der falschen Stelle steht, wird das Programm gar nicht erst kompiliert. Delphi bricht dann bereits mit einer entsprechenden Fehlermeldung ab.

Direkt im Programmrumpf deklarierte Variablen heißen globale Variablen. Da auf diese von allen nachfolgenden Prozeduren und vom Hauptprogramm gleichermaßen zugegriffen werden kann, werden diese (leider oftmals nicht nur von Programmieranfängern) gern zum Datenaustausch eingesetzt. Zum Beispiel wird im Hauptprogramm ein Wert abgelegt, den ein Unterprogramm auswertet und gegebenenfalls überschreibt. Dies sollte strengstens vermieden werden! Das Programm lässt sich dadurch zwar eventuell schneller schreiben, jedoch erschwert diese Art des Datenaustauschs die Fehlersuche. Man weiß dann oftmals nicht, welchen Wert einer Variable man in welchem Programmabschnitt erwarten kann. Daher sollte man globale Variablen nach Möglichkeit nur zum Beispiel zur Konfiguration des Programms verwenden und nur in einer einzigen Prozedur Änderungen an den Werten vornehmen.


Das erste Programm

[Bearbeiten]

Die Konsole

[Bearbeiten]

Obwohl die schnelle Entwicklung einer grafischen Oberfläche (engl. Graphical User Interface, Abk. GUI) eine der größten Stärken von Delphi ist, eignet sie sich nur bedingt zum Einstieg in die Programmierung, da die GUI-Entwicklung ein eher komplexes und vielseitiges Thema ist. Außerdem setzt sie ein grobes Verständnis allgemeiner Programmiertechniken (wie z.B. objektorientierte Programmierung) voraus. In den folgenden Kapiteln wird als Einstieg die Konsolenprogrammierung genutzt, um Sie auf einfache Art und Weise in die verschiedenen Sprachelemente des Software Engineerings einzuführen. Unter Windows auch als „DOS-Fenster“ bekannt, stellt eine Konsole grundlegende Methoden zur Ein- und Ausgabe von Daten dar, die heutzutage zwar oft als veraltet gilt, aber dennoch nicht wegzudenken ist.

Wundern Sie sich bitte nicht, wenn in den folgenden Beispielen die deutschen Umlaute als ae, oe usw. ausgeschrieben sind. Das Konsolenfenster verwendet einen anderen Zeichensatz als Windows und kann diese Buchstaben deshalb nicht ohne Konvertierung darstellen, was leider nicht automatisch erfolgt.

Die Programmvorlage

[Bearbeiten]

Startet man Delphi, so öffnet es direkt ein leeres Projekt zur Erstellung eines grafischen Programms. Da wir zunächst aber ein Konsolenprogramm erstellen wollen, müssen wir dieses Projekt schließen und ein anderes erstellen.

Delphi:

Datei / Neu / Weitere... / Konsolen-Anwendung

program Project1;

{$APPTYPE CONSOLE}

uses
  SysUtils;

begin
  { TODO -oUser -cConsole Main : Hier Code einfügen }
end.


Dies ist nun die Vorlage für unser erstes Programm. Zeile 1: Jedes Programm beginnt mit dem Schlüsselwort program gefolgt von dem Programmnamen (der übrigens identisch mit dem Dateinamen sein muss) gefolgt von einem Semikolon.

Die Zeilen 3–6 sollen uns zunächst nicht interessieren, sie sorgen für die nötigen Rahmenbedingungen, die unser Programm benötigt.

In Zeile 8 leitet das begin nun den Hauptanweisungsblock ein, in dem sich später unser Quelltext befinden wird. In Zeile 10 endet sowohl der Anweisungsblock als auch das Programm mit end.. Dieser Anweisungsblock wird beim Programmstart ausgeführt und danach beendet sich das Programm wieder.

Zeile 9 enthält nur einen Kommentar, der für uns unwichtig ist und bedenkenlos entfernt werden kann.

Unter Lazarus sieht der Programmrumpf ähnlich aus, wobei wir auch hier die Zeilen 3–12 zunächst nicht weiter beachten:

Datei / Neu... / Projekt / Programm

program Project1;

{$mode objfpc}{$H+}

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  {$ENDIF}{$ENDIF}
  Classes
  { you can add units after this };

{$IFDEF WINDOWS}{$R project1.rc}{$ENDIF}

begin
end.


Der Befehl Readln

[Bearbeiten]

Delphi stellt mit der Programmvorlage ein zwar funktionierendes aber funktionsloses Programm zur Verfügung. Wenn wir einmal dieses Programm starten (mittels F9), so sehen wir im besten Fall für den Bruchteil einer Sekunde ein Konsolenfenster. Denn da unser Programm noch leer ist, wird es sofort beendet. Um dies zu verhindern, fügen wir den Befehl Readln ein:

begin
  Readln;
end.


Readln steht für „read line“ (deutsch: lies Zeile). Das heißt, Readln macht nichts anderes als eine Zeile Text von der Konsole zu lesen. Die Eingabe einer Zeile wird mit der ENTER-Taste beendet. Bis die ENTER-Taste gedrückt wurde, liest der Readln-Befehl alle Zeichen, die man eingibt, ein. Also müsste unser Konsolen-Fenster nun solange geöffnet bleiben, bis wir die ENTER-Taste drücken.

Tipp:

Pascal und Delphi sind case-insensitive. Das heißt, dass sie nicht zwischen Groß- und Kleinschreibung unterscheiden. Es spielt also keine Rolle, ob man „program“, „Program“, „PROGRAM“ oder „PrOgRaM“ schreibt. Sinnvoll ist allerdings, sich für eine Schreibweise zu entscheiden, damit der Quelltext lesbar bleibt (siehe Kapitel Programmierstil).


Wie man die durch Readln eingelesenen Zeichen verarbeitet, erfahren Sie später.

Die Ausgabe

[Bearbeiten]

Nachdem unser Programm nun geöffnet bleibt, damit wir die Ausgaben des selbigen betrachten können, wird es nun Zeit Ausgaben hinzuzufügen. Dazu verwenden wir den Befehl Writeln:

begin
  Writeln('Hello World');
  Readln;
end.


Analog zu Readln steht Writeln für „write line“ (deutsch: schreibe Zeile). Wie sich daher bereits erahnen lässt, führt dieser Befehl dazu, dass der Text „Hello World“ auf der Konsole ausgegeben wird.

Tipp:

Die Routine Writeln ist nicht auf Zeichenketten begrenzt, sondern kann alle Arten von Daten aufnehmen. So ergibt z.B.: Writeln(42); dass die Zahl 42 auf der Konsole ausgegeben wird. Man muss sich auch nicht bestimmte „Formatbezeichner“ merken, wie es bei der Programmiersprache C der Fall ist. Da nur Writeln eine solche automatische Umwandlung vornimmt, existiert jedoch eine ähnliche Funktion, die eine Verwendung von Formatbezeichnern erlaubt. Diese heißt Format und findet sich in der Unit SysUtils.


Exkurs: Zeichenketten

[Bearbeiten]

In Pascal werden Zeichenketten durch ein einfaches Apostroph (') gekennzeichnet und im Allgemeinen als String bezeichnet. Will man allerdings ein ' in einem String verwenden, so ist dies durch zwei Apostrophe ('') möglich. Mehrere Strings (oder String-Variablen oder Ergebnisse von Funktionen, die einen String zurückgeben) lassen sich mit dem Plus-Operator (+) zu einer Zeichenkette verbinden.

Zeichenkette in Pascal Entsprechung
'Hello World' Hello World
'Hello '' World' Hello ' World
'Hello '+'World' Hello World

Näheres siehe Strings.


Nun lassen sich selbstverständlich noch beliebig viele weitere Ausgaben hinzufügen. Allerdings entbehrt die reine Ausgabe von Text auf der Konsole dem eigentlichen Sinn von Programmen, die ja Informationen verarbeiten können sollen. Genau dies ist also Thema des nächsten Kapitels: Wenn Dann Sonst, aber zunächst beschäftigen wir uns erst mal mit den Datentypen.


Datentypen

[Bearbeiten]

Grundlegende Datentypen

[Bearbeiten]

Allgemeines

[Bearbeiten]

Um in Pascal programmieren zu können, muss man wissen, dass verschiedene Datentypen existieren. Pascal ist dabei eine sehr genaue bzw. strikte Sprache, die jede falsche Verwendung von Datentypen mit einer Fehlermeldung ahndet. Doch was sind nun Datentypen?

Die von einem Programm verwendeten Daten werden in einer Bitfolge im Speicher abgelegt. Je nachdem, wie diese Bitfolge wieder ausgelesen wird, können sich wieder völlig andere Daten ergeben. Um hier keine ungewollten Programmabstürze zu produzieren, wird jedem Speicherbereich ein bestimmter Datentyp zugeordnet. Mit den Datentypen können verschiedene Operationen durchgeführt werden, zum Beispiel kann man mit Zahlen rechnen, mit Buchstaben jedoch nicht. Durch die strikte Verwendung von Datentypen ist sichergestellt, dass die gespeicherten Daten nur ihrer Bestimmung gemäß verwendet werden. Nun ja, fast immer, denn keine Regel ohne Ausnahme. Aber dazu später mehr.

Texttypen

[Bearbeiten]

Für die Bearbeitung von Text gibt es zwei verschiedene Zeichentypen: für einzelne Zeichen und für Zeichenketten. Ein Zeichen ist dabei im Grunde alles das, was Sie mit Ihrer Tastatur auf den Bildschirm zaubern können, also Buchstaben, Ziffern (mit denen man aber nicht rechnen kann), Interpunktions- und andere Sonderzeichen usw.

Der Typ Char
[Bearbeiten]

Der Typ Char dient dazu, ein einzelnes ASCII-Zeichen zu speichern.

var
  c: Char;
begin
  c := 'a'; // c den Buchstaben a zuweisen
end.


Um nicht druckbare Zeichen in einem Char zu speichern, oder einfach ein Zeichen mit einem bestimmten ASCII-Code einzufügen, kann das #-Zeichen, gefolgt von einer Ganzzahl genutzt werden:

var
  c: Char;
begin
  c := #64; // c das Zeichen mit dem ASCII-Code 64 zuweisen (= '@')
end.


Tipp:

Jedoch sind nicht alle Zeichen gleich ASCII-Zeichen. Solange wir uns in der Konsole befinden, gibt es für uns förmlich keine anderen Zeichen. Jedoch werden Sie feststellen, dass grafische Anwendungen wie Word auch Unicode anbieten. Unicode enthält aber mehr Zeichen als ASCII. Dadurch ist zum Speichern eines Zeichens auch mehr als ein Byte notwendig. Somit dürfte es auch klar sein, dass wir in Pascal ohne weiteres nicht mit Unicode arbeiten können. Und das wird sich auch mit Delphi (bis Version 2007) nicht ändern.


Der Typ String
[Bearbeiten]

Der Typ String bezeichnet eine Zeichenkette mit variabler Länge. Das heißt, er kann keine Zeichen, ein Zeichen oder auch mehrere beinhalten.

Strings müssen im Quelltext immer von Apostrophen umschlossen werden, dies wird von Delphi standardmäßig blau hervorgehoben:

var
  s: string;
begin
  s := 'Dies ist ein String';
end.


Mehrere Strings lassen sich miteinander mit Hilfe des +-Operators zu einem größeren Text verknüpfen (konkatenieren). Wenn wir beispielsweise an „Dies ist ein String“ noch „ und wird in s gespeichert.“ hängen wollen, so können wir das mit dem so genannten Verknüpfungs- oder Verkettungsoperator tun. Dieser Operator ist ein Pluszeichen (+). Doch genug zur Theorie. Unser Beispiel sähe so aus:

s := 'Dies ist ein String';             // s = "Dies ist ein String"
s := s + ' und wird in s gespeichert.'; // s = "Dies ist ein String und wird in s gespeichert."


Auch Steuerzeichen können genauso wie bei Char verwendet werden:

s := s + #10;  // Linefeed an s anhängen (dezimale Schreibweise)
s := s + #$20; // Leerzeichen an s anhängen (hexadezimale Schreibweise)


So wird im Prinzip jedes Zeichen im String gespeichert. Dies erklärt auch, warum man mit Zahlen in einem String nicht rechnen kann, obwohl sie im String gespeichert werden können.

Um ein einzelnes Zeichen eines Strings anzusprechen, genügt es, in eckigen Klammern dahinter zu schreiben, welches Zeichen man bearbeiten oder auslesen möchte.

Writeln('Der erste Buchstabe des Strings ist ein "' + s[1] + '".');


Verwendung beider Typen untereinander
[Bearbeiten]

Die Datentypen Char und String sind in gewisser Weise kompatibel. Man kann z.B. ein Char an einen String anhängen oder ein Zeichen aus einem String in einem Char speichern:

c := 'X';                      // c = "X"
s := 'Hier ein Zeichen: ' + c; // s = "Hier ein Zeichen: X"
c := s[3];                     // c = "e"


Ganzzahlige Typen

[Bearbeiten]

Ganzzahlige Variablen können, wie der Name schon sagt, ganze bzw. natürliche Zahlen speichern. Der am häufigsten gebräuchliche Typ in dieser Variablenart ist „Integer“. Mit diesen Zahlen lässt sich dann auch rechnen.

Datentypen zum Umgang mit natürlichen Zahlen
[Bearbeiten]

Eine Variable vom Typ „Integer“ bezeichnet eine 32 Bit große Ganzzahl mit Vorzeichen, es können somit Werte von -231 bis 231-1 dargestellt werden.

Das nachfolgende Beispiel zeigt einige Möglichkeiten zur Verwendung von Variablen. Nach jedem Schritt wird mittels Writeln der Name der Variable und danach der Wert der selbigen ausgegeben.

Code:

var
  i, j: Integer;
begin
  i := 10;
  WriteLn('i ', i);
  j := i;
  WriteLn('j ', j);
  i := 12;
  WriteLn('i ', i);
  WriteLn('j ', j);
  ReadLn;
end.

Ausgabe:

i 10
j 10
i 12
j 10


Hier wird deutlich, dass bei der Zuweisung einer Variablen an eine andere nur der Wert übertragen (kopiert) wird.

Außerdem kann wie gewohnt mit +, - und * gerechnet werden. Eine Division wird für Ganzzahltypen durch div dargestellt. Um den Rest einer Division zu erhalten, wird der mod-Operator verwendet. Der Compiler beachtet auch Punkt-vor-Strich Rechnung und Klammerung.

i := 10;        // i ist jetzt 10
i := i * 3 + 5; // i ist jetzt 35
i := 10 div 3;  // i ist jetzt 3
i := 10 mod 3;  // i ist jetzt 1 (Rest von 10 ÷ 3)


Weitere ganzzahlige Datentypen
[Bearbeiten]
Name Wertbereich
Shortint -128..127
Smallint -32768..32767
Longint (entspricht Integer) -231..231-1
Int64 -263..263-1
Byte 0..255
Word 0..65535
Cardinal/Longword 0..232-1

Gleitkommazahlen

[Bearbeiten]
Die Datentypen Single, Double und Extended
[Bearbeiten]

Mit den ganzzahligen Datentypen lassen sich nur gerundete Divisionen durchführen. Brüche wie 1/2 sind daher nicht sinnvoll auszuwerten. Um auch Nachkommastellen berechnen zu können, existieren in Pascal die Gleitkommatypen Single, Double und Extended. Der Unterschied zwischen diesen Typen besteht in der Genauigkeit, mit der die Daten gespeichert werden. Die folgende Tabelle zeigt den jeweiligen Wertebereich der einzelnen Typen:

Name Wertbereich Speicherbedarf
Single -1,5 × 1045 .. 3,4 × 1038 4 Byte
Double -5,0 × 10324 .. 1,7 × 10308 8 Byte
Extended -3,6 × 104951 .. 1,1 × 104932 10 Byte

Daneben existieren unter Delphi noch die folgenden besonderen Typen:

Real48 -2,9 × 1039 .. 1,7 × 1038 6 Byte
Real Derzeit das Gleiche wie Double  
Comp -263+1 .. 263-1 8 Byte
Currency -922337203685477.5808..922337203685477.5807 8 Byte
  • Extended bietet zwar die höchste Genauigkeit, ist aber stark an das System gebunden. Wenn Daten in diesem Format gespeichert werden und auf anderen Rechnern verwendet werden sollen, kann dies zu Problemen führen.
  • Real48 ist veraltet und sollte nur noch zur Kompatibilität mit alten Pascal-Programmen verwendet werden.
  • Real sollte ebenfalls nicht unbedingt verwendet werden, da dieser Typ keine festgelegte Genauigkeit besitzt. In älteren Programmen war dieser Typ gleichbedeutend mit Real48, heute mit Double.
  • Comp ist ein „Mischtyp“ zwischen ganzen Zahlen und Gleitkommazahlen und ebenfalls veraltet (genaueres in der Hilfe von Delphi)
  • Currency ist ein spezieller Typ einer Festkommazahl zur Verwendung mit Währungen. Er weist ebenfalls ein besonderes Verhalten bei der Wertzuweisung auf, näheres in der Delphi-Hilfe.

Die Division können Sie nun mit Hilfe des Operators „/“ statt mit div durchführen. Als Zähler und Nenner können sowohl Ganzzahl- als auch Gleitkommavariablen verwendet werden. Bei der direkten Zuweisung eines gebrochenen Wertes wird die englische Schreibweise verwendet, sprich: das Komma wird durch einen Punkt ersetzt:

 x := 1.34;


Wahrheitswerte

[Bearbeiten]
Der Datentyp Boolean
[Bearbeiten]

Der Typ Boolean ermöglicht es, einen Wahrheitswert zu speichern. Mögliche Werte sind true (wahr) und false (falsch). Auch der Wert kann wiederum der Variable zugewiesen werden:

var
  a: Boolean;
begin
  a := True;
end.


Auf diesen Variablentyp wird im Kapitel Verzweigungen näher eingegangen.


Erweiterte Datentypen

[Bearbeiten]

Zeiger

[Bearbeiten]
Was sind Zeiger?
[Bearbeiten]

Ein Zeiger bzw. Pointer ist eine Variable, die auf einen Speicherbereich des Computers verweist (genauer: in einem Zeiger kann die Adresse eines bestimmten Speicherbereichs des Computers gespeichert werden). Zum besseren Verständnis hier ein kleines Beispiel:

Nehmen wir einmal an, wir haben einen Schrank mit 12 verschiedenen Fächern. Jedes Fach ist mit einer Nummer gekennzeichnet, dazu hat er in jedem Fach einen Gegenstand.

Im Sinne von Zeiger in einer Programmiersprache ist der Schrank der Speicherbereich des Computers, welcher dem Programm zur Verfügung gestellt wird. Jedes einzelne Fach des Schrankes repräsentiert eine Adresse auf diesem Speicher. Die Nummer des Faches entspräche der Speicheradresse, der Inhalt des Faches dem Wert des Objektes, auf das der Zeiger verweist. Auf jedem Speicher kann nun auch eine beliebige Bitreihenfolge abgespeichert werden.

Im nebenstehenden Beispiel ist a ein Zeiger auf ein Zeichen (Datentyp char), wie man ihn sich im handlichen Schrankformat vorstellen kann.

Wozu dienen Zeiger?
[Bearbeiten]

Es gibt mehrere Gründe, warum man Zeiger benötigt. Zum einen sind hinter den Kulissen von Pascal sehr viele Zeiger versteckt, die große dynamische Speicherblöcke benötigen. Beispielsweise ist ein String ein Zeiger, wie auch eine Klasse nur ein einfacher Zeiger ist.

In vielen Fällen sind aber auch Zeiger dazu da, um die strikte Typisierung von Pascal zu umgehen. Dazu benötigt man einen untypisierten Zeiger, der nur den Speicherblock enthält, ohne Information dazu, was sich auf dem Speicherblock befindet.

Weiterhin kann man Zeiger dazu verwenden, um Speicher zu sparen. Dabei fordert man vom System immer nur genau soviel Speicherplatz an, wie man gerade für seine Daten benötigt. Das ist vor allem in den (zugegebenermaßen seltenen) Fällen sinnvoll, wenn man bei der Entwicklung eines Programms noch nicht weiß, welcher Art diese Daten sind und damit natürlich auch nicht weiß, wie groß die Datenmenge sein wird. Hierfür wird sich jedoch meistens eine elegantere Lösung finden. Man kann durch Zeiger auch Speicher sparen, wenn man ihn immer nur dann anfordert, wenn man ihn benötigt. Ein nicht zugewiesener Zeiger erfordert nämlich immer nur 4 Byte! Ein großes Record kann dagegen auch völlig unbenutzt mehrere KByte belegen.

Anwendung
[Bearbeiten]
Deklaration
[Bearbeiten]

Es gibt zwei Arten, wie man einen Zeiger deklarieren kann. Eine typisierte und eine untypisierte Variante.

var
  Zeiger1: ^Integer;  // typisiert
  Zeiger2: Pointer;   // untypisiert


Die typisierte Variante erwartet an der Speicheradresse immer einen bestimmten Datentyp, im obigen Fall Integer. Ein untypisierter Zeiger kann alle möglichen Datentypen aus der Speicheradresse auslesen, zur weiteren Verarbeitung muss jedoch eine explizite Typumwandlung durchgeführt werden.

Speicheradresse auslesen
[Bearbeiten]

Im Code kann man nun entweder die Adresse oder den Inhalt des Speicherblockes auslesen. Das Auslesen der Adresse des Speicherblockes funktioniert bei beiden der oben genannten Varianten gleich.

begin
  Zeiger2 := @Zeiger1;
  Zeiger2 := Addr(Zeiger1);
end.


Wie Sie sehen, gibt es für das Auslesen der Adresse zwei Varianten. Entweder nutzt man den @-Operator oder die Funktion Addr(), wobei der @-Operator schneller ist. Hinter einem @-Operator kann jede beliebige Variable stehen, aber nur einem untypisierten Zeiger kann jede Adresse jedes Speicherblocks zugeordnert werden, ohne auf den Typ zu achten.

Inhalt auslesen
[Bearbeiten]

Hier gibt es einen Unterschied zwischen typisierten und untypisierten Zeiger.

var
  i, j: Integer;
  p1: ^Integer;
  p2: Pointer;
begin
  i := 1;

  { typisiert }

  p1 := @i;       // dem Zeiger wird die Adresse der Integer-Variable übergeben
  p1^ := p1^ + 1; // hier wird der Wert um eins erhöht
  j := p1^;       // typisiert: der Variable j wird 2 übergeben

  { untypisiert }

  p2 := @i;       // analog oben
  Integer(p2^) := i + 1;
  j := Integer(p2^);
end.


Bei einem untypisierten Zeiger muss immer der Typ angegeben werden, welcher aus dem Speicher ausgelesen werden soll. Dies geschieht durch die so genannte Typumwandlung: Typ(Zeiger).

Neuen Speicher anfordern und freigeben
[Bearbeiten]

In den obigen Beispielen wurde bereits vorhandener Speicher an den Zeiger übergeben und diese Daten verändert. Für gewöhnlich möchte man jedoch neue Daten im Arbeitsspeicher ablegen. Hierzu muss man sich explizit Speicher anfordern und diesen nach der Verwendung des Zeigers wieder freigeben.

Bei typisierten Zeigern erfolgt die Anforderung des Speichers durch die Anweisung New(var P: Pointer) und die Freigabe mittels Dispose(var P: Pointer). Die Größe des erforderlichen Speichers wird dabei anhand des Datentyps, auf den der Zeiger verweist, automatisch bestimmt.

Achtung: Auch wenn beide Routinen untypisierte Zeiger annehmen, funktioniert dies nicht! Delphi kompiliert zwar das Programm fehlerfrei, bei der Ausführung wird es dann jedoch abstürzen. Das liegt daran, dass untypisierte Zeiger keine festgelegte Größe haben. Pointer fungiert hier sozusagen nur als Basistyp für alle Zeigervariablen. Um auch untypisierten Zeigern Speicher zuzuweisen und ihn wieder freizugeben, müssen Sie die (sonst als veraltet geltenden) Routinen GetMem und FreeMem verwenden. GetMem erwartet als zweiten Parameter eine Größenangabe in Byte. Bei FreeMem können Sie die Größenangabe weglassen, ansonsten muss diese mit dem Wert bei der Zuweisung des Speichers übereinstimmen.

var
  p1: ^Integer;
  p2: Pointer;

begin
  New(p1);
  p1^ := 1024;
  Dispose(p1);

  GetMem(p2, 4);          // Integer ist 32 Bit, also 4 Byte groß
  Integer(p2^) := 2048;
  FreeMem(p2);            // oder: FreeMem(p2, 4);
end.


In Pascal gibt es Zahlreiche Typen, deren Größe nicht immer dokumentiert ist oder nachgeschlagen werden muss. Um zu ermitteln wie viel Speicher für den nichttypisierte Zeiger p2 reserviert werden soll bietet sich die Standardfunktion SizeOf() an. Diese Funktion arbeitet mit allen Variablen und Typenbezeichnern. Ein Beispiel:

var
  p2 : Pointer;

begin
  GetMem(p2, SizeOf(Integer)); // Statt Integer kann hier jeder andere Var/Typ stehen         
  Integer(p2^) := 2048; // Zuweisungen sind daraufhin anzupassen 
  FreeMem(p2);           
end.


Siehe hierzu auch Schnelleinstieg: Pointer

Quelle: http://docwiki.embarcadero.com/RADStudio/XE2/de/Datentypen,_Variablen_und_Konstanten


Aufzählungen

[Bearbeiten]

Oftmals dienen Variablen dazu, aufgrund ihres Wertes bestimmte Aktionen im Programmablauf auszulösen. Als einfachstes dient das Beispiel einer Ampel:

Ampel rot:  Anhalten
Ampel gelb: Fahrbereitschaft herstellen
Ampel grün: Losfahren

Dabei stehen die Farben der Ampel in einer logischen Reihenfolge zueinander. Ohne einen Aufzählungstyp zu verwenden, könnte man z.B. jeder Farbe eine Zahl zuweisen, wie rot=0, gelb=1, grün=2. Das macht den Programmtext jedoch am Ende schwer zu lesen und schwer zu warten, da immer auch die Bedeutung der Zahl bekannt sein muss. Alternativ könnte man entsprechend benannte Konstanten verwenden, was aber ebenfalls kein schöner Programmierstil ist. Eine weitere Möglichkeit wäre es, die Farbe als Text in einem String zu speichern. Mal abgesehen von der Größe des Programms kann dies zu Abstürzen wegen Tippfehlern führen, der Speicherbedarf erhöht sich und das Programm wird langsamer ausgeführt.

Aufzählungen mit automatischen Positionen
[Bearbeiten]

Da wir nun wissen, was wir nicht tun sollten, wie umgehen wir die Probleme?

In Pascal gibt es dafür so genannte Aufzählungstypen. Dabei wird ein Typ definiert, der Elemente in einer angegebenen Reihenfolge besitzt. Als „Werte“ werden dabei Bezeichner eingesetzt, die bisher noch nicht im Programm verwendet werden:

type
  TAmpel = (rot, gelb, gruen);

var
  Ampel: TAmpel;


Die Aufzählung sollte nicht direkt hinter der Variablendefinition angegeben, sondern möglichst immer als eigenständiger Typ definiert werden. Einzige Ausnahme: man verwendet in dem Programm tatsächlich nur genau diese eine Variable mit der Aufzählung.

Um nun der Ampel eine Farbe zu geben, weist man einfach den entsprechenden Bezeichner zu:

Ampel := gruen;


Da es sich bei diesem Typ, wie der Name schon sagt, um eine Aufzählung handelt, kann man die Werte inkrementieren (erhöhen) und dekrementieren (erniedrigen), sowie den Vorgänger bzw. Nachfolger bestimmen oder auch in einer case-Verzweigung auswerten. Dies ist möglich, da den Bezeichnern intern automatisch eine Position zugewiesen wird. Das erste Element hat immer die Position 0, das zweite die Position 1 usw. Man kann diese Position mit der Funktion Ord auslesen. Andersherum kann man mittels Typumwandlung aus einer Zahl wieder die Position im Aufzählungstyp ermitteln:

var
  b: Byte;
  a: TAmpel;

begin
  a := gelb;
  b := Ord(a);     // b = 1

  b := 2;
  a := TAmpel(b);  // a = gruen
end.


Aufzählungen mit definierten Positionen
[Bearbeiten]

Wem die Regel 0, 1, 2,... zu starr ist und wer eine andere Einteilung benötigt, kann hinter jedem Element eine Position vorgeben. Diese lässt sich auch aus den anderen Positionen errechnen. Ebenso sind mehrfach benannte Positionen möglich:

type
  TAmpel = (rot = 1, gelb = 2, gruen = 4);
  TArbeitstage = (Mo = 1, Di = 2, Mi = Mo + Di, Don = 4, Fr = Di + Mi);  // Mi = 1+2=3, Fr = 2+3=5
  TDoppelt = (Bez1 = 1, Bez2 = 2, Bez3 = 3, Bez4 = 2);  // sowohl Bez2 als auch Bez4 ergeben mit Ord = 2


In diesem Beispiel von TAmpel wurden die Positionen 0 und 3 übersprungen und nicht benannt. Das heißt, dass man diese Positionen nicht namentlich zuweisen kann. Aber sie existieren trotzdem und lassen sich zuweisen! Das ermöglicht die Typumwandlung:

a := TAmpel(3);
b := Ord(a);        // b = 3
a := TAmpel(1024);  // ebenfalls zulässig!

// ACHTUNG!
a := gelb;          // Ord(a) = 2
a := Succ(a);       // Ord(a) = 3 und nicht 4!


Inwiefern dies jedoch in einem Programm sinnvoll ist, muss jeder für sich selbst entscheiden.

Die Anzahl der möglichen Positionen, die auch nach der zuletzt benannten zugewiesen werden können, richtet sich nach ebendieser. Ist die letzte benannte Position kleiner als 256, ergibt eine Zuweisung von 256 gleich 0, da die Zählung wieder von vorne beginnt. Ebenso hat a im oberen Beispiel den Wert 0, da 1024 = 256 × 4. Ist die letzte Position 256 oder höher, können Werte bis 65535 angegeben werden usw. Auch negative Werte sind möglich, wenn entsprechende benannte Positionen definiert werden.


Bei einem Set handelt es sich um einen Datentyp, der eine Menge von Werten eines Basistyps speichert. Ein Set speichert dabei zu jedem möglichen Wert des Basistyps, ob dieser enthalten ist. Der Basistyp muss ein Typ sein, der höchstens 256 verschiedene Werte annehmen kann, also etwa Byte, Char oder ein selbst definierter Aufzählungstyp. Möglich sind natürlich auch Teilbereichstypen dieser Typen. Ein Set benötigt für jeden möglichen Wert des Basistyps ein Bit, hat also eine Größe von (Zahl der möglichen Werte des Basistyp / 8) Byte, maximal also 32 Byte.

Mengen sind z.B. nützlich, um Programmeinstellungen oder auch bestimmte kumulative Eigenschaften von Objekten zu speichern. Anders als beim Array spielt die Reihenfolge der Elemente keine Rolle, und jedes Element kann auch nur einmal enthalten sein.

Ein Mengentyp wird durch die Angabe von set of deklariert:

type
  Typbezeichner = set of <Aufzählungstyp>;


Um einer Mengenvariablen Werte zuzuweisen, setzt man diese in eckige Klammern [] und trennt die einzelnen Werte mit einem Komma. Für eine leere Menge geben Sie nur die eckigen Klammern an. Mit dem Operator in fragt man ab, ob ein Element in einer Menge enthalten ist.

Zur Verdeutlichung ein Beispiel zur Ziehung der Lottozahlen:

type
  TLottozahlen = 1..49;

var
  Lottozahlen: set of TLottozahlen = [];  // leere Menge
  i: Byte;
  z: TLottozahlen;

begin
  { Zufallszahlen initialisieren }
  Randomize;
 
  { Ziehung 1 bis 6 }
  for i := 1 to 6 do
  begin
    { Eine Zufallszahl ziehen und prüfen, ob diese bereits vorhanden ist }
    repeat
      z := Random(49) + 1;
    until not (z in Lottozahlen);
    { Zahl der Ziehung hinzufügen }
    Lottozahlen := Lottozahlen + [z];
  end;

  { alle Lottozahlen sortiert ausgeben }
  for z in Lottozahlen do
    Writeln(z);
end.


Die Gesamtmenge aller gültigen Lottozahlen reicht von 1 bis 49. Hierzu deklarieren wir uns einen entsprechenden Typ. Da wir aus dieser Gesamtmenge 6 Zahlen zufällig auswählen, benötigen wir noch eine Zählschleife, die den Vorgang nach der 6. Ziehung beendet. Da wir ja keine echten Kugeln herausnehmen können, kann es passieren, dass eine Zahl mehrfach gezogen wird. Um dies zu vermeiden, prüfen wir, ob die Zahl eventuell bereits vorhanden ist und ziehen in diesem Fall eine neue. Eine Menge speichert zwar (im Gegensatz zu einem Array) jeden Wert nur einmal, allerdings würde die for...to Schleife beendet werden, bevor 6 unterschiedliche Zahlen gezogen wurden.
Anschließend fügen wir die gezogene Zahl unserer Teilmenge hinzu. Da wir auch wissen wollen, ob wir richtig getippt haben, geben wir dann alle Lottozahlen auf dem Bildschirm aus. Da es sich bei den Lottozahlen um einen Aufzählungstyp handelt, sind diese automatisch von der kleinsten zur größten sortiert. Die Zusatzzahl dürften wir nicht in unsere Menge aufnehmen, sondern müssten diese gesondert speichern, da aus einer sortierten Liste nicht mehr die zuletzt gezogene Zahl zu erkennen wäre.

Für die Ziehung wäre auch der folgende Code möglich. Dieser zählt eine Zahl nur dann als gezogen, wenn es sich dabei um eine neue handelt:

i := 0;
repeat
  z := Random(49) + 1;
  if not (z in Lottozahlen) then
  begin
    Inc(i);
    Lottozahlen := Lottozahlen + [z];
  end;
until i = 6;


Mehr über die Verwendung von Schleifen erfahren Sie später.

Genauso, wie Sie über den Plus-Operator + Elemente in eine Menge aufnehmen können, entfernt der Minus-Operator - diese. Dabei können Sie jeweils auch mehrere Elemente gleichzeitig angeben. Normalerweise sollte man jedoch statt des Plus- und des Minusoperators die Funktionen include und exclude verwenden, weil diese effizienteren Code erzeugen. Mehrere direkt aufeinanderfolgende Elemente können mit dem Aufzählungszeichen .. angegeben werden:

Lottozahlen := [2, 3, 5];
Lottozahlen := Lottozahlen + [7, 11, 13];  // ergibt [2, 3, 5, 7, 11, 13]
Lottozahlen := Lottozahlen - [3, 7];       // ergibt [2, 5, 11, 13]
Lottozahlen := [1..4, 8, 16];              // ergibt [1, 2, 3, 4, 8, 16]


Mehrfaches Entfernen desselben Elementes ist dabei genauso ohne Fehlermeldung möglich wie mehrfaches Hinzufügen.

Verwechseln Sie Sets bitte nicht mit Arrays. Beide benutzen die eckigen Klammern, jedoch zu einem anderen Zweck. Man kann zum Beispiel bei einem Set nicht das dritte Element erfragen, indem man folgenden Code zu kompilieren versucht:

 z := Lottozahlen[3];


Arrays

[Bearbeiten]
Was sind Arrays?
[Bearbeiten]

Ein Array ist vereinfacht gesagt, eine Liste von Werten des gleichen Datentyps.

Arrays anlegen
[Bearbeiten]

Wir wollen eine Gästeliste mit 10 Gästen anfertigen. Bisher hätten wir in etwa folgendes gemacht:

var
  gast1, gast2, gast3, gast4, gast5, gast6, gast7, gast8, gast9, gast10: string;


Der Nachteil dieses Verfahrens liegt auf der Hand - spätestens wenn zwanzig Gäste kommen.

Nun erzeugen wir einfach ein Array vom Datentyp String mit 10 Elementen:

var
  gaeste: array[1..10] of string;


Die genaue Struktur der Array-Deklaration ist:

array [<startindex> .. <endindex>] of <Datentyp>;


startindex..endindex ist dabei eine so genannte Bereichsstruktur mit dem wir den Bereich zwischen Startwert und Endwert angeben (Randwerte werden mit eingeschlossen). Es ist auch möglich, einen Bereich wie -3..5 anzugeben.

Der Name einer Array-Variablen sollte immer ein Substantiv sein und in der Mehrzahl stehen.

Auf Arrays zugreifen
[Bearbeiten]

Um nun auf die einzelnen Elemente zuzugreifen, verwenden wir folgende Syntax:

gaeste[1] := 'Axel Schweiß';
gaeste[2] := 'Peter Silie';
gaeste[3] := 'Jack Pot';
gaeste[4] := 'Ngolo Kante';
gaeste[5] := 'Manuel Neuer';
{ ... },


Die Zahl in den eckigen Klammern ist der so genannte Index. Er gibt an, auf welches Element des Arrays wir zugreifen wollen. Gültige Werte sind hier die Zahlen 1 bis 10. Ein weiterer Vorteil von Arrays ist, dass wir anstatt eines fixen Indexes auch einen ordinalen Datentyp angeben können. Das heißt z.B. eine Integer-Variable. Die Abfrage der Namen von 10 Gästen ließe sich also so sehr einfach implementieren:

var
  index: Integer;
  gaeste: array[1..10] of string;
begin
  for index := 1 to 10 do
  begin
    Writeln('Bitte geben Sie den Namen des ', index, '. Gastes ein:');
    Readln(gaeste[index]);
  end;
end.


Dynamische Arrays
[Bearbeiten]

Ändern wir unser Szenario so ab, dass wir eine Gästeliste erstellen wollen, aber nicht wissen, wieviele Gäste diese beinhalten soll. Nun könnten wir zwar ein Array erzeugen, das auf jeden Fall groß genug ist um alle Gäste der Welt aufzunehmen. Allerdings wäre dies eine Verschwendung von Speicher und nicht gerade effektiv. Hier kommen uns die dynamischen Arrays zu Hilfe. Dabei handelt es sich, wie man vielleicht vermuten kann, um Arrays, deren Länge man zur Laufzeit verändern kann. Erstellt werden sie praktisch genauso wie normale Arrays, nur geben wir diesmal keinen Indexbereich an:

var
  gaeste: array of string;


Der Indexbereich eines dynamischen Arrays ist zwar dynamisch, aber er beginnt zwingend immer mit 0. Zu Beginn hat dieser Array die Länge 0, d.h. er beinhaltet momentan keine Werte.

Länge des Arrays verändern
[Bearbeiten]

Nun verändern wir die Länge des Arrays auf 10:

SetLength(gaeste, 10);


Unser Array hat nun eine Länge von 10. Das bedeutet, wir können 10 Strings in ihm verstauen. Allerdings hat das höchste Element im Array den Index 9. Das liegt daran, dass das erste Element den Index 0 hat und wir daher mit dem Index 9 schon 10 Elemente zusammen haben.

Nun könnten wir zum Einlesen unserer Gästeliste so vorgehen:

var
  index, anzahlgaeste: Integer;
  gaeste: array of string;
begin
  Writeln('Bitte geben Sie die Anzahl der Gäste ein:');
  Readln(anzahlgaeste);
  SetLength(gaeste, anzahlgaeste);
  for index := 0 to anzahlgaeste-1 do
  begin
    Writeln('Bitte geben Sie den Namen des ', index + 1, '. Gastes ein:');
    Readln(gaeste[index]);
  end;
end.


Dies würde zwar zum gewünschten Erfolg führen, allerdings benötigen wir so ständig eine weitere Variable, die die Länge unseres Arrays angibt. Um dies zu umgehen, bedienen wir uns der Routinen High und Low.

Erster und letzter Index
[Bearbeiten]

Die Routine High liefert den höchsten Index des übergeben Arrays zurück:

Code:

SetLength(gaeste, 10);
Writeln(High(gaeste));

SetLength(gaeste, 120);
Writeln(High(gaeste));

Ausgabe:

9
119


Die Routine Length gibt, wie sich vermuten lässt, die Länge des Arrays zurück:

Code:

SetLength(gaeste, 10);
Writeln(Length(gaeste));

Ausgabe:

10


Mit der Routine Low ermitteln Sie den ersten Index des übergebenen Arrays. Bei einem dynamischen Array wäre dies immer 0. Daher benötigt man diese Funktion in einem realen Programm eigentlich nicht. Lediglich bei Arrays mit festen Indexbereichen erhält diese Funktion einen tieferen Sinn. So kann man auf einfache Weise den unteren Index abändern, indem man diesen einfach in der Deklaration überschreibt. Um den Rest des Programms braucht man sich dann nicht zu kümmern, da man mit Low auf der sicheren Seite ist.

Nun können wir unser Programm ein weiteres bisschen vereinfachen. Um die Funktionsweise eines dynamischen Array deutlich zu machen, fragen wir auch nicht mehr nach der Anzahl der Gäste, sondern fragen so lange nach weiteren Namen, bis das Ganze mit einer leeren Eingabe beendet wird:

var
  name: string;
  gaeste: array of string;
begin
  repeat
    Writeln('Bitte geben Sie den Namen des ', Length(gaeste) + 1, '. Gastes ein (leer zum Beenden):');
    Readln(name);
    if (name <> '') then
    begin
      SetLength(gaeste, Length(gaeste) + 1);
      gaeste[High(gaeste)] := name;
    end;
  until (name = '');
end.


Hier wird nach jeder Eingabe eines Namens das Array vergrößert und dann der Name am Ende der Liste eingetragen. Dies hat den Vorteil, dass die Liste zu jedem Zeitpunkt immer nur die benötigte Größe hat und keine unnötigen leeren Elemente an deren Ende enthält. Man benötigt in diesem Beispiel dadurch zwar einen zusätzlichen Prüfblock und kann das Array nicht mehr direkt befüllen, spart sich aber gleichzeitig eine Variable. In komplexeren Programmen, bei denen man nicht einfach jemanden nach der Anzahl der Werte fragen kann (z.B. beim Auslesen von Datensätzen aus einer Datei) ist diese Programmiertechnik sehr hilfreich, wenn nicht gar notwendig.

Array freigeben
[Bearbeiten]

Da wir beim Erstellen des Arrays Speicher belegt haben, müssen wir diesen noch freigeben. Das geschieht ganz einfach mittels:

SetLength(gaeste, 0);


Dabei wird die Länge des Arrays wieder auf 0 gesetzt und er beansprucht so keinen weiteren Platz im Speicher mehr. Dies sollte man allerdings immer dann ausführen, wenn der verwendete Array nicht mehr benötigt wird. Unser finales Programm sieht also so aus (wieder etwas vereinfacht mit abgefragter Anzahl der Gäste):

var
  index, anzahlgaeste: Integer;
  gaeste: array of string;
begin
  Writeln('Bitte geben Sie die Anzahl der Gäste ein:');
  Readln(anzahlgaeste);
  SetLength(gaeste, anzahlgaeste);
  for index := 0 to High(gaeste) do
  begin
    Writeln('Bitte geben Sie den Namen des ', index + 1, '. Gastes ein:');
    Readln(gaeste[index]);
  end;
  SetLength(gaeste, 0);
end.


Mehrdimensionale Arrays
[Bearbeiten]

Bis jetzt haben wir uns nur mit eindimensionalen Arrays beschäftigt. Wir haben in Pascal aber auch die Möglichkeit, mehrdimensionale Arrays anzulegen. Dabei kann jeder Unterbereich mit einem festen oder dynamischen Indexbereich versehen sein. Ein mehrdimensionales Array kann man sich wie eine Tabelle vorstellen, was bei zwei und drei Indexbereichen noch nicht schwerfallen dürfte, Pascal erlaubt aber auch weitere Dimensionen darüber hinaus. Bei einem zweidimensionalen Array kann zum Beispiel in Gedanken der erste Bereich für eine Zeile und der zweite Bereich für eine Spalte in dieser Zeile stehen.

Das einfachste sind wiederum mehrdimensionale statische Arrays mit festen Indexbereichen. Die einzelnen Bereiche werden mit Kommata voneinander getrennt.

var
  a1: array[1..10, 0..5] of Byte;          // zweidimensional, 10 "Zeilen" á 6 "Spalten"
  a2: array[1..10, 1..10, 1..10] of Byte;  // dreidimensional, 10 Zeilen á 10 Spalten á 10 Felder in die Tiefe


Auch die Auswertung der gespeicherten Daten erfolgt über komma-getrennte Indizes. Alternativ kann man jede Dimension in eigenen eckigen Klammern notieren:

a1[1, 0] := 15;      // gleichbedeutend mit: a1[1][0] := 15;
a2[2, 4, 8] := 0;    // gleichbedeutend mit: a2[2][4][8] := 0;


Mehrdimensionale dynamische Arrays lassen sich nach folgender Vorlage erstellen:

var a: array of array [of array...] of <Datentyp>;


Die einzelnen Unterbereiche können dabei sowohl statisch als auch dynamisch sein, man kann also Indexbereiche vorgeben. Für jeden dynamischen Unterbereich kann dann mittels SetLength() die Größe festgelegt werden.

Wir wollen nun den Vornamen und den Nachnamen auf unserer Gästeliste getrennt voneinander abspeichern. Dazu erzeugen wir zuerst ein Array mit zwei Elementen, eins für den Vornamen eins für den Nachnamen:

type
  TName = array[0..1] of string; // Index 0 = Vorname; 1 = Nachname

var
  gaeste: array of TName;


Und so einfach haben wir ein mehrdimensionales Array erzeugt. Wenn man sich dieses Array wieder als Tabelle vorstellt, hat es eine beliebige Anzahl von Zeilen (das dynamische „äußere“ Array gaeste), sowie eine Spalte für den Vornamen und eine für den Nachnamen (das statische „innere“ Array TName).

Natürlich können wir das Ganze auch in einer einzelnen Zeile deklarieren:

var
  gaeste: array of array[0..1] of string;


Nun wollen wir unsere Gästeliste erneut einlesen:

var
  index, anzahlgaeste: Integer;
  gaeste: array of array[0..1] of string;
begin
  Writeln('Bitte geben Sie die Anzahl der Gäste ein:');
  Readln(anzahlgaeste);
  SetLength(gaeste, anzahlgaeste);
  for index := 0 to High(gaeste) do
  begin
    Writeln('Bitte geben Sie den Vornamen des ', index + 1, '. Gastes ein:');
    Readln(gaeste[index, 0]);
    Writeln('Bitte geben Sie den Nachnamen des ', index + 1, '. Gastes ein:');
    Readln(gaeste[index, 1]);
  end;
  SetLength(gaeste, 0);
end.


Mit unseren fünf Gästen vom Anfang dieses Kapitels befüllt und als Tabelle dargestellt, sähe das Ganze so aus:

index gaeste[index, 0] gaeste[index, 1]
0 Axel Schweiß
1 Peter Silie
2 Jack Pot
3 Ngolo Kante
4 Manuel Neuer


Für diese Art der Datenspeicherung, wenn zwei oder mehr zusammenhängende Daten immer gemeinsam verwendet werden sollen (wie der Vor- und Nachname in unserem Beispiel), benutzt man besser Records. Wie das funktioniert, sehen wir uns als nächstes an.


Records

[Bearbeiten]
Was sind Records?
[Bearbeiten]

Records ermöglichen es, mehrere Variablen zu gruppieren. Dies ist beispielsweise dann hilfreich, wenn oft die gleiche Menge an Variablen benötigt wird, oder eine Menge Variablen logisch zusammengefasst werden soll. Eine weitere Situation in der Records unverzichtbar sind ist, wenn im Programm mehrere Datensätze gespeichert und verwaltet werden sollen, beispielsweise in einem Adressbuch.

Wie funktionieren Records?
[Bearbeiten]

Um zu unserem Beispiel vom Adressbuch zurückzukommen: Wir wollen alle Daten, also Vorname, Nachname, etc. in einem Record speichern. Dazu legen wir einen neuen Typ TPerson an, in dem wir alle Variablen auflisten:

type
  TPerson = record
    Vorname: string;
    Nachname: string;
    Anschrift: string;
    TelNr: string;
  end;


Wenn jetzt eine Variable vom Typ TPerson deklariert wird, enthält diese all diese Variablen:

var
  Person: TPerson;
begin
  Person.Vorname := 'Hans';
  Person.Nachname := 'Müller';
  { ... }
end;


Die Variablen im Record verhalten sich genauso wie „normale“ Variablen. Benötigt man ein Record nur zur einmaligen Strukturierung von Daten, ist es nicht nötig, einen Verbund-Typ anzulegen:

var
  Datei: record
    Name, Pfad: string;
  end;


Die with-Anweisung
[Bearbeiten]

Falls Sie mit mehreren Record-Feldern nacheinander arbeiten wollen, ist es sehr mühselig, immer den Namen der Variablen vornweg zu schreiben. Diese Aufrufe lassen sich mithilfe der with-Anweisung logisch gruppieren:

with Person do
begin
  Vorname := 'Hans';
  Nachname := 'Müller';
  Anschrift := 'Im Himmelsschloss 1, 12345 Wolkenstadt';
  TelNr := '03417/123456';
end;


Es ist auch möglich, die With-Anweisung auf mehrere Records anzuwenden. Dazu müssen die Bezeichner zwischen with und do mit Kommas getrennt aufgezählt werden. Natürlich müssen die Records zum gleichen Typ gehören.

Variante Teile in Records
[Bearbeiten]

Ein Record kann so genannte variante Teile enthalten. Dies sind Felder eines Records, die den gleichen Speicherplatz belegen, aber unterschiedliche Typen haben und/oder in verschiedener Anzahl vorhanden sind. Je nach verwendetem Bezeichner werden beim Schreiben und Lesen die Daten im Speicher entsprechend seines Typs anders interpretiert.

Deklaration
[Bearbeiten]

Der variante Teil eines Records ähnelt dabei einer case-Anweisung bei der verzweigten Datenverarbeitung (siehe Abschnitt „Verzweigungen“). Der variante Teil wird ebenfalls mit case eingeleitet und steht immer am Ende der Record-Deklaration. Ein solches Record ist daher so aufgebaut:

type
  Typname = record
    Feld_1: <Typ_1>;
    Feld_2: <Typ_2>;
    ...
    Feld_n: <Typ_n>;
    case [Markierungsname:] <Typ> of
      Wert_1: (<Feldliste_1>);
      Wert_2: (<Feldliste_2>);
      ...
      Wert_n: (<Feldliste_n>);
  end;


Von Feld_1 bis Feld_n erstreckt sich der statische Teil des Records wie in den oberen Abschnitten beschrieben. Vom Schlüsselwort case bis zum abschließenden end; folgt der variante Teil. Anders als bei den Verzweigungen schließt hierbei das end sowohl den varianten Teil als auch das gesamte Record ab. Daraus ergibt sich, dass die varianten Teile immer am Ende stehen und keine statischen Teile mehr folgen können.

Der Markierungsname muss hierbei nicht angegeben werden. Er dient zur Unterscheidung, welche der varianten Feldlisten die gültigen Daten enthält und kann daher wie die statischen Felder mit einem Wert belegt werden. Man sollte der Markierung immer einen eindeutigen Wert für die jeweilige Liste zuweisen, sobald man das Record mit Daten füllt. Beim Auslesen der Daten entscheidet dann der Wert der Markierung, welche varianten Felder gültige Daten enthalten und abgefragt werden dürfen. In manchen Fällen werden Sie vielleicht keine Markierung benötigen, Sie können dann den Bezeichner und den Doppelpunkt weglassen. Ein Typ und eine Wertliste muss aber in jedem Falle (sozusagen fiktiv) angegeben werden, wobei Sie jeden Aufzählungstyp verwenden können, also auch selbst definierte.

Die Feldlisten werden für jeden Wert von Klammern eingeschlossen. Diese Liste selbst unterscheidet sich nicht von anderen Felddeklarationen, sie entspricht also der Form:

 VarFeld_1: <Typ_1>;
 VarFeld_2: <Typ_2>;
 ...
 VarFeld_n: <Typ_n>;


Dabei müssen Sie jedoch beachten, dass kein Typ mit variabler Größe verwendet werden darf, da der variante Teil immer einen festen Speicherplatz belegt. Es ist daher kein string (mit Ausnahme von ShortString), dynamisches Array und keine Varianten (Datentyp Variant) erlaubt. Es darf auch kein Record verwendet werden, das einen solchen Typ enthält. Sie können stattdessen jedoch einen Zeiger auf solche Typen verwenden, da Zeiger immer eine feste Größe von 4 Byte haben.

Speicherbelegung
[Bearbeiten]

Die Größe des varianten Teils bestimmt sich nach der größten Feldliste. Um Anordnung und Größe der Daten zu verstehen, betrachten Sie bitte folgendes Beispiel:

type
  TVarRecord = record
    StatischesFeld1: Byte;
    StatischesFeld2: Boolean;
    case Byte of
      0:
        (VariantesFeld1: Byte;
         VariantesFeld2: Integer);
      1:
        (VariantesFeld3: array[1..10] of Char);
      2:
        (VariantesFeld4: Double;
         VariantesFeld5: Boolean);
  end;


Dieses Record wird im Speicher folgendermaßen abgelegt:

Falls Sie eine Markierung verwenden, wird diese zwischen dem letzten statischen und dem Beginn der varianten Feldliste gespeichert, das Record vergrößert sich entsprechend dem Typ der Markierung.

Wichtig ist für die Verwendung von varianten Records auch, dass zu jeder Zeit jedes der varianten Felder gelesen und geschrieben werden kann. Es kann daher immer nur eine der Feldlisten gültige Werte enthalten; Sie können also nicht die verschiedenen Werte gleichzeitig speichern. Sobald Sie im obigen Beispiel VariantesFeld3 mit Daten füllen, ändert sich automatisch der Wert aller anderen varianten Felder. Sie müssen daher besonders aufpassen, dass Sie keine benötigten Daten mit ungültigen überschreiben, weil Sie versehentlich einen falschen Feldnamen benutzen. Delphi wird Sie hiervor weder bei der Kompilierung noch bei der Ausführung Ihres Programms warnen!

Geben Sie zur Verdeutlichung einmal folgendes Programm ein und starten Sie es. Die Ausgabe wird Sie überraschen!

program VariantRecord;

{$APPTYPE CONSOLE}

type
  TVarRec = packed record
    case Byte of
      0:
        (FByte: Byte;
         FDouble: Double);
      1:
        (FStr: ShortString);
  end;

var
  rec: TVarRec;

begin
  rec.FByte := 6;
  rec.FDouble := 1.81630607010916E-0310;

  { Der Speicherbereich von FByte und FDouble
    wird jetzt als Zeichenkette interpretiert. }
  Writeln(rec.FStr);
  Readln;
end.


Hinweis: Das Schlüsselwort packed sorgt dafür, dass die Felder des Records lückenlos im Speicher aneinander gereiht und nicht für einen schnelleren Zugriff optimiert abgelegt werden.

Beispiel
[Bearbeiten]

In einem Adressbuch gibt es leider keine voneinander abhängigen Einträge, daher wenden wir uns wieder unserer Gastliste zu und erweitern diese.

Statt nur den Namen des Gastes zu speichern, wollen wir auch aufnehmen, ob der jeweilige Gast eingeladen ist, welche Gastnummer und welcher Platz ihm in diesem Falle zugewiesen wurde, oder ob sein Besuch andernfalls erwünscht ist. Wir können davon ausgehen, dass ein eingeladener Gast in jedem Fall erwünscht ist, benötigen hierzu also keine Information. Andererseits kann ein nicht eingeladener Gast keinen Platz und keine Gastnummer zugewiesen bekommen haben.

Diese Situation können wir in einem varianten Record darstellen:

type
  TGast = record
    Name, Vorname: string;  // bei statischen Feldern erlaubt
    case eingeladen: Boolean of
      True:
        (Platz, Gastnummer: Byte);
      False:
        (erwuenscht: Boolean);
  end;

  TGastListe = array of TGast;

var
  GastListe: TGastListe;
  Zaehler: Integer;
  Gast: TGast;


Name, Vorname und eingeladen sind bei jedem Gast vorhanden, die anderen Felder sollen (und dürfen) nur abhängig vom Wert des Feldes eingeladen verwendet werden. Nun können wir einige Gäste erfassen:

// Anzahl der Gäste festlegen
SetLength(GastListe, 4);

GastListe[0].Name := 'Schweiss';
GastListe[0].Vorname := 'Axel';
GastListe[0].eingeladen := False;
GastListe[0].erwuenscht := False;

GastListe[1].Name := 'Silie';
GastListe[1].Vorname := 'Peter';
GastListe[1].eingeladen := True;
GastListe[1].Platz := 42;
GastListe[1].Gastnummer := 1;

GastListe[2].Name := 'Pot';
GastListe[2].Vorname := 'Jack';
GastListe[2].eingeladen := False;
GastListe[2].erwuenscht := True;

GastListe[3].Name := 'Schluepfer';
GastListe[3].Vorname := 'Rosa';
GastListe[3].eingeladen := True;
GastListe[3].Platz := 14;
GastListe[3].Gastnummer := 2;


Anschließend wollen wir noch einen Türsteher beauftragen, die Meute am Eingang zu sortieren. Dazu schreiben wir in den Programmrumpf eine Funktion, die prüft, ob der Gast eingelassen werden kann (Hinweis: Zu der Verwendung von Funktionen, Schleifen und Verzweigungen erfahren Sie später mehr. Mit ein paar grundlegenden Englischkenntnissen lässt sich jedoch herausfinden, was die einzelnen Anweisungen bewirken.)

// Gast nur einlassen, wenn er eingeladen oder als nicht Eingeladener erwünscht ist
function GastEinlassen(AGast: TGast): Boolean;
begin
  if AGast.eingeladen then
    Result := True
  else
    Result := AGast.erwuenscht;
end;


Wenn der Gast eingeladen wurde, gibt diese Funktion True zurück, wenn er nicht eingeladen wurde, entsprechend, ob er erwünscht ist oder nicht.

Wenn Sie den Free Pascal Compiler verwenden, müssen Sie ihn mit fpc -Mdelphi aufrufen, oder {$MODE DELPHI} in die Quelldatei schreiben, damit die Pseudovariable Result erstellt wird, siehe Prozeduren und Funktionen.

Im Hauptprogramm läuft dann eine Schleife, die alle Gäste prüfen lässt und uns anzeigt, was unser Türsteher festgestellt hat.

// GastListe von Index 0 bis 3 durchlaufen
for Zaehler := 0 to 3 do
begin
  Gast := GastListe[Zaehler];
  Writeln('Name: ', Gast.Vorname, ' ', Gast.Name);
  if GastEinlassen(Gast) then
    Writeln('einlassen: ja')
  else
    Writeln('einlassen: nein');
  if Gast.eingeladen then
  begin
    Writeln('Gastnummer: ', Gast.Gastnummer);
    Writeln('Sitzplatz: ', Gast.Platz);
  end;
  Writeln;
end;


Vergessen Sie nicht, am Ende des Programms den vom dynamischen Array verwendeten Speicher wieder freizugeben, indem Sie SetLength(GastListe, 0); aufrufen.

Wenn Sie das Programm ausführen, werden Sie folgende Ausgabe erhalten:

Name: Axel Schweiss
einlassen: nein

Name: Peter Silie
einlassen: ja
Gastnummer: 1
Sitzplatz: 42

Name: Jack Pot
einlassen: ja

Name: Rosa Schluepfer
einlassen: ja
Gastnummer: 2
Sitzplatz: 14


Wie Sie sehen, erlaubt diese Art von Records größere Flexibilität für die Programmierung. Sie birgt jedoch auch die Gefahr schwer zu entdeckender Fehler, wenn man nicht sorgfältig genug programmiert.

Tricks mit varianten Records
[Bearbeiten]

Da bei varianten Records unterschiedliche Datentypen im selben Speicherbereich liegen, die je nach Zugriff anders interpretiert werden, kann man hiermit trickreiche und schnelle Datenumwandlungen durchführen. Man muss hierbei beachten, immer ein packed record zu verwenden. Bei nicht gepackten Records werden die Daten zur Erhöhung der Zugriffsgeschwindigkeit an Byte-Grenzen ausgerichtet. So können Lücken zwischen den Daten entstehen, die einer solchen Art der Datenumwandlung im Wege stehen.

Beispielsweise lässt sich mit einem varianten Record in einem Rutsch eine gesamte Zeichenkette quasi automatisch in die dezimalen ASCII-Werte umwandeln. Da keine tatsächliche Umwandlung erfolgt, sondern die Daten nur anders interpretiert werden, erfolgt dies zum „Nulltarif“, benötigt also keine Rechenzeit oder zusätzlichen Speicher. Es funktioniert wie folgt:

type
  TUmwandler = packed record
  case Byte of
    0: (Str: ShortString);
    1: (Bytes: array[0..255] of Byte);
  end;

var
  Umwandler: TUmwandler;
  i: Integer;

begin
  Umwandler.Str := 'Hallo Welt!';
  for i := 1 to Length(Umwandler.Str) do
    Writeln(Umwandler.Str[i], ' = ', Umwandler.Bytes[i]);
end.


Eine echte Zeichenkette als Feld des Records funktioniert hingegen nicht, da Zeichenketten vom Typ string intern nur Zeiger auf einen anderen Speicherbereich sind. Die Zählschleife beginnt bei 1 statt 0, da ShortString im Index 0 die Länge der Zeichenkette speichert.

Bei dem obigen Beispiel spart man sich die Umwandlung jedes einzelnen Zeichens mittels der Funktion Ord, bzw. in die andere Richtung mittels Chr.

Als zweites Beispiel kann man mit einem varianten Record auch prüfen, wie Daten im Arbeitsspeicher abgelegt werden. Wichtig für die Programmierung ist z.B. die so genannte Endianness, also die Reihenfolge, in der höher- und niederwertige Bytes abgelegt werden. Dazu kann man folgenden Trick verwenden:

type
  TEndianness = packed record
  case Byte of
    0: (IntWert: Integer);
    1: (Byte0, Byte1: Byte);
  end;

var
  Endian: TEndianness;

begin
  Endian.IntWert := $AFFE;  // höchstwertiges Byte steht links, also $AF
 
  case Endian.Byte0 of
    $AF: Writeln('Big-Endian');
    $FE: Writeln('Little-Endian');
  else
    Writeln('Endianness des Systems ist unbekannt.');
  end;
end.


Heutige PCs verwenden Little-Endian, das sollte also bei der Ausführung des Programms auch herauskommen. Byte0 entspricht dabei dem ersten im Speicher abgelegten Byte und Byte1 dem zweiten. Auch wenn unser Wert $AFFE ist, wird dieser im Arbeitsspeicher tatsächlich umgekehrt, also als $FEAF, abgelegt.

Sie werden eventuell bei Ihrer Programmiertätigkeit weitere Verwendungsmöglichkeiten hierfür entdecken. Diese Form der Datenumwandlung bzw. -auswertung ist sehr effizient. Dies betrifft sowohl Ausführungsgeschwindigkeit wie auch Speicherverbrauch. Zudem spart dies eine Menge Tipparbeit, die man sonst gegebenenfalls in die Programmierung mehr oder weniger umfangreicher Funktionen investieren musste.


Varianten

[Bearbeiten]
Was sind Varianten?
[Bearbeiten]

Varianten sind dynamische Variablentypen. Das heißt, eine Variante kann verschiedene Variablentypen (Integer, String, usw.) annehmen. Strukturierte Datentypen, wie Records, Mengen, statische Arrays, Dateien, Klassen, Klassenreferenzen und Zeiger können jedoch nicht zugewiesen werden.

Verwendung
[Bearbeiten]

Varianten werden häufig eingesetzt, wenn während der Entwicklung von Programmen der Datentyp einer Variable nicht bekannt ist oder sich während der Laufzeit ändert. Varianten bieten demnach die größtmögliche Flexibilität, jedoch verbrauchen sie mehr Speicher und verhalten sich während Operationen deutlich langsamer als statische Variablentypen.

Eine Variante benötigt 16 Bytes im Speicher und besteht aus einem Typencode und einem Wert oder einem Zeiger auf einen Wert, dessen Typ durch den Code festgelegt ist. Wenn eine Variante leer ist, bekommt sie den speziellen Wert Unassigned. Der Wert Null weist auf unbekannte oder fehlende Daten hin.

Um Varianten verwenden zu können, muss man die Unit Variants in die Uses-Klausel einbinden, falls diese dort noch nicht vorhanden ist. Die Zuweisung von Varianten erfolgt wie bei allen Variablen.

var
  vExample: Variant;
begin
  vExample := 'Hallo';      // Stringzuweisung
  vExample := 3.141592;     // Gleitkommazuweisung
  vExample := 666;          // Integerzuweisung
end.


Beim Auslesen von Varianten wird es schon komplizierter. Zur Vereinfachung stellt Delphi die Funktion VarType zur Verfügung. Dem Befehl VarType muss die betreffende Variante mitgegeben werden.

case VarType(vExample) of
  varWord:     Writeln('Es ist ein Word: ', vExample);
  varSmallInt: Writeln('Es ist ein SmallInt: ', vExample);
  varString:   Writeln('Es ist ein String: ', vExample);
  varDouble:   Writeln('Es ist ein Double: ', vExample);
end;


Bei der Zuweisung von Ganzzahlen verwendet Delphi immer automatisch den kleinstmöglichen Datentyp. Im oberen Beispiel weisen wir die Zahl 666 zu. Diese hat kein Vorzeichen und ist größer als ein Byte. Da der nächstgrößere Datentyp ohne Vorzeichen Word ist, erhält die Variante den Typ varWord. Über die anderen ganzzahligen und ebenfalls passenden Datentypen, wie varInteger, lässt sich diese Variable dann nicht auswerten.

Varianten verhalten sich genauso, wie der ihnen zugrunde liegende Datentyp. Z.B. kann man mit ihnen rechnen, wenn sie einen Zahlentyp besitzen oder Zeichenketten an andere Zeichenketten anhängen. Hat die Variante einen Zahlentyp und man will diese Zahl an eine Zeichenkette anhängen, funktioniert das ebensowenig wie z.B. beim Typ Integer. Hierbei muss man erst eine Datenumwandlung mittels IntToStr (für Ganzzahlen) oder FloatToStr (für Gleitkommazahlen) durchführen. Beide Funktionen befinden sich in der Unit SysUtils, die zu diesem Zweck eingebunden sein muss. Im obigen Beispiel ist diese Umwandlung nicht notwendig, da der Wert der Variante nicht direkt an die Zeichenkette angehängt, sondern als weiterer Parameter an Writeln übergeben wird. Writeln übernimmt dann automatisch die notwendige Umwandlung.

Eine vollständige Liste der von Delphi mitgelieferten Rückgabewerte von VarType(vExample) erhalten Sie in der Delphi-Hilfe.

Probleme
[Bearbeiten]

Unzulässige Operationen erzeugen bei statischen Variablen während des Kompilierens Fehler und können bereinigt werden. Bei Varianten treten jedoch erst zur Laufzeit Fehler auf, was die Fehlersuche und -bereinigung erschwert.

Varianten werden generell als unschöner Programmierstil angesehen und sollten in jedem Falle umgangen werden. Es ist immer möglich, eine andere Lösung als die Verwendung einer Varianten zu finden. Wenn Sie zum Beispiel an eine Funktion sowohl Ganzzahlen als auch Gleitkommazahlen übergeben möchten, können Sie dies durch eine Überladung der Funktion erreichen. Im Kapitel über Prozeduren und Funktionen lernen Sie später mehr darüber.


Variablen

[Bearbeiten]

Eine Variable ist eine Möglichkeit, Daten innerhalb eines Programms zu speichern und zu verwenden. Eine Variable steht repräsentativ für einen Bereich im Speicher, in dem der Wert der Variablen gespeichert ist. Über den Variablennamen kann dann einfach auf diese Speicherstelle zugegriffen werden. Eine Variable besteht in Delphi also immer aus einem Namen, einem Typ und einer Adresse im Speicher. Um die Adresse und den dazugehörigen Speicher müssen Sie sich jedoch nicht kümmern. Delphi weist automatisch jeder Variablen eine Adresse und den benötigten Speicherplatz zu, abhängig von deren Datentyp. Nur, wenn Sie einen Zeiger als Datentyp der Variablen verwenden, sind Sie selbst für diese Dinge – und die anschließenden „Aufräumarbeiten“ zuständig.

Variablen müssen deklariert, d.h. bekannt gemacht werden, bevor sie im Programm verwendet werden können. Dies geschieht im sogenannten Deklarationsabschnitt (vor begin), der mit dem Schlüsselwort var eingeleitet wird:

var
  s: string;
begin
  { ... }
end.


Dies erzeugt die Variable s vom Typ String. Nun können wir mittels s auf die Zeichenkette zugreifen.

Es ist ebenfalls möglich, mehrere Variablen eines Typs gleichzeitig zu deklarieren, indem die Namen der Variablen mit Kommata getrennt werden.

var
  s, s2: string;
  i: Integer;
begin
  { ... }
end.


Dies deklariert sowohl zwei Variablen vom Typ String (s und s2), als auch eine vom Typ Integer (i).

Werte zuweisen und auslesen

[Bearbeiten]

Um mit Variablen arbeiten zu können, müssen diesen im Programmablauf Werte zugewiesen werden. Dies geschieht mit dem Operator :=. Dabei steht auf der linken Seite des Operators die Variable, die einen Wert erhalten soll und auf der rechten Seite der entsprechende Wert. Zuweisungen können nur im Anweisungsblock erfolgen:

begin
  s := 'Ich bin eine Zeichenkette!';
  i := 64;
end.


Der Wert einer Variablen lässt sich auslesen, indem man den Variablenbezeichner an jeder Stelle einsetzt, an dem auch der Wert direkt eingesetzt werden kann. Man kann also den Wert einer Variablen auf dem Bildschirm ausgeben, oder auch mit ihr Berechnungen anstellen:

begin
  s := 'Kannst du das lesen?';
  Writeln(s);      // Gleichbedeutend mit Writeln('Kannst du das lesen?');
  i := 17;
  i := i * 2;      // anschließend ist i = 34
end.


Initialisierung von Variablen bei der Deklaration

[Bearbeiten]

Es ist sogar möglich (und auch ziemlich elegant) Variablen schon bei ihrer Deklaration einen Startwert mitzugeben. Damit kann man sich große Initialisierungsorgien nach dem begin ersparen.

var
  i: Integer = 42;
begin
  { ... }
end.


Die Initialisierung ist allerdings nur bei globalen Variablen möglich. Lokale Variablen von Prozeduren und Funktionen können dagegen auf diese Weise nicht mit Startwerten belegt werden.

Konstanten

[Bearbeiten]

Falls man ungewollten Änderungen eines Wertes im Laufe des Programms vorbeugen will oder sich der Wert per definitionem niemals ändern wird, sollte man Konstanten anstelle von Variablen verwenden. Diese werden ähnlich wie initialisierte Variablen deklariert. Statt des Schlüsselwortes var wird jedoch const benutzt. Der Typ einer Konstanten ergibt sich automatisch aus deren Wert:

const
  Zahl = 115;       // Konstante vom Typ Integer
  Text = 'Wort';    // Konstante vom Typ String
  Buchstabe = 'B';  // sowohl als String wie auch als Char einsetzbar


Konstanten können überall dort verwendet werden, wo Sie auch Variablen einsetzen können. Es gibt jedoch einige Funktionen, die Variablen als Parameter erfordern, da sie deren Wert bei der Ausführung ändern. Diesen Funktionen bzw. Prozeduren können dann keine Konstanten übergeben werden.

var
  i: Integer = 1;

const
  c = 1;

begin
  Inc(i);  // erhöht i um 1
  Inc(c);  // Fehlermeldung, da c kein neuer Wert zugewiesen werden darf
end.


Typisierte Konstanten

[Bearbeiten]

Typisierten Konstanten wird, wie der Name schon sagt, ein definierter Typ zugewiesen (statt dass der Typ aus dem Wert entnommen wird). Damit ist es möglich, einer Konstanten z.B. auch Records und Arrays zuzuweisen. Sie werden genau wie initialisierte Variablen definiert:

const
  Programmversion: Single = 1.0;
  { konstantes Array der ersten 6 Primzahlen }
  Prim6: array[1..6] of Byte = (2, 3, 5, 7, 11, 13);


Standardmäßig werden typisierte Konstanten wie normale Konstanten behandelt, d.h. dass diese während der Laufzeit nicht geändert werden können. Man kann Delphi jedoch mit der Compiler-Direktive {$J+} anweisen, diese ähnlich wie Variablen zu behandeln. Free Pascal unterstützt Zuweisungen an typisierte Konstanten ohne weiteres.


Operatoren

[Bearbeiten]

Wie kann man nun mit den vorhandenen Werten, Variablen und Konstanten arbeiten, also zum Beispiel rechnen, Werte übertragen oder vergleichen? Dazu benötigt man Operatoren. Einige davon haben Sie in den vorherigen Beispielen bereits kennen gelernt.

In diesem Kapitel erfahren Sie, welche Operatoren existieren und für welche Zwecke Sie diese verwenden können.

Verwendete Begriffe

[Bearbeiten]

Im Zusammenhang mit Operatoren werden bestimmte Begriffe verwendet. Rätselraten ist nicht jedermanns Sache, deswegen wollen wir diese vorher klären.

Zum einen unterscheiden sich die Operatoren in binäre und unäre. Binär (aus dem lateinischen bini, „je zwei“ bzw. bina, „paarweise“) werden solche Operatoren genannt, die jeweils zwei Operanden benötigen, also z.B. X + Y oder 3 * a. Daneben gibt es noch die unären Operatoren (lat. unus „ein, einer“), welche nur einen Operanden haben. Hiermit sind die Vorzeichen + und - gemeint, die in Delphi ebenfalls direkt vor einer Zahl oder einer Variablen stehen, z.B. -21, +15.9 oder -X. Weitere unäre Operatoren sind der Wahrheits- und logische Operator not, der Adressoperator @, sowie der Zeigeroperator ^, der ausnahmsweise hinter dem Operanden steht.

Allgemeine Operatoren

[Bearbeiten]

Zuweisungsoperator :=

[Bearbeiten]

Wie in den vorangegangen Beispielen schon kurz beschrieben, ist der Operator := für Zuweisungen von Werten an Variablen bestimmt. Hierbei steht auf der linken Seite die Variable, die einen Wert erhalten soll (hier sind nur Variablen, Klasseneigenschaften, sowie - mit entsprechender Kompilierung (siehe vorheriges Kapitel) - zuweisbare typisierte Konstanten erlaubt). Auf der rechten Seite können z.B. feste Werte stehen, Konstanten, andere Variablen, Klasseneigenschaften, Funktionsaufrufe und so weiter. Der Datentyp auf der rechten Seite muss dabei kompatibel zum Datentyp der Variablen auf der linken Seite sein.

const
  Konstante = 255;

var
  Fliesskomma: Double;
  Ganzzahl: Integer;
  Text: string;
  Zeichen: Char;

begin
  Fliesskomma := 6.5;     // erlaubt
  Fliesskomma := 20;      // erlaubt
  Ganzzahl := 5;          // erlaubt
  Ganzzahl := 17.385;     // nicht erlaubt, da nicht kompatibel
  Ganzzahl := Konstante;  // erlaubt
  Text := 'Hallo';
  Zeichen := 'Z';         // erlaubt, da nur 1 Zeichen
  Zeichen := 'Hallo';     // nicht erlaubt, da mehrere Zeichen = Zeichenkette
end.


Adressoperator @

[Bearbeiten]

Mit dem Adressoperator @ lässt sich die Speicheradresse jeder Variablen, Routine oder Klassenmethode ermitteln. Nicht erlaubt sind Typen, Konstanten, Interfaces und konstante Werte. Der Adressoperator gibt einen generischen Zeiger (Typ Pointer) auf die jeweilige Speicheradresse zurück.

Arithmetische Operatoren

[Bearbeiten]

Die arithmetischen Operatoren verwenden Sie für Berechnungen. Wie im „echten Leben“ gilt auch hier die Regel: Punktrechnung geht vor Strichrechnung. Wenn Sie also keine Klammern setzen, wird immer zuerst multipliziert und geteilt, bevor Addition und Subtraktion erfolgen. In Delphi gibt es außerdem noch weitere Operatoren, daher erfahren Sie am Ende dieses Kapitels, in welcher Rangfolge diese zueinander stehen.

Folgende arithmetische Operatoren gibt es:

Operator Funktion Ergebnistyp
+ Addition Ganzzahl, Fließkommazahl
- Subtraktion Ganzzahl, Fließkommazahl
* Multiplikation Ganzzahl, Fließkommazahl
/ Gleitkommadivision Fließkommazahl
div Ganzzahldivision Ganzzahl
mod Divisionsrest Ganzzahl

Der Ergebnistyp richtet sich nach den Datentypen der Operanden. Ist mindestens einer davon ein Fließkommatyp, so trifft dies auch auf das Ergebnis zu. Nur, wenn ausschließlich ganzahlige Typen verwendet werden, ist auch das Ergebnis ganzzahlig. Bei der Gleitkommadivision mittels / erhalten Sie, wie der Name schon sagt, immer eine Gleitkommazahl als Ergebnis. Bei div und mod erhalten Sie hingegen immer eine Ganzzahl, daher wird bei div das Ergebnis immer auf die nächste Ganzzahl abgerundet.

Wahrheitsoperatoren

[Bearbeiten]

Die Wahrheitsoperatoren werden nach dem englischen Mathematiker George Boole auch Boolesche Operatoren genannt. Diese verknüpfen verschiedene Wahrheitswerte miteinander und geben als Ergebnis wieder einen Wahrheitswert zurück. Als Operanden sind daher nur solche Ausdrücke erlaubt, die sich sozusagen mit „wahr“ oder „falsch“, bzw. „ja“ oder „nein“ beantworten lassen. Es sind auch Vergleiche als Operanden möglich, diese müssen dann in Klammern gesetzt werden.

Operator Funktion Beispiel
not Verneinung not (X > Y)
and Und-Verknüpfung DateiGefunden and (Eintraege > 2)
or Oder-Verknüpfung (PLZ in [20001..22769]) or (Ort = 'Hamburg')
xor exklusive Oder-Verknüpfung rechts xor links
  • Die Verneinung not kehrt den Wert des Operanden um.
  • Die Und-Verknüpfung and ergibt nur dann „wahr“, wenn beide Operanden wahr sind.
  • Die Oder-Verknüpfung or ergibt „wahr“, wenn mindestens 1 Operand wahr ist.
  • Die exklusive Oder-Verknüpfung xor ergibt hingegen nur „wahr“, wenn genau 1 Operand wahr ist.

Eine Und-Verknüpfung ergibt immer „falsch“, sobald ein Operand falsch ist. Aus diesem Grund bietet Delphi die Möglichkeit, alle weiteren Prüfungen abzubrechen, wenn bereits der erste Operand „falsch“ ist. Diese vollständige Auswertung kann mit {$B-} deaktiviert werden, was bei Delphi bereits voreingestellt ist.

Logische Operatoren

[Bearbeiten]

Die logischen Operatoren verwendet man zum bitweisen Auswerten und Ändern von Ganzzahlen. Sie verhalten sich so ähnlich wie die Wahrheitsoperatoren. Bei den Wahrheitsoperatoren wird immer nur 1 Bit bearbeitet und dieses als „wahr“ oder „falsch“ interpretiert. Die logischen Operatoren hingegen verwenden mehrere Bits gleichzeitig, das Ergebnis ist dann jeweils wieder eine neue Zahl.

Operator Funktion Beispiel
not bitweise Negation not X
and bitweise Und-Verknüpfung Y and 32
or bitweise Oder-Verknüpfung X or Y
xor bitweise exklusive Oder-Verknüpfung Z xor B
shl bitweise Verschiebung nach links C shl 4
shr bitweise Verschiebung nach rechts T shr 2


Exkurs: binäres Zahlensystem

Um mit der bitweisen Verschiebung zu arbeiten, muss man sich im binären Zahlensystem auskennen. Jede Zahl setzt sich dabei aus den einzelnen Zahlwerten 20, 21, 22,...2n zusammen. Die 2 bedeutet hierbei, dass es nur zwei Möglichkeiten gibt, also 0 oder 1. „n“ ist das für den Zahltyp höchstmögliche Bit. Das kleinste Bit steht dabei ganz rechts, das höchste links.

Ein kurzer Überblick, bezogen auf ein Byte:

Bit-Nr. 7 6 5 4 3 2 1 0
Wert (2Bit) 128 64 32 16 8 4 2 1
Beispiel für 17 0 0 0 1 0 0 0 1

Die 17 hat damit den Binärcode 00010001 und errechnet sich aus 1*24 + 1*20. 17 shl 2 ergibt somit 01000100 = 1*26 + 1*22 = 64 + 4 = 68.

Merke: Es gibt nur 10 Typen von Menschen - solche, die Binärcode lesen können, und solche, die es nicht können!

Vergleichsoperatoren

[Bearbeiten]

Wie der Name schon sagt, dienen diese Operatoren dazu, zwei Werte miteinander zu vergleichen. Die Datentypen auf beiden Seiten des Operators müssen zueinander kompatibel sein. Als Ergebnis erhält man immer einen Wahrheitswert, also true oder false.

Operator Funktion Beispiel
= gleich X = 17
<> ungleich Text <> "Hallo"
< kleiner als Laenge < Max
> größer als Einkommen > 999.99
<= kleiner als oder gleich Index <= X
>= größer als oder gleich Alter >= 18

Zeichenkettenoperatoren

[Bearbeiten]

Bis auf die Vergleichsoperatoren gibt es für Zeichenketten nur den Operator +, mit dem man zwei Zeichenketten oder eine Zeichenkette mit einem einzelnen Zeichen verketten kann.

Beispiele:

'Hallo' + ' Welt' ergibt 'Hallo Welt'
'Hallo Welt' + Chr(33) ergibt 'Hallo Welt!'

Mengenoperatoren

[Bearbeiten]

Mengen können ebenfalls auf einfache Weise bearbeitet werden. Hierfür stehen folgende Operatoren zur Verfügung:

Operator Funktion Ergebnistyp Beispiel Ergebnis
+ Vereinigung Menge Menge + [1, 3, 5] alle Elemente von Menge, zusätzlich 1, 3 und 5
- Differenz Menge Menge1 - Menge2 alle Elemente von Menge1 ohne die von Menge2
* Schnittmenge Menge Menge1 * Menge2 alle Elemente, die sowohl in Menge1, als auch in Menge2 enthalten sind
<= Untermenge Wahrheitswert [7, 8] <= Menge true, wenn 7 und 8 in Menge enthalten sind
>= Obermenge Wahrheitswert Menge1 >= Menge2 true, wenn Menge2 vollständig in Menge1 enthalten ist
= gleich Wahrheitswert Menge = [2, 4, 6] true, wenn Menge nur aus den Zahlen 2, 4 und 6 besteht
<> ungleich Wahrheitswert Tage <> [1, 8, 15, 22, 29] true, wenn Tage nicht aus den Elementen 1, 8, 15, 22 und 29 besteht
in Element von Wahrheitswert X in Menge true, wenn der Wert X in Menge enthalten ist

Hinweis: Bei in muss der erste Operand ein Einzelwert vom Typ der Elemente der geprüften Menge sein. Alle anderen Operatoren erfordern Mengen auf beiden Seiten.

Zeigeroperatoren

[Bearbeiten]

Für die Arbeit mit Zeigern sind die folgenden Operatoren bestimmt:

Operator Funktion Ergebnistyp Beispiel
+ Addition Zeiger auf ein Zeichen P + X
- Subtraktion Zeiger auf ein Zeichen oder Ganzzahl P - X
^ Dereferenz Basistyp des Zeigers PFachnummer^
= gleich Wahrheitswert Pointer1 = Pointer2
<> ungleich Wahrheitswert Pointer1 <> Pointer2

Bei der Addition und Subtraktion können immer nur Zeiger auf Zeichen verwendet werden, also PChar, PAnsiChar und PWideChar. Es kann immer nur eine Ganzzahl zu einem solchen Zeiger addiert werden, was bedeutet, dass die Adresse um diese Anzahl Zeichen nach hinten verschoben wird. Die Subtraktion funktioniert hierbei genauso, verschiebt den Zeiger jedoch nach vorne. Man kann auch zwei Zeichenzeiger voneinander subtrahieren. In diesem Falle erhält man eine Ganzzahl, die die Differenz zwischen beiden Zeigeradressen angibt (den so genannten Offset).

Die Dereferenz ist bei allen typisierten Zeigern möglich, man erhält damit den Zugriff auf die Daten, die an dieser Speicheradresse liegen. Da beim generischen Typ Pointer die Art der Daten an dessen Speicheradresse nicht bekannt ist, kann bei Variablen dieses Typs keine direkte Dereferenzierung verwendet werden. Man muss zuerst eine Typumwandlung auf die auszulesenden oder zu ändernden Daten durchführen.

Der Vergleich zweier Zeiger mittels = und <> erfolgt nur in Bezug auf die Speicheradresse, auf die diese zeigen, nicht auf den Inhalt der Daten an diesen Adressen. Also nur, wenn bei beiden Zeigern die Speicheradresse identisch ist, gibt der Operator = den Wert true zurück. Ein kleines Beispiel:

var
  p1, p2: ^Integer;
  p3: Pointer;

begin
  New(p1);            // Speicher für den 1. Zeiger reservieren
  New(p2);            // Speicher für den 2. Zeiger reservieren
  p1^ := 200;         // an beiden Speicheradressen werden dieselben Daten abgelegt
  p2^ := 200;         // oder: p2^ := p1^
  Writeln(p1 = p2);   // Ausgabe: FALSE
  Writeln(p1 <> p2);  // Ausgabe: TRUE
  Dispose(p2);        // Speicher des 2. Zeigers freigeben
  p2 := p1;           // Adresse des 2. Zeigers auf die des 1. setzen
  Writeln(p1 = p2);   // Ausgabe: TRUE
  Writeln(p1 <> p2);  // Ausgabe: FALSE
  Dispose(p1);

  { Arbeit mit untypisierten Zeigern }

  GetMem(p3, 1);      // Speicher für den 3. Zeiger reservieren, Größe muss der Verwendung entsprechen
  Byte(p3^) := 42;    // eine Zahl eintragen
  Writeln(Byte(p3^)); // gibt 42 aus
  Char(p3^) := 'o';   // jetzt ein Zeichen zuweisen
  Writeln(Char(p3^)); // gibt o aus
  Writeln(Byte(p3^)); // erlaubt! Gibt Dezimalwert von o, also 111 aus
  FreeMem(p3);        // Speicher für den 3. Zeiger freigeben
end.


Klassenoperatoren

[Bearbeiten]

Um mit Klassen zu arbeiten, können folgende Operatoren verwendet werden:

Operator Funktion Ergebnistyp Beispiel
= gleich Wahrheitswert Instanz1 = Instanz2
<> ungleich Wahrheitswert Var1 <> Var2
is Klassentyp prüfen Wahrheitswert Instanz is TKlasse
as Klassentyp interpretieren Klasseninstanz Instanz as TKlasse

Bei den Vergleichen mittels = und <> müssen zwei Klasseninstanzen als Operanden angegeben werden. Da es sich bei Klasseninstanzen intern um Zeiger handelt, funktionieren diese beiden Operatoren genau wie oben bei den Zeigeroperatoren beschrieben.

Mit dem Operator is kann geprüft werden, ob eine Klasseninstanz von einem bestimmten Klassentyp bzw. von diesem abgeleitet ist. Links steht dabei die Klasseninstanz, rechts der zu prüfende Klassentyp.

Mittels as lässt sich eine Klasseninstanz wie ein anderer Klassentyp interpretieren. Der Klassentyp auf der rechten Seite muss dabei ein Nachfahre der Instanz auf der linken Seite sein und die Instanz muss als dieser Typ erstellt worden sein. Daher muss vorher mittels is geprüft werden, ob die Instanz auch den benötigten Typ hat.

Da sich alle Klassen von TObject ableiten, kann man hiermit generisch verschiedene Klassen in seiner Anwendung „herumreichen“. Je nachdem, welcher Typ dann tatsächlich vorliegt, können die entsprechenden Methoden aufgerufen werden. Das Thema Klassenoperationen wird ausführlich in einem späteren Kapitel behandelt.

Rangfolge der Operatoren

[Bearbeiten]

Die verschiedenen Operatoren werden in einer bestimmten Reihenfolge ausgeführt. Wenn man nichts anderes benötigt, gilt auch in Delphi der Satz „Punktrechnung vor Strichrechnung“. Hier ist eine kleine Übersicht, in welcher Rangfolge alle Operatoren zueinander stehen.

Rang Operatoren
1. @, not
2. *, /, div, mod, and, shl, shr, as
3. +, -, or, xor
4. =, <, >, <>, <=, >=, in, is

Die höherwertigsten Operationen werden immer zuerst ausgeführt, gleichrangige von links nach rechts. Um die Reihenfolge zu ändern, muss man die gewünschten vorrangigen Operationen in Klammern setzen (z.B. 1 + 2 * 3 ergibt 7, (1 + 2) * 3 hingegen 9).


Eingabe und Ausgabe

[Bearbeiten]

Um mit dem Benutzer zu kommunizieren, muss der Computer die eingegebenen Daten (Input) speichern, verarbeiten und dann später ausgeben (Output). Um das Verarbeiten kümmert sich das nächste Kapitel; in diesem Kapitel dreht sich alles um das Ein- und Ausgeben von Daten.

Eingaben erfassen

[Bearbeiten]

Kommen wir nun zu unserem eigentlichen Ziel, dem Einlesen und Verarbeiten von Eingaben auf der Konsole. Unser Ausgangs-Programm:

program Eingabeerfassung;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  { ... }

begin
  { ... }
  Readln;
end.


Nun erweitern wir den Anweisungsblock um die Ausgabe der Nachricht: 'Bitte geben Sie ihren Vornamen ein:'; Dies sollte kein größeres Problem darstellen. Als nächstes legen wir eine Variable an, in der wir den Vornamen des Benutzers speichern wollen. Wir fügen also

Vorname: string;


in den „Var-Abschnitt“ ein. Nun wollen wir den Vornamen von der Konsole einlesen und in der Variable Vorname speichern dies geschieht mittels des folgenden Aufrufes im Hauptprogrammteil:

Readln(Vorname);


Vorname wird hier der Routine Readln als Parameter übergeben. Da es sich bei Readln um eine sehr spezielle Funktion handelt, gehen wir nicht näher auf dieses Konstrukt ein. Uns genügt die Tatsache, dass sich der in die Konsole eingegebene Text, nach dem Ausführen des Befehls in der Variable Vorname befindet.

Variablen ausgeben

[Bearbeiten]

Ähnlich der Eingabe mit Readln, kann man mit Writeln Variablen ausgeben. Beispielsweise so:

Writeln(Vorname);


Nun wird der gerade eingegebene Vorname auf dem Bildschirm ausgegeben.

Schreiben wir jetzt das erste Programm, das aus der Eingabe eine Ausgabe „berechnet“, denn dazu sind Programme im Allgemeinen da.

Unser Programm soll den Benutzer fragen, wie er heißt und ihn dann mit dem Namen begrüßen, und zwar so, dass er die Begrüßung auch sieht.

program Eingabeerfassung;

{$APPTYPE CONSOLE}

uses
  SysUtils;

var
  Vorname: string;

begin
  Writeln('Wie heisst du?');
  Readln(Vorname);
  Writeln('Hallo ', Vorname, '! Schoen, dass du mal vorbeischaust.');
  Readln;
end.


Hier wird deutlich, dass Writeln mehrere Parameter (nämlich eine beliebige Anzahl) annimmt und deren Wert auf dem Bildschirm ausgibt. Dabei ist es egal, ob dieser aus einer Variablen stammt, direkt angegeben wird oder wie hier eine Kombination von beidem darstellt. Die Parameter können jeden beliebigen einfachen Datentyp haben, jedoch nicht Aufzählungen, Mengen, Arrays, Records, Klassen und Zeiger. Die Typ Variant wiederum ist erlaubt.

Alternativ kann man Strings und String-Variablen übrigens auch miteinander verketten:

Writeln('Hallo ' + Vorname + '! Schoen, dass du mal vorbeischaust.');


In diesem Kapitel wollen wir uns mit Verzweigungen beschäftigen. Dabei geht es darum, den Programmablauf anhand von Bedingungen zu steuern.

If-Abfrage (Wenn-Dann-Sonst)

[Bearbeiten]

Aufbau

[Bearbeiten]

Bisher hat der Computer auf jede Eingabe in gleicher Art reagiert. Nun wollen wir beispielsweise, dass der Computer nur Hugo begrüßt. Dazu brauchen wir eine so genannte „If-Abfrage“. Dabei überprüft der Computer, ob ein bestimmter Fall wahr (true) oder falsch (false) ist. Die Syntax sieht, da in Pascal ja alles im Englischen steht, so aus:

if Bedingung then Anweisung

Die Bedingung ist dabei eine Verknüpfung von Vergleichen, oder ein einzelner Wahrheitswert. Aber dazu später mehr. Erstmal wollen wir uns dem Vergleichen widmen:

Es gibt in Delphi die folgenden sechs Vergleichsoperatoren:

Operator(en) prüft, ob...
= die beiden Werte gleich sind.
> und < einer der Werte größer ist (bei Strings die Länge).
>= und <= einer der Werte größer oder gleich ist.
<> die beiden Werte unterschiedlich sind.


Tipp:

Die Abfrage a <> b ist dasselbe wie not (a = b)


Einfache if-Abfragen

[Bearbeiten]

Nun aber erstmal wieder ein Beispiel, bevor wir zu den Verknüpfungen weitergehen:

Wir wollen jetzt nachsehen, ob Hugo seinen Namen eingegeben hat. Dazu setzen wir vor den Befehl Writeln('Hallo '+Vorname+'! Schoen, dass du mal vorbeischaust.'); die folgende If-Abfrage:

if Vorname = 'Hugo' then

Die Bedingung ist hier wahr, wenn der eingegebene Vorname „Hugo“ ist. Dabei steht Vorname nicht in Apostrophen, da es der Name der Variablen ist. Hugo ist jedoch der Wert, mit dem die Variable verglichen werden soll, so dass hier die Apostrophe zwingend sind.

Das gesamte Programm sieht jetzt also so aus:

program Eingabeerfassung;
  
{$APPTYPE CONSOLE}
 
uses
  SysUtils;
 
var
  Vorname: string;

begin
  Writeln('Wie heisst du?');
  Readln(Vorname);
  if Vorname = 'Hugo' then
    Writeln('Hallo '+Vorname+'! Schoen, dass du mal vorbeischaust.');
  Readln;
end.

Erweiterte if-Abfragen mit „sonst“

[Bearbeiten]

Wer dieses Programm einmal getestet hat, wird festgestellt haben, dass es keine weitere Ausgabe gibt, wenn der eingegebene Name ungleich „Hugo“ ist. Dies wollen wir nun ändern. Dazu erweitern wir unsere Syntax ein klein wenig:

if Bedingung then Anweisung else Anweisung

Die auf else folgende Anweisung wird dann aufgerufen, wenn die Bedingung der if-Abfrage nicht zutrifft (also false ist). Unser so erweitertes Programm sieht nun also in etwa so aus:

program Eingabeerfassung;
  
{$APPTYPE CONSOLE}
 
uses
  SysUtils;
 
var
  Vorname: string;

begin
  Writeln('Wie heisst du?');
  Readln(Vorname);
  if Vorname = 'Hugo' then
    Writeln('Hallo '+Vorname+'! Schoen, dass du mal vorbeischaust.')
  else
    Writeln('Hallo '+Vorname+'! Du bist zwar nicht Hugo, aber trotzdem willkommen.');
  Readln;
end.

Zusammenhang mit dem Typ Boolean

[Bearbeiten]

If-Abfragen hängen eng mit dem Typ Boolean zusammen: So können Booleans den Wahrheitswert der Abfrage speichern, indem ihnen die Bedingung (bzw. das Ergebnis einer Wahrheitsprüfung) „zugewiesen“ wird:

var
  IstHugo: Boolean;
  ...
begin
  ...
  IstHugo := Vorname = 'Hugo';
  ...
end.

Diese Variable kann jetzt wiederum in if-Abfragen verwendet werden:

if IstHugo then
  ...

Für Boolean-Variablen ist also kein Vergleich nötig, da sie bereits einen Wahrheitswert darstellen.

Verknüpfung von Bedingungen

[Bearbeiten]

Wollen wir jetzt mehrere Bedingungen gleichzeitig abfragen, wäre es natürlich möglich, mehrere if-Abfragen zu schachteln:

program Eingabeerfassung2;
  
{$APPTYPE CONSOLE}
 
uses
  SysUtils;
 
var
  Vorname, Nachname: string;
begin
  Writeln('Wie ist dein Vorname?');
  Readln(Vorname);
  Writeln('Wie ist dein Nachname?');
  Readln(Nachname);
  if Vorname = 'Hugo' then
    if Nachname = 'Boss' then
      Writeln('Hallo '+Vorname+' '+Nachname+'! Schoen, dass du mal vorbeischaust.');
  Readln;
end.

Hier fallen aber gleich mehrere Probleme auf:

  • Für komplizierte Abfragen wird dies sehr unübersichtlich
  • Wollten wir eine Nachricht an alle abgeben, die nicht „Hugo Boss“ heißen, müssten wir für jede der if-Abfragen einen eigenen else-Zweig erstellen.

Um diese Probleme zu lösen, wollen wir die beiden Abfragen in eine einzelne umwandeln. Dazu bietet Delphi den and-Operator, mit dem wir zwei Vergleiche mit „und“ verknüpfen können. Alles was wir tun müssen, ist um jeden der Vergleiche Klammern und ein „and“ dazwischen zu setzen:

  ...
  if (Vorname = 'Hugo') and (Nachname = 'Boss') then
    Writeln('Hallo '+Vorname+' '+Nachname+'! Schoen, dass du mal vorbeischaust.')
  else
    Writeln('Hallo '+Vorname+' '+Nachname+'! Du bist zwar nicht Hugo Boss, aber trotzdem willkommen.')
  ...

and ist nicht der einzige Operator:

Operator Funktion
and beide Bedingungen müssen erfüllt sein
or mindestens eine Bedingung muss erfüllt sein
xor genau eine Bedingung muss erfüllt sein
not kehrt das Ergebnis um (Boolean)


Fallunterscheidung (case)

[Bearbeiten]

If-Abfragen können schnell unübersichtlich werden, wenn man mehrere Werte unterschiedlich behandeln möchte. Für solche Fälle steht eine mehrfache Verzweigung zur Verfügung, also eine Fallunterscheidung – die Case-Abfrage. Mit ihrer Hilfe wird nur einmal der Wert einer Variablen abgerufen und das Programm springt dann direkt an die Stelle, die für den entsprechenden Wert ausgeführt werden soll. Eine Case-Abfrage ist folgendermaßen aufgebaut:

case <Variable> of
  Wert1:
    <Anweisung>
  Wert2:
    <Anweisung>
  Wert3, Wert4:
    <Anweisung>
  ...
else
  <Anweisung>
end;

Mehrere Anweisungen können in einen begin...end Block eingeschlossen werden.

Sollen bei verschiedenen Werten die gleichen Aktionen ausgeführt werden, können diese Werte mit Kommata von einander getrennt werden. Ebenfalls möglich ist die Angabe eines ganzen Wertebereichs in der Form Wert1..Wert4. Hierbei ist jedoch zu beachten, dass immer nur ein einzelner Wert einer einzelnen Variablen ausgewertet wird. Eine Verknüpfung wie bei der If-Abfrage ist so nicht möglich, dies ginge nur in einer weiteren verschachtelten Case- oder If-Abfrage.

Die Angabe eines else-Abschnitts ist optional. Besitzt die Variable einen Wert, der nicht angegeben wurde, werden die Anweisungen hinter else ausgeführt. Fehlt else, wird die Abfrage ohne weitere Aktionen verlassen.

Ein anderer (großer) Nachteil der Case-Abfrage liegt darin, dass diese nur Aufzählungsdatentypen verarbeiten kann. Der sehr häufig abgefragte Datentyp string kann nicht mit case verwendet werden. Unser obiges Beispiel lässt sich daher nicht auf Case-Abfragen umstellen, wir nehmen also ein neues: Die Auswertung von Zeichen.

var
  Zeichen: Char;

begin
  Write('Geben Sie bitte ein Zeichen ein: ');
  Readln(Zeichen);

  Write('Das Zeichen war ');
  case Zeichen of
    #0..#31:
      Writeln('ein Steuerzeichen.');
    'a'..'z':
      Writeln('ein Kleinbuchstabe.');
    'A'..'Z':
      Writeln('ein Grossbuchstabe.'); 
    '0', '2', '4', '6', '8':
      Writeln('eine gerade Ziffer.');
    '1', '3', '5', '7', '9':
      Writeln('eine ungerade Ziffer.');
    '.':
      Writeln('ein Punkt.');
    ',':
      Writeln('ein Komma.');
  else
    Writeln('eines, auf das ich nicht programmiert bin.');
  end;
  Readln;
end.

Das Programm fordert wieder zur Eingabe auf und erwartet diesmal ein einzelnes Zeichen. Sollte mehr eingegeben werden, „ignoriert“ Readln einfach den Rest. Bei der anschließenden Auswertung springt das Programm nun an die entsprechende Stelle der Case-Abfrage. Ist dort das eingegebene Zeichen nicht aufgelistet, wird der Text unter else ausgegeben.

Zu den Aufzählungstypen zählt neben Ganzzahlen und Zeichen im Übrigen auch Boolean. Solche Variablen können Sie daher auch in einer Case-Abfrage verarbeiten.


Schachtelungen

[Bearbeiten]

Bisher haben wir bei unseren Abfragen immer nur einen einzelnen Befehl ausgeführt. Natürlich ist es aber auch möglich, mehrere Befehle nacheinander auszuführen, wenn die Bedingung erfüllt ist.

Dabei wäre es unübersichtlich, für jeden Befehl eine neue If-Abfrage zu starten, wie hier:

if IstHugo then Writeln('Hallo Hugo!');
if IstHugo then Writeln('Wie geht es dir?');

Man kann die Befehle, die zusammen gehören auch schachteln. Dann ist nur noch eine If-Abfrage nötig.

Die zu schachtelnden Befehle werden dabei zwischen ein begin und ein end geschrieben. Diese Schachtel wird nun als ein eigenständiger Befehl angesehen. Deshalb muss das end oftmals mit einem Semikolon geschrieben werden.

Beispiel:

var
  ErsterAufruf: Boolean;
begin
  ...

  if ErsterAufruf then
  begin
    WriteLn('Dies ist der erste Aufruf!');
    ErsterAufruf := False;
  end;
 
  ...
end.

In diesem Fall wird, sofern ErsterAufruf wahr (True) ist, eine Nachricht ausgegeben und ErsterAufruf auf False gesetzt.

Schachtelungen werden vor allem in Zusammenhang mit if-Abfragen und Schleifen angewandt. Wie man in dem Beispiel auch erkennen kann, ist es üblich, die geschachtelten Befehle einzurücken, um die Übersichtlichkeit zu verbessern.

Schleifen

[Bearbeiten]

Die while-Schleife (Kopfgesteuerte-Schleife)

[Bearbeiten]

Die while-Schleife ermöglicht es, einen Programmcode so oft auszuführen, solange eine Bedingung erfüllt ist. Ist die Bedingung schon vor dem ersten Durchlauf nicht erfüllt, wird die Schleife übersprungen.

while <Bedingung> do
  <Programmcode>

Hierbei wird vor jedem Durchlauf die Bedingung erneut überprüft.

Beispiel:

var
  a, b: Integer;
begin
  a := 100;
  b := 100;
  while a >= b do
  begin
    Writeln('Bitte geben Sie einen Wert < ', b, ' ein.');
    Readln(a);
  end;
end.

Die Schleife wird mindestens einmal durchlaufen, da zu Beginn die Bedingung 100 = 100 erfüllt ist.

Die repeat-until-Schleife (Fußgesteuerte-Schleife)

[Bearbeiten]

Die repeat-until-Schleife ähnelt der while-Schleife. Hier wird die Bedingung jedoch nach jedem Durchlauf überprüft, so dass sie mindestens einmal durchlaufen wird.

repeat
  <Programmcode>
until <Bedingung>;

Des Weiteren wird die repeat-until-Schleife im Gegensatz zur while-Schleife so lange durchlaufen, bis die Bedingung erfüllt ist.

Das obere Beispiel:

var
  a, b: Integer;
begin
  b := 100;
  repeat
    Writeln('Bitte geben Sie einen Wert < ', b, ' ein.');
    Readln(a);
  until a < b;
end.

Hier ist dieser Schleifentyp von Vorteil, da die Initialisierung von a entfällt, weil die Schleife mindestens einmal durchlaufen wird.

Ein weiterer Vorteil der repeat-until-Schleife ist, dass sie die einzige Schleife ist, die ohne Schachtelung auskommt, da der Programmcode von repeat und until bereits eingegrenzt ist. Bei den anderen Schleifen folgt der Programmcode immer der Schleifendeklaration.

Die for-to-/for-downto-Schleife

[Bearbeiten]

Im Gegensatz zu den anderen beiden Schleifentypen basiert die for-Schleife nicht auf einer Bedingung, sondern auf einer bestimmten Anzahl an Wiederholungen.

 for variable := <untergrenze> to <obergrenze> do
   <Programmcode>
 for variable := <obergrenze> downto <untergrenze> do
   <Programmcode>

Der Unterschied zwischen den beiden Schleifen liegt darin, dass die erste von unten nach oben, die zweite jedoch von oben nach unten zählt. Falls die Untergrenze größer als die Obergrenze ist, wird jedoch nichts ausgeführt.

Beispiele:

for i := 0 to 4 do
  Writeln(i);
Ausgabe:
0
1
2
3
4
for i := 4 downto 0 do
  Writeln(i);
Ausgabe:
4
3
2
1
0
for i := 4 to 3 do
  Writeln(i);
Ausgabe:
-keine-
for i := 4 to 4 do
  Writeln(i);
Ausgabe:
4

Es ist auch möglich, als Ober- und/oder Untergrenze Variablen anzugeben:

for i := a to b do
  Writeln(i);

Die Schleife verhält sich hierbei genauso wie die Variante mit Konstanten.

Als Variablentyp ist nicht nur Integer erlaubt: for-Schleifen unterstützen alle Ordinalen Typen, also Integer, Aufzählungen und Buchstaben.

Beispiele:

var
  i: Integer;
begin
  for i := 0 to 10 do
    Writeln(i);
end.
var
  c: Char;
begin
  for c := 'a' to 'z' do
    Writeln(c);
end.
type
  TAmpel = (aRot, aGelb, aGruen);
var
  ampel: TAmpel;
begin
  for ampel := aRot to aGruen do
    Writeln(Ord(ampel));
end.

Im Gegensatz zu while- und repeat-until-Schleifen darf in einer for-Schleife die Bedingungsvariable nicht geändert werden. Daher führt folgender Abschnitt zu einer Fehlermeldung:

for i := 0 to 5 do
begin
  i := i - 1;  // <-- Fehler
  Writeln(i);
end;


Die for-in-Schleife

[Bearbeiten]

Bei dieser, erst in neueren Delphi-Versionen vorhandenen Schleifenart, handelt es sich um eine aus Listen auswählende Schleife. Diese geht alle Elemente einer Liste durch und übergibt an die Laufvariable jeweils den aktuellen Listeneintrag. Die Anzahl der Durchläufe bestimmt sich durch die Länge der Liste. Die for-in-Schleife ist folgendermaßen aufgebaut:

for variable in liste do
  <Anweisung>;

Der Begriff „Liste“ ist hierbei recht weit gefasst. Als Listenvariable kann sowohl ein Array, wie auch ein Set oder ein String verwendet werden. Auch einige Klassen (z.B. TList, TStrings oder TToolBar) unterstützen diese Auswertung.

Der Typ der Laufvariablen muss immer einem einzelnen Element der Liste entsprechen. Bei String wäre dies Char, bei einem array of Byte der Typ Byte.

Der Vorteil der for-in-Schleife liegt darin, dass man eine Zählvariable spart und an Ort und Stelle den Werte eines Listenelements geliefert bekommt. Vor allem muss man hierbei die Grenzen der Liste nicht kennen oder auswerten, also mit welchem Index sie beginnt und endet.

Die folgenden beiden Beispiele sind gleichbedeutend:

{ ab Delphi 2005 }
var
  Lottozahlen: array[1..6] of Byte = (2, 3, 5, 7, 11, 13);
  Element: Byte;

begin
  for Element in Lottozahlen do
    Writeln(Element);
end.
{ vor Delphi 2005 }
var
  Lottozahlen: array[1..6] of Byte = (2, 3, 5, 7, 11, 13);
  Zaehler: Byte;

begin
  for Zaehler := 1 to 6 do
    Writeln(Lottozahlen[Zaehler]);
end.

Hinweis für Benutzer von FreePascal: FreePascal unterstützt for-in-Schleifen erst ab Version 2.4.2.

Vorzeitiger Abbruch einer Schleife

[Bearbeiten]

In manchen Fällen ist es hilfreich, dass nicht jeder Schleifendurchgang komplett ausgeführt wird, oder dass die Schleife bereits früher als von der Schleifenbedingung vorgegeben verlassen werden soll. Hierfür stehen die Routinen Continue und Break zur Verfügung.

Beispiel: Sie suchen in einer Liste von Strings eine bestimmte Zeichenfolge:

var
  a: array[1..5] of string;
  i: Integer;
  e: Boolean;

begin
  a[1] := 'Ich';
  a[2] := 'werde';
  a[3] := 'dich';
  a[4] := 'schon';
  a[5] := 'finden!';

  e := False;
  i := 1;

  repeat
    if a[i] = 'dich' then
      e := True
    else
      Inc(i);
  until e or (i > 5);

  if i <= 5 then
    Writeln('Das Wort "dich" wurde an Position ', i, ' gefunden.')
  else
    Writeln('Das Wort "dich" wurde nicht gefunden.');
end.

In diesem Beispiel steht das gesuchte Wort „dich“ an der Position 3 in der Liste. Um nicht alle Elemente durchzusuchen, haben wir hier eine repeat-until-Schleife verwendet, die dann beendet wird, sobald das Wort gefunden wird. Jedesmal, wenn das Wort nicht gefunden wurde, wird der Index um 1 erhöht.

Einfacher wäre eine for-Schleife. Diese durchsucht alle Elemente und gibt nach einem definierten Abbruch den letzten Zählwert zurück. Um den rechtzeitigen Abbruch zu erreichen, verwenden wir die Anweisung Break:

  for i := 1 to 5 do
  begin
    if a[i] = 'dich' then
      Break;
  end;

Hier benötigen wir weder eine Prüfvariable, noch muss i vorher initialisiert werden. Auch hier erfolgt der Schleifenabbruch, sobald das Wort "dich" gefunden wurde. Dieser Abbruch kann jedoch in einer Zählschleife nicht sauber durch eine Bedingung erfolgen, sondern muss durch die Anweisung Break erzwungen werden. In diesem Falle erfolgen die Schleifendurchläufe mit den Werten 4 und 5 gar nicht.

Übrigens: Wird das Wort nicht gefunden und die Schleife somit vollständig durchlaufen, funktioniert unser Programm unter Delphi dennoch korrekt. Das liegt daran, dass die Zählvariable i praktisch immer am Ende des Schleifendurchlaufs erhöht wird und dann beim nächsten Sprung an den Anfang geprüft wird, ob die Obergrenze überschritten wurde. Läuft die Schleife also vollständig durch, hat die Zählvariable anschließend den Wert von Obergrenze + 1, in unserem Beispiel also 6. Bei der for-downto-Schleife gilt analog, dass die Zählvariable am Ende den Wert Untergrenze - 1 besitzt. FreePascal verhält sich hier anders. Da hat die Zählvariable am Ende der Schleife den jeweiligen Wert der Ober- bzw. Untergrenze (5 in unserem Falle). Hier müssten wir die 6 also selbst mit einbeziehen, um noch feststellen zu können, ob sich das Wort tatsächlich an der letzten Stelle befindet oder einfach nur die Schleife erfolglos durchgelaufen ist:

  for i := 1 to 6 do
  begin
    if i = 6 then
      Break;
    if a[i] = 'dich' then
      Break;
  end;

Die erste Prüfung auf i = 6 ist hierbei zwingend erforderlich. Würden wir bei der Zahl 6 keinen Abbruch vornehmen, käme als nächste Prüfung a[6] = 'dich'. Da das Array nur 5 Elemente besitzt, würde die zweite Prüfung zu einem Programmabsturz führen!


Man sollte nach Möglichkeit darauf verzichten, eine Schleife vorzeitig abzubrechen. Der Programmcode kann damit viel unübersichtlicher werden als nötig. In unserem Beispiel ist die Repeat-Until-Schleife vorzuziehen.

Überspringen von Werten

[Bearbeiten]

Ein Schleifendurchlauf kann mit dem Befehl Continue an der entsprechenden Stelle unterbrochen und wieder am Anfang begonnen werden.

Beispiel: Ausgabe aller geraden Zahlen zwischen 1 und 10:

var
  i: Integer;

begin
  for i := 1 to 10 do
    if i mod 2 = 0 then
      Writeln(i);
end.

oder:

var
  i: Integer;

begin
  for i := 1 to 10 do
  begin
    if i mod 2 <> 0 then
      Continue;
    Writeln(i);
  end;
end.

Im oberen Beispiel werden die Werte vor der Ausgabe „gefiltert“. Das zweite Beispiel startet hingegen einen neuen Durchlauf, wenn eine Zahl nicht ohne Rest durch 2 teilbar ist. Der Vorteil der zweiten Variante liegt darin, dass mit einer geringeren Verschachtelungsebene gearbeitet werden kann. Die Ausgabe von i erfolgt noch immer im Programmcode der for-Schleife, während sie beim ersten Beispiel im Programmcode der if-Anweisung erfolgt.


Bisher haben wir alle Anweisungen in unserem Hauptprogramm ausgeführt. Sobald wir jedoch bestimmte Programmteile wiederverwenden möchten, z.B. um eine Berechnung mit verschiedenen Zahlen mehrmals auszuführen oder um Abfragen zu wiederholen, wird dies unpraktisch. Wir müssten dann jedes Mal den gesamten Programmtext erneut schreiben. Auch Kopieren & Einfügen wäre keine wirklich schöne Alternative.

Allgemeines

[Bearbeiten]

Um dies zu vermeiden, speichern wir einfach den sich wiederholenden Programmtext unter einem eigenen Namen zusammengefasst ab und rufen dann im Hauptprogramm nur noch diesen Namen auf. Dies funktioniert genauso wie bei den bisher bekannten und von Delphi mitgelieferten Routinen.

Bei diesen Aufrufen wird zwischen Prozeduren und Funktionen unterschieden. Dabei übermitteln Funktionen nach deren Beendigung einen Rückgabewert, den man dann weiter verarbeiten kann, Prozeduren jedoch nicht. Zur Unterscheidung im Programmtext dienen die Schlüsselwörter procedure und function.

Wenn in diesem Kapitel der Begriff Routinen verwendet wird, sind gleichermaßen Prozeduren und Funktionen gemeint.

Prozeduren und Funktionen haben folgenden allgemeinen Aufbau:

procedure Prozedurname[(Parameterliste)];               // Prozedurkopf
[Deklaration lokaler Typen und Variablen]
begin
  <Anweisungen>
end;

function Funktionsname[(Parameterliste)]: Rückgabetyp;  // Funktionskopf
[Deklaration lokaler Typen und Variablen]
begin
  <Anweisungen>
end;

Die Verwendung von Parametern ist optional, genauso die Deklaration von lokalen Typen Variablen. Die Parameterliste besteht aus einer durch Semikolons getrennte Liste der Form Parametername: Parametertyp.

Routinen werden wie die globalen Typen, Konstanten und Variablen im Deklarationsblock oberhalb des begin vom Hauptprogramm deklariert. Die Routinen können sich auch untereinander aufrufen. Dabei gilt die Lesereihenfolge von oben nach unten: alle oben genannten Routinen sind denen darunter bekannt und können von diesen aufgerufen werden.

Prozeduren ohne Parameter

[Bearbeiten]

Dies ist die einfachste Form von Unterprogrammen. Diese nehmen keinen Wert entgegen und geben auch keinen zurück.

Will man nun die Anweisungen aus dem Eingangsbeispiel:

var
  Name: string;
begin
  Writeln('Wie heisst du?');
  Readln(Name);
  Writeln('Hallo ' + Name + '! Schoen, dass du mal vorbeischaust.');
  Readln;
end.


gleich zweimal aufrufen und dafür eine Prozedur verwenden, dann sähe das Ergebnis so aus:

var
  Name: string;

procedure Beispielprozedur;
begin
  Writeln('Wie heisst du?');
  Readln(Name);
  Writeln('Hallo ' + Name + '! Schoen, dass du mal vorbeischaust.');
  Readln;
end;  // Ende procedure Beispielprozedur

begin // Beginn des Hauptprogramms
  Beispielprozedur;
  Beispielprozedur;
end.  // Ende Hauptprogramm


In diesem Beispiel besteht der Prozedurkopf nur aus dem Schlüsselwort procedure und dem Prozedurnamen.

Die Variable Name ist hierbei global, also am Programmanfang außerhalb der Prozedur deklariert. Dadurch besitzt sie im gesamten Programm samt aller (nachfolgenden) Prozeduren Gültigkeit, kann also an jeder Stelle gelesen und beschrieben werden. Dies hat jedoch Nachteile und birgt Gefahren, da verschiedene Prozeduren eventuell ungewollt gegenseitig den Inhalt solcher globaler Variablen verändern. Die Folge wären Fehler im Programm, die schwer zu erkennen, aber glücklicherweise leicht zu vermeiden sind. Man kann nämlich auch schreiben:

procedure Beispielprozedur;
var
  Name: string;
begin
  Writeln('Wie heisst du?');
  Readln(Name);
  Writeln('Hallo ' + Name + '! Schoen, dass du mal vorbeischaust.');
  Readln;
end;  // Ende procedure Beispielprozedur

begin // Beginn des Hauptprogramms
  Beispielprozedur;
  Beispielprozedur;
end.  // Ende Hauptprogramm

Jetzt ist die Variable innerhalb der Prozedur deklariert, und sie besitzt auch nur dort Gültigkeit, kann also außerhalb weder gelesen noch beschrieben werden. Man nennt sie daher eine lokale Variable. Mit Erreichen der Stelle end// Ende procedure Beispielprozedur verliert sie ihren Wert, sie ist also auch temporär.

Lokale Variablen besitzen zu Beginn einen Zufallswert. Sie müssen daher vor dem ersten Auslesen mit einem Wert belegt werden. Leider kann man sie nicht wie die globalen Variablen direkt bei der Deklaration initialisieren, sondern muss dies im Prozedurrumpf tun.

Prozeduren mit Wertübergabe (call by value)

[Bearbeiten]

Oft ist es jedoch nötig, der Prozedur beim Aufruf Werte zu übergeben, um Berechnungen auf unterschiedliche Daten anzuwenden oder individuelle Nachrichten auszugeben. Hierzu geben wir im Prozedurkopf einen oder mehrere Parameter an. Ein Beispiel:

var
  Name: string;
  AlterInMonaten: Integer;

procedure NameUndAlterAusgeben(Name: string; AlterMonate: Integer);
var
  AlterJahre: Integer;
begin
  AlterJahre := AlterMonate div 12;
  AlterMonate := AlterMonate mod 12; 
  Writeln(Name, ' ist ', AlterJahre, ' Jahre und ', AlterMonate, ' Monate alt');
  Readln;
end;

begin
  NameUndAlterAusgeben('Konstantin', 1197);
  Name := 'Max Mustermann';
  AlterInMonaten := 386;
  NameUndAlterAusgeben(Name, AlterInMonaten)
end.

Der Prozedur NameUndAlterAusgeben werden in runden Klammern zwei durch Semikolon getrennte typisierte Variablen (Name: string; AlterMonate: Integer) bereitgestellt, die beim Prozeduraufruf mit Werten belegt werden müssen. Dies kann wie im Beispiel demonstriert mittels konstanten Werten oder auch Variablen des geforderten Typs geschehen. Diese Variablen werden genau wie die unter var deklarierten lokalen Variablen behandelt, nur dass sie eben mit Werten vorbelegt sind. Sie verlieren also am Ende der Prozedur ihre Gültigkeit und sind außerhalb der Prozedur nicht lesbar. Veränderungen an ihnen haben auch keinen Einfluss auf die Variablen mit deren Werten sie belegt wurden, im Beispiel verändert also AlterMonate := AlterMonate mod 12; den Wert von AlterInMonaten nicht.

Weiterhin können Parameter, wie oben gezeigt, den gleichen Namen wie globale Variablen haben. Die Prozedur verwendet in diesem Falle immer den Parameter.

Prozeduren mit Variablenübergabe (call by reference)

[Bearbeiten]

Was aber, wenn man der Prozedur nicht nur Werte mitteilen will, sondern sie dem Hauptprogramm auch ihre Ergebnisse übermitteln soll? In diesem Fall benötigt man im Hauptprogramm eine Variable, deren Werte beim Prozeduraufruf übergeben werden und in die am Ende das Ergebnis geschrieben wird. Außerdem muss man im Prozedurkopf einen Platzhalter definieren, der die Variable aufnehmen kann (eine Referenz darauf bildet). Dies geschieht, indem man genau wie bei der Wertübergabe hinter dem Prozedurnamen in runden Klammern eine typisierte Variable angibt, ihr aber das Schlüsselwort var voranstellt. Ein Beispiel:

var
  Eingabe: Double;

procedure BerechneKubik(var Zahl: Double);
begin
  Zahl := Zahl * Zahl * Zahl;
end;

begin
  Eingabe := 2.5;
  BerechneKubik(Eingabe);

  { ... }

end.

Als Platzhalter dient hier die Variable Zahl, ihr wird beim Aufruf der Wert von Eingabe übergeben, am Ende wird der berechnete Wert zurückgeliefert. Im Beispiel erhält Eingabe also den Wert 15.625. Bei der Variablenübergabe darf keine Konstante angegeben werden, da diese ja das Ergebnis nicht aufnehmen könnte, es muss immer eine Variable übergeben werden.

Objekte (s. Kapitel Klassen) werden in Delphi immer als Zeiger übergeben. Man kann also auf Eigenschaften und Methoden eines Objekts direkt zugreifen:

 procedure LoescheLabel(Label: TLabel);
 begin
    Label.Caption := ''
 end;

Hinweis: Statt Parameter als Referenz zu übergeben, sollte man lieber Funktionen verwenden, da diese für eben diesen Zweck vorhanden sind. Nur wenn man gleichzeitig mehrere Rückgabewerte benötigt, ist eine Prozedur oder Funktion mit Referenzparametern sinnvoll.

Aber: Der große Vorteil von VAR Parameter ist es, das nicht die Variable selber umkopiert wird (wie es bei einer Funktion der Fall wäre), sondern nur ein Zeiger auf die Variable im Hauptspeicher übergeben wird. Damit können quasi beliebig große Variablen übergeben werden, während bei "normalen" Parametern der Stack die limitierende Größe ist. Außerdem ist diese Art der Werteübergabe sehr schnell, da nichts herumkopiert werden muß.

Routinen mit konstanten Parametern

[Bearbeiten]

Parameter können mit const gekennzeichnet sein, um zu verhindern, dass ein Parameter innerhalb einer Prozedur oder Funktion geändert werden kann.

 function StrSame(const S1, S2: AnsiString): Boolean;
 begin
   Result := StrCompare(S1, S2) = 0;
 end;

Bei String-Parametern ergibt sich ein Geschwindigkeitsvorteil, wenn diese mit const gekennzeichnet werden.

Standardparameter

[Bearbeiten]

Es kann manchmal sehr nützlich sein, einer Routine Standardparameter zu übergeben. So ist es möglich, einen Sonderfall mit derselben Routine abzuarbeiten, ohne bei jedem „normalen“ Aufruf einen Mehraufwand zu haben. Im folgenden Beispiel kann die Routine "Meldung" sowohl als Meldung('Text') als auch als Meldung('Text', IsError) aufgerufen werden. Wenn "Meldung" ohne Angabe des Parameters "IsError" aufgerufen wird, wird IsError = 0 gesetzt:

procedure Meldung( text:string; IsError: Integer = 0 );
begin
if IsError <> 0 then Showmessage('Error: '+text)
else Showmessage(text);
end;

Meldung('Hallo') ist gleichbedeutend mit Meldung('Hallo', 0) und wird "Hallo" anzeigen.

Meldung('Hallo', 1) wird "Error: Hallo" anzeigen.

Erste Zusammenfassung

[Bearbeiten]

Prozeduren können ohne Ein- und Ausgabe, mit einer oder mehreren Eingaben und ohne Ausgabe, oder mit Ein- und Ausgabe beliebig vieler Variablen deklariert werden. Wertübergaben und Variablenübergaben können selbstverständlich auch beliebig kombiniert werden. Bei Variablenübergaben ist zu beachten, dass die Prozedur immer den Wert der Variable einliest. Will man sie allein zur Ausgabe von Daten verwenden, darf deren Wert nicht verwendet werden, bevor er nicht innerhalb der Prozedur überschrieben wurde, beispielsweise mit einer Konstanten oder dem Ergebnis einer Rechnung.

Prozeduren können natürlich auch aus anderen Prozeduren oder Funktionen heraus aufgerufen werden.

Werden Variablen im Hauptprogramm deklariert, so nennt man sie global, und ihre Gültigkeit erstreckt sich demnach über das gesamte Programm bzw. die gesamte Unit; somit besitzen auch Prozeduren Zugriff darauf. Innerhalb von Prozeduren deklarierte Variablen besitzen nur dort Gültigkeit, man nennt sie daher lokale Variablen.

Aber Vorsicht: Konnte man sich bei Zahlen (vom Typ Integer, Int64, Single, Double, ...) in globalen Variablen noch darauf verlassen, dass sie zu Anfang den Wert null haben, so ist dies in lokalen Variablen nicht mehr der Fall, sie tragen Zufallswerte. Außerdem kann man keine typisierten Konstanten innerhalb von Prozeduren deklarieren.

Funktionen

[Bearbeiten]

Funktionen liefern gegenüber Prozeduren immer genau ein Ergebnis. Dieses wird sozusagen an Ort und Stelle geliefert, genau dort wo der Funktionsaufruf im Programm war, steht nach dem Ende ihr Ergebnis. Das hat den Vorteil, dass man mit den Funktionsausdrücken rechnen kann als ob man es bereits mit ihrem Ergebnis zu tun hätte, welches erst zur Laufzeit des Programms berechnet wird.

Sie werden durch das Schlüsselwort function eingeleitet, darauf folgt der Funktionsname, in runden Klammern dahinter ggf. typisierte Variablen zur Wertübergabe, gefolgt von einem Doppelpunkt und dem Datentyp. Innerhalb der Funktion dient ihr Name als Ergebnisvariable. Ein Beispiel:

var
  Ergebnis: Double;

function Kehrwert(Zahl: Double): Double;
begin
  Kehrwert := 1/Zahl;   // oder: Result := 1/Zahl;
end;

begin
  Ergebnis := Kehrwert(100) * 10; // wird zu:   Ergebnis := 0.01 * 10;

  { ... }

  Kehrwert(100) * 10;
end.


Im ersten Aufruf wird der Funktion der (Konstanten-) Wert 100 übergeben, Sie liefert ihr Ergebnis, es wird mit 10 multipliziert, und in Ergebnis gespeichert. Der nächste Aufruf bleibt ohne Wirkung, und verdeutlicht, dass man mit Funktionsausdrücken zwar beliebig weiterrechnen kann, aber am Ende das Ergebnis immer speichern oder ausgeben muss, da es sonst verloren geht.

Funktionen erfordern nicht notwendigerweise Wertübergaben. Z.B. wäre es möglich, dass eine Funktion mit Zufallszahlen arbeitet oder ihre Werte aus globalen Variablen oder anderen Funktionen bezieht. Eine weitere Anwendung wäre eine Funktion ähnlich einer normalen Prozedur, die jedoch einen Wert als Fehlermeldung zurückgibt. Beispielsweise gibt die Funktion Pi den Wert der Zahl Pi zurück und benötigt dafür keine Werte vom Benutzer.

Result

[Bearbeiten]

Die Variable Result ist eine sogenannte Pseudovariable. Sie wird automatisch für jede Funktion erstellt und ist ein Synonym für den Funktionsnamen. Der Wert, der ihr zugewiesen wird, wird von der Funktion zurückgegeben.

function Kehrwert(Zahl: Double): Double;
begin
  Result := 1/Zahl;
end;

ist also gleichbedeutend mit

function Kehrwert(Zahl: Double): Double;
begin
  Kehrwert := 1/Zahl;
end;

Unterprozeduren / Unterfunktionen

[Bearbeiten]

Prozeduren und ebenso Funktionen können bei ihrer Deklaration auch in einander verschachtelt werden (im Folgenden wird nur noch von Prozeduren gesprochen, alle Aussagen treffen aber auch auf Funktionen zu). Aus dem Hauptprogramm oder aus anderen Prozeduren kann dabei nur die Elternprozedur aufgerufen werden, die Unterprozeduren sind nicht zu sehen. Eltern- und Unterprozeduren können sich jedoch gegenseitig aufrufen.

Die Deklaration dazu an einem Beispiel veranschaulicht sieht folgendermaßen aus:

procedure Elternelement;
var
  Test: string;

  procedure Subprozedur;
  begin
    Writeln(Test);
    Readln(Test);
  end;

{ ...
  Beliebig viele weitere Prozeduren
  ... }

begin  // Beginn von Elternelement
  Test := 'Ein langweiliger Standard';
  Subprozedur;
  Writeln(Test);
  Readln;
end;


Nach dem Prozedurkopf folgen optional die Variablen der Elternprozedur, dann der Prozedurkopf der Unterprozedur, ihr Rumpf, ggf. weitere Variablen der Elternprozedur und schließlich der Rumpf der Elternprozedur.

Sofern die Variablen der Elternprozedur vor den Unterprozeduren deklariert werden, können diese darauf ähnlich einer globalen Variable zugreifen, wodurch sich der Einsatz weiterer Variablen reduzieren lässt. Im Zusammenhang mit Rekursionen, also dem Aufruf einer Routine aus sich selbst heraus, wird ein solcher Einsatz manchmal angebracht sein. Unterprozeduren können auch selbst wieder Unterprozeduren haben, die dann nur von ihrem Elternelement aufgerufen werden können. Es sind beliebige Verschachtelungen möglich.

forward-Deklaration

[Bearbeiten]

Alle folgenden Aussagen treffen auch auf Funktionen zu.

Eigentlich muss der Code einer Prozedur vor ihrem ersten Aufruf im Programm stehen. Manchmal ist dies jedoch eher unpraktisch, etwa wenn man viele Prozeduren alphabetisch anordnen will, oder es ist gar unmöglich, nämlich wenn Prozeduren sich gegenseitig aufrufen. In diesen Fällen hilft die forward-Deklaration, die dem Compiler am Programmanfang mitteilt, dass später eine Prozedur unter angegebenen Namen definiert wird. Man schreibt dazu den gesamten Prozedurkopf ganz an den Anfang des Programms, gefolgt von der Direktive forward; Der Prozedurrumpf folgt dann weiter unten im Deklarationsteil oder im Implementation-Teil der Unit. Hierbei kann der vollständige Prozedurkopf angegeben werden oder man lässt die Parameter weg. Am Beispiel der Kehrwertfunktion sähe dies so aus:

function Kehrwert(Zahl: Double): Double; forward;

var
  Ergebnis: Double;

function Kehrwert;
begin
  Kehrwert := 1/Zahl;
end;

{ ... }

Überladene Prozeduren / Funktionen

[Bearbeiten]

Alle bisher betrachteten Prozeduren/Funktionen verweigern ihren Aufruf, wenn man ihnen nicht genau die Zahl an Werten und Variablen wie in ihrer Deklaration gefordert übergibt. Auch deren Reihenfolge und Datentyp muss beachtet werden.

Will man beispielsweise eine Prozedur mit ein- und demselben Verhalten auf unterschiedliche Datentypen anwenden, so wird dies durch die relativ strikte Typisierung von Pascal verhindert. Versucht man also, einer Prozedur, die Integer-Zahlen in Strings wandelt, eine Gleitkommazahl als Eingabe zu übergeben, so erhält man eine Fehlermeldung. Da dies aber eigentlich ganz praktisch wäre, gibt es eine Möglichkeit, dies doch zu tun. Man muss jedoch die Prozedur in allen benötigten Varianten verfassen und diesen den gleichen Namen geben. Damit der Compiler von dieser Mehrfachbelegung des Namens weiß, wird jedem der Prozedurköpfe das Schlüsselwort overload; angefügt. Wichtig ist es zu beachten, dass die Parameter tatsächlich in der Reihenfolge ihrer Datentypen unterschiedlich sind. Es reicht nicht, unterschiedliche Parameternamen zu verwenden.

Ein Beispiel:

function ZahlZuString(Zahl: Int64): string; overload;
begin
  ZahlZuString := IntToStr(Zahl);
end;

function ZahlZuString(Zahl: Double): string; overload;
begin
  ZahlZuString := FloatToStr(Zahl);
end;

procedure ZahlZuString(Zahl: Double; Ausgabe: TEdit); overload;
begin
  Ausgabe.Text := FloatToStr(Zahl);
end;

Die erste Version kann Integer-Werte in Strings wandeln, die zweite wandelt Fließkommawerte, die dritte ebenso, speichert sie dann jedoch in einer Delphi-Edit-Komponenten.

Wenn Typen zuweisungskompatibel sind, braucht man keine separate Version zu schreiben, so nimmt z.B. Int64 auch Integer (Longint)-Werte auf, genau wie Double auch Single-Werte aufnehmen kann.

Der Begriff überladen (overload) beschreibt das mehrmalige Einführen einer Funktion oder Prozedur mit gleichem Namen, aber unterschiedlichen Parametern. Der Compiler erkennt hierbei an den Datentypen der Parameter, welche Version er nutzen soll. Das Überladen ist sowohl in Klassen, für die dort definierten Methoden, als auch in globalen Prozeduren und Funktionen möglich. Wie man in dem Beispiel sieht, können als überladene Routinen sowohl Prozeduren wie auch Funktionen gemischt verwendet werden.

Vorzeitiges Beenden

[Bearbeiten]

In einigen Situationen kann es sinnvoll sein, den Programmcode einer Prozedur oder Funktion nicht vollständig bis zum Ende durchlaufen zu lassen. Hierfür gibt es die Möglichkeit, mittels der Anweisung Exit vorzeitig hieraus auszusteigen. Exit kann an jeder Stelle im Programmablauf vorkommen, bei Funktionen ist jedoch zusätzlich zu beachten, dass vor dem Verlassen ein gültiges Funktionsergebnis zugewiesen wird.

Stellen Sie sich vor, Sie haben eine Liste von Namen und möchten wissen, an welcher Stelle dieser Liste sich ein Name befindet. Die entsprechende Funktion könnte wie folgt aussehen:

type
  TNamensListe = array[0..99] of string;

function HolePosition(Name: string; Liste: TNamensListe): Integer;
var
  i: Integer;
begin
  Result := -1;         // Funktionsergebnis bei Fehler oder "nicht gefunden"
  if Name = '' then
    Exit;               // Funktion verlassen, wenn kein Name angegeben wurde

  for i := 0 to 99 do   // komplette Liste durchsuchen
    if Liste[i] = Name then
    begin
      Result := i;      // gefundene Position als Funktionsergebnis zurückgeben
      Break;            // verlässt die Zählschleife (oder Exit, verlässt die Funktion)
    end;
end;

In diesem Beispiel wird als erstes das Funktionsergebnis -1 festgelegt. In diesem Falle soll es die Bedeutung „Fehler“ oder „Name nicht gefunden“ besitzen. Da der Index der Liste erst bei 0 beginnt, ist eine Stelle unter 0 als gültiges Ergebnis nicht möglich. Dadurch können wir diesen Wert als so genannte „Magic number“ gebrauchen, sprich, als Ergebnis mit besonderer Bedeutung.

Als nächstes wird getestet, ob überhaupt ein Name angegeben wurde. Wenn die Zeichenkette leer ist, erfolgt der sofortige Ausstieg aus der Funktion. Die anschließende Prüfschleife wird nicht durchlaufen. Als Ergebnis wird der zuvor zugewiesene Wert -1 zurückgegeben. Nur wenn Name nicht leer ist, beginnt die eigentliche Suche. Hierbei werden alle Elemente der Liste geprüft, ob sie dem angegebenen Namen entsprechen. Wenn dies der Fall ist, wird die Position dem Funktionsergebnis zugewiesen und die Suche beendet.

Für den besonderen Fall, dass zwar ein Name angegeben wurde, dieser aber in der Liste nicht enthalten ist, wird ebenfalls -1 zurückgegeben. Da die Bedingung Liste[i] = Name niemals zutrifft, bekommt das Funktionsergebnis in der Schleife keinen neuen Wert zugewiesen. Nach 99 wird die Schleife verlassen und Result ist immer noch -1.

Das folgende Programm verdeutlicht den Ablauf:

var
  Liste: TNamensListe;

begin
  Liste[0] := 'Alfons';
  Liste[1] := 'Dieter';
  Liste[2] := 'Gustav';
  Liste[3] := 'Detlef';

  WriteLn(HolePosition('Gustav', Liste));  // Ergibt:  2
  WriteLn(HolePosition('Peter', Liste));   // Ergibt: -1
end.
Tipp:

Ab Delphi 2009 ist es möglich, das Funktionsergebnis als Parameter von Exit anzugeben. Man kann die obige Beispielfunktion ab dieser Version mit Exit(-1) bzw. Exit(i) direkt verlassen und muss das Funktionsergebnis vorher nicht mehr extra zuweisen.


Fortgeschritten

[Bearbeiten]

Typdefinition

[Bearbeiten]

Bei einer Typdefinition wird einem Datentyp ein eigener Bezeichner zugeordnet. Die Typdefinitionen stehen vor den Variablendefinitionen und werden mit dem Schlüsselwort type eingeleitet. Die Syntax zeigen die folgenden Beispiele.

Code:

type
  TInt = Integer;
  THandle = Word;

var
  zaehler: TInt;
  fensterhandle: THandle;

In den obigen Beispielen wird den in Delphi definierten Typen Integer sowie Word ein zusätzlicher Bezeichner, hier TInt sowie THandle gegeben. In Variablendefinitionen können danach die neuen Bezeichner verwendet werden.

Im Folgenden werden weitere mögliche Typdefinitionen beschrieben.

Es wird als guter Stil empfunden, wenn Typdefinitionen, die einen Zeiger (englisch: Pointer) definieren, mit P beginnen und alle anderen Typdefinitionen mit T.

Teilbereiche von Ganzzahlen

[Bearbeiten]

Code:

type
  TZiffer = 0..9;    // Die Zahlen 0 bis 9
  TSchulnote = 1..6;

var
  a, b: TZiffer;

begin
  a := 5;       // zulässig
  //  a := 10;     Diese Zuweisung erzeugt beim Kompilieren eine Fehlermeldung.
  b := 6;
  a := a + b;   // Dies führt bei Laufzeit zu einer Fehlermeldung, da das
                // Ergebnis (11) außerhalb des Wertebereiches von TZiffer liegt.
end.

Aufzählungen

[Bearbeiten]

Code:

type
  TWochentag = (Montag, Dienstag, Mittwoch, Donnerstag, Freitag, Samstag, Sonntag);

var
  Tag: TWochentag;

begin
  Tag := Mittwoch;
  if Tag = Sonntag then Writeln('Sonntag');
end.

Aufzählungen sind identisch mit Teilbereichen von Ganzzahlen, die mit 0 beginnen. Sie machen aber den Code besser lesbar, wie im letzten Beispiel verdeutlicht.

Records, Arrays

[Bearbeiten]

Code:

type
  TKomplex = record
    RealTeil: Double;
    ImaginaerTeil: Double;
  end;
  TSchulnote = (sehr_gut, gut, befriedigend, ausreichend, mangelhaft, ungenuegend);
  TSchulfach = (Deutsch, Mathe, Physik, Sport, Englisch, Kunst, Musik, Biologie);
  TNotenArray = array[TSchulfach] of TSchulnote;
 
var
  a, b: TKomplex;
  Noten: TNotenArray;
 
function Addieren(a1, a2: TKomplex): TKomplex;
begin
  Result.RealTeil := a1.RealTeil + a2.RealTeil;
  Result.ImaginaerTeil := a1.ImaginaerTeil + a2.ImaginaerTeil;
end;
 
begin
  a.RealTeil := 3;
  a.ImaginaerTeil := 1;
  b := a;
  a := Addieren(a, b);

  Noten[Deutsch] := gut;
  Noten[Mathe] := mangelhaft;
  Noten[Physik] := sehr_gut;
end.

Records oder Arrays als Typdefinition haben den Vorteil, dass sie direkt einander zugewiesen werden können (a := b) und dass sie als Parameter sowie Rückgabewert einer Funktion zulässig sind. Vergleiche (if a = b then...) sind allerdings nicht möglich.

Zeiger

[Bearbeiten]

Code:

type
  PWord = ^Word;
  PKomplex = ^TKomplex;  // nach obiger Typdefinition von TKomplex

var
  w: PWord;
  k: PKomplex;

begin
  New(w);
  New(k);

  w^ := 128;
  k^.RealTeil := 20.751;
  k^.ImaginaerTeil := 12.5;

  Dispose(k);
  Dispose(w);
end.

Zeiger als Typdefinion sind ebenfalls als Parameter sowie Rückgabewert von Funktionen zulässig. Da ein Zeiger immer eine Adresse speichert, unabhängig vom Typ, auf den der Zeiger zeigt, kann die Typdefinition auch nach der Definition des Zeigers erfolgen, wie im nachfolgenden Beispiel.

Code:

type
  PKettenglied = ^TKettenglied;
  TKettenglied = record
    GliedId: Integer;
    Naechstes: PKettenglied;
  end;

Klassen

[Bearbeiten]

Die Deklarierung von Klassen ist ebenfalls eine Typdefinition. Diese werden wir aber aufgrund des Umfangs in einem späteren Kapitel gesondert behandeln.


Typumwandlung

[Bearbeiten]

Von einer Typumwandlung spricht man, wenn Daten eines Typs in einen anderen, kompatiblen Typ konvertiert werden sollen. Einige solcher Typumwandlungen erledigt Delphi bereits automatisch, zum Beispiel wenn man eine Ganzzahl einer Gleitkommavariablen zuweisen möchte. Da die Menge der reellen Zahlen auch die ganzen Zahlen enthält, ist hierbei nichts besonderes zu beachten.

Wie führt man nun eine Typumwandlung durch? Bei der Zuweisung an eine Variable des Zieltyps wird dabei der Typbezeichner wie ein Funktionsname verwendet. Als „Parameter“ wird ein Wert oder eine Variable des Ausgangstyps mitgegeben:

Var2 := Zieltyp(Var1);

Das Ergebnis der Umwandlung kann im Übrigen auch direkt verwendet werden, z.B. bei der Datenausgabe oder als Parameter einer weiteren Funktion.

Eine Typumwandlung ist immer dann möglich, wenn die Daten sowohl bei dem einen als auch bei dem anderen Datentyp die gleiche interne Darstellung im Arbeitsspeicher haben. So ist zum Beispiel eine Typumwandlung zwischen Char und Byte möglich, auch wenn diese für uns in der Betrachtung völlig verschiedene Daten, nämlich ein Textzeichen und eine Zahl darstellen. Im Arbeitsspeicher bleibt der Wert in beiden Fällen der Gleiche. Hierzu ein Beispiel:

var
  b: Byte;
  c: Char;

begin
  b := 88;
  c := Char(b);
  Writeln(c);          // gibt X aus, da X den ASCII-Wert 88 (dezimal) besitzt
  Writeln(Byte('o'));  // gibt 111 aus, den ASCII-Wert des Zeichens o
end.

Hinweis: Für die Umwandlung von Byte in Char gibt es ebenfalls die Funktion Chr, für den umgekehrten Weg die Funktion Ord.

Aufzählungen

[Bearbeiten]

Bei Aufzählungen wurden ja, wir erinnern uns, jeder Position in der Aufzählungsliste ein Name zugeordnet. In dem entsprechenden Kapitel hatten wir schon kurz die Typumwandlung angesprochen.

Gerade bei Aufzählungen kann es von Interesse sein, den Namen in eine Zahl oder andersherum zu konvertieren, z.B. wenn man bestimmte Programmeinstellungen in einer Konfigurationsdatei speichern möchte. Da in diesem Falle eine automatische Umwandlung in eine Zahl (oder beim Einlesen zurück zum Bezeichner) nicht funktioniert, müssen wir hier eine explizite Typumwandlung anwenden. Als Beispiel soll uns noch einmal die Ampel dienen:

type
  TAmpel = (rot, gelb, gruen);

var
  Ampel: TAmpel;
  Zahl: Byte;

begin
  Ampel := gelb;

// Writeln(Ampel);       <-- würde eine Fehlermeldung ergeben (ungültiger Typ)
  Writeln(Byte(Ampel));  // gibt 1 aus - oder: Writeln(Ord(Ampel));

  Write('Bitte eine Zahl von 0 bis 2 eingeben: ');
  Readln(Zahl);
  Ampel := TAmpel(Zahl);
end.

Im ersten Teil des Beispiels wird der Positionswert des Aufzählungsbezeichners gelb in seinen Zahlwert 1 umgewandelt. Auch hier können wir wieder für die Typumwandlung Byte oder Ord verwenden. Im zweiten Teil wird die vom Benutzer eingegebene Zahl wieder in den entsprechenden Wert vom Typ TAmpel, also rot, gelb oder gruen umgewandelt.

Zeichenketten

[Bearbeiten]

Bei Zeichenketten ist eine Typumwandlung normalerweise nicht nötig. Wer jedoch zum Beispiel mit Windows-Systemfunktionen arbeitet, kommt an dem Datentyp PChar nicht vorbei. Da dies ein besonderer Datentyp ist, haben wir ihn bisher nicht behandelt.

Die Betriebssystem-Bibliotheken sind in den Programmiersprachen C bzw. C++ geschrieben. In diesen Sprachen gibt es keinen eigenständigen Datentyp für Zeichenketten. Daher wird ein Zeiger auf ein bei Null beginnendes Array von Zeichen verwendet, das mit dem ASCII-Zeichen #0 endet. Diese Form der Zeichenketten heißt nullterminierter String. PChar ist eine solche nullterminierte Zeichenkette.

Um die Arbeit zu erleichtern, kann man einer Variablen vom Typ PChar zwar einen konstanten Zeichenkettenwert direkt zuweisen, jedoch keine Variable vom Typ String. Dazu benötigen wir eine Typumwandlung:

var
  p: PChar;
  s: string;

begin
  p := 'Hallo';             // kein Problem, konstanter String-Wert
  s := 'Welt';
// p := s;                  Fehler, inkompatible Typen
  p := PChar(s);            // so funktioniert's
  Writeln(p);               // gibt "Welt" aus

  p := 'Hallo ' + 'Welt!';  // auch das ist möglich
  s := p;                   // in diese Richtung geht's auch ohne Typumwandlung
end.

Da PChar lediglich den Zeiger auf das erste Zeichen der Zeichenkette darstellt, kann man einen String auch auf folgende Weise in den Typ PChar umwandeln:

p := @s[1];

Zeiger und Adressen

[Bearbeiten]

Mit einem Zeiger erhalten wir ja für gewöhnlich Zugriff auf die Daten, auf welche dieser verweist. Die Speicheradresse eines Zeigers selbst erhalten wir wiederum durch Typumwandlung. Hierbei sollte man eine Umwandlung in den Typ LongWord vornehmen, da dieser eine 32-Bit-Zahl ohne Vorzeichen zurückgibt:

var
  p: ^Integer;

begin
  p^ := 666;
  Writeln(LongWord(p));   // gibt die Speicheradresse von p aus, nicht 666
end.

Hinweis: Bei einem 64-Bit-Compiler bietet sich eine Typumwandlung nach Int64 an.

Achtung: Obwohl auch per Typumwandlung eine Zahl in eine Speicheradresse konvertiert und diese einem Zeiger zugewiesen werden könnte, sollte man davon tunlichst die Finger lassen (es sei denn, man weiß genau, was man da tut). Das Auslesen von Adressen, z.B. für Zwecke der Fehlersuche oder um bestimmte Funktionsweisen nachzuvollziehen, ist dagegen jedoch völlig unbedenklich.

Objekte

[Bearbeiten]

Zu Objekten kommen wir zwar erst später, aber gerade bei diesen werden Sie die Typumwandlung regelmäßig benötigen. Als kurze Vorschau sei schonmal gesagt, das ein Objekt die initialisierte Variable eines Klassen-Typs ist.

Klassen können vererbt und dabei immer mehr im Funktionsumfang erweitert werden. Die Nachkommen einer Klasse enthalten immer alle Felder und Methoden der Basisklasse. Man kann also generische Methoden schreiben, die einen Parameter der Basisklasse erwarten und kann diesen auch alle Erben der Basisklasse übergeben. Um auf den erweiterten Funktionsumfang der Nachkommen zuzugreifen, kann man dann wiederum die Typumwandlung einsetzen. Dazu wollen wir uns nur ein kurzes Beispiel ansehen, quasi als Vorgeschmack. Hoffentlich werden Sie von dessen Umfang nicht gleich abgeschreckt:

type
 TBasis = class
   Name: string;
 end;

 // erbt Name von TBasis und fügt Zahl hinzu
 TErbe = class(TBasis)
   Zahl: Byte;
 end;

{ Gibt den Namen der Klasse aus und bei Nachkommen
  vom Typ TErbe auch den Wert des Feldes Zahl }
procedure SchreibeDaten(Klasse: TBasis);
begin
  Writeln(Klasse.Name);
  if Klasse is TErbe then
    Writeln(TErbe(Klasse).Zahl);   // hier erfolgt die Typumwandlung
  Writeln;
end;

var
  b: TBasis;
  e: TErbe;

begin
  b := TBasis.Create;              // Objekt erstellen
  b.Name := 'Basis-Klasse';        // und mit Daten füllen

  e := TErbe.Create;               // nochmal mit dem Nachkommen
  e.Name := 'Nachkommen-Klasse';
  e.Zahl := 71;

  SchreibeDaten(b);                // gibt nur den Namen aus
  SchreibeDaten(e);                // gibt den Namen und die Zahl aus

  b.Free;                          // Speicher wieder freigeben
  e.Free;
end.

Was nicht geht

[Bearbeiten]

Typumwandlungen sind immer dann nicht einsetzbar, wenn wie gesagt die Daten im Speicher dazu geändert werden müssten. Man kann zum Beispiel nicht per Typumwandlung die Zeichenkette '1234' in den Integer-Wert 1234 konvertieren. Die Zeichenkette liegt als Hexadezimalfolge $31323334 vor, der Integer-Wert hätte hingegen den Hexadezimalwert $04D2. Für eine solche tatsächliche Datenumwandlung müssen entsprechende Funktionen eingesetzt werden, in diesem Falle erhalten Sie mit IntToStr das gewünschte Ergebnis.


Prozedurale Typen

[Bearbeiten]

Nein, diese Bezeichnung dient nicht Ihrer Verwirrung! Hierunter versteht man tatsächlich Datentypen, die eine Funktion oder Prozedur darstellen. Dieser Typ gibt dabei vor:

  • ob es sich um eine Prozedur oder Funktion handelt
  • ob und wenn ja, welche Parametertypen diese besitzen muss
  • bei Funktionen, welchen Typ der Rückgabewert haben muss

Zur Laufzeit kann man dann einer Variablen dieses Typs eine entsprechende Prozedur bzw. Funktion zuweisen (und dies natürlich auch mehrfach ändern). Sie verwenden dann diese Variable wie die Funktion selbst. Doch verwirrt? Dazu zwei Beispiele:

Quelltext:

type
  // nimmt nur parameterlose Prozeduren an
  TProzedur = procedure;

procedure HilfeAnzeigen;
begin
  Writeln('Programmhilfe');
end;

procedure FehlerAusgeben;
begin
  Writeln('Fehler!');
end;

var
  Prozedur: TProzedur;

begin
  Prozedur := HilfeAnzeigen;
  Prozedur;

  Prozedur := FehlerAusgeben;
  Prozedur;
end.

Ausgabe:

Programmhilfe
Fehler!


Quelltext:

type
  // nimmt Funktionen mit 2 Integer-Parametern an, die einen Integer-Wert zurückgeben
  TBerechnung = function(WertA, WertB: Integer): Integer;

function Addieren(X, Y: Integer): Integer;
begin
  Result := X + Y;
end;

function Multiplizieren(X, Y: Integer): Integer;
begin
  Result := X * Y;
end;

var
  Berechnen: TBerechnung;

begin
  Berechnen := Addieren;
  Writeln(Berechnen(11, 31));

  Berechnen := Multiplizieren;
  Writeln(Berechnen(11, 31));
end.

Ausgabe:

42
341

Mithilfe von prozeduralen Variablen können Sie sehr dynamische Programmabläufe bewirken und auch unter Umständen Ihren Programmcode reduzieren. Wenn Sie zum Beispiel abhängig vom Wert einer Variablen an mehreren Stellen im Programm eine der verschiedenen Funktionen aufrufen möchten, müssen Sie den Wert dieser Variablen nur einmal prüfen und können einer globalen prozeduralen Variablen die entsprechende Funktion zuweisen. Alle anderen Programmteile verwenden nun einfach diese prozedurale Variable.

Eine weitere Verwendung besteht beim Einbinden von Funktionen aus Bibliotheken (DLL-Dateien), wie Sie in einem späteren Kapitel noch lernen werden.

Mit einem prozeduralen Typ, wie oben definiert, können Sie jedoch nur globale Prozeduren und Funktionen aufnehmen. Auf Methoden von Klassen können Sie damit nicht zurückgreifen, da diese ganz anders im Speicher abgelegt werden.

Prozedurale Typen und Objekte

[Bearbeiten]

Um die Methode eines Objekts (also einer instanziierten Klasse) wie im ersten Abschnitt gesehen verwenden zu können, setzen Sie zwischen die Typdefinition und das abschließenden Semikolon noch die Schlüsselwörter of object:

Quelltext:

type
  TBerechnung = function(WertA, WertB: Integer): Integer of object;

Nun können Sie die Methode einer Klasse sowohl innerhalb als auch außerhalb der Klasse einer Variablen dieses Typs zuweisen und aufrufen. Dies funktioniert selbstverständlich nur so lange, bis der Speicher der ursprünglichen Klasseninstanz freigegeben wurde. Dieses Verfahren wird bei den Ereignissen von Klassen angewandt. Dies sind Eigenschaften von prozeduralem Typ, die einfachsten Ereignisse verwenden den Typ TNotifyEvent, der in der Unit Classes folgendermaßen definiert ist:

Quelltext:

type
  TNotifyEvent = procedure(Sender: TObject) of object;

Wie vorhin schon kurz gesagt werden diese prozeduralen Typen im Speicher anders abgelegt als die zuvor behandelten, nämlich als Methodenzeiger. Dies ist ein Record zweier aufeinander folgender Zeiger im Speicher, wobei der erste (Code) auf die Adresse der Methode verweist, während der zweite (Data) die Adresse des Objekts enthält. Das nur, damit Sie einmal davon gehört haben, denn an diese Zeiger kommen Sie ohne Typumwandlungen nicht heran. Sie verwenden auch einen solchen Methodenzeiger wie andere prozedurale Typen.

Als nächstes kommt ein Beispiel, wie Sie eine Methode in einer Klasse als Ereigniseigenschaft einsetzen. Mit einem Ereignis kann die Klasse praktisch Code ausführen, der nicht zur Klasse selbst gehört. Die Klasse kann und muss daher auch nicht wissen, was dieser Code bewirkt. Sie gibt lediglich im Rahmen eines prozeduralen Typs vor, wie diese Methode beschaffen sein muss und kann dabei auch weitere Daten in Form vom Parametern übergeben. Wenn Sie variable Parameter verwenden, kann der andere Programmteil auch das Ergebnis seiner Ereignisbehandlung wieder an die Klasse zurückliefern. Das wird zum Beispiel bei Benutzeroberflächen verwendet, wobei ein Fenster dem Hauptprogramm meldet, dass es sich schließen will. Nur wenn es - vereinfacht ausgedrückt - als Antwort ein Okay zurückgemeldet bekommt, schließt es sich auch wirklich, sonst bleibt es geöffnet. Solche Ereigniseigenschaften beginnen immer mit On (oder, wenn man auf Deutsch programmieren möchte, mit Bei).

Damit das Hauptprogramm weiß, welche Instanz der Klasse das Ereignis ausgelöst hat, sollte man immer wenigstens die Instanz selbst mitliefern, also den Typ TNotifyEvent verwenden.

Da es sich hierbei um einen Methodenzeiger handelt, muss die an die Eigenschaft übergebene Methode selbst eine Funktion einer Klasse sein. Globale Funktionen außerhalb von Klassen funktionieren nicht!

Quelltext:

uses
  Classes;

{ ===== Typdefinitionen ===== }

type
  TInnenKlasse = class
  private
    FBeiAddition: TNotifyEvent;
  public
    function Addieren(WertA, WertB: Integer): Integer;
    property BeiAddition: TNotifyEvent read FBeiAddition write FBeiAddition;
  end;

  TAussenKlasse = class
  private
    procedure InnenKlasseAddition(Sender: TObject);
  public
    procedure Starten;
  end;

{ ===== Methodendeklarationen ===== }

function TInnenKlasse.Addieren(WertA, WertB: Integer): Integer;
begin
  // Ereignis auslösen, falls eine Methode zugewiesen wurde
  if Assigned(BeiAddition) then
    BeiAddition(Self);

  Result := WertA + WertB;
end;

procedure TAussenKlasse.InnenKlasseAddition(Sender: TObject);
begin
  Writeln('In InnenKlasse findet gleich eine Addition statt!');
end;

procedure TAussenKlasse.Starten;
var
  ik: TInnenKlasse;
begin
  ik := TInnenKlasse.Create;
  ik.BeiAddition := InnenKlasseAddition;
  Writeln('Ergebnis von 2 + 4 = ', ik.Addieren(2, 4));
  ik.Free;
end;

{ ===== Hauptprogrammteil ===== }

var
  ak: TAussenKlasse;

begin
  ak := TAussenKlasse.Create;
  ak.Starten;
  ak.Free;
end.

Ausgabe:

In InnenKlasse findet gleich eine Addition statt!
Ergebnis von 2 + 4 = 6

Puh! Zugegeben, das ist ziemlich umfangreich. Versuchen Sie einmal in Ruhe, den Ablauf nachzuvollziehen, wenn ak.Starten ausgeführt wird.

Zunächst wird eine lokale Variable vom Typ TInnenKlasse dynamisch erzeugt und dann dessen Ereigniseigenschaft BeiAddition die entsprechende Methode zugewiesen. Hier könnte stattdessen auch ein Feld verwendet werden, das bereits im Konstruktor entsprechend erstellt wird.

Dann wird die Methode Addieren aufgerufen. Diese prüft als erstes, ob eine Ereignisbehandlungsmethode zugewiesen wurde und führt diese dann aus. Die Funktion Assigned ist in Delphi eingebaut und prüft lediglich, ob die Adresse der Eigenschaft nil ist. Da diese Methode wiederum in der äußeren Klasse definiert ist, springt das Programm also kurzzeitig zurück und zeigt den Text "In InnenKlasse findet gleich eine Addition statt!" an.

Anschließend wird erst die Berechnung durchgeführt und das Ergebnis zurückgegeben, das dann - wieder zurück in ak.Starten - angezeigt wird. Falls das zu schnell ging, führen Sie das Programm mit F7 schrittweise aus und beobachten Sie den Ablauf.

Tipp:

Das Ereignis kann an jeder Stelle einer Methode ausgelöst werden, üblich ist jedoch meistens am Anfang oder am Ende. Ereignisse, die am Anfang einer Methode auftreten, nehmen oft über einen var-Parameter eine Rückmeldung entgegen, die sie entweder weiter verarbeiten oder die auch zum Abbruch der Methode führen können. Wenn Sie ein Ereignis nicht benötigen, reicht es aus, der entsprechenden Eigenschaft keinen Wert zuzuweisen. Im oberen Beispiel lassen Sie dann einfach die Zeile ik.BeiAddition := InnenKlasseAddition; weg und es wird kein Ereignis ausgelöst.


Rekursion

[Bearbeiten]

Als Rekursion bezeichnet man das Verfahren, dass eine Funktion ihren Rückgabewert durch Aufruf von sich selbst berechnet. Dabei muss die Funktion mindestens einen Startwert entgegennehmen und diesen in jedem Durchgang wieder neu berechnen. Damit dies kein endloser Vorgang wird, muss weiterhin sichergestellt sein, dass die Rekursion bei Erreichen eines festgelegten Startwertes oder Ergebnisses innerhalb der Berechnung endet.

Ein bekanntes Beispiel für ein Problem, das meistens (und sinnvollerweise) durch Rekursion gelöst wird, sind die Türme von Hanoi[1], ein mathematisches Spiel. Prinzipiell lässt sich dieses Problem auch iterativ lösen. Ein anderes sinnvolles Beispiel ist Floodfill[2], ein Algorithmus zum Einfärben von benachbarten Pixeln gleicher Farbe.

Beispiel einer Rekursionsfunktion

[Bearbeiten]
function Quersumme(n: Integer): Integer;
begin
  if n = 0 then
    Result := 0
  else
    Result := (n mod 10) + Quersumme(n div 10);
end;

Das Programm trennt bei jedem Durchgang die letzte Ziffer ab und addiert diese zur Quersumme der übrigen Ziffern. Wenn keine Ziffern mehr übrig sind (Startwert n = 0), endet die Rekursion.

Um den Ablauf dieses Beispiels zu verstehen, seien hier einmal die Schritte beim Aufruf mit dem Startwert 713 erklärt.

  • 1. Aufruf: 713 <> 0, also 3 + Quersumme(71)
  • 2. Aufruf: 71 <> 0, also 1 + Quersumme(7)
  • 3. Aufruf: 7 <> 0, also 7 + Quersumme(0)
  • 4. Aufruf: 0 = 0, Rückgabe von 0 an den 3. Aufruf
  • 3. Aufruf: Rückgabe von 7 + 0 = 7 an den 2. Aufruf
  • 2. Aufruf: Rückgabe von 1 + 7 = 8 an den 1. Aufruf
  • 1. Aufruf: Rückgabe von 3 + 8 = 11 an das Hauptprogramm
  • Ergebnis: Quersumme(713) = 11

Rekursive und iterative Programmierung im Vergleich

[Bearbeiten]

Rekursive Algorithmen sind oft kürzer als iterative, leichter zu verstehen und gelten als eleganter. Iteration ist dafür ressourcensparender und schneller, weil der wiederholte Funktionsaufruf entfällt. Wie man im obigen Beispiel sieht, bleiben alle Aufrufe im Speicher, bis die Rückgabewerte rückwärts wieder verarbeitet werden können. Im Extremfall kann es bei der Rekursion daher zum Stapelüberlauf kommen, wenn der Stapelspeicher nicht mehr ausreicht. Zudem sind in iterativen Lösungen Fehler leichter zu finden. Daher ist die iterative Vorgehensweise der rekursiven immer vorzuziehen, wenn es eine iterative Lösung gibt (und sich diese auch in vernünftiger Zeit finden lässt). Beispielsweise sollte Rekursion nicht zur Berechnung von Fakultät, Summen, der Fibonacci-Zahlen und ähnlichem Verwendung finden.

[Bearbeiten]

Wikipedia-Artikel zu rekursiver Programmierung: http://de.wikipedia.org/wiki/Rekursive_Programmierung

  1. Türme von Hanoi: http://de.wikipedia.org/wiki/Türme_von_Hanoi
  2. Floodfill: http://de.wikipedia.org/wiki/Floodfill


Threads

[Bearbeiten]

Threads werden benötigt, um Programmteile parallel ablaufen zu lassen. So können verschiedene Aufgaben im Hintergrund gleichzeitig bearbeitet werden, z.B. das Sortieren einer Liste, die Übertragung von Daten über ein Netzwerk oder das Speichern von Spielständen in einer Online-Highscoreliste.

Allen Threads (zu deutsch etwa: Fäden) ist gemein, dass sie keine Eingaben vom Benutzer erwarten.

Um einen Thread zu erstellen, binden Sie in Ihr Programm bzw. in die Unit die Unit Classes ein. Darin ist die Klasse TThread deklariert, mit der wir nun arbeiten wollen.

Bei TThread handelt es sich um eine abstrakte Klasse. Dies bedeutet, dass diese zwar grundlegende Funktionen enthält, aber selbst noch nicht funktionsfähig ist. Der Programmcode, der in einem Thread ablaufen soll, kann ja in der Klasse TThread noch nicht enthalten sein. TThread enthält dafür die abstrakte Methode Execute, die wir in einem Nachkommen von TThread überschreiben und mit unserem gewünschten Code füllen müssen:

type
  TMyThread = class(TThread)
    procedure Execute; override;
  end;

procedure TMyThread.Execute;
begin
  { Threadcode }
end;

Kommunikation mit dem Hauptprogramm

[Bearbeiten]

Da Execute im Hintergrund abläuft, gibt es verschiedene Möglichkeiten, mit dem Thread zu kommunizieren. So zum Beispiel können Sie den Thread anhalten bzw. fortsetzen oder ganz abbrechen. Dazu bringt TThread die Methoden Suspend, Resume und Terminate mit. Über die Eigenschaften Suspended und Terminated lässt sich der jeweilige Status ermitteln.

Wenn Sie in Execute einen voraussichtlich zeitaufwändigen Vorgang ausführen, sollten Sie in regelmäßigen Abständen prüfen, ob die Eigenschaft Terminated auf True gesetzt ist. In diesem Falle ist die Schleife so bald wie möglich zu verlassen und die Methode Execute zu beenden. Auch alle von Execute aufgerufenen Methoden sollten regelmäßig den Status von Terminated überprüfen. So wäre es korrekt:

procedure TMyThread.Execute;
var i: Integer;
begin
  DateiOeffnen;
  for i := 0 to FeldAnzahl - 1 do
  begin
    LeseAusDatei(Feld[i]);
    if Terminated then Break;  // hier erfolgt der Abbruch der Schleife
  end;
  DateiSchließen;              // dieser Code wird auch bei Abbruch ausgeführt
end;

Über selbst deklarierte Ereignisse können Sie auch Zwischeninformationen an das Hauptprogramm weitergeben, wie den Fortschritt in Prozent oder Statusinformationen für die Fehlersuche.

Starten und Beenden

[Bearbeiten]

Einen Thread können Sie direkt beim Erstellen der Instanz oder erst später starten. Der Konstruktor nimmt dabei den Parameter CreateSuspended entgegen. Wenn Sie hier True übergeben, wird der Thread im pausierten Zustand erstellt, bei False hingegen wird der Thread gleich nach dem Erstellen gestartet:

MyThread := TMyThread.Create(False); // startet sofort
MyThread := TMyThread.Create(True);  // startet später
MyThread.Resume;                     // pausierten Thread starten

Sofern Sie keine Endlosschleife verwenden, endet der Thread automatisch, sobald der Programmcode von Execute endet. Wenn Sie den Thread zwischendurch anhalten möchten, rufen Sie die Methode Suspend auf. Um den Thread vorzeitig abzubrechen, verwenden Sie Terminate.

Ein einmal beendeter Thread lässt sich nicht wieder starten. Um den Code erneut auszuführen, müssen Sie den Speicher freigeben und das Threadobjekt erneut erstellen. Resume funktioniert nur, wenn der Thread pausiert erstellt oder zwischendurch angehalten wurde.

Weiterhin sollten Sie einen Start des Threads über den direkten Aufruf von Execute vermeiden, da Sie hiermit alle Kontrollmechanismen umgehen würden. Sie führen den Programmcode dann nicht in einem Extra-Thread, sondern im Hauptprogramm aus. Sie könnten ihn somit weder anhalten noch abbrechen, da das Hauptprogramm auf das Ende von Execute warten würde. Bei einer Endlosschleife bliebe nur ein erzwungener Abbruch des Hauptprogramm über den Task-Manager. Verwenden Sie also immer Resume.

Auf Beendigung eines Thread warten

[Bearbeiten]

Sobald ein Thread gestartet wurde, wird die Kontrolle wieder an das Hauptprogramm zurückgegeben und dieses setzt die Verarbeitung fort. Es kann nun also vorkommen, dass z.B. das Hauptprogramm beendet wird (von selbst oder durch den Benutzer), während im Hintergrund noch Threads ihre Arbeit verrichten. Wenn sie nichts unternehmen, werden die Threads abgebrochen. Sollen diese aber in jedem Falle ihre Arbeit beenden (z.B. eine Datei noch zuende schreiben), rufen Sie vor dem Freigeben des Speichers die Methode WaitFor auf:

MyThread.WaitFor;
MyThread.Free;

Hiermit ist sichergestellt, dass der Thread wirklich durchläuft und seine Arbeit beendet, bevor dessen Speicher freigegeben wird. WaitFor kann auch das Ergebnis des Threads zurückgeben. Abhängig davon können Sie weitere Aktionen veranlassen oder auch das Beenden des Programms verhindern, z.B. wenn der Thread nicht erfolgreich beendet wurde.

Synchronisation

[Bearbeiten]

Da Threads im Hintergrund ablaufen, können Sie das zeitliche Verhalten der Programmausführung nicht vorherbestimmen. Um keinen Datenmüll oder gar Programmabstürze zu erzeugen, gibt es verschiedene Möglichenkeiten, den Zugriff auf gemeinsam verwendete Daten zu schützen.

Die einfachste Methode ist, von einem Thread aufgerufene Objekte so zu programmieren, dass immer nur ein Aufruf möglich ist und weitere gesperrt werden. Der zweite Thread muss dann so lange warten, bis die Sperre aufgehoben wurde und er seine Änderungen vornehmen kann:

TMyObject = class
private
  FLocked: Boolean;
public
  procedure Lock;
  procedure Unlock;
  property Locked: Boolean read FLocked;
end;

procedure TMyThread.Execute;
begin
  // Warten, bis MyObject frei ist oder der Thread abgebrochen wurde
  while MyObject.Locked and not Terminated do;
  MyObject.Lock;
  { Änderungen an MyObject vornehmen }
  MyObject.Unlock;
end;

Ein Beispiel für diese Sperrung ist in der Klasse TCanvas enthalten, die eine Zeichenoberfläche für verschiedene Windows-Steuerelemente und für den Drucker zur Verfügung stellt. Es kann immer nur ein Thread zur Zeit auf ein TCanvas-Objekt zeichnen.

Bei der Programmierung grafischer Oberflächen unter Windows gibt es eine zweite Möglichkeit: Sie können bestimmte Aufrufe, die mit der Oberfläche interagieren (z.B. Textausgabe, Erweiterung von Listen, Füllen von Tabellen usw.) im Hauptthread ausführen lassen. Dazu übergeben Sie eine Methode des gleichen oder eines anderen Threads an die Methode Synchronize. Synchronize ist in TThread deklariert. Dieses nimmt sozusagen Ihre Anfrage in die Botschaftenliste von Windows auf und wartet so lange, bis Windows diese abgearbeitet hat. Da Synchronize auf Windows angewiesen ist, funktioniert es nicht in reinen Konsolenanwendungen und ist auch nur für Objekte der VCL (Visual Component Library) bestimmt.

Daneben existieren noch weitere Möglichkeiten: kritische Abschnitte, Synchronisierungsobjekte, die mehrfaches Lesen aber nur einfaches Schreiben erlauben sowie globale Threadvariablen. Eine umfassende Erläuterung würde hier jedoch zu weit führen. Bitte ziehen Sie gegebenenfalls die Delphi-Hilfe zu Rate.


Objektorientierung

[Bearbeiten]

Einleitung

[Bearbeiten]

Die Grundlage für die objektorientierte Programmierung, kurz OOP, bilden Klassen. Diese kapseln, ähnlich wie Records, verschiedene Daten zu einer Struktur. Gleichzeitig liefern sie Methoden mit, welche die vorhandenen Daten bearbeiten.

Der Vorteil von Klassen besteht also darin, dass man mit ihnen zusammengehörige Variablen, Funktionen und Prozeduren zusammenfassen kann. Weiterhin können - bei entsprechender Implementation - die Daten einer Klasse nicht „von außen“ geändert werden.

Aufbau einer Klasse

[Bearbeiten]

Allgemeiner Aufbau

[Bearbeiten]

Die einfachste Klasse entspricht in ihrem Aufbau einem Record:

program Klassentest;

type
  TMyRec = record
    EinByte: Byte;
    EinString: string;
  end;

  TMyClass = class
    FEinByte: Byte;
    FEinString: string;
  end;

var
  MyRec: TMyRec;
  MyClass: TMyClass;

begin
  MyRec.EinByte := 15;
  MyClass := TMyClass.Create;
  MyClass.FEinString := 'Hallo Welt!';
  MyClass.Free;   // bzw. MyClass.Destroy;
end.

Hierbei kann man bereits einen Unterschied zu Records erkennen: Während man jederzeit auf die Daten eines Records zugreifen kann, muss bei einer Klasse zunächst Speicher angefordert und zum Schluss wieder freigeben werden. Die speziellen Methoden Create und Destroy bzw. Free sind in jeder Klasse enthalten und müssen nicht gesondert programmiert werden. Zur Freigabe des Speichers sollte dabei Free bevorzugt verwendet werden. Hierzu mehr im Kapitel Konstruktoren und Destruktoren.

Weiterhin hat es sich durchgesetzt, die Variablen einer Klasse, Felder genannt, immer mit dem Buchstaben F zu beginnen. So werden Verwechselungen mit globalen und lokalen Variablen vermieden.

Sichtbarkeit der Daten

[Bearbeiten]

Im oben gezeigten Beispiel kann auf die Felder wie bei einem Record zugegriffen werden. Dies sollte man unter allen Umständen vermeiden!

Hierfür gibt es eine ganze Reihe von Möglichkeiten. Zunächst einmal können Felder, sowie Methoden und Eigenschaften einer Klasse nach außen hin „versteckt“ werden. Das erreicht man mit folgenden Schlüsselwörtern:

  • strict private - auf diese Daten kann außerhalb der Klasse nicht zugegriffen werden
  • private - wie strict private, allerdings kann auch in der beinhaltenden Unit darauf zugegriffen werden
  • strict protected - hierauf kann man nur innerhalb der Klasse und ihrer Nachfahren zugreifen
  • protected - wie strict protected, allerdings kann auch in der beinhaltenden Unit darauf zugegriffen werden
  • public - diese Daten sind uneingeschränkt zugänglich
  • published - zusätzlich zu public können diese Daten auch im Objekt-Inspektor von Delphi™ bearbeitet werden (nur bei Eigenschaften von Komponenten sinnvoll).

Das entsprechende Schlüsselwort wird der Gruppe von Daten vorangestellt, für die diese Sichtbarkeit gelten soll. Um die Felder unserer Klasse zu verstecken, schreiben wir also:

type
  TMyClass = class
  private
    FEinByte: Byte;
    FEinString: string;
  end;

Wenn man jetzt versucht, wie oben gezeigt, einem Feld einen Wert zuzuweisen oder ihn auszulesen, wird bereits bei der Kompilierung eine Fehlermeldung ausgegeben. Mit neueren Delphi Versionen gab es keinen Fehler, erst das Wort "strict" vor "private" löste o.a. Fehler aus.

Methoden

[Bearbeiten]

Wie erhält man nun aber Zugriff auf die Daten? Dies erreicht man über öffentlich zugängliche Methoden, mit denen die Daten ausgelesen und geändert werden können.

Eine Methode ist eine fest mit der Klasse verbundene Funktion oder Prozedur. Daher wird sie auch wie Felder direkt innerhalb der Klasse definiert:

TMyClass = class
private
  FEinString: string;
public
  function GetString: string;
  procedure SetString(NewStr: string);
end;

Die Ausführung der Methoden erfolgt direkt über die Variable dieses Klassentyps:

var
  MyClass: TMyClass;

...

MyClass.SetString('Hallo Welt!');
WriteLn(MyClass.GetString);

In der Typdefinition werden nur der Name und die Parameter von Methoden definiert. Die Implementation, also die Umsetzung dessen, was eine Methode tun soll, erfolgt außerhalb der Klasse, genau wie bei globalen Funktionen und Prozeduren. Allerdings muss der Klassenname vorangestellt werden:

function TMyClass.GetString: string;
begin
  Result := FEinString;
end;

procedure TMyClass.SetString(NewStr: string);
begin
  FEinString := NewStr;
end;

Da die Methoden GetString und SetString Mitglieder der Klasse sind, können diese auf das private Feld FEinString zugreifen.

Ebenso wie globale Funktionen und Prozeduren lassen sich auch Methoden überladen. Dies bedeutet, dass mehrere Prozeduren mit dem gleichen Namen aber unterschiedlichen Parametern innerhalb einer Klasse deklariert werden. Hierzu ein vollständiges Programm als Beispiel:

program Ueberladen;

{$APPTYPE CONSOLE}

uses
  SysUtils;

type
  TTestKlasse = class
  public
    function ZaehleStellen(zahl: Cardinal): Integer; overload;
    function ZaehleStellen(wort: string): Integer; overload;
  end;

function TTestKlasse.ZaehleStellen(zahl: Cardinal): Integer;
begin
  Result := Length(IntToStr(zahl));
end;

function TTestKlasse.ZaehleStellen(wort: string): Integer;
begin
  Result := Length(wort);
end;

var
  Zaehler: TTestKlasse;

begin
  Zaehler := TTestKlasse.Create;

  Writeln(Zaehler.ZaehleStellen(16384));
  Writeln(Zaehler.ZaehleStellen('Donnerstag'));
  Readln;
  Zaehler.Free;
end.

Im ersten Fall – Writeln(ZaehleStellen(16384)); – wird die Methode TTestKlasse.ZaehleStellen(zahl: Cardinal): Integer aufgerufen, da der Übergabeparameter vom Typ Cardinal ist. Es wird 5 ausgegeben.

Im zweiten Fall – Writeln(ZaehleStellen('Donnerstag')); – wird die Methode TTestKlasse.ZaehleStellen(wort: string): Integer aufgerufen, da der Übergabeparameter ein String ist. Dementsprechend wird der Wert 10 ausgegeben.

In beiden Fällen wird die Stellenanzahl mittels Length bestimmt. Da Length aber eine Zeichenkette erwartet, wird der Zahlwert im ersten Fall zunächst in eine Zeichenkette umgewandelt und dann die Länge dieser Zeichenkette bestimmt.

Eigenschaften

[Bearbeiten]

Um den Zugriff auf die Daten einer Klasse zu vereinfachen und flexibler zu gestalten, gibt es noch eine andere Möglichkeit: Eigenschaften (engl. properties).

Eigenschaften lassen sich wie Variablen behandeln, das heißt, man kann sie (wenn gewünscht) auslesen oder (wenn gewünscht) ändern. Die interne Umsetzung bleibt dabei jedoch verborgen. So kann eine Eigenschaft direkt auf ein Feld oder mittels einer Methode die Daten der Klasse auswerten:

type
  TTestKlasse = class
  private
    FZugriffe: Integer;
    FText: string;
    procedure SetzeString(Neu: string);
  public
    property SchreibZugriffe: Integer read FZugriffe;   // nur lesbar
    property Text: string read FText write SetzeString; // les- und schreibbar
  end;

Sowohl die Felder als auch die Methode sind versteckt. Die einzige Verbindung zur Außenwelt besteht über die Eigenschaften. Hier greift die Eigenschaft „SchreibZugriffe“ beim Lesen direkt auf das Feld zu und erlaubt keinen Schreibvorgang, während „Text“ zwar beides erlaubt, beim Schreiben aber auf eine Methode zurückgreift. Die Methode wird nun wie folgt implementiert:

procedure TTestKlasse.SetzeString(Neu: string);
begin
  FText := Neu;
  Inc(FZugriffe);
end;

Diese Klasse erhöht den Wert des Feldes FZugriffe, ohne dass es der Benutzer mitbekommt. Bei jedem Schreibzugriff auf die Eigenschaft Text wird FZugriffe um 1 erhöht. Dieser Wert kann mit der Eigenschaft SchreibZugriffe nur ausgelesen, aber nicht geändert werden. „Text“ erscheint daher nach außen hin eher wie eine Variable und „SchreibZugriffe“ eher wie eine Konstante. Hier ein kleines Beispiel mit einem Objekt dieser Klasse:

TestKlasse := TTestKlasse.Create;
with TestKlasse do
begin
  Text := 'Hallo!';
  Writeln(SchreibZugriffe, ', ', Text);  // gibt "1, Hallo!" aus
  Text := 'Und auf Wiedersehen!';
  Writeln(SchreibZugriffe, ', ', Text);  // gibt "2, Und auf Wiedersehen!" aus
end;
TestKlasse.Free;

Die verschiedenen Arten von Eigenschaften werden ausführlicher im Kapitel Eigenschaften behandelt.


Konstruktoren und Destruktoren

[Bearbeiten]

Allgemeines

[Bearbeiten]

Wie bereits im Kapitel über Klassen beschrieben, muss der Speicher für die Datenstruktur einer Klasse angefordert werden, bevor man mit ihr arbeiten kann. Da eine Klasse mehr als nur eine Datenansammlung ist - nämlich ein sich selbst verwaltendes Objekt - müssen gegebenenfalls weitere Initialisierungen durchgeführt werden. Genauso kann nach der Arbeit mit einer Klasse noch Speicher belegt sein, der von Delphis Speicherverwaltung nicht erfasst wird.

Für zusätzliche anfängliche Initialisierungen stehen die Konstruktoren („Errichter“) zur Verfügung, wogegen die Destruktoren („Zerstörer“) die abschließende Aufräumarbeit übernehmen. Diese sind spezielle Methoden einer Klasse, die nur zu diesem Zweck existieren.

Konstruktoren

[Bearbeiten]

Für die Arbeit mit einfachen Klassen, die keine weitere Initialisierung benötigen, braucht kein spezieller Konstruktor verwendet zu werden. In Delphi stammt jede Klasse automatisch von der Basisklasse TObject ab und erbt von dieser den Konstruktor Create. Folgender Aufruf ist daher gültig, obwohl keine Methode Create definiert wurde:

type
  TErsteKlasse = class
  end;

var
  Versuch: TErsteKlasse;

begin
  Versuch := TErsteKlasse.Create;
end.

Wie zu sehen ist, erfolgt der Aufruf des Konstruktors über den Klassentyp. Eine Anweisung wie Variable.Create führt zu einer Fehlermeldung.

Das „einfache“ Create erzeugt ein Objekt des Typs TErsteKlasse und gibt einen Zeiger darauf zurück.

Verwendet man Klassen, welche die Anfangswerte ihrer Felder einstellen oder z.B. weiteren Speicher anfordern müssen, so verdeckt man die geerbte Methode mit seiner eigenen:

type
  TErsteKlasse = class
    constructor Create;
  end;

constructor TErsteKlasse.Create;
begin
  inherited;
  { Eigene Anweisungen }
end;

Das Schlüsselwort inherited ruft dabei die verdeckte Methode TObject.Create auf und sorgt dafür, dass alle notwendigen Initialisierungen durchgeführt werden. Daher muss dieses Schlüsselwort immer zu Beginn eines Konstruktors stehen.

Solange ein gleichnamiger Vorfahre aufgerufen wird, reicht das Schlüsselwort inherited, ohne weitere Angaben. Unterscheidet sich der Bezeichner vom Vorfahr im Namen oder Parametern, sollte nach inherited der Vorfahre genannt werden. Besteht der Konstruktor also nur aus dem Bezeichner Create, reicht inherited, bekommt er weitere Parameter, z. B. Create(Index: Word), kann der Vorfahre nicht mehr erkannt werden. In solchen Fällen sollte der Vorfahre genannt werden: inherited Create.

Beispiel: Sie wollen eine dynamische Adressliste schreiben, bei der der Name Ihrer Freundin immer als erster Eintrag erscheint.

type
  TAdresse = record
    Vorname, Nachname: string;
    Anschrift: string;
    TelNr: string;
  end;

  TAdressListe = class
    FListe: array of TAdresse;
    constructor Create;
  end;

In diesem Falle erstellen Sie den Konstruktor wie folgt:

constructor TAdressListe.Create;
begin
  inherited;
  SetLength(FListe, 1);  // Speicher für 1. Eintrag anfordern
  FListe[0].Vorname   := 'Barbie';
  FListe[0].Nachname  := 'Löckchen';
  FListe[0].Anschrift := 'Puppenstube 1, Kinderzimmer';
  FListe[0].TelNr     := '0800-BARBIE';
end;

Wenn Sie jetzt eine Variable mit diesem Konstruktor erstellen, enthält diese automatisch immer den Namen der Freundin:

var
  Adressen: TAdressListe;

begin
  Adressen := TAdressListe.Create;
  with Adressen.FListe[0] do
    {Ausgabe: "Barbie Löckchen wohnt in Puppenstube 1, Kinderzimmer und ist erreichbar unter 0800-BARBIE."}
    Writeln(Vorname+' '+Nachname+' wohnt in '+Anschrift+' und ist erreichbar unter '+TelNr+'.')
end.

Destruktoren

[Bearbeiten]

Destruktoren dienen, wie schon oben beschrieben dazu, den von einer Klasse verwendeten Speicher wieder freizugeben. Auch hierfür stellt die Basisklasse TObjekt bereits den Destruktor Destroy bereit. Dieser gibt grundsätzlich zwar den Speicher einer Klasse wieder frei, aber nur den des Grundgerüstes. Wenn eine Klasse zusätzlichen Speicher anfordert, z.B. weil sie mit Zeigern arbeitet, wird nur der Speicher für den Zeiger freigegeben, nicht aber der Speicherplatz, den die Daten belegen.

Eine Klasse sollte daher immer dafür sorgen, dass keine Datenreste im Speicher zurückbleiben. Dies erfolgt durch Überschreiben des Destruktors TObject.Destroy mit einem eigenen:

type
  TZweiteKlasse = class
    destructor Destroy; override;
  end;

destructor TZweiteKlasse.Destroy;
begin
  { Anweisungen }
  inherited;
end;

var
  Versuch: TZweiteKlasse;

begin
  Versuch := TZweiteKlasse.Create;
  Versuch.Destroy;
end.

Eine eigene Implementation von Destroy muss immer mit dem Schlüsselword override versehen werden, da Destroy eine virtuelle Methode ist (mehr dazu unter Virtuelle Methoden). Auch hier kommt wieder das Schlüsselwort inherited zum Einsatz, welches den verdeckten Destruktor TObject.Destroy aufruft. Da dieser Speicher frei gibt, muss inherited innerhalb eines Destruktors immer zuletzt aufgerufen werden.

Wenn wir unser Beispiel der Adressliste erweitern und auf das Wesentliche reduzieren, ergibt sich folgendes Programm:

type
  TAdresse = record
    Vorname, Nachname: string;
    Anschrift: string;
    TelNr: string;
  end;

  TAdressListe = class
    FListe: array of TAdresse;
    destructor Destroy; override;
  end;

destructor TAdressListe.Destroy;
begin
  SetLength(FListe, 0);  // Speicher der dynamischen Liste freigeben
  inherited;             // Objekt auflösen
end;

var
  Adressen: TAdressListe;

begin
  Adressen := TAdressListe.Create;
  { Anweisungen }
  Adressen.Destroy;
end.

Die Methode Free

[Bearbeiten]

Free dient ebenso wie Destroy dazu, den Speicher einer Klasse freizugeben. Dabei funktioniert Free jedoch etwas anders.

Destroy gibt ausschließlich den Speicher des Objektes frei, von dem aus es aufgerufen wurde. Bei allen Objekten, die von TComponent abstammen, gibt Free zusätzlich den Speicher aller Unterkomponenten frei. TComponent wird in der Programmierung grafischer Oberflächen verwendet.

Weiterhin können Sie Free auch bei Objekten anwenden, deren Wert nil ist, die also noch nicht mittels Create erstellt wurden. Destroy löst in diesem Falle eine Exception aus. Hierbei ist jedoch zu beachten, dass nur globalen Objektvariablen und Klassenfeldern automatisch nil zugewiesen wird. Lokale Variablen von Routinen bzw. Methoden enthalten anfangs meist Datenmüll, also nicht nil. Free versucht daher einen ungültigen Speicherbereich freizugeben, was zu einer Exception führt. Objektvariablen, die nicht mit Sicherheit initialisiert werden, sollten Sie daher am Anfang einer Routine oder Methode den Wert nil manuell zuweisen.

Aufgrund dieser Vorteile sollten Sie immer Free bevorzugen, um ein Objekt aufzulösen.

Richtige Verwendung von Free und Destroy

[Bearbeiten]

Wenn Sie in einer Nachfahren-Klasse die Funktion des Destruktors erweitern wollen, überschreiben Sie immer Destroy direkt und rufen dort die benötigten Anweisungen auf. Zum tatsächlichen Freigeben eines Objektes verwenden Sie jedoch Free! Diese Methode ruft automatisch den Destruktor auf.

type
  TMyClass = class
  public
    destructor Destroy; override;
  end;

destructor TMyClass.Destroy;
begin
  { eigene Freigaben }
  inherited;
end;

var
  MyClass: TMyClass;

begin
  MyClass := TMyClass.Create;
  MyClass.Free;
end;

Überladen

[Bearbeiten]

Das Überladen von Konstruktoren und Destruktoren ist genauso möglich wie bei anderen Methoden auch. Für das Beispiel unserer Adressliste könnten zum Beispiel zwei Konstruktoren bestehen: einer, der eine leere Liste erzeugt, und einer, der eine Liste aus einer Datei lädt. Genauso könnte ein Destruktor die Liste einfach verwerfen, während ein anderer die Liste vorher in einer Datei speichert.

Sie können die Konstruktoren und Destruktoren jeweils gegenseitig aufrufen. Sie sollten hierbei jedoch darauf achten, dass nur genau einer das Schlüsselwort inherited aufruft.

type
  TAdressListe = class
    constructor Create; overload;
    constructor Create(Dateiname: string); overload;
    procedure DateiLaden(Dateiname: string);
  end;

procedure TAdressListe.DateiLaden(Dateiname: string);
begin
  {...}
end;

constructor TAdressListe.Create;
begin
  inherited;
  {...}
end;

constructor TAdressListe.Create(Dateiname: string);
begin
  Create;  // ruft TAdressListe.Create auf
  Dateiladen(Dateiname);
end;

Weiteres zum Thema Überladen in den Kapiteln „Prozeduren und Funktionen“ sowie „Klassen“.


Eigenschaften

[Bearbeiten]

Einleitung

[Bearbeiten]

Objekte haben Eigenschaften. Dies ist eine Regel, die in der freien Natur wie auch in der objektorientierten Programmierung unter Delphi zutrifft.

Eigenschaften stellen einen Zwischenweg zwischen Feldern und Methoden einer Klasse dar. So kann man zum Beispiel beim Auslesen einer Eigenschaft einfach den Wert eines Feldes zurück erhalten, löst aber beim Ändern dieser Eigenschaft eine Reihe von Folgeaktionen aus. Dies liegt daran, dass man bei Eigenschaften genau festlegen kann, was im jeweiligen Falle passieren soll. Ebenso kann jeweils das Lesen oder das Schreiben einer Eigenschaft unterbunden werden.

Wie man bereits sieht, kann ein Programm über Eigenschaften sicherer und gleichzeitig flexibler gestaltet werden.

Eigenschaften definieren

[Bearbeiten]

In der Klassendefinition legt man Eigenschaften mit folgender Syntax an:

property Name: <Typ> [read <Feld oder Methode>] [write <Feld oder Methode>];

Die Schlüsselwörter read und write sind so genannte Zugriffsbezeichner. Sie sind beide optional, wobei jedoch mindestens einer von beiden angegeben werden muss. Die folgenden Beispiele sind alle gültig:

property Eigenschaft1: Integer read FWert;
property Eigenschaft2: Integer write ZahlSetzen;
property Eigenschaft3: string read FText write TextSetzen;

Zur kurzen Erklärung:

  • mit Zugriff auf Eigenschaft1 wird der Wert des Feldes FWert zurück gegeben, eine Zuweisung an Eigenschaft1 ist jedoch nicht möglich (Nur-Lesen-Eigenschaft)
  • Eigenschaft2 dürfen nur Werte zugewiesen werden, diese werden dann an die Methode ZahlSetzen weitergereicht. Das Auslesen des Wertes ist über Eigenschaft2 nicht möglich (Nur-Schreiben-Eigenschaft)
  • Eigenschaft3 ermöglicht hingegen sowohl das Schreiben als auch das Lesen von Daten (Lesen-Schreiben-Eigenschaft)

Im Normalfall wird man überwiegend Lese-Schreib-Eigenschaften verwenden und ggf. einige Nur-Lesen-Eigenschaften. Nur-Schreib-Eigenschaften sind hingegen sehr selten anzutreffen, können aber im Einzelfall einen ebenso nützlichen Dienst erweisen.

Verwendet man für den Zugriff ein Feld, muss dieses lediglich den gleichen Typ haben wie die Eigenschaft. Sollen jedoch Methoden beim Zugriff zum Einsatz kommen, müssen diese bestimmten Regeln folgen: Für den Lesezugriff benötigt man eine Funktion ohne Parameter, deren Ergebnis vom Typ der Eigenschaft ist. Beim Schreibzugriff muss eine Prozedur verwendet werden, deren einziger Parameter den gleichen Typ wie die Eigenschaft besitzt.

Gut, das alles hört sich etwas kompliziert an. Gestalten wir doch mal ein kleines Gerüst für die Verwaltung eines Gebrauchtwagenhändlers:

type
  TFarbe = (fSchwarz, fWeiß, fSilber, fBlau, fGruen, fRot, fGelb);
  TAutoTyp = (atKlein, atMittelStufe, atMittelFliess, atKombi, atLuxus, atGelaende);

  TAuto = class
  private
    FFarbe: TFarbe;
    FTyp: TAutoTyp;
    procedure FarbeSetzen(Farbe: TFarbe);
    function PreisBerechnen: Single;
  public
    property Farbe: TFarbe read FFarbe write FarbeSetzen;
    property Typ: TAutoTyp read FTyp;
    property Preis: Single read PreisBerechnen;
    constructor Create(Farbe: TFarbe; Typ: TAutoTyp);
  end;

Wie man bereits ohne das vollständige Programm erkennen kann, haben wir mit dieser Klasse ähnliche Möglichkeiten wie in der realen Welt. Zum Beispiel ist es dem Händler möglich, das Auto umzulackieren. Hierbei wird eine Methode aufgerufen, die möglicherweise Folgeaktionen auslöst (z.B. das Auto für eine Weile als „nicht im Bestand“ markiert). Andererseits kann der Händler zwar abrufen, um welchen Autotyp es sich handelt, kann diesen aber selbstverständlich nicht verändern. Der Preis ist nicht starr festgelegt, sondern wird über eine Funktion berechnet (die sich wahrscheinlich unter anderem an der Farbe und am Typ des Autos orientiert).

Weiterhin ist diese Klasse so angelegt, dass man „von außen“ nur Zugriff auf die Eigenschaften und den Konstruktor von TAuto hat. Die Felder und Methoden dienen nur der internen Verwaltung der Eigenschaften und sind daher im private-Abschnitt verborgen.

Array-Eigenschaften

[Bearbeiten]

Eigenschaften können auch als Arrays definiert werden. Dies ist immer dann sinnvoll, wenn man Daten in Form einer Tabelle bzw. Auflistung speichern möchte. Der Zugriff erfolgt wie bei Variablen über Indizes in eckigen Klammern. Hinter den Zugriffsbezeichnern darf bei Array-Eigenschaften allerdings kein Feld angegeben werden, hier sind nur Methoden zulässig:

property Zeile[Nr: Integer]: string read HoleZeile write SchreibeZeile;
property Punkt[X, Y: Integer]: Integer read HolePunkt;

Die zugehörigen Methoden werden dann wie folgt definiert:

function HoleZeile(Nr: Integer): string;
procedure SchreibeZeile(Nr: Integer; Text: string);
function HolePunkt(X, Y: Integer): Integer;

Als erste Parameter werden also immer die Felder des Arrays angesprochen, dann folgen die Parameter analog den einfachen Eigenschaften. Die Bezeichner der Variablen müssen mit den Indizes der Eigenschaften nicht notwendigerweise übereinstimmen. Um die Anwendung jedoch übersichtlich zu gestalten, sollte man gleiche Bezeichner verwenden. Grundsätzlich wäre also auch folgende Notation gültig:

function HoleZeile(ZeilenNr: Integer): string;
property Zeile[X: Integer]: string read HoleZeile;

Überschreiben

[Bearbeiten]
Sichtbarkeit ändern
[Bearbeiten]

Bei der Vererbung von Klassen kann es vorkommen, dass man Eigenschaften mit der Sichtbarkeit protected erbt, diese aber in der eigenen Klasse als public kennzeichnen möchte. Hierfür ist es möglich, die Eigenschaften mit der gewünschten Sichtbarkeit neu zu definieren.

Beispiel: Wir haben von der Klasse TBasis die als protected gekennzeichnete Eigenschaft „Name“ geerbt. Um diese nun allgemein zugänglich zu machen, schreiben wir einfach:

public
  property Name;

Damit wurde an der Funktionsweise der Eigenschaft nichts geändert, sie ist jetzt lediglich „besser sichtbar“. Damit ist der Programmcode bereits vollständig. Es müssen keine weiteren Zugriffsfelder oder -methoden hierfür geschrieben werden.

Anders herum ist es auf diese Weise nicht möglich, Eigenschaften zu verstecken. Selbst wenn Sie eine öffentliche Eigenschaft in einem Nachkommen als strict private kennzeichnen, können Sie immer noch darauf zugreifen. Das Programm verwendet dann die Eigenschaft des Vorfahren mit der höheren Sichtbarkeit.

Neu definieren
[Bearbeiten]

Wenn Sie nun von einer Klasse eine Eigenschaft vererbt bekommen, die Sie anders verarbeiten wollen, können Sie diese wie eine gewöhnliche Eigenschaft mit dem gleichen Namen neu definieren. Hierzu sind ebenfalls die Angaben der Zugriffsbezeichner erforderlich. So lässt sich zum Beispiel eine Lese-Schreib-Eigenschaft als Nur-Lesen-Eigenschaft neu definieren:

type
  TBasis = class
    FFarbe: string;
    property Farbe: string read FFarbe write FFarbe;
  end;
  
  TErbe = class(TBasis)
    property Farbe: string read FFarbe;
  end;

„FFarbe“ muss in der Klasse „TErbe“ nicht neu eingeführt werden, es wird das Feld aus der Basis-Klasse verwendet. Falls ein Zugriffsbezeichner in der Basisklasse als private gekennzeichnet wurde, gibt Delphi bei der Kompilierung eine entsprechende Warnung aus. Für reine Delphi-Programme kann diese Warnung ignoriert werden. Sie ist für die Portierung des Programms in andere Programmiersprachen gedacht, da dort eine solche Schreibweise unter Umständen nicht erlaubt ist.

Falls eine Eigenschaft eine Methode für den Zugriff verwendet und Sie das Verhalten ändern wollen, müssen Sie lediglich die geerbte Methode überschreiben. Mehr hierzu im nächsten Kapitel „Vererbung“.

Zugriff auf Eigenschaften

[Bearbeiten]

Der Zugriff auf Eigenschaften erfolgt wie bei Feldern und Methoden:

Objekt.Eigenschaft := Wert;
Wert := Objekt.Eigenschaft;

Hierbei ist jedoch zusätzlich zur Sichtbarkeit auch die Zugriffsmöglichkeit der Eigenschaft zu beachten, das heißt, ob Sie eine Nur-Lesen-, Nur-Schreiben- oder eine Lese-Schreib-Eigenschaft verwenden wollen. Die Eigenschaften verhalten sich wie eine Mischung aus Variablen und Methoden, z.B. kann man Werte auslesen und übergeben. Nicht möglich ist dagegen die Übergabe einer Eigenschaft als Variablen-Parameter einer Funktion. So erzeugt dieser Programmschnipsel eine Fehlermeldung:

type
  TKlasse = class
  private
    FZahl: Integer;
  public
    property Zahl: Integer read FZahl write FZahl;
  end;

var
  Test: TKlasse;

begin
  Test := TKlasse.Create;
  Test.Zahl := 0;
  Inc(Test.Zahl);  // <-- Fehlermeldung
  Test.Destroy;
end.

Obwohl hier die Daten letztlich in der Klassenvariablen FZahl gespeichert werden, darf die dazugehörige Eigenschaft Zahl nicht an die Routine Inc übergeben werden. Hier bleibt letztlich nur der direkte Weg:

Test.Zahl := Test.Zahl + 1;

Standard-Eigenschaften

[Bearbeiten]

Wie oben beschrieben muss beim Zugriff auf eine Eigenschaft immer der Name der Eigenschaft selbst angegeben werden. Für Array-Eigenschaften gibt es eine praktische Besonderheit: die Standard-Eigenschaft. Wenn eine Array-Eigenschaft als Standard definiert wird, kann der Name dieser Eigenschaft beim Zugriff weggelassen werden. Dies erreicht man durch Angabe der Direktive default hinter der Eigenschaften-Definition:

 property Eigenschaft[X: Integer]: string read LeseMethode write SchreibMethode; default;

Jetzt sind diese beiden Angaben gleichbedeutend:

 Wert := Objekt.Eigenschaft[Index];
 Wert := Objekt[Index];

Selbstverständlich kann nur jeweils eine Eigenschaft je Klasse als Standard definiert werden.


Vererbung

[Bearbeiten]

Einleitung

[Bearbeiten]

Das Vererben von sämtlichen Methoden, Feldern, etc. von einer alten Klasse ist eine ganz wichtige Eigenschaft von Klassen.
So ist es z.B. möglich, von einer Klasse TWirbeltier die Klasse TMensch abzuleiten. So kann man eine bestehende Klasse spezialisieren, oder auch verändern.
Jede Klasse ist ein Nachkomme von TObject.

Deklaration

[Bearbeiten]

Es wird einfach hinter das Schlüsselwort class die Vorfahrklasse in Klammern gesetzt, und schon kann man die Nachkommenklasse benutzen.

TObject
[Bearbeiten]

Da jede Klasse ein Nachkomme von TObject ist, ist es bei Delphi nicht nötig, TObject extra hinzuschreiben. Deswegen sind die beiden Deklarationen absolut gleichwertig und gleichbedeutend:

type
  TMeineKlasse = class (TObject)   
  end;

oder

type
  TMeineKlasse = class
  end;
Beispiel
[Bearbeiten]
type
  TWirbeltier = class
  private
    AnzKnochen: Integer;
  public
    Lebt: Boolean;

    procedure Iss(Menge: Integer);
    function Gesundheit: Integer;
  end;
  

  TMensch = class (TWirbeltier)
  end;

TMensch besitzt alle Methoden und Felder von TWirbeltier. Folgender Aufruf ist also völlig legitim (abgesehen davon, dass Mensch.Gesundheit nicht gesetzt wurde):

var Mensch: TMensch;
begin
  Mensch := TMensch.Create;
  Mensch.Iss(10);
  Writeln(Mensch.Gesundheit);
  Mensch.Free;
end;

Ersetzen von Methoden und Feldern

[Bearbeiten]

Wenn man eine Methode/Feld in der Nachkommenklasse deklariert, die den selben Namen hat wie in der Vorfahrklasse, dann wird die Vorfahr-Methode/Feld durch die neu deklarierte ersetzt.

type
  TWirbeltier = class
  private
    AnzKnochen: Integer;
  public
    Lebt: Boolean;

    procedure Iss(Menge: Integer);
    function Gesundheit: Integer;
  end;
  

  TMensch = class (TWirbeltier)
    procedure Iss(Menge: Integer);
  end;

procedure TWirbeltier.Iss(Menge: Integer);
begin
  Writeln(Menge);
end;

procedure TMensch.Iss(Menge: Integer);
begin
  Writeln(Menge*2);
end;

var Mensch: TMensch;

begin
  Mensch := TMensch.Create;
  Mensch.Iss(10);
  Mensch.Free;
end.

Die procedure Iss von TMensch ersetzt Iss von der Vorfahrklasse. Und damit wird 20 ausgegeben werden.

Überschreiben von Methoden und Feldern

[Bearbeiten]

Wenn man eine Methode/Feld ersetzen will, aber trotzdem noch unter Umständen auf die Vorfahr-Methode/Feld zugreifen will, dann überschreibt man sie mit dem Schlüsselwort override und muss in der Vorfahrklasse diese Methode/Feld mit dem Schlüsselwort virtual kennzeichnen.
Die Vorfahr-Methode/Feld ist dann sozusagen virtuell im Hintergrund schlummernd.
Dann ist die virtuelle Vorfahr-Methode/Feld über das Schlüsselwort inherited zu erreichen.

type
  TWirbeltier = class
  private
    AnzKnochen: Integer; 
  public
    Lebt: Boolean;

    procedure Iss(Menge: Integer); virtual;
    function Gesundheit: Integer;
  end;
  

  TMensch = class (TWirbeltier)
    procedure Iss(Menge: Integer); override;
  end;

procedure TWirbeltier.Iss(Menge: Integer);
begin
  Writeln(Menge);
end;

procedure TMensch.Iss(Menge: Integer);
begin
  Writeln(Menge); 
  inherited Iss(Menge*2);  // hier wird die Vorfahr-Methode aufgerufen
end;

var Mensch: TMensch;

begin
  Mensch := TMensch.Create;
  Mensch.Iss(10);
  Mensch.Free;
end.

Hier wird erst 10 ausgegeben werden, dann 20.
Dies ist ein sehr großer Vorteil bei dem Überschreiben, dass man eine Methode sehr gut erweitern kann, ohne den Code nochmal abschreiben zu müssen.
Also in dem Beispiel: Wieso nochmal Writeln aufrufen? Wir benutzen einfach die virtuelle Methode.


dynamic
[Bearbeiten]

Anstatt des Schlüsselwortes virtual kann man auch dynamic benutzen. Jedoch arbeitet es nicht so schnell wie virtual. Die Benutzung ist aber genau dieselbe.


Zugriff auf Klassen

[Bearbeiten]

Hauptsächlich arbeitet man mit Instanzen von Klassen, also Variablen, die über einen Konstruktor erstellt wurden. Die regulären Felder, Methoden und Eigenschaften lassen sich erst aus dieser Instanz heraus verwenden.

Es kann jedoch auch vorkommen, dass man nicht mit einer Instanz, sondern mit der Klasse selbst arbeiten möchte, zum Beispiel um grundlegende Daten über eine Klasse zu erhalten. Hierfür gibt es so genannte Klassenvariablen, Klassenmethoden und Klasseneigenschaften. Diese werden ähnlich wie der Konstruktor über die Typbezeichnung einer Klasse aufgerufen, statt über eine Variable. Vor einem Klassenmember steht immer das Schlüsselwort class:

type
  TKlasse = class
  private
    class var FVariable: Byte;
  public
    class procedure SchreibeVariable;
    class property Variable: Byte read FVariable write FVariable;
  end;

class procedure TKlasse.SchreibeVariable;
begin
  Writeln(Variable);
end;

begin
  TKlasse.Variable := 15;
  TKlasse.SchreibeVariable;
end.

Dieses Beispiel ist bereits so lauffähig, es wird keine Variable benötigt, über die eine Instanz angelegt werden muss.

Klassenmethoden können nur auf Klassenvariablen und -eigenschaften zugreifen, auf normale (instantiierte) nicht. Andersherum können jedoch Instanzmethoden Klassenelemente lesen, ändern bzw. aufrufen. Von einem solchen Konstrukt sollte man jedoch Abstand nehmen, da jede Instanz die Klassenvariable beliebig verändern kann. Dies kann zu unvorhergesehenem Verhalten führen. Folgendes ist also möglich:

type
  TVorfahr = class
  public
    class var Text: string;
    procedure NeuenWertSetzen;
  end;

  TNachfahr = class(TVorfahr)
  public
    procedure NeuenWertSetzen;    // überschreibt die geerbte Methode
  end;

procedure TVorfahr.NeuenWertSetzen;
begin
  Text := 'Vorfahr';
end;

procedure TNachfahr.NeuenWertSetzen;
begin
  Text := 'Nachfahr';
end;

var
  Vorfahr: TVorfahr;
  Nachfahr: TNachfahr;

begin
  Vorfahr := TVorfahr.Create;     // neue Instanz vom Vorfahr, Text = ''
  Nachfahr := TNachfahr.Create;   // neue Instanz vom Nachfahr, Text = ''

  TVorfahr.Text := 'Geht los!';   // TVorfahr.Text = 'Geht los!'
  Vorfahr.NeuenWertSetzen;        // TVorfahr.Text = 'Vorfahr'
  Nachfahr.NeuenWertSetzen;       // TVorfahr.Text = 'Nachfahr'!!!

  Nachfahr.Free;
  Vorfahr.Free;
end.

In diesem Beispiel wird eine Vorfahrklasse angelegt, die eine Klassenvariable „Text“ enthält. Die Methode dieser Klasse füllt die Variable mit „Vorfahr“. Anschließend wird die Klasse an TNachfahr vererbt. Diese enthält ebenfalls die Klassenvariable „Text“, und die überschriebene Methode füllt die Variable mit „Nachfahr“. Die Methode ist eine reguläre Objektmethode, d.h. sie kann nur über eine Instanz der jeweiligen Klasse aufgerufen werden.

Als erstes legen wir also entsprechende Objektinstanzen für beide Klassen an. Dann tragen wir in der Klassenvariablen TVorfahr.Text einen Wert ein, der auch bei einer Ausgabe auf dem Bildschirm so erscheinen würde. Als nächstes lassen wir das Vorfahr-Objekt diesen Text überschreiben. Dabei passiert noch immer nichts unerwartetes, TVorfahr.Text hat nun den entsprechenden Inhalt. Nun aber trägt das Nachfahr-Objekt einen neuen Wert ein und damit besitzt auch TVorfahr.Text diesen neuen Wert.

Auch wenn es nicht so aussieht, es gibt auch eine praktische Anwendung hierfür: Instanzzähler. Unter Umständen möchte oder muss man wissen, wie oft von einer Klasse Objekte erstellt wurden. Dafür kann man einfach eine Klassenvariable als Zähler verwenden und diesen im Konstruktor erhöhen bzw. im Destruktor verringern.


Interfaces

[Bearbeiten]
Dieses Kapitel muss noch angelegt werden. Hilf mit unter:

http://de.wikibooks.org/wiki/Programmierkurs:_Delphi


Exceptions

[Bearbeiten]

Exceptions (deutsch: Ausnahmen) sind Fehler, die während der Ausführung eines Programms auftreten können.

Exceptions werden immer dann ausgelöst, wenn es erst während des laufenden Programms zu unerlaubten Aufrufen kommt, die während der Kompilierung noch nicht zu erkennen waren. Meist werden dabei falsche oder ungültige Daten an Routinen weitergegeben oder in Berechnungen eingebunden. Die häufigsten Fehler sind dabei Divisionen durch Null oder das Verwenden noch nicht initialisierter Zeiger oder Klassen. Auch der Versuch, eine nicht vorhandene Datei zu laden, gehört hierzu.

Bei all solchen Laufzeitfehlern wird eine Exception ausgelöst, die Sie in Ihrem Programm abfangen und weiter behandeln können.

Exceptions sind spezielle Klassen, die sich alle von der Klasse Exception (Unit SysUtils) ableiten. Alle Exceptions beginnen mit E, viele sind bereits in Delphi vordefiniert. Die bekanntesten Exception-Typen sind wahrscheinlich EMathError, EConvertError und EDivByZero bzw. EZeroDivide. Vor allem die letzten beiden treten häufig auf, wenn man den Divisor nicht prüft. EDivByZero wird ausgelöst, wenn versucht wird, eine Ganzzahl durch Null zu teilen, EZeroDivide wird bei Gleitkommazahlen ausgelöst. EDivByZero tritt z.B. hier auf:

var
  a, b: Integer;
begin
  a := 2;
  b := 0;
  Writeln(a div b);
end;

Ein weiteres Beispiel ist der EConvertError, der zum Beispiel auftritt, wenn man StrToInt eine ungültige Zeichenkette übergibt, also außer einer umwandelbaren Zahl auch Buchstaben oder andere Zeichen enthalten waren.

Abfangen von Exceptions

[Bearbeiten]

In der Delphi-IDE kann man zwar Exceptions abschalten, das unterdrückt jedoch nur die Fehlermeldung, die der Delphi-Debugger anzeigt. Das grundlegende Problem ist immer noch vorhanden und kann unter Umständen den weiteren Programmablauf instabil machen. Besser ist es, angemessen auf solche Ausnahmefehler zu reagieren und anschließend seine Routine oder Methode sauber (und gegebenenfalls mit einem entsprechenden Rückgabewert) zu verlassen.

Delphi bietet hierfür zwei Möglichkeiten an. Der try...except-Block dient dazu, auf verschiedene Exceptions gezielt zu reagieren, während der try...finally-Block auf alle Exceptions gleichermaßen reagiert und meist für Aufräumarbeiten verwendet wird.

Damit eine Exception ausgelöst und unter diesen beiden Blöcken verarbeitet werden kann, müssen Sie die Unit SysUtils in Ihr Programm einbinden. Diese enthält die Basisklasse Exception und stellt damit die grundlegende Möglichkeit der Fehlerbehandlung zur Verfügung.

Mit try...except lässt sich, je nach Fehler, z.B. eine genaue Fehlermeldung ausgeben. Der Aufbau des Blocks ist wie folgt:

try
  <unsichere Anweisungen>
except
  on <Exception> do
    <Anweisung>
  [on <Exception> do
    <Anweisung>]
  ...
else
  <Anweisungen>
end;

Es wird versucht, alle Anweisungen unter try auszuführen. Wenn dabei eine Exception ausgelöst wird, springt das Programm in den except-Block und führt dort die Anweisung für die entsprechende Exception aus. Falls die ausgelöste Exception dort nicht genannt ist, werden die Anweisungen unter else aufgerufen. Der else-Block muss nicht angegeben werden, dient aber dazu, ganz auf Nummer sicher zu gehen. Nach der Behandlung des Fehlers wird das Programm normal fortgesetzt.

Bei dem folgenden Beispiel werden falsche Benutzereingaben verarbeitet:

var
  a, b, c: Byte;

begin
  Write('Wert fuer a? '); Readln(a);
  Write('Wert fuer b? '); Readln(b);
  try
    c := a * b;    // ERangeError, wenn das Ergebnis größer 255 ist
    Writeln(a, ' * ', b, ' = ', c);
    c := a div b;  // EDivByZero, wenn b = 0 ist
    Writeln(a, ' / ', b, ' = ', c);
  except
    on EDivByZero do
      Writeln('Fehler: Division durch 0!');
    on ERangeError do
      Writeln('Fehler: Ergebnis ausserhalb des gueltigen Bereichs!');
  else
    Writeln('Unbekannter Fehler!');
  end;
end.

Der Benutzer wird nun über seine falsche Eingabe gezielt informiert.

Die andere Möglichkeit, der try...finally-Block ist ähnlich aufgebaut:

try
  <unsichere Anweisungen>
finally
  <Anweisungen>
end;

Wie Sie sehen, ist dieser Block weniger umfangreich, da ja bei allen Exceptions die gleichen Anweisungen ausgeführt werden. Unter finally sollten Sie Aufräumarbeiten durchführen, um den Zustand vor Eintritt in den try...finally-Block wieder herzustellen. Z.B. haben Sie ein neues Objekt erstellt, das Sie an eine Anweisung übergeben wollten, wobei der Fehler auftrat. Dann geben Sie den Speicher des Objektes wieder im finally-Block frei.

Bei try...finally ist zu beachten, dass die Anweisungen unter finally in jedem Falle ausgeführt werden, egal ob eine Exception auftritt, ob der try-Block erfolgreich oder mittels Break oder Exit verlassen wird. Weiterhin müssen Sie wissen, dass die Exception am Ende des finally-Blocks erneut ausgelöst wird. Wenn Sie also einen try...finally-Block in einer Methode verwenden, müssen Sie den aufrufenden Programmteil in einer try...except-Klausel einfassen, damit diese Exception dort abschließend bearbeitet werden kann. Geschieht das nicht, wird das Programm am Ende des try...finally-Blocks beendet. Alternativ können Sie auch die Blöcke ineinander verschachteln, die äußerste Fehlerbehandlung sollte dabei immer durch try...except erfolgen, damit das Programm anschließend weiterlaufen kann.

Deklaration

[Bearbeiten]

Wie eingangs schon erwähnt, handelt es sich bei Exceptions um Klassen. Sie können daher eigene Exceptions erstellen, indem Sie Nachkommen von anderen Exceptions deklarieren und dort Anpassungen, wie zum Beispiel die Fehlermeldung, vornehmen.

type
  EMyError = class(Exception)
    constructor Create; overload;
  end;

constructor EMyError.Create;
begin
  inherited Create('Meine eigene Fehlermeldung');
end;

Die Hierarchie würde nun so aussehen:

Exception
   │
   └─── EMyError

Auslösen von Exceptions

[Bearbeiten]

Um an einer Programmstelle eine Exception auszulösen, verwenden Sie die Anweisung raise zusammen mit einem Exception-Objekt:

raise EMyError.Create;

Sie können auch innerhalb einer Ereignisbehandlungsroutine (also im except- oder finally-Block) die aktuell behandelte Exception erneut auslösen. Dazu verwenden Sie raise ohne weitere Angaben.

Exceptions sollten immer dann ausgelöst werden, wenn der weitere Programmablauf durch einen festgestellten Fehler nachhaltig gestört werden würde. Bei der oben genannten Formel a/b erwartet man ein Ergebnis, mit dem man weiterarbeiten kann. Da im Falle von b=0 jedoch kein Ergebnis möglich ist, müssen hier weitere Aktionen angestoßen werden, um den Programmablauf wieder zu korrigieren.

Vor- und Nachteile

[Bearbeiten]

Exceptions und die dazu gehörige Ereignisbehandlung bieten eine einfache Möglichkeit, schwer aufzufindende oder seltene Fehler im Programm festzustellen und zu beheben.

Weiterhin kann man mit Exceptions schwierige und umfangreiche – und damit meist auch unübersichtliche – Vorabprüfungen von Bedingungen vermeiden. Man übergibt also seine Daten ungeprüft an eine Anweisung und kümmert sich erst dann um einen Fehler, falls tatsächlich einer auftritt. Nehmen wir einmal an, Sie haben eine Anweisung, die einen Dateinamen entgegen nimmt, aus dieser Datei Werte ausliest und mit ihnen rechnet. Sie müssten nun vorher prüfen, ob die Datei vorhanden ist, ob sie nicht leer ist und ob die Daten im richtigen Format vorliegen, bevor Sie die Anweisung aufrufen. Dies wäre sehr aufwändig und würde Ihren Programmtext unübersichtlich machen. Hier ist es sinnvoller, die Anweisung einfach „machen zu lassen“ und auf eventuelle Fehler anschließend und gezielt zu reagieren.

Ein weiterer Vorteil von Exceptions ist, dass diese automatisch über mehrere Anweisungsebenen, bis zurück in den Hauptanweisungsblock eines Programms hinaufgereicht werden können. Mit anderen Worten: Sie können selbst bestimmen, an welcher Stelle in Ihrer Anweisungskette welche Exception behandelt wird. Sie haben z.B. eine Routine B, in welcher die Exceptions E1 und E2 auftreten können. Aus dem Hauptprogramm rufen Sie nun eine Routine A auf, die wiederum die Routine B ausführt. In diesem Fall können Sie beispielsweise beide Exceptions im Hauptprogramm oder in A behandeln oder auch E1 in A und E2 erst im Hauptprogramm.

Wenn Sie jedoch eine Exception-Behandlung vermeiden können, tun Sie es bitte grundsätzlich auch. Zum Einen kann der Programmcode gerade bei verschachtelten Behandlungsblöcken schnell unübersichtlich werden. Zum Anderen verbraucht die Ereignisbehandlung natürlich auch kostbare Rechenzeit. Dies passiert beispielsweise, wenn Sie unnötig Objekte erstellen, Dateien laden und eventuell diverse Berechnungsschleifen durchführen, obwohl bereits von vornherein klar war, dass ein übergebener Wert zu einem Fehler führen würde. Auch hierzu möchten wir Ihnen ein kleines Beispiel geben: Sie haben eine Gästeliste von 20 Gästen, für die Sie mithilfe einer Schleife Tischkärtchen ausdrucken möchten. Der Benutzer darf angeben, bis zu welcher Gastnummer der Ausdruck erfolgen soll. Der Benutzer vertippt sich und gibt 22 statt 11 ein. Die Exception bei Gast 21 ist das geringere Problem: Bis sie (endlich) ausgelöst wird, werden neun Seiten zuviel ausgedruckt.

Tipp: Verwenden Sie Exception-Behandlungen nur, wenn Sie Fehler in unterschiedlichen Ebenen einer Anweisungskette abfangen wollen oder wenn eine vorherige Prüfung zu umfangreich bzw. unmöglich ist. Bei einfachen Vorabprüfungen bevorzugen Sie bitte diese. Welche Variante Ihnen besser gefällt, bleibt aber letztlich Ihnen überlassen.


Schnelleinstieg

[Bearbeiten]

Einstieg

[Bearbeiten]

Was ist Delphi

[Bearbeiten]

Borland Delphi ist eine RAD-Programmierumgebung von Borland. Sie basiert auf der Programmiersprache Object Pascal. Borland benannte Object Pascal jedoch 2003 in Delphi-Language um, mit der Begründung, dass sich bereits soviel in der Sprache verändert habe, dass man es nicht mehr mit Pascal vergleichen könne. RAD-Umgebungen für Object Pascal bzw. Delphi existieren nur wenige. Der bekannteste ist der Delphi-Compiler von Borland. Für Linux gibt es Kylix (Anmerkung: Kylix wurde eingestellt), das ebenfalls von Borland stammt und mit Delphi (ggf. mit wenigen Code-Änderungen) kompatibel ist. Darüber hinaus gibt es das freie Projekt Lazarus, das versucht eine ähnliche Entwicklungsumgebung bereitzustellen. Dieses nutzt FreePascal für die Kompilierung und ist für verschiedene Betriebssysteme erhältlich.

Warum Delphi?

[Bearbeiten]

Es gibt viele Gründe, Delphi zu benutzen. Es gibt aber wahrscheinlich auch genauso viele dagegen. Es ist also mehr Geschmacksache, ob man Delphi lernen will oder nicht. Wenn man allerdings Gründe für Delphi sucht, so fällt sicher zuerst auf, dass Delphi einfach zu erlernen ist, vielleicht nicht einfacher als Basic aber doch viel einfacher als C/C++. Für professionelle Programmierer ist es sicher auch wichtig zu wissen, dass die Entwicklung von eigenen Komponenten unter Delphi einfach zu handhaben ist. Durch die große Delphi-Community mangelt es auch nicht an Funktionen und Komponenten.

Erstellt man größere Projekte mit Borlands Delphi Compiler, so ist die Geschwindigkeit beim Kompilieren sicher ein entscheidender Vorteil. Auch die einfache Modularisierung, durch Units, Functions und Procedures ist sicherlich ein Vorteil der Sprache gegenüber primitiveren Sprachen.

Mit Delphi lässt sich zudem fast alles entwickeln, abgesehen von Systemtreibern. Dennoch ist es auch teilweise möglich, sehr hardwarenahe Programme zu entwickeln.

Delphi-Programme laufen in aller Regel auf jedem Windows-Betriebssystem ohne Installation zusätzlicher Software und auch mit sehr alten Delphi-Versionen erstellte Programme funktionieren häufig problemlos auf den neueren Betriebssystemen. Dafür haben die mit Standardmitteln erstellten Programme eine gewisse „Grundgröße“ von einigen hundert Kilobyte.

Die Oberfläche

[Bearbeiten]

Ältere Delphi-Versionen

[Bearbeiten]

Beim ersten Start von Delphi erscheint ein in mehrere Bereiche unterteiltes Fenster. Oben sieht man die Menüleiste, links befindet sich der Objektinspektor und in der Mitte ist eine so genannte Form. Im Hintergrund befindet sich ein Texteditor. Aus der Menüleiste kann man verschiedene Aktionen, wie Speichern, Laden, Optionen, und anderes ausführen. Unten rechts in der Menüleiste (bzw. bei neueren Delphi-Versionen am rechten Bildschirmrand) kann man die verschiedenen Komponenten auswählen, die dann auf der Form platziert werden. Im Objektinspektor kann man die Eigenschaften und Funktionen der Komponenten ändern. Der Texteditor dient dazu, den Quellcode des Programms einzugeben.

Neuere Delphi-Versionen

[Bearbeiten]

Neuere Versionen haben alles in einem Fenster (standardmäßig). Vielen erschwert dies das Arbeiten. Deshalb hat man oben in der Toolbar die Möglichkeit, in einem Drop-Down-Menü (ComboBox) das klassische Layout einzustellen, das ähnlich dem der älteren Versionen ist (abgesehen davon, dass der Objektinspektor noch nach Kategorien unterteilt ist und dass die Komponentenpalette (GUI-Anwendungen) vertikal - nicht horizontal - sortiert ist.

Das erste Programm (Hello world)

[Bearbeiten]

Wenn man nun mit dem Mauszeiger auf die Komponente Label (Menükarte „Standard“) und dann irgendwo auf die Form klickt, dann erscheint die Schrift „Label1“. Das so erzeugte Label kann man nun überall auf der Form verschieben. Um die Schrift zu ändern, wählt man das Label an und sucht im Objektinspektor die Eigenschaft „Caption“ und ändert den Wert von „Label1“ in „Hello world!“. Wenn man nun auf den grünen Pfeil in der Menüleiste klickt (oder F9 drückt), kompiliert Delphi die Anwendung, d.h. Delphi erstellt eine ausführbare Datei und startet diese. Nun sieht man ein Fenster mit der Schrift „Hello world!“. Das ist unsere erste Anwendung in Delphi! War doch gar nicht so schwer, oder?

Erweitertes Programm

[Bearbeiten]

Nachdem wir unser „Hello world“-Programm mit Datei / Alles speichern... gespeichert haben, erstellen wir mit Datei / Neu / Anwendung eine neue GUI-Anwendung. Auf die neue Form setzt man nun einen Button und ein Label (beides Registerkarte Standard). Wenn man nun doppelt auf den Button klickt, öffnet sich das Code-Fenster. Hier geben Sie folgendes ein:

Label1.Caption := 'Hello world';

Wenn man nun das Programm mit F9 startet und dann auf den Button klickt sieht man, dass sich das Label verändert und nun „Hello world“ anzeigt. Damit haben wir eine Möglichkeit, Labels (und eigentlich alles) während der Laufzeit zu verändern.

Beenden Sie nun die Anwendung und löschen Sie den eben getippten Text wieder. Nun bewegen Sie den Cursor vor das begin und geben folgendes ein:

var
  x, y, Ergebnis: Integer;

Wenn Sie wieder unter dem begin sind, dann geben Sie

x := 40;
y := 40;
Ergebnis := x + y;
Label1.Caption := IntToStr(Ergebnis);

ein. Nun kompilieren Sie mit F9 (oder Start / Start) und sehen sich das Ergebnis an. Sie sehen, wenn man auf den Button klickt, verändert sich das Label und zeigt nun das Ergebnis der Addition an. Dies kann man natürlich auch mit anderen Zahlen oder Operationen durchführen. Es sind Zahlen von -2147483648 bis 2147483647 und die Operatoren +, -, * , div (Ganzzahlendivision) und mod (Divisionsrest) möglich.

Nun fassen wir mal zusammen, was Sie bis jetzt gelernt haben könnten bzw. jetzt lernen:

  • In Delphi fungiert das „:=“ als so genannter Zuweisungsoperator, d.h. die linke Seite enthält nach der Operation den Inhalt der rechten Seite. z.B.:
x := 40;
x := y;
Label1.Caption := 'Text';
  • Es gibt unter Delphi Variablen, die vorher in einem speziellen var-Abschnitt definiert werden, z.B.: x: Integer;, so kann x Zahlen von -2147483648 bis 2147483647 aufnehmen oder x, y: Integer; definiert x und y als Integer (spart Schreibarbeit!)

Die Syntax

[Bearbeiten]

(Fast) jeder Programmcode-Befehl wird mit einem Semikolon/Strichpunkt (;) abgeschlossen. Ausnahmen davon sind ein nachfolgendes end (nicht nötig), else (nicht erlaubt) oder until (nicht nötig).

ShowMessage('Hallo Welt');

Strings werden von einfachen Anführungszeichen eingeschlossen

var
  s: string;
...
  s := 'ich bin ein String';

Ebenso wie einzelne Zeichen (Char)

var
  c: Char;
...
  c := 'A';


Die Strukturen von Delphi

[Bearbeiten]

Delphi verfügt über die aus Pascal bekannten Kontrollstrukturen zur Ablaufsteuerung: Reihenfolge, Auswahl und Wiederholung. Als Auswahlkonzepte stehen zur Verfügung: ein-, zwei- und mehrseitige Auswahl (Fallunterscheidung). Als Wiederholstrukturen verfügt Delphi über abweisende, nicht abweisende Schleifen und Zählschleifen. Die Syntax der Strukturen stellt sich wie folgt dar:

Reihenfolge

 <Anweisung>;
 <Anweisung>;
 <Anweisung>;
 <...>

Einseitige Auswahl

 if <Bedingung> then <Anweisung>;

Zweiseitige Auswahl

 if <Bedingung> then
   <Anweisung> 
 else
   <Anweisung>;

Fallunterscheidung

 case <Fall> of
   <wert1> : <Anweisung>;
   <wert2> : <Anweisung>;
   <.....> : <Anweisung>
 else
   <Anweisung>
 end; // von case

Abweisende Schleife

 while <Wiederhol-Bedingung> do
   <Anweisung>;

Nicht abweisende Schleife

 repeat
   <Anweisung>;
   <Anweisung>;
   ...
 until <Abbruch-Bedingung>;

Zählschleife

 for <Laufvariable> := <Startwert> to <Endwert> do
   <Anweisung>;

Strukturen können ineinander verschachtelt sein: Die Auswahl kann eine Reihenfolge enthalten, in einer Wiederholung kann eine Auswahl enthalten sein oder eine Auswahl kann auch eine Wiederholung enthalten sein. Enthält die Struktur <Anweisung> mehr als eine Anweisung, so sind Blöcke mittels begin und end zu bilden. Ausnahme davon ist die Repeat-Schleife: Hier wird zwischen repeat und until automatisch ein Block gebildet.

Programmbeispiel mit allen Strukturen

 program example001;
 {$APPTYPE CONSOLE}
 uses
   SysUtils;
 var
   i      : Integer;
   Zahl   : Real;
   Antwort: Char;
 begin
   WriteLn('Programmbeispiel Kontrollstrukturen');
   WriteLn;
   repeat                  // nicht abweisende Schleife
     Write('Bitte geben Sie eine Zahl ein: ');
     ReadLn(Zahl);
     if Zahl <> 0 then     // einseitige Auswahl
       Zahl := 1 / Zahl;
     for i := 1 to 10 do   // Zählschleife
       Zahl := Zahl * 2;
     while Zahl > 1 do     // abweisende Schleife
       Zahl := Zahl / 2;
     i := Round(Zahl) * 100;
     case i of             // Fallunterscheidung
       1: Zahl := Zahl * 2;
       2: Zahl := Zahl * 4;
       4: Zahl := Zahl * 8
     else
       Zahl := Zahl * 10
     end;
     if Zahl <> 0 then     // zweiseitige Auswahl
       WriteLn(Format('Das Ergebnis lautet %.2f', [Zahl]))
     else
       Writeln('Leider ergibt sich der Wert von 0.');
     Write('Noch eine Berechnung (J/N)? ');
     ReadLn(Antwort)
   until UpCase(Antwort) = 'N'
 end.

Prozeduren und Funktionen

[Bearbeiten]

Grundsätzlich wird zwischen Prozeduren und Funktionen unterschieden. Dabei liefert eine Funktion immer einen Rückgabewert zurück, hingegen eine Prozedur nicht.

Deklaration

[Bearbeiten]

Der eigentliche Code einer Prozedur oder Funktion (in OOP auch als Methode bezeichnet) beginnt immer nach dem begin und endet mit end;

procedure TestMethode;
begin
  ShowMessage('TestMethode!');
end;

Die folgende Funktion liefert beispielsweise einen String zurück.

function TestMethode: string;
begin
  Result := 'ich bin der Rückgabewert';
end;

Parameter können wie folgt verwendet werden:

procedure TestMitParameter1(Parameter1: string);

Mehrere Parameter werden mit „;“ getrennt.

procedure TestMitParameter2(Parameter1: string; Parameter2: Integer);

Mehrere Parameter vom gleichen Typ können auch mit „,“ getrennt werden:

procedure TestMitParameter3(Parameter1, Parameter2: string);

Aufruf

[Bearbeiten]

Eine Methode - egal, ob Prozedur oder Funktion - wird prinzipiell nach folgendem Muster aufgerufen:

Methode(Parameter1, Parameter2, Parameter3, Parameter_n);

Es fällt auf, dass Parameter - im Gegensatz zur Deklaration - durch ein Komma, nicht durch ein Semikolon, getrennt werden. Bei Funktionen kann das Resultat (Result) verwendet werden, muss aber nicht. Falls nicht, geschieht das wie bei der Prozedur, falls doch, wird es wie folgt erweitert:

Variable := Funktion(Parameter1, Parameter2, Parameter3, Parameter_n);

Parameter, die bei der Deklaration in eckige Klammern geschrieben wurden, werden entweder ignoriert oder zugewiesen, jedoch nicht in eckige Klammern geschrieben. Delphi erkennt an der Anzahl der Parameter, ob diese Parameter verwendet wurden.


Datentypen (Array, Records und Typen)

[Bearbeiten]

Arrays

[Bearbeiten]

Ein Array ermöglicht es, Daten zu indizieren und Tabellen zu erstellen. Es gibt zwei verschiedene Arten von Arrays:

Eindimensionale Arrays

[Bearbeiten]
Statische Arrays
[Bearbeiten]
  • Ein statisches Array besitzt eine in der Variablendeklarierung festgelegte Größe, die in eckigen Klammern angegeben wird:
var
  test_array: array[1..10] of Byte;

definiert ein Array, das mit 10 Byte-Werten gefüllt werden kann. Man unterscheidet in Delphi zwischen 0-basierten und 1-basierten Arrays, wobei der Unterschied eigentlich nur in der Zählweise liegt:

var
  array_0: array[0..9] of Byte;

definiert ein gleich großes Array mit 0 als ersten Index. Dies ist eigentlich Geschmacksache, aber wer nebenbei C/C++ programmiert, dem wird die 0-basierte Schreibweise sicher vertrauter sein.

Dynamische Arrays
[Bearbeiten]
  • Ein dynamisches Array ist in seiner Größe dynamisch (wie der Name schon sagt). Das heißt, dass man die Größe während der Laufzeit verändern kann, um beliebig große Datenmengen aufzunehmen. Die Deklaration erfolgt mit
var
  array_d: array of Byte;

Will man zur Laufzeit dann die Größe verändern, so erfolgt die über die procedure SetLength(array, neue_Laenge). Mit den Funktionen High(array) und Low(array) erhält man den höchsten und den niedrigsten Index des Arrays. Bei dynamischen Arrays ist zu beachten, dass diese immer 0-basiert sind.
Mit Length(tabelle1) bekommt man die Länge des Arrays heraus. Hier ist zu beachten, dass bei einem Array von 0 bis 2, 3 Elemente vorhanden sind und darum die Zahl 3 zurückgegeben wird.

Der Zugriff auf ein Array erfolgt mit arrayname[5] := 10;. Der Vorteil von Arrays ist, dass man sie einfach in Schleifen einbauen kann:

var
  arrayname: array[1..10] of Byte;
begin
  for i := 1 to 10 do
    arrayname[i] := 10;
end;

Mehrdimensionale Arrays

[Bearbeiten]

Außerdem kann man mit Arrays mehrere Dimensionen definieren:

Statische Arrays
[Bearbeiten]
var
  tabelle1: array[1..10] of array[1..10] of Byte;   // dies ist gleichbedeutend mit:
  tabelle2: array[1..10, 1..10] of Byte;

definiert beides eine Tabelle mit der Größe 10x10. Der Zugriff erfolgt über

tabelle[5, 8] := 10;

Damit wird die Zelle in der 8. Reihe und in der 5. Spalte mit 10 belegt.

Dynamische Arrays
[Bearbeiten]
var
  Tabelle1: array of array of Byte;

Natürlich muss man auch hier mit SetLength die Länge des Arrays bestimmen. Und das geht so:

SetLength(Tabelle1, 9, 9);

Dies definiert eine 2-dimensionale Tabelle mit der Größe 9×9. Der Zugriff kann nun wie bei einem statischen Array erfolgen. Beide Indizes liegen im Bereich 0..8.

Typen

[Bearbeiten]

Mit einer Typendeklaration kann man eigene Datentypen festlegen, dies erfolgt z.B. durch

type
  Zahl = Integer;

Nach dieser Deklaration kann man überall anstatt Integer Zahl einsetzen:

var
  x: Zahl;

Interessant werden Typen, wenn man zum Beispiel eine Arraydefinition, z.B. eine bestimmte Tabelle öfter einsetzen will:

type
  Tabelle = array[1..10, 1..10] of Byte;

oder wenn man Records oder Klassen benutzt.
Natürlich funktioniert das mit allen Arten von Arrays, also auch mehrdimensional, dynamisch, etc.

Records

[Bearbeiten]

Mit einem Record kann man einer Variable mehrere Untervariablen geben, dies ist z.B. bei Datensammlungen der Fall. Man definiert einen Record fast immer über einen Typen:

type
  THighscoreEintrag = record
    Nr: Byte;
    Name: string;
    Punkte: Integer;
  end;

Wenn man nun eine 10-stellige Highscoreliste erstellen will, dann benutzt man die Anweisung:

var
  Hscr: array[1..10] of THighscoreEintrag;
begin
  Hscr[1].Nr := 1;
  Hscr[1].Name := 'Der Erste';
  Hscr[1].Punkte := 10000;
end;


Pointer

[Bearbeiten]

Einleitung

[Bearbeiten]
Der RAM als Modell

Betrachtet man den RAM, so fällt folgende Struktur auf:

Der RAM[1] ist unter den verschiedenen Anwendungen und dem Betriebssystem aufgeteilt; Den RAM, den eine Anwendung verbraucht, kann man wiederum in Heap und Stack unterteilen. Im Stack werden alle Variablen gespeichert, die z.B. mit var i: Integer; deklariert worden sind. Das Problem am Stack ist, dass er in der Größe sehr beschränkt ist. Bei großen Datenstrukturen sollte man daher auf den Heap zurückgreifen. Dazu benötigt man Zeiger (Pointer).

Grundlagen

[Bearbeiten]
Ein Zeiger in den Heap

Ein Pointer ist in der Regel 4 Bytes groß, d.h. es lohnt sich normalerweise nicht, einen Pointer auf einen Bytewert zu erzeugen, da dieser nur einen Byte belegt! Ein Pointer auf eine bestimmte Datenstruktur wird folgenderweise definiert:

var
  zgr: ^TDaten;

Einem Pointer muss immer ein Wert zugewiesen werden! Für den Fall, dass ein Pointer vorerst nicht gebraucht wird, kann ein Zeiger auch ins Leere zeigen, dies geschieht mit der Zuweisung:

zgr := nil; // nil = not in list

Bevor man den Zeiger allerdings auf Daten zeigen lassen kann muss man zuerst Speicher im Heap reservieren:

New(zgr);

Nun kann man auf die Daten zugreifen, als wäre es eine normale Variable, allerdings hängt man dem Zeigernamen beim Zugriff ein Zirkumflex (^) hinten an:

zgr^ := 50000;

Dies ist nötig, da in zgr selber die Anfangsadresse von zgr^ im Heap gespeichert ist. Wenn TDaten allerdings ein Record ist, so kann man theoretisch beim Zugriff auf die einzelnen Unterpunkte den Zirkumflex weglassen:

zgr.nummer := 1;

Wenn man einen Pointer im weiterem Programmverlauf nicht mehr braucht, dann sollte man unbedingt den belegten Speicher wieder freigeben:

Dispose(zgr);

Mit diesen Grundlagen sollte das Kapitel über Listen, Bäume und Schlangen eigentlich kein Problem mehr darstellen!

Weiterführende Literatur

[Bearbeiten]
  1. RAM: siehe http://de.wikibooks.org/wiki/Computerhardware:_RAM


Dynamische Datenstrukturen

[Bearbeiten]

Die klassische Methode: Listen

[Bearbeiten]

Die hier gezeigte Vorgehensweise ist stark veraltet, fehleranfällig und umständlich. Seit Delphi 4 gibt es bessere und sicherere Methoden, dynamische Datenstrukturen zu handhaben. Siehe hierzu Die moderne Methode: Klassen. Die klassische Vorgehensweise ist nur für erfahrene Programmierer geeignet, die aus dieser Laufzeit-Geschwindigkeitsvorteile ziehen wollen.

Eine Liste ist wie alle dynamischen Datenstrukturen eine rekursive Datenstruktur, d.h.: sie referenziert sich selbst. Schauen wir uns mal folgendes Beispiel für eine Typendefinition für Listen an:

Code:

type
  PListItem = ^TListItem;
  TListItem = record
    data: Integer;
    next: PListItem;  // Verweis auf das nächste Item
  end;

Man sieht, dass der Zeiger in PListItem auf ein Objekt gelegt wird, das noch nicht definiert ist. Eine solche Definition ist nur bei rekursiven Strukturen möglich.

Möchte man nun eine neue, leere Liste erzeugen, so reicht folgender Code:

Code:

var
  Liste: PListItem;
begin
  New(Liste);
  Liste^.data := 0;
  Liste^.next := nil; // WICHTIG!!!
end;

Vergessen Sie bitte niemals, einen nicht benötigten Zeiger (wie in diesem Fall) auf nil zu setzen, da das gerade bei dynamischen Datenstrukturen zu nicht vorhersehbaren Fehlern führen kann.

Wollen sie nun ein Item der Liste hinzufügen, ist folgender Code zu benutzen:

Code:

New(Liste^.next);

und die entsprechende Belegung mit Nichts:

Code:

 Liste^.next^.data := 0;
 Liste^.next^.next := nil;

Es ist natürlich lästig und aufwändig, die Liste auf diese Weise zu vergrößern. Deshalb benutzt man eine Prozedur, um die Liste bis zu ihrem Ende zu durchlaufen und an ihrem Ende etwas anzuhängen:

Code:

procedure AddItem(List: PListItem; data: Integer);
var
  tmp: PListItem;
begin
  tmp := List;
  while tmp^.next <> nil do
    tmp := tmp^.next;
  New(tmp^.next);
  tmp^.next^.data := data;
  tmp^.next^.next := nil;
end;

Da dies aber sehr zeitaufwändig ist, sollte immer das letzte Element der Liste gespeichert werden, um nicht zuerst die gesamte Liste durchlaufen zu müssen, bevor das neue Element angehängt werden kann.

Um eine Liste wieder aus dem Speicher zu entfernen, genügt es nicht, die Variable Liste auf nil zu setzen. Dabei würde der verbrauchte Speicherplatz belegt bleiben und auf die Dauer würde das Programm den Speicher „zumüllen“. Um die Objekte wieder freizugeben, muss Dispose verwendet werden:

Code:

 Dispose(Item);

Da hierbei jedoch das Element in next (falls vorhanden) nicht ordnungsgemäß freigegeben würde, muss man mit einer Schleife alle Elemente einzeln freigeben:

Code:

procedure DisposeList(var List: PListItem);
var
  current, temp: PListItem;
begin
  current := List;
  while current <> nil do
  begin
    temp := current^.next;
    Dispose(current);
    current := temp;
  end;
  List := nil;
end;

Hier wird in jedem Schleifendurchlauf das aktuelle Element freigegeben und das nächste Element zum aktuellen gemacht. Hier wäre zwar prinzipiell auch eine rekursive Funktion möglich (und eventuell auch die zunächst offensichtliche Lösung), diese würde aber bei sehr großen Listen einen Stack Overflow auslösen.

Die moderne Methode: Klassen

[Bearbeiten]

Nachteile der Listenmethode sind vor allem die Fehleranfälligkeit und die umständliche Referenzierung. Sinnvoller - und natürlich auch komplexer - ist hier der Einsatz einer sich selbst verwaltenden Klasse. Hierbei hat man als Anwender lediglich Zugriff auf Methoden, nicht jedoch auf die Datenstruktur selbst. Dies bewirkt einen höchstmöglichen Schutz vor Fehlern.

Grundgerüst

[Bearbeiten]

Als Basis für die dynamische Liste wird ein dynamisches Array verwendet. Der Speicherbedarf hierfür lässt sich dem Bedarf entsprechend anpassen. Weiterhin werden Methoden benötigt, um neue Daten hinzuzufügen, nicht mehr benötigte zu löschen, vorhandene auszulesen oder zu ändern. Dies alles wird in einer Klasse gekapselt.

Code:

type
  TMyList = class
  private
    FFeld: array of Integer;
  public
    constructor Create;
    destructor Destroy; override;
    function Count: Integer;
    procedure Add(NewValue: Integer);
    procedure Delete(Index: Integer);
    procedure Clear;
    function GetValue(Index: Integer): Integer;
    procedure SetValue(Index: Integer; NewValue: Integer);
  end;

Implementation

[Bearbeiten]

Im Konstruktor muss dem Feld Speicher zugewiesen werden, ohne diesen jedoch bereits mit Daten zu füllen:

Code:

constructor TMyList.Create;
begin
  inherited;
  SetLength(FFeld, 0);
end;

Der Destruktor sollte auf gleiche Weise den Speicher wieder freigeben:

Code:

destructor TMyList.Destroy;
begin
  SetLength(FFeld, 0);
  inherited;
end;

Für die Verwendung der Liste ist es oftmals notwendig, deren Größe zu kennen, um nicht eventuell über deren Ende hinaus Daten auszulesen. Das wird mit der simplen Methode Count verwirklicht.

Code:

function TMyList.Count: Integer;
begin
  Result := Length(FFeld);
end;

Um nun Daten hinzuzufügen, wird eine weitere Methode benötigt: Add. Hierbei ist zu beachten, dass der erste Index der Liste immer 0 (Null) ist. Wenn nur ein Eintrag enthalten ist (Count = 1), dann ist dieser in FFeld[0] zu finden. Durch den Aufruf von SetLength wird das Feld um einen Eintrag erweitert, womit sich auch das Ergebnis von Count um 1 erhöht. Demnach muss beim Speichern des Wertes wieder 1 subtrahiert werden.

Code:

procedure TMyList.Add(NewValue: Integer);
begin
  SetLength(FFeld, Count + 1);    // Größe des Feldes erhöhen
  FFeld[Count - 1] := NewValue;   // Neuen Eintrag ans Ende der Liste setzen
end;

Das Löschen eines Eintrages gestaltet sich etwas schwieriger, da dieser sich am Anfang, in der Mitte oder am Ende der Liste befinden kann. Je nachdem müssen die Daten gegebenenfalls umkopiert werden.

Code:

procedure TMyList.Delete(Index: Integer);
var
  i: Integer;
begin
  if (Index < 0) or (Index >= Count) then
    Exit;

  if Index = Count - 1 then        // letzter Eintrag
    SetLength(FFeld, Count - 1)    // Es reicht, den Speicher vom letzten Eintrag freizugeben
  else                             // erster Eintrag oder irgendwo in der Mitte
  begin
    for i := Index to Count - 2 do
      FFeld[i] := FFeld[i + 1];
    SetLength(FFeld, Count - 1);
  end;
end;

Man sollte auch die Liste leeren können, ohne jeden einzelnen Eintrag zu löschen. Hierfür muss nur wieder die Größe des Feldes auf 0 gesetzt werden.

Code:

procedure TMyList.Clear;
begin
  SetLength(FFeld, 0);
end;

Das Auslesen und Ändern vorhandener Daten gestaltet sich ebenfalls sehr einfach.

Code:

function TMyList.GetValue(Index: Integer): Integer;
begin
  if (Index < 0) or (Index >= Count) then
    Result := -1                      // Oder ein anderer Wert, der einen Fehler darstellt
  else
    Result := FFeld[Index];
end;

procedure TMyList.SetValue(Index: Integer; NewValue: Integer);
begin
  if (Index < 0) or (Index >= Count) then
    Exit
  else
    FFeld[Index] := NewValue;
end;

Erweiterungsmöglichkeit

[Bearbeiten]

Um einen noch komfortableren Zugriff auf die Daten zu erhalten, können diese über eine Eigenschaft aufgerufen werden. Hierzu werden die Methoden GetValue und SetValue „versteckt“, das heißt, im Bereich private der Klasse untergebracht. Im Bereich public kommt folgendes hinzu:

Code:

public
  property Items[I: Integer]: Integer read GetValue write SetValue; default;

Auf diese Weise ist ein wirklich einfacher Zugriff auf die Daten möglich:

Code:

var
  MyList: TMyList;
  i: Integer;
begin
  MyList := TMyList.Create;
  MyList.Add(2);       // MyList[0] = 2
  MyList.Add(3);       // MyList[1] = 3
  MyList.Add(5);       // MyList[2] = 5

  i := MyList[2];      // i = 5
  MyList[0] := 13;     // MyList[0] = 13
  MyList.Free;
end;

Anmerkung

[Bearbeiten]

Die hier gezeigte Lösung stellt nur den einfachsten Ansatz einer dynamischen Datenverwaltung dar. Sie kann jedoch Daten jeden Typs speichern, es muss lediglich der Typ von FFeld und aller betroffenen Methoden angepasst werden. Auch andere Klassen können so gespeichert werden.

Dieses Grundgerüst ist auf einfachste Weise erweiterbar. Zum Beispiel können Methoden zum Suchen und Sortieren von Daten eingebaut werden. Auch das Verschieben von Einträgen wäre denkbar.


DLL-Programmierung

[Bearbeiten]

Was ist eine DLL?

[Bearbeiten]

Eine DLL (Dynamic Link Library) kann Routinen und Forms beinhalten, die dann in ein beliebiges Programm eingebunden werden können, auch in andere Sprachen. Dazu wird die DLL in den Speicher geladen, von wo aus alle Anwendungen auf sie zurückgreifen können. Aus diesen Eigenschaften ergeben sich folgende Vorteile:

  • Programmiersprachenunabhängiges Programmieren
  • Beim Benutzen durch mehrere Anwendungen wird der Code nur einmal geladen
  • Die DLL kann dynamisch in den Code eingebunden werden

Das Grundgerüst einer DLL

[Bearbeiten]

Klickt man auf Datei->Neu->DLL erhält man folgendes Grundgerüst für eine DLL:

Code:

library Project1;
{  ...Kommentare (können gelöscht werden)... }
uses
  SysUtils,
  Classes;
 
begin
end.

Wie man sieht, wurde das für ein Programm übliche program durch library ersetzt, dadurch „weiß“ der Compiler, dass er eine DLL kompilieren soll und dieser die Endung .dll geben muss.

In den Bereich zwischen dem Ende des uses und dem begin können nun beliebig viele Prozeduren und Funktionen eingegeben werden. Wenn man die DLL nun kompiliert, kann man allerdings noch nichts mit ihr anfangen! Man muss die Funktionen/Prozeduren, die man aus der DLL nutzen soll, exportieren! Dazu benutzt man vor dem abschließenden begin...end folgenden Code:

Code:

exports
  Funktion1 index 1,
  Funktion2 index 2;

Jeder exportierten Funktion einer DLL wird ein numerischer Index zugewiesen. Wenn man in der exports-Klausel den Index weglässt, wird automatisch einer vergeben. Der Index sollte heutzutage nicht mehr explizit angegeben werden.

Nun kann man diese Funktionen aus jedem Windows-Programm nutzen. Da andere Sprachen allerdings andere Aufrufkonventionen besitzen, muss der Funktionsdeklaration ein stdcall; hinzugefügt werden, das für den Standard-Aufruf von anderen Sprachen steht. Damit erhält man z.B. folgenden Code:

Code:

library DemoDLL;

uses
  SysUtils, Classes;
 
function Addieren(x, y: Byte): Word; stdcall;
begin
  Result := x + y;
end;

function Subtrahieren(x, y: Byte): ShortInt; stdcall;
begin
  Result := x - y;
end;
 
exports
  Addieren index 1,
  Subtrahieren index 2;
 
begin
end.

Einbinden von DLLs

[Bearbeiten]

Das Einbinden von DLLs kann auf zwei verschiedenen Wegen erfolgen. Bei einem statischen Aufruf werden alle Bibliotheken bereits zum Programmstart automatisch geladen. Dies kann dazu führen, dass ein Programm bereits den Start verweigert, wenn eine bestimmte DLL oder Funktion nicht vorhanden ist.

Daher kann man Bibliotheken auch dynamisch laden. Dies erfolgt zu dem von Ihnen gewählten Zeitpunkt im Programmablauf. Da Sie hierfür Windows-Funktionen verwenden, bekommen Sie auch Rückmeldungen, ob eine DLL geladen bzw. die entsprechende Funktion eingebunden werden konnte oder nicht.

Wie Sie oben schon gesehen haben, werden in einer Bibliothek nur Prozeduren und Funktionen exportiert. Alles andere, also Typdefinitionen, Konstanten und Variablen - auch Klassen und deren Methoden - lassen sich nicht exportieren. Sie haben also immer nur einen so genannten flachen Zugriff darauf, was die Bibliothek anbietet. Eventuell in der Bibliothek definierte Typen oder Konstanten, die von den Funktionen als Parameter erwartet oder als Rückgabewert verwendet werden, müssen Sie in Ihrem Programm noch einmal neu definieren. Eine Unit, die alles zusammen umsetzt, also die Definitionen plus das Linken der Funktionen, nennt man einen Wrapper.

Statisches Einbinden

[Bearbeiten]

Bibliotheken lassen sich auf einfache Weise statisch einbinden. Sie geben dazu den Funktionskopf an, so wie er von der Bibliothek vorgegeben wird, gefolgt von external und dem Dateinamen der Bibliothek. Sie können weiterhin den Namen der Funktion oder deren Index in der Bibliothek angeben, die Sie verwenden wollen:

function Funktionsname[(Parameter: Typ; ...)]: Rückgabewert; [Konvention;] external 'dateiname.dll' [name 'Funktionsname'|index Funktionsindex];

Statt function kann hier natürlich auch procedure stehen, wenn die Bibliotheksfunktion keinen Wert zurück gibt.

Beim Einbinden von Bibliotheken, die in anderen Programmiersprachen geschrieben wurden (meistens C/C++) geben Sie als Aufrufkonvention das Schlüsselwort stdcall an. Wenn Sie Funktionen des Windows-Betriebssystems einbinden wollen, verwenden Sie immer diese Konvention, sonst wird Ihr Programm nicht funktionieren. Andere Betriebssysteme verwenden eventuell auch die Konvention cdecl. Falls Ihr Programm Funktionen nicht einbinden kann und Sie nicht wissen, welche Konvention die richtige ist, probieren Sie diese einfach aus. Weitere Konventionen finden Sie in der Delphi-Hilfe. Die Aufrufkonvention legt fest, wie Parameter an die DLL-Funktion übergeben werden.

Auch hier sollten Sie den Aufruf über den Funktionsnamen dem Index vorziehen. Im Regelfalle wird Ihnen der Index auch nicht bekannt sein. Wenn Sie in Ihrer Funktionsdeklaration den gleichen Namen wie in der Bibliothek verwenden, müssen Sie diesen nicht explizit angeben.

Da das jetzt ein bisschen viel Theorie war, wollen wir uns ein praktisches Beispiel ansehen:

Code:

type
  HKEY = type LongWord;  // aus der Unit Windows übernommen
  HDC = type LongWord;
 
function RegCloseKey(Key: HKEY): Integer; stdcall; external 'advapi32.dll';
function HolePixel(Handle: HDC; X, Y: Integer): LongWord; stdcall; external 'gdi32.dll' name 'GetPixel';

Zunächst erfolgen die Typdefinitionen für die von den Funktionen verwendeten Typen. Der Einfachheit halber wurden diese aus der mitgelieferten Unit Windows übernommen. Anschließend wird die Funktion RegCloseKey aus der Bibliothek advapi32 geladen. Da unsere Delphi-Funktion den gleichen Namen verwendet wie die Bibliotheksfunktion, muss die name...-Klausel nicht angegeben werden.

Im zweiten Fall importieren wir GetPixel aus der gdi32. Da unsere Funktion einen anderen Namen hat, muss hier explizit der Name der Bibliotheksfunktion angegeben werden.

Gerade in der Windows-Programmierung ist die name-Klausel sinnvoll. Windows bietet viele Systemfunktionen sowohl als Ansi- wie auch als Unicode-Version an. Die Funktionsnamen enden dann jeweils auf A oder W. Da man meist nur eins von beidem verwendet, kann man intern diese Endung weglassen.

Dynamisches Einbinden

[Bearbeiten]

Um Bibliotheken dynamisch einzubinden, ist etwas mehr Programmieraufwand gefordert. Sie müssen hierbei die gewünschte Bibliothek selbst laden und die Adressen der Bibliotheksfunktionen zuvor definierten prozeduralen Variablen zuweisen. Das hört sich aber schwieriger an als es ist. Um einen Vergleich zu haben, ändern wir das obige Beispiel ab.

Die Typdefinition aus dem obigen Beispiel benötigen wir nicht mehr, da wir die Unit Windows einbeziehen müssen. In ihr sind diese Typen bereits vorhanden, ebenso die benötigten Funktionen LoadLibrary, GetProcAddress und FreeLibrary. Anschließend legen wir zwei prozedurale Variablen an, die nachher unsere Bibliotheksfunktionen aufnehmen:

Code:

uses
  Windows;

var
  MyRegCloseKey = function(Key: HKEY): Integer; stdcall;
  HolePixel = function(HDC; X, Y: Integer): LongWord; stdcall;

Da RegCloseKey ebenfalls in Windows bereits vorhanden ist, haben wir den Namen dieser Funktion hier leicht abgeändert. In einem anderen Teil des Programms (z.B. in einer Initialisierungsroutine) werden dann die Bibliotheken geladen und die Funktionen zugewiesen:

Code:

var
  advapi32, gdi32: HMODULE;

procedure Laden;
begin
  advapi32 := LoadLibrary('advapi32.dll');
  if advapi32 <> 0 then
  begin
    @MyRegCloseKey := GetProcAddress(advapi32, 'RegCloseKey');
    { ggf. weitere Funktionen laden }
  end;

  gdi32 := LoadLibrary('gdi32.dll');
  if gdi32 <> 0 then
  begin
    @HolePixel := GetProcAddress(gdi32, 'GetPixel');
    { ggf. weitere Funktionen laden }
  end;
end;

Zur kurzen Erläuterung: LoadLibrary gibt ein Handle auf die Bibliothek zurück. Da die Bibliothek so lange geladen bleiben muss, bis wir deren Funktionen nicht mehr benötigen, bietet sich eine globale Variable an, um das Handle aufzunehmen. Um Manipulationen zu vermeiden, sollte man diese aber nicht öffentlich, sondern im implementation-Teil einer Unit deklarieren.

Wenn LoadLibrary den Wert 0 zurückgibt, ist das Laden der Bibliothek fehlgeschlagen. Ebenso gibt GetProcAddress nil zurück, falls die Funktion nicht geladen werden konnte.

Nachdem die Funktionen nicht mehr benötigt werden, spätestens aber am Ende des Programms, muss der Speicher freigegeben werden:

Code:

procedure Entladen;
begin
  if advapi32 <> 0 then
  begin
    FreeLibrary(advapi32);

    { folgendes ist wichtig, wenn das Programm weiterläuft }
    advapi32 := 0;
    @MyRegCloseKey := nil;
    { ggf. weitere Funktionen auf nil setzen }
  end;

  if gdi32 <> 0 then
  begin
    FreeLibrary(gdi32);
    gdi32 := 0;
    @HolePixel := nil;
     { ggf. weitere Funktionen auf nil setzen }
  end;
end;

Optionale dynamische Dll Einbindung mit type:

Code:

uses
  Windows;
type Tadvapi32_MyRegCloseKey = function(Key: HKEY): Integer; stdcall;
procedure dynamischerDllAufruf;
var
  LHnd  : THandle;
  Lfunc : Tadvapi32_MyRegCloseKey;
  Key   : HKEY;
  IntResult:Integer;
begin
  try
    LHnd := LoadLibrary('advapi32.dll');
    Lfunc := GetProcAddress(LHnd, 'MyRegCloseKey')
    Key :=?;
    IntResult := Lfunk (Key);
  finally
    if LHnd <> 0 then
    begin
      FreeLibrary(LHnd);
      Lfunc := nil;
    end;
  end;   
end;

  
end;


Assembler und Delphi

[Bearbeiten]

Allgemeines

[Bearbeiten]

In Delphi wird der Assemblerteil immer mit asm eröffnet und mit dem obligatorischen end geschlossen. Die Register edi, esi, esp, ebp und ebx müssen innerhalb des Assemblerteils unverändert bleiben! Will man sie trotzdem verwenden, muss man sie vorher auf dem Stack ablegen:

push edi
push esi
push esp
push ebp
push ebx

Und am Ende der Assembler-Anweisungen wieder zurückholen:

pop ebx
pop ebp
pop esp
pop esi
pop edi

Syntax

[Bearbeiten]

Es können wahlweise Assemblerblocks mit Delphicode vermischt werden, oder ganze Prozeduren/Funktionen in Assembler verfasst werden.

Beispiel für Assembler-Funktion

function Add(a, b: Integer): Integer; assembler;
asm
  mov eax, a
  add eax, b
end;

Das Schlüsselwort assembler ist hierbei nicht verpflichtend.

Beispiel für Assembler in Delphi

function Add(a, b: Integer): Integer;
begin
  asm
    mov eax, a
    add b, eax
  end;
  result := b;
end;

Anstatt die Parameter selbst in die einzelnen Register zu schieben, kann man diese auch direkt verwenden. Die Parameter werden nämlich auf den Registern gespeichert, der erste Parameter auf EAX, der zweite auf EDX und der dritte auf ECX. Außerden kann man als Result das Register EAX verwenden. Demnach könnte die Funktion oben folgendermaßen aussehen:

function Add(a, b: Integer): Integer;
begin
  asm
    add eax, edx
  end;
end;

In der Regel wird in Assembler-Abschnitten nicht mehr als ein Befehl pro Zeile untergebracht. Mehrere Befehle können durch ein Semikolon oder einem Kommentar voneinander getrennt werden.

Assembler-Zeile:

[Label] [Befehl] [Operand(en)]

Beispiele:

xor eax, eax
mov variable, eax
xor eax, eax; mov variable, eax
xor eax, eax {eax leeren} mov variable, eax

Alle drei Varianten sind gleichwertig, die erste Variante mit einem Befehl pro Zeile ist jedoch die übliche und sollte vorgezogen werden. Kommentare dürfen in Assembler-Blocks nie zwischen Befehl und Operanden stehen, zwischen Label und Befehl jedoch schon.

Labels müssen als lokale Labels definiert sein:

@xxx:
{ ... }
jmp @xxx

Einige unterstützte Befehle gibt es in der Delphi-Hilfe unter dem Index „Assembler-Anweisungen“. Der integrierte Assembler unterstützt die Datentypen Byte(db), Word(dw) und DWord(dd), ein Pointer entspricht einem DWord. Wenn Sie eine Variable deklarieren, die denselben Namen hat wie ein reserviertes Wort, dann hat das reservierte Wort Vorrang. Auf die deklarierte Variable kann durch Voransetzen eines kaufmännischen Unds zugegriffen werden:

var
  ax: Integer;
asm
  mov ax, 4  {verändert das Register ax}
  mov &ax, 4 {verändert die Variable ax}
end;


RAD-Umgebung

[Bearbeiten]

Warum eine RAD-Umgebung?

[Bearbeiten]

Um diese Frage zu beantworten, muss man erst einmal den Begriff RAD definieren: RAD ist ein Akronym für Rapid Application Development (dt. etwa: „Schnelle Anwendungsentwicklung“). Sprich, es behauptet, man könne schnell Anwendungen entwickeln. Verwirklicht wird dies durch:

  • IDE: Die integrierte Entwicklungsumgebung („Integrated Development Environment“) bietet Design, Coding, Compiler und Debugging in einer Oberfläche
  • Automatisierung: per Knopfdruck (bei Delphi „F9“) wird das Projekt geprüft, kompiliert, gelinkt und dann ausgeführt. Das geschieht meist innerhalb von wenigen Sekunden, man kann also eine kleine Änderung vornehmen und sofort sehen, wie sich diese im Programm auswirkt.

Nun mag vielleicht die Frage aufkommen „Was gibt es denn dann noch außer RAD?“. Antwort: Es gibt zum Beispiel Kommandozeilen-Compiler, bei denen der Vorgang so abläuft:

  • Code komplett in eine Textdatei schreiben
  • Compiler mit der Datei ausführen
  • Wenn ein Compiler-Fehler auftritt, zurück zum Anfang und korrigieren, dann kommt der nächste, denn der Compiler spuckt immer nur einen Fehler auf einmal aus
  • Dann die Objektfiles linken - bei Linkerfehler das selbe Spiel wie beim Kompilieren
  • Und dann endlich sollte ein ausführbares Programm rauskommen, bei dem man dann überprüfen kann, ob man alles richtig programmiert hat. Wenn das nicht der Fall ist, zurück zum Anfang.

Der Vorteil eines solchen Systems: Man gewöhnt sich bald an, sehr, sehr sauber zu programmieren - Die Einsteigerfreundlichkeit ist aber gleich Null.

Die Antwort lautet also:

  • Mit einer RAD-Umgebung kann man schnell und flexibel programmieren.
  • Eine RAD-Umgebung ist anfängerfreundlich.
  • Eine RAD-Umgebung ist kompakt und in sich stimmig.


Der Debugger

[Bearbeiten]

Der Debugger (von engl. bug: Wanze; wörtlich: Entwanzer) ist ein nützliches Tool, das einem die Arbeit erleichtert, einen Fehler zu finden. Es gibt für fast jede Programmiersprache einen Debugger, wir sind hier nicht auf die RAD-Umgebung angewiesen! Für den C-Compiler GCC gibt es beispielsweise einen dazugehörigen Debugger. Jedoch wird er per Kommandozeile bedient und ist nicht nur deshalb nicht so bequem wie der Delphi-Debugger.

Die Aufgabe des Debuggers

[Bearbeiten]

Der Debugger hat - wie schon oben erwähnt - die Aufgabe, einem Programmierer beim Finden eines Fehlers zu helfen. Man kann damit beispielsweise prüfen, ob man eine Bedingung richtig formuliert hat oder welchen Wert eine Variable im laufenden Programm hat. Diese Sachen sind das Einzige, was der Debugger bietet. Auch wenn es nach wenig klingt: Allein das kann oftmals sehr nützlich sein!

Setzen eines Haltepunkts

[Bearbeiten]
Gesetzter Haltepunkt

Ein Haltepunkt - englisch Breakpoint - ist eine Zeile im Code, die überprüft wird. Genauer: Das Programm wird an dieser Stelle angehalten. Man erhält Informationen über alle Variablen und ihre Werte, die sie an dieser Stelle des Programms haben.

Um einen Haltepunkt zu setzen, muss man mit der linken Maustaste auf den Rand neben der gewünschten Codezeile klicken. Auf dieselbe Weise lässt er sich wieder entfernen.

Danach wird das Programm mit F9 kompiliert und ausgeführt. Beim Kompilieren wird auf den roten Punkt ein Haken gesetzt. Damit zeigt der Compiler an, dass dieser Haltepunkt korrekt ist und im Programmablauf auch erreicht werden kann. Wenn das Programm diese Stelle dann erreicht, wird es vor der Ausführung dieser Zeile angehalten und es wird das Editorfenster wieder angezeigt. Ein blauer Pfeil in der linken Spalte zeigt die Position der Zeile an, die als nächstes ausgeführt werden soll. Sollte Ihr Haltepunkt durchgekreuzt sein, kann das Programm an dieser Stelle nicht angehalten werden oder diese Stelle wird niemals erreicht.

Tipp:

Wenn Sie das Programm kompilieren lassen, zeigt der Editor von Delphi vor allen Zeilen einen blauen Punkt an, die im Programmablauf angesprungen werden können. Auf jede dieser Zeilen können Sie einen Haltepunkt setzen. In Lazarus funktioniert dies nicht.


Werte anzeigen und ändern

[Bearbeiten]

Wenn Sie nun den Mauszeiger über eine Variable oder über die Eigenschaft eines Objekts (z. B. auch Edit1.Text) bewegen, bekommen Sie als Hint den Wert der Variablen mitgeteilt. Somit können Sie auch feststellen, ob diese die von Ihnen vorgesehenen Werte besitzen. Bei einer Klasse können Sie sogar durch sämtliche Felder und Eigenschaften navigieren.

Da das bei mehreren Variablen sehr mühselig werden kann, bietet Delphi im Debug-Modus zwei Auswertungsfenster: eines für lokale Variablen der aktuellen Methode inklusive Result und bei Klassen auch Self, eines für selbst angelegte Überwachungen.

Bei einigen Variablen können Sie während des Debuggens auch den Wert ändern. Dies funktioniert bei den einfachen Datentypen (Gleitkomma- oder Ganzzahlen, Char, Boolean), bei Arrays unterstützter Datentypen, sowie Record- und Klassen-Feldern der unterstützen Datentypen. Nicht erlaubt sind Änderungen von Zeigern, dazu zählen also auch Strings. Um eine Änderung vorzunehmen, doppelklicken Sie in der Liste der lokalen Variablen auf den gewünschten Namen. In der "Liste überwachter Ausdrücke" klicken Sie mit der rechten Maustaste auf die Variable und wählen Untersuchen. Alternativ können Sie in beiden Fällen die Variable anklicken und dann die Tastenkombination Strg+I verwenden. Im sich dann öffnenden Fenster klicken Sie auf den Button mit den drei Punkten und können nun den Wert ändern.

Hinweis: Das Ändern von Werten wird derzeit nicht in Lazarus unterstützt. Die Listen überwachter Ausdrücke und lokaler Variablen können dort über das Menü angezeigt werden. Der Wert als Hint beim Draufzeigen mit der Maus wird ebenfalls angezeigt.

Programme schrittweise ausführen

[Bearbeiten]

Wenn Sie nun den Programmablauf studieren wollen, um zum Beispiel festzustellen, an welcher Stelle ein Fehler genau auftritt, können Sie das Programm Schritt für Schritt fortsetzen. Mit der Taste F7 springt der Debugger auch in jede Methode und Routine hinein und lässt Sie dort die Anweisungen einzeln ausführen. Dies funktioniert natürlich nur, wenn der Quelltext für diese Methoden und Routinen auch zugänglich ist. Wenn Sie den Ablauf einer Methode nicht benötigen, können Sie mit der Taste F8 die aktuelle Zeile ausführen lassen, ohne in die Methode hineinzuspringen.

Um die Fehlersuche zu beenden und den normalen Programmablauf an der aktuellen Stelle wieder aufzunehmen, drücken Sie F9. Falls Sie das Programm vollständig abbrechen und zum Editor zurückzukehren möchten, verwenden Sie die Tastenkombination Strg+F2.


Erstellung einer grafischen Oberfläche

[Bearbeiten]

Grundsätze

[Bearbeiten]

Um eine neue Anwendung mit grafischer Benutzeroberfläche zu erstellen, wählen Sie aus dem Menü Datei/Neu/VCL-Formularanwendung. Daraufhin wird Ihnen im Designer ein leeres Fenster mit dem Titel „Form1“ angezeigt. Der dazugehörige Code sieht folgendermaßen aus:

Code:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs;

type
  TForm1 = class(TForm)
  private
    { Private-Deklarationen }
  public
    { Public-Deklarationen }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

end.

Um zwischen dem Formdesigner und dem Code-Editor zu wechseln, benutzen Sie die Taste F12.

Eine grafische Oberfläche wird mittels Drag&Drop erstellt: In der Komponentenpalette wählt man die Komponente durch einen einfachen Klick aus und bewegt dann die Maus an die Stelle, wo die linke obere Ecke der Komponente sein soll. Dann zieht man mit der Maus nach rechts unten, bis die gewünschte Position erreicht ist. Wenn man nur auf das Formular klickt, wird die Komponente in einer Standardgröße, die veränderlich ist, erstellt.

Anschließend kann man im Objektinspektor die Eigenschaften der Komponente ändern, wie z.B. die Aufschrift (Caption), den Inhalt (Text, Lines oder Items), Schriftart, -größe, -farbe etc. (Font) und komponentenspezifische Eigenschaften.

Um ein Ereignis zu programmieren, das bei einer bestimmten Aktion geschehen soll, wählt man im Objektinspektor die Registerseite „Ereignisse“ und wählt das entsprechende Ereignis durch Doppelklick auf das leere Feld hinter dem Namen der Methode aus (OnClick für einen Klick, OnDblClick für einen Doppelklick, OnMouseDown für ein Runterdrücken einer Maustaste, OnMouseMove für ein Drüberfahren mit der Maus, OnKeyDown für das Drücken einer Taste, usw.). Im Code-Editor kann man allerdings auch jede Eigenschaft ohne Objektinspektor regeln. Dabei wird das Ereignis OnCreate des Formulars gewählt (wird durch Doppelklick auf das Formular erstellt):

Code:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Button1.Font.Name := 'Courier New';
end;

Dies bewirkt, dass Button1 die Schriftart „Courier New“ verwendet.

Kompiliert wird das Ganze wie bei einer Konsolenanwendung mit F9 oder Start / Start.

Weitergabe

[Bearbeiten]

Einem Formular steht genau eine Unit zu. Beim Abspeichern des Projekts über Datei / Alles speichern ... gelten folgende Regeln:

  • Die Projektdatei fasst alle Formulare / Units zusammen. In ihr kann auch der Programmablauf verändert werden, beispielsweise wenn ein Info-Fenster am Anfang erstellt werden soll. Sie hat die Dateiendung „.dpr“ für Delphi Project.
  • Die einzelnen Units bekommen die Dateiendung „.pas“ für Pascal. Sie beinhalten entweder den Code einer Komponente, den eines Formulars oder Funktionen. Keine Unit darf denselben Namen wie die Projektdatei haben.
  • Die Formulare bekommen in Delphi (wie im C++ Builder) die Endung „.dfm“ für Delphi Formular. Sie haben denselben Namen wie die dazugehörige Unit, bloß eine andere Endung. Delphi- und C++ Builder-Formulare sind zueinander kompatibel.
  • Hinzu kommen weitere Konfigurations- und Projektdateien (.cfg, .bdsproj, .dsk und ggf. weitere)

Bei allen Dateinamen sind Leerzeichen oder Zahlen am Anfang verboten, auch wenn das Windows theoretisch zulassen würde. Weiterhin werden folgende Dateien beim Kompilieren erzeugt:

  • Die Anwendung bekommt denselben Namen wie das Projekt, bloß mit der Endung „.exe“ für Executable. Sie ist das fertige Programm und ist (neben eventuellen Ressourcen oder Datenbanken, Grafiken, ...) die einzige Datei, die zur Ausführung relevant ist. Demnach muss man auch nur sie für eine Weitergabe als Nicht-OpenSource / Freeware mit Source weitergeben.
  • Alle Units werden kompiliert. Die kompilierte Unit ist binär und bekommt die Endung „.dcu“ für Delphi Compiled Unit. Eine DCU-Datei ist nur mit der Delphi-Version, mit der sie erstellt wurde, kompatibel. Höhere Delphi-Versionen bieten es einem an, eine DCU-Datei eines älteren Formats zu konvertieren. Andersherum ist das logischerweise nicht möglich.
  • Eine Ressourcendatei (*.res) wird ebenfalls erstellt. Diese Datei enthält das Programmsymbol und weitere Informationen, die Sie in den Eigenschaften der EXE-Datei im Windows-Explorer angezeigt bekommen. Diese Datei wird in die EXE-Datei gelinkt und muss nicht einzeln weitergegeben werden.

Für OpenSource-Projekte müssen folgende Dateien mitgeliefert werden:

  • die Projektdateien (.dpr und .bdsproj, falls vorhanden)
  • die Units (.pas)
  • die Formulare (.dfm)
  • wenn ein Anwendungsicon erstellt wurde, die Ressourcendatei (.res)
  • die Konfigurationsdatei (.cfg)

Mehrere Formulare erstellen

[Bearbeiten]

Mehrere Formulare können erstellt werden, um z.B. einen Dialog oder ein Info-Fenster zu integrieren. Dies kann über Datei / Neu / Formular geschehen.

Beim Aufruf eines Formulars gibt es zwei Möglichkeiten. Nachdem die Unit des Formulars (z. B. Unit2 für Form2) in die Uses-Klausel aufgenommen wurden, kann man das Formular aufrufen:

  • Form2.Show;
  • Form2.ShowModal;

Mit Show wird das Fenster angezeigt, ohne dass das Hauptfenster blockiert wird. Der Anwender kann also in beiden Fenstern gleichzeitig arbeiten. ShowModal erlaubt es dem Anwender, nur das neue Fenster zu verwenden und blockiert alle Eingaben in andere Fenster. Erst, wenn dieses Fenster geschlossen wird, werden alle anderen wieder freigegeben. Man kennt das z.B. vom Infofenster der meisten Anwendungen.

Wenn man einen Dialog hat, kann man Buttons unterbringen und denen einen anderen ModalResult-Wert zuweisen (mrOk für OK, mrCancel für Abbrechen, ...). Beim Klick auf einen Button mit einem ModalResult-Wert ungleich mrNone wird das Fenster automatisch geschlossen. Nun ist Show eine Prozedur und ShowModal eine Funktion. ShowModal gibt den gedrückten Button (bzw. dessen ModalResult) zurück. Damit kann man feststellen, mit welcher Schaltfläche der Diaglog geschlossen wurde:

Code:

if Form2.ShowModal = mrOk then
  ShowMessage('Sie haben OK geklickt');

Bei mehreren Möglichkeiten sollte man die Fallunterscheidung verwenden:

Code:

case Form2.ShowModal of
  mrOk:
    ShowMessage('Sie haben OK geklickt');
  mrCancel:
    ShowMessage('Aktion wurde abgebrochen.');
end;

Die vordefinierten ModalResult-Werte haben folgende Bedeutung:

Konstante Bedeutung
mrNone Kein Wert. Wird als Vorgabewert verwendet, bevor der Benutzer das Dialogfeld verlässt.
mrOk Der Benutzer verlässt das Dialogfeld mit der Schaltfläche OK.
mrCancel Der Benutzer verlässt das Dialogfeld mit der Schaltfläche Abbrechen.
mrAbort Der Benutzer verlässt das Dialogfeld mit der Schaltfläche Abbruch.
mrRetry Der Benutzer verlässt das Dialogfeld mit der Schaltfläche Wiederholen.
mrIgnore Der Benutzer verlässt das Dialogfeld mit der Schaltfläche Ignorieren.
mrYes Der Benutzer verlässt das Dialogfeld mit der Schaltfläche Ja.
mrNo Der Benutzer verlässt das Dialogfeld mit der Schaltfläche Nein.
mrAll Der Benutzer verlässt das Dialogfeld mit der Schaltfläche Alle.
mrNoToAll Der Benutzer verlässt das Dialogfeld mit der Schaltfläche Nein für alle.
mrYesToAll Der Benutzer verlässt das Dialogfeld mit der Schaltfläche Ja für alle.

Quelle: Delphi 2006-Hilfe

Komponenten dynamisch erstellen

[Bearbeiten]

Alle Objekte und Komponenten können dynamisch, also während der Laufzeit erstellt werden. Dazu sind einige Eigenschaften wichtig, um sie sichtbar zu machen. Hier beschreiben wir kurz mit Beispielcode, wie man ein Image dynamisch erstellt:

Code:

var
  myimage: TImage;
begin
  try
    myimage          := TImage.Create(Self);
    myimage.Parent   := Form1;    // Image soll auf Form1 angezeigt werden
    myimage.Left     := 100;
    myimage.Top      := 100;
    myimage.AutoSize := True;
    myimage.Picture.LoadFromFile(ExtractFilePath(Application.ExeName) + 'bild.bmp');
    myimage.Visible  := True;
    myimage.Name     := 'DynamicImage';
  finally
    myimage.Free;    // Hier wird die Komponente sofort wieder gelöscht.
  end;
end;

Benötigt werden immer die Eigenschaften Parent, Left, Top und Visible, um eine Komponente anzuzeigen. Falls Sie nicht die Standardgröße verwenden wollen, setzen Sie zusätzlich die Werte für Width und/oder Height (bzw. in diesem spezielle Falle AutoSize). Name muss nicht angegeben werden. Dies dient dazu, falls Sie eine Komponente mittels FindComponent suchen möchten.

Wenn das Image weiterhin sichtbar bleiben soll, verlagern Sie myimage.Free in den OnClose-Teil des Formulars. Dazu muss myimage als Feld der Klasse TForm1 vereinbart werden.


Fortgeschrittene Themen

[Bearbeiten]

Grafiken

[Bearbeiten]

Für Grafiken gibt es die Komponente TImage. Man findet sie in dem Register „Zusätzlich“. Doppelklickt man darauf bzw. wählt man die Eigenschaft „Picture“ und klickt auf die drei Punkte daneben, öffnet sich der Bild-Editor. In ihm wird per „Laden ...“ die Grafik in einem Dialog geladen, die erscheinen soll.

An Eigenschaften bietet das Image noch Stretch. Bei Stretch = True bedeutet dies, dass das Bild auf die Größe der TImage-Komponente gezerrt werden soll, ansonsten behält es seine Originalgröße, unabhängig von der Größe des Images.

Die Eigenschaft Proportional = True (neuere Delphi-Versionen) bewirkt, dass das Image beim Strecken das Verhältnis von Höhe und Breite des Originals beibehält.

Bei AutoSize = True erhält die Image-Komponente automatisch die Größe des Originalbildes. Somit werden Stretch und Proportional ignoriert und die Breite und Höhe der Komponente verändert.

Die Eigenschaften Left und Top geben (auch bei anderen Komponenten) die linke obere Position an. Wenn man eine Image-Komponente für Spiele einsetzt, sollte man allerdings die Eigenschaft DoubleBuffered des Formulars auf True stellen (geht nur im Code-Editor im OnCreate-Ereignis). Hiermit wird das Flackern reduziert:

Code:

procedure TForm1.FormCreate(Sender: TObject);
begin
  Form1.DoubleBuffered := True;
end;

Es wird jedoch im allgemeinen davon abgeraten, eine Image-Komponente als Zeichenfläche für Spiele oder ähnliches zu missbrauchen. Für dynamische Grafiken (also bei Grafiken die sich öfter ändern, wie bei Spielen) sollte man die TPaintbox-Komponente verwenden. Diese eignet sich auch nur bedingt für Spiele, alles was über einfache Dinge wie Tetris hinausreicht, sollte man eher OpenGL verwenden. Dafür gibt es auch einige 2D-Engines die von der Delphi-Community entwickelt wurden. Im deutschsprachigem Raum werden vor allem Fear2D[1] und Andorra 2D[2] benutzt. Für 3D-Spieleprogrammierung ist die DGL-Community[3] besonders zu empfehlen, da diese sehr gute Tutorials zur 3D-Programmierung verfasst hat.

Menüs

[Bearbeiten]

Hauptmenüs

[Bearbeiten]

Fast alle Anwendungen haben ein Hauptmenü. Dazu wird die Komponente TMainMenu benötigt. Man findet sie in der Komponentenpalette unter Standard. Setzt man sie auf das Formular, so sieht man erstmal nichts außer eine nicht-visuelle Komponente (eine Komponente, die zwar im Erstellungsmodus gekennzeichnet wird, zur Laufzeit jedoch nicht sichtbar ist). Nun klickt man doppelt auf das TMainMenu. Der Menü-Editor öffnet sich. Dort gibt man einem Platzhalter einen Caption. Über Enter bestätigt man die Eingabe. Ein neuer Platzhalter wird erstellt. In erster Ebene verwendet man gewöhnlich nur Menüpunkte wie „Datei“, „Bearbeiten“, ... Diese können Untermenüs enthalten wie z.B. Datei / Neu, Datei / Öffnen. Selektiert man einen Menüpunkt, sieht man darunter einen Platzhalter. Ihm kann genauso ein Caption zugewiesen werden, z.B. "Neues Dokument". Per Enter wird bestätigt und ein neues Item wird erstellt, z.B. „Öffnen“. Das geht endlos weiter. Wenn Sie als Caption „-“ eingeben, wird dies als Trennlinie interpretiert. Einem Buchstaben kann ein „&“ vorangestellt werden, um das Item per Alternativtaste (Alt) aufzurufen. Weiterhin hat jedes Item die Eigenschaft Shortcut. Dort kann ein Tastenkürzel wie z.B. „Strg+S“, „Strg+Alt+V“, „F1“ oder „Strg+Leer“ eingestellt werden. Per Klick wird genauso wie über Betätigung des Shortcuts das OnClick-Ereignis ausgelöst, das wie oben beschrieben definiert werden kann.

Weiterhin kann jedes Item Untermenüs erhalten. Diese können per Kontextmenü / Untermenü erstellen hinzugefügt werden und wieder Untermenüs erhalten.

Kontextmenüs

[Bearbeiten]

Für ein Kontextmenü wird die Komponente TPopupMenu verwendet. Sie befindet sich unter Standard neben TMainMenu. Die Vorgehensweise ist dieselbe wie beim MainMenu. Der Komponente, die per Rechtsklick das Kontextmenü aufruft (es können auch mehrere sein), wird bei der Eigenschaft „PopupMenu“ (wenn vorhanden, in neueren Delphi-Versionen rot gekennzeichnet) die PopupMenu-Komponente zugewiesen.

Toolbars

[Bearbeiten]

Toolbars bzw. Werkzeugleisten sind ebenfalls in professionellen Anwendungen oft anzutreffen. Man verwendet dazu die Windows-Komponente TToolbar im Register „Win32“. Weiterhin wird eine ImageList benötigt (ebenfalls im Register Win32). Auf diese wird nun doppelt geklickt. Im ImageList-Editor werden per Hinzufügen die benötigten Glyphs (Bitmaps in den Formaten 16x16, 24x24, 32x32, ...) zugewiesen. Diese können per Drag&Drop verschoben werden.

Nun wird der Toolbar-Komponente unter der Eigenschaft „Images“ (rot hervorgehoben) die ImageList zugewiesen. Per Rechtsklick auf die Toolbar kann im Kontextmenü „Neuer Schalter“ oder „Neuer Trenner“ gewählt werden. Bei Schaltern (TToolButton) gibt es die Eigenschaft ImageIndex, die den Index des Glyphs innerhalb der ImageList bestimmt. Erwartet wird eine Ganzzahl. -1 bedeutet: Kein Glyph. Die Nummerierung beginnt bei 0.

Soll der Schalter ein Checker sein, also angeben, ob ein Zustand 1 (true) oder 0 (false) hat, z.B. ob eine andere Toolbar sichtbar ist, wird ihr bei der Eigenschaft „Style“ der Wert „tbsCheck“ zugewiesen. Bei der Eigenschaft Down findet man den Wert False, wenn er nicht gedrückt ist, und True, wenn nicht. Wichtig dazu ist noch der Name des Buttons, den man unter der Eigenschaft „Name“ findet bzw. verändern kann.

Soll der Schalter ein Menü sein, also eine Liste enthalten, die bei Klick „aufpoppt“, stellt man den Wert der Eigenschaft „Style“ auf tbsDropDown. Der Eigenschaft „DropDownMenu“ wird ein PopupMenu zugewiesen, das sich bei einem Klick mit der linken Maustaste auf den Pfeil neben dem Button öffnet.

Ribbon Bar

[Bearbeiten]

In den neueren Delphi-Versionen (ab Delphi 2009) gibt es weiterhin die Ribbon Bar-Komponente, um den Programmen ein ähnliches Aussehen wie den neueren Microsoft Office-Anwendungen zu verpassen. Will man diese Komponenten jedoch benutzen, muss man bei Microsoft eine kostenlose Lizenz beantragen und bestimmte Design-Richtlinien beachten. Gedacht ist diese Komponente als einfache und schnelle Alternative zu den Menüs. Hiermit lassen sich Menüleisten und Toolbars als eine Einheit zusammenführen und - bedingt durch die vielen Möglichkeiten - oftmals ganze Einstellungsdialoge hiermit ersetzen.

Statuszeilen

[Bearbeiten]

Eine Statuszeile enthält wertvolle Informationen für den Benutzer. Sie befindet sich (meist) im unteren Teil des Formulars. Im einfachsten Fall zeigt sie eine Beschreibung zu einer Schaltfläche an. Diese Beschreibung wird angezeigt, sobald der Benutzer mit der Maus über die Komponente fährt. Sie wird zusätzlich zu dem gelben Hinweis (QuickInfo oder Hint genannt) angezeigt.

Um diesen zu definieren, muss die Eigenschaft „ShowHint“ der jeweiligen Komponente auf True gestellt werden. Die Eigenschaft „Hint“ enthält den Text, der angezeigt wird.

Um eine Statuszeile zu erstellen, zieht man eine TStatusbar (Register Win32) auf das Formular. Die Eigenschaft „AutoHint“ wird ebenso wie die Eigenschaft „SimplePanel“ auf True gestellt. Damit erscheinen nun in der Statuszeile die Hints, die bei der Eigenschaft „Hint“ der Komponente unter der Verwendung von AutoHint = True definiert wurde.

Vorlagen

[Bearbeiten]

Für grafische Benutzeroberflächen (GUIs) gibt es drei wesentliche Vorlagen:

  • VCL-Formularanwendung: Anwendung mit einem leeren Formular; wird (bei älteren Delphi-Versionen) standardmäßig erstellt
  • MDI-Anwendung: Multiple Document Interface. Eine Basis, die es möglich macht, mehrere Dokumente zur gleichen Zeit zu verwalten. Beispiele: Photoshop, Phase5. Diese Vorlage erzeugt die Basis eines Texteditors zum gleichzeitigen Bearbeiten mehrerer Dateien.
  • SDI-Anwendung: Single Document Interface. Eine Basis, die grundsätzlich nur ein geöffnetes Dokument unterstützt. Dennoch ist es auch mit SDI möglich, mehrere Dokumente zu verwalten, wenn man die Komponenten dynamisch zur Laufzeit erzeugt. Diese Vorlage erzeugt die Basis für einen Texteditor, mit dem immer nur eine Datei zur Zeit bearbeitet werden kann.

Alle Vorlagen lassen sich über Datei / Neu / Weitere ... verwenden.

MDI-Anwendungen

[Bearbeiten]

Eine MDI-Anwendung besteht aus einem Hauptformular (Parent) und mehreren MDI-Kindfenstern (MDI-Children). Im Parent sind Werkzeugleisten (TToolbar oder Zusätze wie Toolbar2000 von Jordan Russell oder TAdvToolbar von TMS Software) und ein Menü (TMainMenu oder gerade genannte Zusätze) untergebracht. Beim Hauptfenster muss die Eigenschaft FormStyle auf fsMDIForm eingestellt sein.

Alle Child-Fenster werden im Formdesigner über Datei / Neu / Formular erstellt, wobei FormStyle auf fsMDIChild gesetzt werden muss. Da Child-Fenster dynamisch erzeugt werden sollten, müssen Sie diese in den Projektoptionen unter Formulare aus der Liste „Automatisch erzeugen“ herausnehmen.

In den Child-Fenstern befindet sich nur eine Editorkomponente für Text, Bilder oder Ähnliches, jedoch keine Menüs oder andere GUI-Komponenten - oder ausschließlich solche. Das Fenster für die direkte Dateibearbeitung sollten Sie von solchen Steuerelementen komplett freihalten und dafür spezielle eigene Fenster anlegen. Nehmen Sie als Beispiel ein Bildbearbeitungsprogramm, das ein Fenster mit den Zeichenwerkzeugen und weitere Fenster enthält. Wenn Sie zum Beispiel eine TMainMenu-Komponente im Child-Fenster platzieren, erhält nicht etwa dieses Fenster das Hauptmenü, sondern das des Hauptfensters wird ersetzt. Das geschieht solange, bis Sie das Child-Fenster schließen oder eines ohne Hauptmenü auswählen.

Die Fenster erhalten in der Titelleiste standardmäßig wie das Hauptfenster Schaltflächen zum Verkleinern, Maximieren und Schließen. Im Maximierten Zustand bekommt das Hauptfenster ein Caption der Form „Hauptfenstertitel - [Kindfenstertitel]“. Damit der verkleinerte Zustand des Fensters wiederhergestellt werden kann, benötigt das Hauptfenster ein Hauptmenü, denn nur dort werden die Titelleistenschaltflächen von maximierten Child-Fenstern angezeigt.

Zu beachten ist, das man beim Schließen des Childfensters das Formular mittels Action := caFree; im OnClose-Ereignis dieses Fensters aus dem Speicher frei gibt.

Tipp: Zum Verwalten der Childfenster bietet sich das dynamisches Array an, welches das Parentfenster automatisch erzeugt. Man kann dann mittels MDIChildren[i] (wobei i den Index des MDI-Childfensters angibt) auf die einzelnen Fenster zugreifen.

SDI-Anwendungen

[Bearbeiten]

SDI-Anwendungen sind solche, bei denen immer nur eine Datei im Hauptfenster bearbeitet werden kann, wie zum Beispiel bei Notepad. Solche Anwendungen erstellen Sie über Datei / Neu / VCL-Formularanwendung.

Dialoge

[Bearbeiten]

Dialoge sind ebenfalls wichtig. Z.B. der SaveDialog, der OpenDialog, der PrintDialog, der PrinterSetupDialog, der FontDialog oder der ColorDialog. Sie befinden sich im Register Dialoge. Alle diese Dialoge lassen sich mit der Prozedur Execute aufrufen:

Code:

 OpenDialog1.Execute;

Bei Execute handelt es sich um eine Funktion, die einen Wahrheitswert, also True oder False, zurückgibt. Diesen sollte man auswerten, da True bedeutet, dass der Benutzer auf OK geklickt hat, während er bei False die Dialog mit Abbrechen verlassen hat.

Wie nun die Daten in den einzelnen Dialogen ausgewertet werden, würde hier zu weit führen. Bitte ziehen Sie hierfür Ihre Delphi-Hilfe zurate.

Weiterhin gibt es noch Dialoge mit denen man Benutzerabfragen gestalten kann. Diese sind in der selben Unit wie die Öffnungs- und Speicherndialoge enthalten.

Mit der Funktion MessageDlg erzeugt man Dialoge, die zum Beispiel abfragen können, ob man das Programm wirklich beenden will. Die Funktion benötigt folgende Parameter: den anzuzeigenden Informationstext, die Art des Dialoges (mtNone, mtInformation, mtConfirmation, mtCustom, mtError), die Schaltflächen des Dialogfensters (da kann man entweder eine vorgefertigte Mischung nehmen oder mittels Array selbst die Buttons bestimmen), sowie gegebenenfalls den Hilfeindex (Standard = 0).

MessageDlg gibt auch einen Wert zurück, mit dem man prüfen kann, welchen Button der Benutzer gedrückt hat. Dieser kann mrYes, mrNo und so weiter sein, wie bereits in den Grundlagen beschrieben.

Ein Beispielaufruf könnte so aussehen:

Code:

procedure TForm1.Button1Click(Sender: TObject);
begin
  if MessageDlg('Wollen Sie beenden?', mtConfirmation, [mbYes, mbNo], 0) = mrYes then
    Close;
end;

Außerdem gibt es dann noch die sogenannte InputBox. Mit Hilfe dieser kann man Eingaben vom Benutzer einfordern, auch Zahlen und Gleitkommawerte. Für nähere Informationen steht ihnen die Delphi-Hilfe zur Verfügung.

Formatierter Text

[Bearbeiten]
Wikipedia hat einen Artikel zum Thema:


Als erste GUI-Anwendung eignet sich ein Texteditor für das Rich Text Format (RTF). Mit ihm ist es möglich, Formatierungen bis hin zu Bildern darzustellen. Auch wenn die Komponente RichEdit (Register Win32) nur formatierten Text ohne Bilder unterstützt, ist es doch eine interessante Komponente, die man kennen sollte.

Öffnen und Speichern

[Bearbeiten]

Als erstes zum Laden und Speichern von Dateien - wir brauchen die Methoden LoadFromFile und SaveToFile. Ihnen wird als Parameter der Dateiname übergeben. Zur Dateiauswahl kann man einfach eine OpenDialog-Komponente verwenden:

Code:

procedure TForm1.LadeDatei;
begin
  try
    if OpenDialog1.Execute then
      RichEdit1.Lines.LoadFromFile(OpenDialog1.FileName);  // OpenDialog1.FileName enthält den Namen der ausgewählten Datei
  except
    ShowMessage('Fehler beim Öffnen der Datei');
  end;
end;

Ähnlich ist es beim Speichern:

Code:

procedure TForm1.SpeichereDatei;
begin
  try
    if SaveDialog1.Execute then
      RichEdit1.Lines.SaveToFile(SaveDialog1.FileName);
  except
    ShowMessage('Fehler beim Speichern der Datei');
  end;
end;

Formatierung

[Bearbeiten]

RichEdit-Komponenten bieten vielfältige Möglichkeiten der Textformatierung (rich = engl. für reichaltig). Dies geschieht jedoch nur auf Quelltext-Ebene, da RichEdit selbst keine Steuerelemente hierfür mitbringt. Wenn Sie in einer Anwendung mit RichEdit arbeiten möchten, müssen Sie also selbst z.B. eine Toolbar oder Kontextmenüs anlegen, über die Sie den Text formatieren können.

Schriftfarbe

[Bearbeiten]

Der ColorDialog wird aufgerufen und die Schriftfarbe des gewählten Bereichs im RichEdit wird verändert. Das Ganze funktioniert folgendermaßen:

Code:

procedure TForm1.Button3Click(Sender: TObject);
begin
  if ColorDialog1.Execute then
    RichEdit1.SelAttributes.Color := ColorDialog1.Color;  // selektieren Bereich in der ausgewählten Farbe darstellen
end;

Kompletter Font

[Bearbeiten]

Für die komplette Schrift sieht es so aus:

Code:

procedure TForm1.Button4Click(Sender: TObject);
begin
  if FontDialog1.Execute then
  begin
    RichEdit1.SelAttributes.Color := FontDialog1.Font.Color;  // Farbe
    RichEdit1.SelAttributes.Name := FontDialog1.Font.Name;    // Schriftart
    RichEdit1.SelAttributes.Size := FontDialog1.Font.Size;    // Schriftgröße
    RichEdit1.SelAttributes.Style := FontDialog1.Font.Style;  // Schriftauszeichnung (fett, kursiv, unterstrichen)
  end;
end;

Style kann keinen, einen oder mehrere der folgenden Werte haben: [fsBold] für fett, [fsItalic] für kursiv, [fsUnderline] für unterstrichen und/oder [fsStrikeOut] für durchgestrichen. Style ist ein Set, also eine Menge der vorgenannten Werte. Möchte man einem Stil Werte hinzufügen oder entnehmen, geht das daher folgendermaßen:

Code:

RichEdit1.SelAttributes.Style := [fsBold];                           // nur fett darstellen
RichEdit1.SelAttributes.Style := RichEdit1.Font.Style + [fsItalic];  // kursiv hinzufügen, Ergebnis: fett, kursiv
RichEdit1.SelAttributes.Style := RichEdit1.Font.Style - [fsBold];    // fett entfernen, Ergebnis: kursiv
RichEdit1.SelAttributes.Style := [];                                 // Standarddarstellung ohne Auszeichnung

Mit RichEdit zu arbeiten, erfordert einiges an Erfahrung. So enthält die Eigenschaft Lines nur den reinen Text ohne jegliche Formatierung. Wenn Sie beispielsweise Text in der gleichen Formatierung an einer Stelle programmatisch einfügen wollen (z.B. bei einer Suchen-Ersetzen-Operation), müssen Sie vorher die Attribute an dieser Stelle auslesen, den neuen Text einfügen, diesen markieren und dann die Attribute darauf anwenden. Alternativ kann man SelStart auf die entsprechende Cursorposition einstellen (vom Beginn des Dokuments an gezählt), SelLength auf 0 setzen und über SelText den einzufügenden Text angeben. Dann wird die Formatierung automatisch übernommen.

Mit etwas Übung werden Sie in RichEdit ein sehr mächtiges Werkzeug für die Textdarstellung und -bearbeitung entdecken.

[Bearbeiten]
  1. Website von Fear2D: http://www.martoks-place.de/delphi/opengl/fear2d
  2. Website von Andorra 2D: http://andorra.sourceforge.net
  3. Website der DGL-Community: http://wiki.delphigl.com/index.php/Hauptseite


Weitere Themen

[Bearbeiten]

Drucken

[Bearbeiten]

Möchte man irgendetwas Drucken, benutzt man das Printer-Objekt. Dazu muss man die Unit Printers in die Uses-Klausel aufnehmen. Beim Drucken kann man mit der Canvas-Eigenschaft des Druckers arbeiten (dazu muss man beachten, dass zwar FontSize der Größe des Bildschirm entspricht, alle anderen Koordinaten sich jedoch auf Druckerpunkte beziehen, die sind wesentlich kleiner als die Pixel des Bildschirms):

Printer.BeginDoc;                                      // Druckauftrag starten
Printer.Canvas.Assign(Image1.Canvas);                  // Canvas des Image1 übernehmen
Printer.EndDoc;                                        // Druckauftrag fertig, Ausdruck beginnen

Damit wird das Bild von Image1 gedruckt.

Möchte man seinen Druck auf verschiedene Druckerauflösungen anpassen, gibt es einen einfachen Weg:

pfactor := Printer.PageWidth / 2400;

Somit multipliziert man dann alle Koordinaten mit pfactor. Funktionsweise: 2400 ist die Breite einer DIN A4 - Seite bei 300 dpi (dots per inch). Es wird hier ermittelt, wie viel mal die Seitenbreite größer als die von 300 dpi / DIN A4. Bei einer Breite von 4800 (600 dpi / DIN A4) wird pfactor 2 sein. So hat das Ergebnis zwar eine andere Auflösung, die Elemente befinden sich aber an derselben Stelle. Möchte man nun das Image drucken, so geschieht das so:


var
  Bereich: TRect;
begin
  Printer.BeginDoc;                                      // Druckauftrag starten
  pfactor := Printer.PageWidth / 2400;
  Bereich.Top := round(10 * pfactor);
  Bereich.Bottom := round((10 + Image1.Height) * pfactor);
  Bereich.Left := round(10 * pfactor);
  Bereich.Right := round((10 + Image1.Width) * pfactor);
  Printer.Canvas.StretchDraw(Bereich, Image1.Canvas);
  Printer.EndDoc;                                        // Druckauftrag fertig, Ausdruck beginnen

Alternativ zu round (runden) kann auch trunc (Dezimalstellen abschneiden) benutzt werden.

Ein zweiter Weg wäre es, den Formularinhalt zu drucken:

Form1.Print;

Tabellen

[Bearbeiten]

Und auch wichtig sind StringGrids. TStringGrid findet man im Register Zusätzlich. Dann zieht man die StringGrid-Komponente auf das Formular. Mit der Eigenschaft FixedRows wird die Anzahl der grauen Reihen festgelegt. Dasselbe tut FixedCols für die grauen Spalten (Col = Column = Spalte). RowCount legt die Anzahl der Zeilen fest, ColCount die Anzahl der Spalten. Die wirklich wichtige Eigenschaft ist Cells. Möchte man der Zelle A1 (0; 0) den Inhalt "A1" zuweisen, so geschieht das folgendermaßen:

StringGrid1.Cells[0, 0] := 'A1';    // StringGrid1.Cells[Spalte, Zeile]

Die Nummerierung beginnt auch hier bei 0 (anders als bei den meisten Dingen unter Delphi), da es sich um eine Windows-Komponente handelt (und bei Windows ziemlich überall die Indizierung bei 0 beginnt), denn das ist Standard bei C.

Aufteilung des Formulars

[Bearbeiten]

Panels

[Bearbeiten]

Panels sind sogenannte Container, vergleichbar mit dem DIV-Tag in HTML. Sie finden das Panel in der Registerkarte "Standard". Zieht es auf euer Formular. Soll es die ganze linke Seite einnehmen, oben, unten, rechts oder alles, so müssen Sie Align verändern. Im Objektinspektor wählen Sie eines der Einträge, also:

  • alClient: alles einnehmen
  • alLeft: Breite wie angegeben, Höhe passt sich dem Formular an, das Panel ist immer ganz links
  • alRight: Breite wie angegeben, Höhe passt sich dem Formular an, das Panel ist immer ganz rechts
  • alBottom: Höhe wie angegeben, Breite passt sich dem Formular an, das Panel ist immer ganz unten
  • alTop: Höhe wie angegeben, Breite passt sich dem Formular an, das Panel ist immer ganz oben
  • alNone: Standardeinstellung, Left, Top, Width und Height veränderbar.

Übrigens werden Sie feststellen, dass Sie so auch die Position einer Toolbar oder Statusbar verändern können, die haben nämlich standardmäßig alTop (Toolbar) / alBottom (Statusbar).

Weiterhin kann man noch die Eigenschaft Anchors nutzen, wobei man festlegt, ob die Position der Komponente abhängig vom Abstand nach links, rechts, oben oder unten zum jeweiligen Rand ihres Parents ist.

Splitter

[Bearbeiten]

Die TSplitter-Komponente finden Sie im Register "Zusätzlich". Zieht sie auf das Formular. Align ist veränderbar. Wenn sie zum Beispiel Align alTop hat und unter einem Panel, das ebenfalls alTop hat, durch Ziehen die Größe verändern. Das ist sinnvoll, weil jeder User eine andere Vorstellung von einer "guten" Größeneinteilung hat.

Registerkarten

[Bearbeiten]

Registerkarten werden mit der Komponente TPageControl, die sich im Register "Win32" befindet, erstellt. Um eine neue Seite zu erstellen, klicken Sie im Kontextmenü eines neuen leeren PageControls auf "Neue Seite". Das geht unendlich oft (theoretisch, da der Speicher begrenzt ist). Dabei fällt auf, dass jede Seite ein einzelnes Objekt ist, nämlich ein TTabSheet. Das heißt, wenn man dem TabSheet1 den Parent PageControl2 zuweist, der vorher PageControl1 war, kann man das PageControl ändern. So kann man Seiten auch dynamisch erstellen (siehe Komponenten dynamisch erstellen).

Weiterhin kann man die Eigenschaft des TabSheets "Caption" für eine andere Aufschrift verändern. TabIndex legt den Rang innerhalb des PageControls fest. Visible stellen Sie auf false, wenn das Item nicht sichtbar sein soll, Visible des PageControls für eine veränderte Sichtbarkeit des PageControls, siehe auch die Methoden Hide und Show.

Mit der Eigenschaft des PageControls "HotTrack" legen Sie fest, ob das Item, über dem sich die Maus befindet, mit einer dunkelblauen Schrift (clNavy) erscheinen soll. Die Eigenschaft MultiLine legt fest, ob die Tabs über mehrere Zeilen verteilt werden sollen oder ob sich am Rand zwei Buttons zum Navigieren nach links und rechts befinden sollen. Style gibt das Layout für die Komponente an.

Mit Align (beim Panel behandelt) kann man, wie bei fast allen Komponenten, die Ausrichtung festlegen.

Trackbar und Scrollbar

[Bearbeiten]

Beide Komponenten sind eine Art "Slider", um bestimmte Werte einzustellen. Sie funktionieren ziemlich gleich: Es gibt Eigenschaften für Maximal- und Minimalwerte, nämlich Max und Min. Weiterhin gibt es das Ereignis OnChange. Diese tritt dann auf, wenn etwas bewegt wurde. Mit Orientation wird die Richtung, also horizontal oder vertikal festgelegt.

Bei der Trackbar kann man bei Frequency einstellen, in welchem Abstand Markierungs-Striche gezogen werden sollen. Und es ist möglich, einen Teil zu markieren, dafür werden wie im RichEdit die Eigenschaften SelStart und SelEnd verwendet, die allerdings als Public, nicht als Published deklariert sind und so nicht im Objektinspektor auffindbar sind.

TTrackbar ist unter "Win32", TScrollbar unter "Standard" aufzufinden.

GroupBox, RadioGroup und Checkbox

[Bearbeiten]

Diese drei Komponenten sind unter "Standard" aufzufinden.

  • Die GroupBox ist eine Art Panel mit anderem Aussehen
  • Die RadioGroup ist eine GroupBox mit mehreren Optionsfeldern, von denen genau eins gewählt werden kann
  • Die Checkbox ist ein Feld für das angeben eines Zustands, also True oder False, welcher mit der Eigenschaft Checked festgelegt wird, auch bekannt als Kontrollfeld oder Check

Die Groupbox verfügt lediglich über die Eigenschaft Caption, die den Titel angibt, ansonsten funktioniert sie wie ein Panel und dient nur der Optik.

Die RadioGroup hat die gleichen Eigenschaften wie die Groupbox. Zusätzlich verfügt sie über "Items" für die Liste der Optionen als TStringList und ItemIndex (Indizierung beginnt bei 0), die die gewählte Option angibt.

Die Checkbox hat zwei wichtige Eigenschaften: Checked für den Zustand und Caption für die Beschriftung.

Fortschritt anzeigen

[Bearbeiten]

Ein Fortschritt wird mit der TProgressBar angezeigt, welche sich im Register Win32 befindet. Mit Max und Min werden obere und untere Grenze festgelegt. Mit Position wird der Fortschritt angezeigt.

Alternativ zur Progressbar kann auch die TGauge genutzt werden. Sie befindet sich im Register "Beispiele" (Samples). Mit MinValue und MaxValue werden die Grenzen angegeben. ForeColor und BackColor treffen Aussagen über die Farbwahl. Mit Font kann die Schriftart verändert werden. Die Eigenschaft Progress gibt den Fortschritt an.

Anwendungsoptionen

[Bearbeiten]

Um der Anwendung einen Titel zu verpassen, der auch dann in der Taskleiste erscheint, sowie ein Icon (was man unter anderem mit kostenlosen Tools wie dem Borland Bildeditor, LiquidIcon oder IcoFX erstellen kann), müssen Sie Projekt / Optionen / Anwendung wählen. In den drei Editierfeldern geben Sie einfach die dazugehörigen Werte ein und klicken dann auf OK.

WinXP-Themes

[Bearbeiten]

Für Delphi ab Version 7: Hier finden Sie im Register "Win32" die Komponente TXPManifest. Diese ist eine nicht-visuelle Komponente und bindet nur die Unit XPMan ein. Für ein Löschen der Komponente muss also auch die Unit aus der Uses-Klausel entfernt werden.

Für ältere Versionen vor Delphi 7: Sie müssen die Manifest- und Resourcen-Dateien etwas komplizierter selbst erstellen. Zunächst legen Sie eine Datei "WindowsXP.manifest" mit folgendem Inhalt an:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity
    type="win32"
    name="DelphiApplication"
    version="1.0.0.0"
    processorArchitecture="*"/>
  <dependency>
    <dependentAssembly>
      <assemblyIdentity
        type="win32"
        name="Microsoft.Windows.Common-Controls"
        version="6.0.0.0"
        publicKeyToken="6595b64144ccf1df"
        language="*"
        processorArchitecture="*"/>
    </dependentAssembly>
  </dependency>
</assembly>

Die Attribute name und version können entsprechend angepasst werden. Den Rest bitte unverändert lassen.

Anschließend benötigen Sie eine Resourcen-Datei. Legen Sie dazu eine weitere Textdatei namens "WindowsXP.rc" an und tragen Sie dort folgende Zeile ein:

1 24 WindowsXP.manifest

Nun an der Eingabeaufforderung eingeben:

brcc32 WindowsXP.rc

Gegebenenfalls müssen Sie noch den Pfad zum Borland-Programmverzeichnis davor schreiben bzw. den entsprechenden Pfad vor die .rc-Datei.

Dies erstellt eine Resourcendatei namens WindowsXP.res im gleichen Verzeichnis. Damit Ihr Programm nun die Windows XP-Stile annimmt, öffnen Sie den Quelltext Ihres Hauptformulars und tragen im 'implementation-Abschnitt, nach {$R *.dfm} folgendes ein:

{$R 'WindowsXP.res'}

Hiermit wird automatisch von Delphi eine Resourcendatei erzeugt und in Ihr Programm eingebunden. Nach dem nächsten Kompilieren verwendet Ihr Programm die XP-Themes.

Tipp:

Die .res-Datei ist wiederverwendbar. Wer auf die gespeicherten Daten über den Programmnamen und die Version keinen Wert legt, kann die Datei z.B. in den Hauptprojektordner kopieren und von dort einbinden, z.B. mit {$R '..\WindowsXP.res'}. Auf diese Weise muss man die oben genannte Prozedur nur einmal durchgehen, bzw. spart sich das Kopieren der Datei.


Arbeit mit Tasten

[Bearbeiten]

Tasten können auch simuliert und abgefangen werden, als negatives Beispiel für ein Backdoor oder einen Keylogger. Allerdings kann man das auch nützlich gebrauchen. Auch, wenn man fast alles davon eleganter lösen könnte.

Simulation

[Bearbeiten]

Ein Screenshot wird mit der Taste DRUCK erstellt. Man kann diesen auch per Windows-API erzeugen, allerdings können wir mit zwei Codezeilen solch einen Tastendruck simulieren:

Keybd_Event(VK_PRINT, 0, 0, 0);
Keybd_Event(VK_PRINT, 0, KEYEVENTF_KEYUP, 0);

Das sollte übrigens nicht nur unter Delphi funktionieren, sondern in allen Programmiersprachen, die einen Zugriff auf die Windows-API erlauben. Übrigens sollten Sie die zweite Zeile nie vergessen, denn diese sorgt dafür, dass die Taste auch wieder losgelassen wird. Sonst bleibt Sie ewig unten! Übrigens: Ein alphanumerischer Buchstabe, wzb. A kann einfach mit Ord('A') als ID simuliert werden.

Abfrage

[Bearbeiten]

Eine Abfrage kann man zum Beispiel für ein Spiel gebrauchen. Man fragt mit einem Timer alle 100 Sekunden ab (Beispiel), ob eine Taste gedrückt ist (auch das geht eleganter, Stichwort Hook: allerdings ist dann die Gefahr groß, dass ein Anti-Viren-Programm eingreift). Dazu benutzen Sie die Funktion(en) GetAsyncKeystate oder GetKeyState:

if GetAsyncKeystate(VK_LEFT) < 0 then
  // Nach links bewegen

Übrigens finden Sie alle Tastencodes in der Delphi-Hilfe unter dem Stichwort Virtuelle Tastencodes.

Fremdkomponenten installieren

[Bearbeiten]

Sie können auch Fremdkomponenten installieren. Diese enthalten meistens zwei Arten von Packages:

  • Runtime-Packages
  • DesignTime-Packages

Beide haben die Dateiendung ".dpk". Man kann sie meistens folgendermaßen unterscheiden:

  • Runtime-Packages: Name = [Name]R[Version].dpk
  • DesignTime-Packages. Name = [Name]D[Version].dpk

Zunächst muss man den Pfad, wo diese Packages gespeichert sind, Delphi kenntlich machen. Das geschieht (unter Delphi 7) über Tools / Umgebungsoptionen / Bibliothek. Nun fügt man im Editierfeld "Bibliothekspfad" ein Semikolon (;) und den Ordner, in dem die Packages gespeichert sind, an.

Jetzt wird der Dialog geschlossen.

Nun öffnet man zuerst das Runtime-, dann das DesignTime-Package. Ein neues Fenster öffnet sich. Nun klickt man dort bei beiden jeweils auf "Kompilieren", beim DesignTime-Package zusätzlich auf "Installieren". Achtung: Es soll hintereinander geschehen, also erst das Runtime-Package fertig, dann das DesignTime-Package beginnen.


Besteht die Komponente nur aus einer Unit, erstellt man über Datei / Neu / Package ein neues Package. Nun fügt man über das grüne Plus-Symbol die entsprechende Unit hinzu und speichert das Package im selben Ordner ab. Dieses Verzeichnis wird Delphi kenntlich gemacht und wie ein DesignTime-Package anschließend geöffnet, kompiliert und installiert.

Alternativ dazu kann man auch nur den Pfad zur Unit angeben (Tools / Umgebungsoptionen / Bibliothek ...) und anschließend die Komponente über Komponente / Komponente installieren ... im Menü in das Package "dclusr.dpk" aufnehmen. Dort wird die neue Unit gewählt, kompiliert und installiert.

Benötigt man sie nur, um sie dynamisch zu erstellen (was ja sowieso nur geht, wenn man den Turbo Delphi Explorer nutzt), reicht es auch, den Pfad zur Unit anzugeben.


Komponentenentwicklung

[Bearbeiten]

Vorfahrtyp

[Bearbeiten]

Zunächst einmal sollten Sie sich im Klaren sein, was für einen Vorfahrtypen man wählt. Für graphische Komponenten ist für eine ganz neue Komponente TGraphicControl eine gute Wahl. Sollte es ein erweitertes Edit sein, muss man natürlich TEdit als Vorfahrtyp wählen, es sei denn, man möchte alles neu schreiben.

Neue Komponente

[Bearbeiten]

Eine neue Komponente wird über Komponente / Neue Komponente ... erstellt. In diesem Dialog geben wir ein, was Delphi von uns wissen will. Wir wissen nun, welchen Vorfahrtypen wir nehmen, hier TGraphicControl, und nennen unsere neue Klasse zum Beispiel TMyShape. Diese soll am Ende dem TShape ähneln.

Zeichnen

[Bearbeiten]

Zum Zeichnen benutzen wir Canvas. Das kann überall gemacht werden. Gut ist es jedoch, es in der Methode Paint zu machen, die TGraphicControl mitbringt. Diese überschreiben wir. Es sieht nun so aus:

TMyShape = class(TGraphicControl)
  private
    { Private-Deklarationen }
  
  protected
    procedure Paint; override; // Hier eintragen
  
  public
    { Public-Deklarationen }
  
  published
    { Published-Deklarationen }
end;

{ ... }

procedure TMyShape.Paint; override;
begin
  { ... }
end;

Da wir uns in TMyShape befinden, müssen wir Canvas nicht mit TMyShape.Canvas ansprechen, sondern dürfen auch Canvas schreiben. Normalerweise schreibt ja auch niemand Form1.Edit1.Text sondern Edit1.Text. Wir stellen uns vor, wir wären in einer with-do-Konstruktion. Wie wir mit Canvas zeichnen, wurde im letzten Kapitel erklärt.

Koordinaten

[Bearbeiten]

Bei den Koordinaten darf man auf gar keinen Fall Left und Top für die linke obere Ecke verwenden, sondern 0. Die Koordinaten beziehen sich nicht auf die auf dem Formular, sondern auf die innerhalb der Komponente. Damit kann man überflüssige Unklarheiten vermeiden. Die Breite und Höhe bekommen wir mit Width und Height geliefert.

Ereignisse

[Bearbeiten]

Ereignisse müssen wir einfach nur importieren. Wir schreiben also in den Published-Teil:

published
  property OnClick;
  property OnMouseMove;
  ...

Denn alle wichtigen Ereignisse werden von TGraphicControl geliefert.

Properties

[Bearbeiten]

Properties sollen meistens im Objektinspektor erscheinen. Dafür wird diese Schreibweise verwendet:

published
  property MyProperty: Datentyp read AMyProperty write FmyProperty;

AMyProperty wird im Private-Teil mit demselben Datentypen deklariert. FMyProperty ist eine Prozedur aus dem Private-Teil und könnte folgendermaßen aussehen:

procedure TMyShape.FMyProperty(A: Datentyp);
begin
  AMyProperty := A;
  Paint;
end;

Damit wird wieder das Paint-Ereignis aufgerufen. Innerhalb der Komponente hat man immer AMyProperty zu verwenden, nie MyProperty!

Komponente installieren

[Bearbeiten]

Hier geht es relativ einfach, da die Komponente nur aus einer Unit besteht: Man ruft Komponente / Komponente installieren ... auf, gibt die Unit an, klickt im Dialog auf "Compilieren" und anschließend auf "Installieren" und wartet auf eine Erfolgsmeldung. Dafür darf aber die automatisch erzeugte Register-Prozedur nicht gelöscht werden!


Multimediafunktionen

[Bearbeiten]

Multimedia

[Bearbeiten]

Man kann unter Delphi sehr einfach multimediale Funktionen implementieren, indem man auf die Windows-API zugreift (Voraussetzung ist natürlich, dass man Windows nutzt). Das ist noch nicht nötig, wenn man ein Video abspielen möchte, wohl aber beim Schneiden. Oder etwa bei der Aufnahme von der Soundkarte und dem Abspeichern als Wave.

Zuerst muss man die Unit MMSystem in die Uses-Klausel aufnehmen. Diese bietet uns nun den Zugriff auf die entsprechenden API-Funktionen.

Tipp:

Was heißt "API"? API steht für Application programming interface, also "Schnittstelle zur Programmierung der Anwendung"


Aufnehmen eines Sounds

[Bearbeiten]

Um einen Sound aufzunehmen, benötigen wir zum Beispiel schon die MMSystem-Unit (MultiMediaSystem). Wir benutzen immer die Funktion "mciSendString". Zuerst wollen wir einen neuen Bezeichner erstellen, vom Typ "waveaudio". Jedoch nicht als Delphi-Variable! Der entsprechende Code sieht folgendermaßen aus:

mciSendString('OPEN NEW TYPE WAVEAUDIO ALIAS mysound', nil, 0, Handle);

Groß- / Kleinschreibung ist auch bei diesem String (genau genommen PChar) egal. Darauf folgt die Aufnahme:

mciSendString('RECORD mysound', nil, 0, Handle);

Möchte man vorher noch etwas an der Qualität feilen, müssen wir folgendes voranstellen:

mciSendString('SET mysound TIME FORMAT MS', nil, 0, Handle);
mciSendString('SET mysound BITSPERSAMPLE 16', nil, 0, Handle);
mciSendString('SET mysound CHANNELS 1', nil, 0, Handle);
mciSendString('SET mysound SAMPLESPERSEC 44000', nil, 0, Handle);
mciSendString('SET mysound BYTESPERSEC 88250', nil, 0, Handle);

Damit haben wir eine gute Qualität, nicht wie standardmäßig! Nun wird die Aufnahme (natürlich nicht direkt danach) gestoppt. Dazu wird folgendes verwendet:

mciSendString('STOP mysound', nil, 0, Handle);

Damit wird nicht nur die Aufnahme, sondern auch die Wiedergabe gestoppt. Die Wiedergabe erfolgt so:

mciSendString('PLAY mysound FROM 0', nil, 0, Handle);

Oder nur 2 Sekunden lang:

mciSendString('PLAY mysound FROM 0 TO 2000', nil, 0, Handle);

Die 2000 bezieht sich auf das Zeitformat, welches wir auf Millisekunden gestellt haben.

Öffnen eines Sounds / Videos

[Bearbeiten]

Für den Sound genügt dazu ein einfacher Befehl:

mciSendString('OPEN sound.wav ALIAS mysound', nil, 0, Handle);

Danach kann er wie gezeigt abgespielt werden. Für das Video ist das Öffnen dasselbe, das Abspielen jedoch anders:

mciSendString(PChar(format('WINDOW mymovie HANDLE %d', [Panel1.Handle])), nil, 0, Handle);
mciSendString('PLAY mymovie FROM 0', nil, 0, Handle);

Bei Dateinamen mit Leerzeichen ist dieser in Gänsefüßchen einzuschließen!

Schneiden eines Sounds / Videos

[Bearbeiten]

Auch das ist möglich! Mit einem einfachen Befehl:

mciSendString('DELETE mysound FROM 100 TO 1000', nil, 0, Handle);

Wichtig ist das Zeitformat!

Speichern

[Bearbeiten]

Zuletzt noch das Speichern:

mciSendString('SAVE mysound sound.wav', nil, 0, Handle);

Bei einem Dateinamen mit Leerzeichen ist dieser in Gänsefüßchen einzuschließen!

TMediaPlayer

[Bearbeiten]

In Delphi gibt es seit mindestens Delphi 3 die Komponente "TMediaPlayer". Diese kapselt so ziemlich alle diese Methoden und Zugriff in einer Komponente.

Standardmäßig hat diese Komponente eine visuelle Benutzerschnittstelle. Jedoch gerade wenn man seinen eigenen Media Player programmieren will, ist diese nicht wirklich erwünscht. Für dieses Problem stellt die Komponente die Eigenschaft "ButtonsVisible" zu Verfügung. Diese blendet die Steuerfläche aus, und man kann die Komponente weiter ansteuern.

Standardmäßig unterstützt diese Komponente die häufigsten Dateiformate wie *.mp3, *.wav und *.wma.


Anhänge

[Bearbeiten]

Der (richtige) Programmierstil

[Bearbeiten]

Wie schon am Anfang des Buches beschrieben, unterscheidet Pascal nicht zwischen Groß- und Kleinschreibung. Ebenso ist es dem Compiler egal, wie viele Leerzeichen oder Tabulatoren man zwischen die einzelnen Schlüsselwörter, Rechenzeichen oder Ähnliches setzt, genauso wie man ein Programm auch komplett in einer Zeile unterbringen könnte.

Der Programmtext sollte jedoch sauber strukturiert sein und man sollte sich dabei an einige Regeln halten. Dies dient dazu, den Code später einfacher warten zu können. Falls man im Team arbeitet ist so auch sichergestellt, dass sich andere im Code schneller zurecht finden.

Hierfür haben sich einige Regeln eingebürgert, die nach Möglichkeit von jedem angewandt werden sollten.

Allgemeine Regeln

[Bearbeiten]

Einrückung

[Bearbeiten]

Um bestimmte Zusammenhänge, wie zum Beispiel Anweisungsblöcke, im Quelltext kenntlich zu machen, rückt man die Zeilen ein. Dabei sollten keine Tabulatoren sondern Leerzeichen verwendet werden, damit der Text auf jedem Rechner gleich aussieht. Standard sind 2 Leerzeichen je Ebene.

Seitenrand

[Bearbeiten]

Gelegentlich wird man auch mal einen Programmtext ausdrucken wollen. Hierfür ist der rechte Seitenrand auf 80 einzustellen. Erkennbar ist dies über eine durchgezogene Linie im Editor. Wenn möglich, sollte diese Linie nicht überschrieben werden.

Kommentare

[Bearbeiten]

Für Kommentare sollte man hauptsächlich die geschweiften Klammern { } verwenden. Den anderen Kommentarblock (* *) sollte man nur verwenden, um Programmabschnitte vorübergehend aus dem Programm auszuschließen (auszukommentieren). Kommentare mittels // bitte ausschließlich für einzeilige Kommentare verwenden.

Compilerdirektiven

[Bearbeiten]

Direktiven werden immer in geschweifte Klammern gesetzt, wobei die Compileranweisung in Großbuchstaben geschrieben wird. Beim Einrücken gelten die gleichen Regeln wie oben.

Anweisungsblöcke

[Bearbeiten]

Bei Anweisungsblöcken zwischen begin und end stehen diese beiden Schlüsselwörter jeweils auf einer eigenen Zeile. Anweisungen wie if ... then begin oder while ... do begin sollten vermieden werden. begin und end stehen immer auf der gleichen Ebene wie die zugehörige If- oder Schleifenabfrage:

if ... then
begin
  ...
end;

Klammern

[Bearbeiten]

Nach einer geöffneten Klammer und vor der geschlossenen Klammer stehen keine Leerzeichen. Bei Klammern zur Parameterübergabe an Funktionen, Prozeduren und Methoden kein Leerzeichen zwischen dem Namen und der geöffneten Klammer verwenden. Dies gilt auch für die eckigen Klammer zur Angabe eines Index. Klammern nur angeben, wenn diese auch notwendig sind!

Writeln ( txt );    // falsch
Writeln (txt);      // falsch
Writeln( txt );     // falsch
Writeln(txt);       // die einzig richtige Variante

x := a * ( b + c ); // falsch
x := a * (b + c);   // richtig

x := (a * b);       // falsch, unnötige Klammer
x := a * b;         // richtig

i[1] := x;          // richtig

Interpunktion

[Bearbeiten]

Semikola sowie der Punkt hinter dem letzten end stehen ohne Leerzeichen direkt hinter dem letzten Zeichen der Anweisung. Sie schließen die Zeile ab, danach folgt ein Zeilenumbruch, sprich: jede Anweisung steht auf einer eigenen Zeile. Ausnahme: Direktiven hinter der Deklaration von Funktionen.

Writeln('Zeilenende hinter Semikolon!');
Writeln('Nächste Anweisung.');

aber:

function Stellenanzahl(Zahl: Integer): Integer; forward;
procedure HilfeAusgeben; overload;
procedure HilfeAusgeben(Text: string); overload;

Vor Kommata zur Trennung von Parametern oder in anderen Auflistungen steht kein Leerzeichen, aber dahinter. Dasselbe gilt für Doppelpunkte bei der Deklaration von Variablen oder Parametern.

var
  x: Integer;
  a: array[0..2, 0..2] of Byte;
{ ... }
x := Pos(chr, zeichenkette);
a[0, 1] := 32;

Rechenoperatoren werden immer von Leerzeichen umschlossen.

Operatoren

[Bearbeiten]

Mit Ausnahme der unären Operatoren @ und ^ stehen vor und nach Operatoren immer Leerzeichen. Hierzu zählt auch der Zuweisungsoperator :=.

z := x+y;        // falsch
z := x + y;      // richtig

a := @Funktion;  // richtig
p := Zeiger^;    // richtig

Reservierte Wörter

[Bearbeiten]

Diese Wörter gehören fest zum Sprachumfang von Pascal/Delphi. Sie werden unter neueren Delphi-Versionen standardmäßig dunkelblau und fett hervorgehoben, unter Lazarus und älteren Delphi-Versionen schwarz und fett. Diese Wörter werden immer kleingeschrieben.

Beispiele:

program
const

aber auch:

string

Routinen und Methoden

[Bearbeiten]

Namensvergabe

[Bearbeiten]

Namen von Prozeduren und Funktionen (=Routinen) sowie Methoden sollten immer großgeschrieben sein. Falls sich der Name aus mehreren Wörtern zusammensetzt, beginnt jedes neue Wort ebenso mit einem Großbuchstaben. Der Name sollte so sinnvoll gewählt sein, dass man daraus bereits die Aufgabe der Routine ableiten kann. Um dies zu erreichen, empfiehlt es sich, ein entsprechendes Verb im Namen unterzubringen. Bei Routinen, die Werte von Variablen ändern, wird das Wort Set (oder deutsch Setze) davorgeschrieben. Bei Routinen, die Werte auslesen, steht das Wort Get bzw. Hole davor.

procedure Hilfe;          // falsch, Aufgabe nicht ganz eindeutig
procedure HilfeAnzeigen;  // richtig
procedure ausgabedateispeichernundschliessen;  // falsch, nur Kleinbuchstaben
procedure AusgabedateiSpeichernUndSchliessen;  // richtig
procedure SetzeTextfarbe;
function HolePersonalnr: Integer;

Parameter

[Bearbeiten]

Die Bezeichnung von Parametern sollte ebenfalls so gewählt werden, dass man auf den ersten Blick den Zweck erkennen kann. Der Name wird auch groß geschrieben. Nach Möglichkeit sollte ihm ein großes A vorangestellt werden, um einen Parameter von Variablen und Klasseneigenschaften unterscheiden zu können.

Bei der Reihenfolge sollten die allgemeineren Parameter weiter links stehen, die spezielleren rechts, z.B. Ort, Straße, Hausnummer. Zusammengehörige Parameter sollten dabei nebeneinander stehen. Genauso sollten, wenn möglich, Parameter gleichen Typs zusammengefasst werden:

procedure ZeichnePunkt(R, F: Integer; L: TColor);                // falsch, Zweck der Parameter nicht erkennbar
procedure ZeichnePunkt(Y: Integer; AFarbe: TColor; X: Integer);  // falsch, unsinnige Reihenfolge
procedure ZeichnePunkt(X, Y: Integer; AFarbe: TColor);           // richtig

Parameter vom Typ Record, Array oder ShortString sollten als konstante Parameter deklariert werden, falls deren Wert in der Routine nicht geändert wird. Dies erhöht die Geschwindigkeit des Programms, da der Inhalt der Parameter nicht lokal kopiert wird. Bei anderen Parametertypen ist dies ebenfalls möglich, bringt aber keinen Geschwindigkeitsvorteil.

Variablen

[Bearbeiten]

Variablen sollte ein Name gegeben werden, der ihren Zweck erkenntlich macht. Sie sollten ebenfalls grundsätzlich großgeschrieben werden. Bei Zählern und Indizes besteht der Name meist nur aus einem Buchstaben: z.B. I, J, K. Boolische Variablen sollten einen Namen haben, bei dem die Bedeutung der Werte True und False sofort klar ist.

var
  Wort: Boolean;           // falsch, Bedeutung von True und False nicht zu erkennen
  WortGeaendert: Boolean;  // richtig

Variablen werden jede auf einer eigenen Zeile deklariert (wie im Beispiel oben). Es sollten nicht mehrere Variablen gleichen Typs zusammengefasst werden.

Sie sollten es dringend vermeiden, globale Variablen zu benutzen. Falls Sie Variablen global benutzen müssen, dann schränken Sie dies so weit wie möglich ein. Deklarieren Sie z.B. eine solche Variable im Implementation-Teil einer Unit und nicht im Interface-Teil.

Typen

[Bearbeiten]

Typen sollten so geschrieben werden, wie sie deklariert wurden. string ist z.B. ein reserviertes Wort und wird demnach auch als Typ klein geschrieben. Viele Typen aus der Windows-Schnittstelle sind vollständig in Großbuchstaben deklariert und sollten dann auch dementsprechend verwendet werden, z.B. HWND oder WPARAM. Andere von Delphi mitgelieferte Typen beginnen mit einem Großbuchstaben, z.B. Integer oder Char.

Typen, die Sie selbst anlegen, stellen Sie bitte ein großes T voran, bei Zeigertypen ein großes P. Ansonsten gilt die gleiche Schreibweise wie bei Variablen. Auch hier achten Sie bitte darauf, dass der Typ einen zweckmäßigen Namen erhält.

Gleitkommazahlen

[Bearbeiten]

Benutzen Sie bitte hauptsächlich den Typ Double, hierfür sind alle Prozessoren ausgelegt. Real existiert nur noch um mit älteren Programmen kompatibel zu sein, in Delphi ist Real derzeit gleichbedeutend mit Double. Single sollten Sie nur verwenden, wenn Ihnen der Speicherbedarf der entsprechenden Variable wichtig ist. Extended ist ein Sonderformat von Intel und könnte beim Datenaustausch zwischen verschiedenen Rechnern zu Problemen führen. Sie sollten Extended daher nur verwenden, wenn Sie auf die erweiterte Genauigkeit angewiesen sind und Werte dieses Typs möglichst nicht in Dateien speichern, die für einen Datenaustausch verwendet werden.

Aufzählungen

[Bearbeiten]

Geben Sie bitte Aufzählungen einen Namen, der dem Zweck des Typs entspricht. Die Bezeichner der Positionen benennen Sie so, dass die ersten zwei oder drei Buchstaben (kleingeschrieben) den Typ der Aufzählung wiedergeben:

TMitarbeiterArt = (maAngestellter, maLeitenderAngestellter, maVorstand);

Eine Variable vom Typ Aufzählung wird genau wie der Typ benannt, lediglich das führende T lässt man weg. Benötigt man mehrere Variablen eines Typs, kann man diese entweder durchnummerieren oder erweitert den Namen um einen passenden Begriff.

Varianten

[Bearbeiten]

Verwenden Sie Varianten so sparsam wie möglich. Varianten sind nicht die bevorzugte Art, Daten unbekannten Typs zu konvertieren. Benutzen Sie stattdessen Zeiger und Typumwandlungen. Bei COM- und Datenbankapplikationen werden Sie vermutlich nicht umhin kommen, Varianten zu verwenden. Sie sollten in diesem Falle den Typ OleVariant nur bei COM-Anwendungen (z.B. ActiveX-Komponenten) verwenden und ansonsten den Typ Variant.

Arrays und Records

[Bearbeiten]

Wie auch schon zuvor, sollte auch der Name von Arrays und Records sinnvoll zu deren Verwendungszweck vergeben werden. Auch vor diese Typbezeichner wird ein T gesetzt (bzw. P bei Zeigern). Ähnlich wie bei Aufzählungen, sollten Sie auch bei Variablen eines Array- bzw. Record-Typs den gleichen Namen nur ohne führendes T verwenden, falls möglich.

Anweisungen

[Bearbeiten]

If-Anweisungen

[Bearbeiten]

Bei Verzweigungen mit if sollten die wahrscheinlichsten Anweisungen nach dem then stehen, die seltender eintretenden Bedingungen nach dem else.

Wenn irgend möglich, sollten Sie vermeiden, mehrere If-Abfragen nacheinander zu schreiben. Verwenden Sie statt dessen die Verzweigung mittels case. Ebenso vermeiden Sie es bitte, If-Abfragen in mehr als fünf Ebenen zu verschachteln. Klammern bitte nur verwenden, wenn diese auch benötigt werden!

In Delphi gibt es die Möglichkeit, Bedingungsabfragen bereits nach der ersten falschen Bedingung abbrechen zu lassen. Dies spart in der Ausführung der Anwendung etwas an Zeit. Um diesen Zeitvorteil weiter auszubauen, sollte man die Abfragen sortieren. Dabei kann man von links nach rechts die Abfragen tätigen, die am wahrscheinlichsten auf "false" lauten werden. Eine weitere Möglichkeit ist, die am schnellsten zu berechnenden Bedingungen am weitesten nach links zu stellen.

Für gewöhnlich stehen die Bedingungen nebeneinander. Sollten dies zu viele werden, können sie auch untereinander geschrieben werden. Dabei sind sie linksbündig zu formatieren:

if Bedingung1 and Bedingung2 and Bedingung3 then
  ...

if Bedingung1 and
   Bedingung2 and
   Bedingung3 then
  ...

Wenn sich die auszuführende Anweisung nach dem then oder else über mehrere Zeilen erstreckt (zum Beispiel, weil sie am Zeilenende umgebrochen werden muss, oder weil Kommentare vorangesetzt werden), bitte alles zwischen begin und end einfassen:

{ Falsch }
if x = 1 then
  // Hier passiert was
  Writeln(x);

{ Richtig }
if x = 1 then
begin
  // Hier passiert was
  Writeln(x);
end;

{ Falsch }
if x = 1 then
  Writeln('Dies ist ein sehr langer Text, der ' +
          'am Zeilenende umgebrochen wurde, ' +
          'damit er auf Ausdrucken immer noch ' +
          'lesbar erscheint.');

{ Richtig }
if x = 1 then
begin
  Writeln('Dies ist ein sehr langer Text, der ' +
          'am Zeilenende umgebrochen wurde, ' +
          'damit er auf Ausdrucken immer noch ' +
          'lesbar erscheint.');
end;

Das Schlüsselwort else sollte immer den gleichen Einzug erhalten wie if.

Case-Anweisungen

[Bearbeiten]

Die einzelnen Unterscheidungsmöglichkeiten einer Case-Verzweigung sollten numerisch, alphabetisch oder (bei anderen Aufzählungstypen) in der Reihenfolge ihrer Deklaration abgefragt werden.

Die Anweisungen, die den Abfragen folgen, sollten möglichst kurz und einfach gehalten werden, da der Programmtext sonst schnell unübersichtlich wird. Empfehlenswert sind vier bis fünf Zeilen, diese sollten immer zwischen begin und end stehen (auch bei einzelnen Zeilen). Größere Anweisungsfolgen am besten in eigene Routinen auslagern.

Den Else-Abschnitt sollten Sie nur verwenden, wenn Sie ein gültiges Default-Verhalten festlegen können.

Alle Abschnitte einer Case-Verzweigung sollen eingerückt werden. Das else steht auf der gleichen Höhe wie das case. Bei größeren Case-Blöcken, die einen Else-Abschnitt enthalten, sollte man das else zum besseren Überblick mit einem Kommentar versehen, um es eindeutig dem Case-Abschnitt zuzuordnen.

case Wert of
  Bedingung:
    begin
      ...
    end;
else  { case }
  ...
end;

While-, For- und Repeat-Schleifen

[Bearbeiten]

Wenn Sie eine Schleife verlassen wollen, vermeiden Sie bitte die Anweisung Exit. Schleifen sollten immer über die Prüfbedingung beendet werden. Alle Variablen, die Sie innerhalb einer Schleife verwenden, sollten Sie direkt davor initialisieren und (wenn nötig) direkt anschließend wieder bereinigen. So stellt man sicher, dass man nichts vergisst.

With-Anweisungen

[Bearbeiten]

With-Anweisungen sollten Sie nur sehr selten einsetzen, wenn Sie zum Beispiel mit einem sehr umfangreichen Record arbeiten. Ansonsten verwenden Sie bitte die Form Variable.Element in Ihrem Programm. Obwohl es möglich ist, verwenden Sie bitte nie mehrere Records in einer With-Anweisung, wie z.B. with Record1, Record2 do

Fehlerbehandlung

[Bearbeiten]

Verwenden Sie die Funktion der Fehlerbehandlung so sparsam wie möglich. Prüfen Sie stattdessen die benötigten Daten vor der Verwendung auf Korrektheit.

Die Fehlerbehandlung sollte immer dann eingesetzt werden, wenn Speicherbereiche zugewiesen werden, um diese nach dem Auftreten einer Exception sauber wieder freizugeben.

Zur Fehlerbehandlung verwendet man gewöhnlich try...finally. Bei Speicherzuweisungen sollte man recht großzügig mit der Anzahl der Blöcke sein und nach jeder Zuweisung einen neuen Block beginnen.

Klasse1 := TKlasse1.Create;
Klasse2 := TKlasse2.Create;
try
  { ... }
finally
  Klasse1.Free;
  Klasse2.Free;
end;

besser:

Klasse1 := TKlasse1.Create;
try
  Klasse2 := TKlasse2.Create;
  try
    { ... }
  finally
    Klasse2.Free;
  end;
finally
  Klasse1.Free;
end;

Da try...finally den Fehler nicht abschließend behandelt sondern erneut auslöst, sollte man diesen Block in try...except verschachteln oder ausschließlich diese Art der Fehlerbehandlung verwenden.

Klassen

[Bearbeiten]

Der Name einer Klasse beginnt immer mit einem T, da es sich hierbei um einen Typ handelt. Der Rest des Namens sollte dem Zweck der Klasse entsprechen. Bei mehreren Wörtern im Namen beginnt jedes Wort mit einem Großbuchstaben. Die dazugehörige Variable trägt den gleichen Namen wie die Klasse, nur ohne führendes T. Falls Sie weitere Variablen einer Klasse benötigen, können Sie diese nummerieren oder Sie verwenden weitere zusätzliche Begriffe im Namen der Variablen.

Felder

[Bearbeiten]

Felder werden wie Variablen benannt, sollten jedoch mit einem F beginnen. Damit sind sie eindeutig von anderen Variablen zu unterscheiden. Weiterhin sollten Felder immer als private deklariert werden. Falls Sie die Daten von außerhalb der Klasse ändern möchten, erstellen Sie eine Methode oder Eigenschaft für diesen Zweck.

Ebenso wie Variablen sollten Felder jedes auf seiner eigenen Zeile deklariert werden. Bitte keine Felder gleichen Typs zusammenfassen.

Methoden

[Bearbeiten]

Methoden benennen Sie wie Prozeduren und Funktionen.

Deklarieren Sie eine Methode als static, wenn diese in Nachkommenklassen nicht überschrieben werden sollen. Im Gegensatz dazu deklarieren Sie eine Methode als virtual, wenn sie ausdrücklich überschrieben werden soll. Verwenden Sie hierfür nicht dynamic! Deklarieren Sie eine Methode nur in Basisklassen als abstract, die niemals direkt verwendet werden.

Methoden zum Zugriff auf Eigenschaften werden immer als private oder protected deklariert. Die Methoden zum Lesen der Eigenschaften beginnen mit Get (bzw. deutsch Hole), die Methoden zum Schreiben der Eigenschaft mit Set (bzw. Setze). Der Parameter der Methode zum Schreiben des Wertes heißt Value.

Eigenschaften

[Bearbeiten]

Eigenschaften, die auf private Felder zugreifen, werden wie das Feld benannt, nur ohne führendes F. Verwenden Sie für den Namen bitte nur Substantive, keine Verben. Eigenschaften mit Index sollten dabei in der Mehrzahl stehen, sonst Einzahl:

property Farbe: TColor read FFarbe write FFarbe;
property Adressen[1..10]: TAdresse read HoleAdresse write SetzeAdresse;

Eigenschaften, die im Objektinspektor von Delphi angezeigt werden sollen, deklarieren Sie als published, sonst als public.


Quelle: frei nach „Delphi 4 Developer's Guide Coding Standards Document“


Befehlsregister

[Bearbeiten]
Dieses Kapitel muss noch angelegt werden. Hilf mit unter:

http://de.wikibooks.org/wiki/Programmierkurs:_Delphi


Glossar

[Bearbeiten]
Dieses Kapitel muss noch angelegt werden. Hilf mit unter:

http://de.wikibooks.org/wiki/Programmierkurs:_Delphi


Autoren

[Bearbeiten]

Diese Autoren haben an diesem Wikibook mitgearbeitet (IP-Adressen nicht aufgelistet):

Letzte Aktualisierung: 5.11.2007, 17:37:23

Lizenz

[Bearbeiten]

Version 1.2, November 2002

Copyright (C) 2000,2001,2002  Free Software Foundation, Inc.
51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.

0. PREAMBLE

The purpose of this License is to make a manual, textbook, or other functional and useful document "free" in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others.

This License is a kind of "copyleft", which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software.

We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.

1. APPLICABILITY AND DEFINITIONS

This License applies to any manual or other work, in any medium, that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. Such a notice grants a world-wide, royalty-free license, unlimited in duration, to use that work under the conditions stated herein. The "Document", below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as "you". You accept the license if you copy, modify or distribute the work in a way requiring permission under copyright law.

A "Modified Version" of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language.

A "Secondary Section" is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (Thus, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them.

The "Invariant Sections" are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. If a section does not fit the above definition of Secondary then it is not allowed to be designated as Invariant. The Document may contain zero Invariant Sections. If the Document does not identify any Invariant Sections then there are none.

The "Cover Texts" are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover Text may be at most 25 words.

A "Transparent" copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, that is suitable for revising the document straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup, or absence of markup, has been arranged to thwart or discourage subsequent modification by readers is not Transparent. An image format is not Transparent if used for any substantial amount of text. A copy that is not "Transparent" is called "Opaque".

Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML, PostScript or PDF designed for human modification. Examples of transparent image formats include PNG, XCF and JPG. Opaque formats include proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML, PostScript or PDF produced by some word processors for output purposes only.

The "Title Page" means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, "Title Page" means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text.

A section "Entitled XYZ" means a named subunit of the Document whose title either is precisely XYZ or contains XYZ in parentheses following text that translates XYZ in another language. (Here XYZ stands for a specific section name mentioned below, such as "Acknowledgements", "Dedications", "Endorsements", or "History".) To "Preserve the Title" of such a section when you modify the Document means that it remains a section "Entitled XYZ" according to this definition.

The Document may include Warranty Disclaimers next to the notice which states that this License applies to the Document. These Warranty Disclaimers are considered to be included by reference in this License, but only as regards disclaiming warranties: any other implication that these Warranty Disclaimers may have is void and has no effect on the meaning of this License.

2. VERBATIM COPYING

You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3.

You may also lend copies, under the same conditions stated above, and you may publicly display copies.

3. COPYING IN QUANTITY

If you publish printed copies (or copies in media that commonly have printed covers) of the Document, numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects.

If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages.

If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a computer-network location from which the general network-using public has access to download using public-standard network protocols a complete Transparent copy of the Document, free of added material. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public.

It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.

4. MODIFICATIONS

You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version:

A. Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission.
B. List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has fewer than five), unless they release you from this requirement.
C. State on the Title page the name of the publisher of the Modified Version, as the publisher.
D. Preserve all the copyright notices of the Document.
E. Add an appropriate copyright notice for your modifications adjacent to the other copyright notices.
F. Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below.
G. Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document's license notice.
H. Include an unaltered copy of this License.
I. Preserve the section Entitled "History", Preserve its Title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section Entitled "History" in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence.
J. Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the "History" section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission.
K. For any section Entitled "Acknowledgements" or "Dedications", Preserve the Title of the section, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein.
L. Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles.
M. Delete any section Entitled "Endorsements". Such a section may not be included in the Modified Version.
N. Do not retitle any existing section to be Entitled "Endorsements" or to conflict in title with any Invariant Section.
O. Preserve any Warranty Disclaimers.

If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles.

You may add a section Entitled "Endorsements", provided it contains nothing but endorsements of your Modified Version by various parties--for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard.

You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one.

The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version.

5. COMBINING DOCUMENTS

You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice, and that you preserve all their Warranty Disclaimers.

The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work.

In the combination, you must combine any sections Entitled "History" in the various original documents, forming one section Entitled "History"; likewise combine any sections Entitled "Acknowledgements", and any sections Entitled "Dedications". You must delete all sections Entitled "Endorsements."

6. COLLECTIONS OF DOCUMENTS

You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects.

You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.

7. AGGREGATION WITH INDEPENDENT WORKS

A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, is called an "aggregate" if the copyright resulting from the compilation is not used to limit the legal rights of the compilation's users beyond what the individual works permit. When the Document is included in an aggregate, this License does not apply to the other works in the aggregate which are not themselves derivative works of the Document.

If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one half of the entire aggregate, the Document's Cover Texts may be placed on covers that bracket the Document within the aggregate, or the electronic equivalent of covers if the Document is in electronic form. Otherwise they must appear on printed covers that bracket the whole aggregate.

8. TRANSLATION

Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License, and all the license notices in the Document, and any Warranty Disclaimers, provided that you also include the original English version of this License and the original versions of those notices and disclaimers. In case of a disagreement between the translation and the original version of this License or a notice or disclaimer, the original version will prevail.

If a section in the Document is Entitled "Acknowledgements", "Dedications", or "History", the requirement (section 4) to Preserve its Title (section 1) will typically require changing the actual title.

9. TERMINATION

You may not copy, modify, sublicense, or distribute the Document except as expressly provided for under this License. Any other attempt to copy, modify, sublicense or distribute the Document is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.

10. FUTURE REVISIONS OF THIS LICENSE

The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/.

Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License "or any later version" applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation.