7 Verbund-Typen

7.1 Was zusammen gehört....
7.2 Ein Adressen-Verzeichnis
7.3 Typisierte Dateien
7.4 Das Luxus-Adressbuch

Zum vorigen Kapitel Zum Inhaltsverzeichnis Zum nächsten Kapitel


In diesem Kapitel geht es um die informatischen Voraussetzungen der Datenverarbeitung im engeren Sinne, nämlich darum, wie Daten in einer Datenbank erfasst und gespeichert werden können. Wir werden dabei lernen, wie man eigene Datenstrukturen definieren kann, die für den Aufbau einer Datenbank geeignet sind.



7.1 Was zusammen gehört....

Wenn wir ein digitales Adressbuch anlegen wollen, dann müssen wir die einzelnen Adressen abspeichern. Dazu könnte man einfach jede Adresse in einen String packen, dann wären alle Informationen erfasst. Ein so organisiertes Adressbuch ist jedoch nahezu unbrauchbar! Um das einzusehen, betrachten wir einige typische Fragen, die mit Hilfe eines (digitalen) Adressbuchs beantwortet werden sollten:
Diese Fragen lassen erkennen, dass es nötig sein wird, die Daten so zu organisieren, dass man später die Datenbank nach bestimmten Kriterien durchsuchen kann. Dazu müssen die Daten aber in einer solchen Form abgelegt werden, dass man einerseits auf die einzelnen Bestandteile einer jeden Adresse direkt zugreifen kann, andererseits aber müssen diese einzelnen Anteile doch stets zuverlässig "beieinander bleiben".

Was sind nun eigentlich die Bestandteile einer Adresse? Wenn uns jemand seine Adresse aufschreiben soll, wird er üblicherweise folgendes angeben:
  1. den Namen (bestehend aus einem [oder mehreren] Vornamen und dem Nachnamen)
  2. die Straße und die Hausnummer
  3. die Postleitzahl und den Ort
  4. eventuell eine Telephonnummer
Statt nun all diese Daten in einen String zu packen, können wir auch jedem einzelnen Bestandteil einen eigenen String spendieren. Im Falle der Postleitzahl oder der Hausnummer liegt auch die Versuchung nahe, eher eine Integer-Variable als einen String zu benutzen. (Von der Schwierigkeit, dass Postleitzahlen durch vorgestellte Länderkennungen und Hausnummern durch nachgestellte Buchstaben ergänzt werden können, sehen wir hier zunächst einmal ab.) In Delphi gibt es für diese Situation die Möglichkeit, einen eigenen Datentyp zu definieren, der sich aus mehreren Anteilen ("Feldern") zusammen setzt. Man nennt so etwas einen "Record-Typ". Die Deklaration eines für unsere Zwecke geeigneten Typs namens TAdresse könnte in Delphi z.B. so lauten:
       type  TAdresse = record
                          Vorname  : String[60];
                          Nachname : String[40];
                          Strasse  : String[30];
                          HausNr   : Integer;
                          PLZ      : Integer;
                          Ort      : String[30];
                          Telefon  : String[20];
                        end;
Zwischen den Schlüsselworten "record" und "end" werden also die einzelnen Felder des Datensatzes aufgelistet, wobei jeweils ein Feldbezeichner und der Typ dieses Feldes angegeben wird. Durch die Feldbezeichner wird die Möglichkeit des direkten Zugriffs auf die Einzeldaten jeder Adresse sichergestellt. Wenn wir nun eine Adresse aufnehmen wollen, muss es in unserem Programm zunächst einmal eine Variable vom Typ TAdresse geben. Dieser können wir dann die einzelnen Daten der Adresse übergeben:
       var NeueAdresse : TAdresse;
         {.........}
       begin
         {.........}
         NeueAdresse.Vorname  := 'Jürgen';
         NeueAdresse.Nachname := 'Müller';
         NeueAdresse.Strasse  := 'Veilchenallee';
         NeueAdresse.HausNr   := 13;
         NeueAdresse.PLZ      := 77652;
         NeueAdresse.Ort      := 'Offenburg';
         {.........}
       end;
Auf die Feldvariablen von NeueAdresse wird also zugegriffen, indem dem Variablennamen ein Punkt hinzugefügt wird, dem der Feldname folgt. Dies ist dieselbe Notation, wie wir sie von den grafischen Objekten aus dem Delphi-Programmier-Kasten schon längst gewöhnt sind! Im übrigen handelt es sich um ganz gewöhnliche Zuweisungen an die Feldvariablen unseres Records "NeueAdresse". Und selbstverständlich kann man die Daten auch von der Programmoberfläche "holen", also vom Benutzer eingeben lassen: die Zuweisungen
       NeueAdresse.Strasse := Edit1.Text;
       NeueAdresse.HausNr  := IntEdit1.Value;
ist natürlich ebenfalls möglich! Dabei fällt die syntaktische Ähnlichkeit zwischen Records und Objekten ins Auge. Wir werden aber noch lernen, dass die Verwandtschaft zwischen diesen Typklassen nicht sehr eng ist: Records sind in gewisser Weise ziemlich primitive Urahnen von Objekten!



Aufgaben:

  1. Daten-Eingabe

    Schreiben Sie ein Programm, das dem Benutzer gestattet, die Daten einer neuen Adresse in entsprechende Felder einzutragen und diese Daten dann auf Knopfdruck in einer entsprechenden Record-Variablen abzuspeichern. Definieren Sie dazu zunächst einen passenden Record-Typ.
    Lösungsvorschlag

  2. Auch ein Datum hat Daten!

    Ergänzen Sie das Programm aus der vorigen Aufgabe um die Möglichkeit, das Geburtsdatum der Person in den Datensatz aufzunehmen. Definieren Sie dazu zunächst einen eigenen Record TDatum, der Tag, Monat und Jahr des Datums enthält, und fügen Sie dann dem Record TAdresse ein Feld "GebDatum" vom Typ TDatum hinzu. (Records können also auch andere Records als Felder enthalten!) Stellen Sie dem Benutzer drei entsprechend kommentierte IntEdit-Felder zur Eingabe des Datums zur Verfügung.

    Bei der Übernahme der Daten können Sie nun auch gleich noch eine Plausibilitätsprüfung implementieren: der Tag muss immmer im Bereich [1..31] sein, der Monat immer im Bereich [1..12]. (Wenn Sie es schaffen, können Sie auch noch zwischen den Monaten mit 30 und 31 Tagen unterscheiden! Den Problemfall des Februar mit der Schwierigkeit der Schaltjahre wollen wir aber für dieses Beispiel erst mal unberücksichtigt lassen.)
    Lösungsvorschlag




7.2 Ein Adressen-Verzeichnis

Elektronische Datenverarbeitung lohnt sich nur, wenn die Datenmenge groß ist: niemand wird ein Adressbuch anlegen, das nur eine Adresse enthält. Wenn wir aber mehrere Adressen in unserem Programm erfassen wollen, dann brauchen wir bisher für jede dieser Adressen eine neue Variable vom Typ TAdresse. Und hier beginnen die Schwierigkeiten!

Die naheliegende Idee, mit einem hinreichend großen Array von Adress-Variablen zu starten, scheint das Problem zunächst zu lösen. Aber was bedeutet eigentlich "hinreichend groß"? Wenn mir für meine Zwecke ein Adressbuch mit 20 Einträgen völlig ausreichend erscheint, könnte ich in meinem Programm eben die Variable
       var Adressen : Array [1..20] of TAdresse
einrichten. Aber der nächste Benutzer dieses Programms will vielleicht 28 Adressen eintragen, und ein anderer 137! Wie groß wir auch immer das Array machen, es ist stets denkbar, dass es einmal zu klein ist!

Bei einem papier'nen Adressbuch ist die Beschränkung auf eine feste Größe konstruktionsbedingt unumgänglich. Verschärfend kommt im Falle eines handelsüblichen alphabetisch geordneten Adressbuches hinzu, dass auch schon die Anzahl der Einträge zu einem bestimmten Anfangsbuchstaben (des Nachnamens) beschränkt ist. Deshalb hat ein solches Adressbuch schon dann seine Kapazitätsgrenze erreicht, wenn auch nur eine der Seiten voll ist - die ganzen anderen Seiten, auf denen noch kaum etwas steht, nützen da wenig!

Wenn wir die Daten in unserem "elektronischen" Adressbuch in ein hinreichend großes Arrays schreiben, dann wird zumindest dieses Problem umgangen, weil ein Array eben nicht in Seiten organisiert ist. Noch besser geeignet wäre aber eine "nach unten offene" Liste, der jedesmal, wenn eine neue Adresse anfällt, ein weiterer Eintrag hinzugefügt werden könnte, ohne dass es eine (merkliche) Kapazitätsgrenze gibt. Tatsächlich lässt sich in vielen Programmiersprachen eine solche "Liste" realisieren, und reale Datenbank-Programme arbeiten wirklich mit solchen Datenstrukturen. Den Umgang mit den dafür benötigen "dynamischen Variablen" müssen wir aber erst noch erlernen, so dass wir dieses Problem jetzt noch nicht lösen können.

Zudem kann die Forderung "keine Kapazitätsgrenze" im strengen Sinne niemals erfüllt werden: auch wenn der Arbeitsspeicher und die Festplatte unseres Rechners ziemlich groß sind, so bleiben sie doch immer endlich! Andererseits werden wir beim normalen Betrieb einer entsprechend aufgebauten Adressdatenbank diese Begrenzungen kaum jemals zu spüren bekommen. Angesichts der Tatsache, dass es in einer endlichen Maschine ohnehin immer Kapazitätsgrenzen geben wird, wollen wir uns daher zunächst einmal damit zufrieden geben, dass wir ein "genügend großes" Array von Adressen zur Verfügung stellen. Mit der Deklaration
       const maxAdrCnt = 1000;
       var Adressen : Array [1..maxAdrCnt] of TAdresse;
geraten wir auf jeden Fall in eine Größenordnung, bei der eher die Grenze Ihrer Geduld beim Eintippen der Daten erreicht werden wird als die Grenze der verfügbaren Adressen-Speicherplätze! Die Verwendung einer Konstanten (maxAdrCnt) für die Obergrenze des Indexbereichs hat den Vorteil, dass man die Größe des Arrays im Programm gegebenenfalls leicht ändern kann, wenn es sich denn doch als zu klein erweisen sollte.



Aufgabe:

  1. Daten-Erfassung

    Kopieren Sie Ihr Projekt aus der vorigen Aufgabe. Ergänzen Sie das Programm dann so, dass es zur Eingabe mehrerer Adressen taugt. Deklarieren Sie dazu ein entsprechendes Array von Adressen. Beim Klick auf den "Daten übernehmen"-Knopf müssen nun die Daten in den nächsten freien Adressenplatz des Arrays geschrieben werden.

    Deklarieren Sie dazu eine Integer-Variable namens ActAdr, die immer den Index des ersten noch freien Platzes im Adressen-Array enthält. Beim Start des Programms muss ActAdr den Wert 1 erhalten, was in der OnCreate-Methode des Formulars erledigt werden kann. Nach jeder Datenübernahme muss ActAdr dann um 1 hochgezählt werden.

    Zur Erinnerung: Wie erzeugt man eine OnCreate-Methode?
    Fokussieren Sie das Formular, wechseln Sie dann im Objektinspektor auf die "Ereignisse"-Seite, und machen Sie dort einen Doppelklick in den Eintrag "OnCreate". Delphi erzeugt dann eine "FormCreate"-Prozedur, und in dieser können Sie die Variable ActAdr initialisieren.
    Lösungsvorschlag


  2. Im Adressbuch blättern

    Ergänzen Sie das Programm aus der vorigen Aufgabe um zwei Knöpfe "Rückwärts" und "Vorwärts", mit denen Sie durch die einzelnen Einträge des Adressbuchs durchblättern können. Beim Klick auf den "Rückwärts"-Knopf soll die Variable ActAdr um 1 erniedrigt werden (wenn das geht!); danach sollen die Daten aus dem entsprechenden Eintrag des Adressen-Arrays in den Editierfeldern angezeigt werden. Der "Vorwärts"-Knopf soll entsprechend den Übergang zum nächsten Datensatz ermöglichen. Wie bemerken Sie, ob es überhaupt einen nächsten Datensatz gibt?

    Sie können bei der Bildschirm-Darstellung das OnPaint-Ereignis des Formulars benutzen: lassen Sie Delphi den Rahmen der entsprechenden "FormPaint"-Prozedur erzeugen (analog zur Erzeugung der "FormCreate"-Prozedur in der vorigen Aufgabe), und füllen Sie nun in dieser Prozedur alle Editierfelder mit den Daten desjenigen Datensatzes, auf den der Index ActAdr "zeigt".

    Wenn Sie Ihr Programm so organisiert haben, können Sie eine Aktualisierung des Fensterinhaltes an jeder beliebigen Stelle des Programms auslösen, indem Sie die parameterlose Prozedur Invalidate des Formulars aufrufen. Diese veranlasst, dass bei nächster Gelegenheit das Fenster neu gezeichnet werden wird - und im Rahmen dieser Aktion wird dann auch unsere FormPaint-Prozedur aufgerufen!

    Machen Sie sich klar, dass mit den bisher vorhandenen drei Knöpfen auch das Ändern eines schon existierenden Eintrags möglich wird. Probieren Sie's aus!
    Lösungsvorschlag




7.3 Typisierte Dateien

Ein gravierender Mangel unseres Adressen-Programms ist, dass bisher alle eingegebenen Daten beim Programmende verloren gehen. Das muss natürlich schnellstens behoben werden! Die eingegebenen Adressdaten sollen nun in einer Datei aufbewahrt werden. Beim Programmstart sollen die in dieser Datei abgelegten Adressen in unser Array geladen werden; und beim Programmende soll diese Datei stets aktualisiert, d.h. neu geschrieben werden.

Wie ist eine solche Datei aufgebaut? Wenn wir Adressen abspeichern wollen, dann benutzen wir vorteilhaft eine "typisierte" Datei: so nennt man Dateien, die aus einer Folge von Datensätzen vom gleichen Typ bestehen. In Delphi müssen wir dazu eine Dateivariable deklarieren, die programmintern die Datei repräsentiert:
       var  f : File of TAdresse;
Die Typbeschreibung einer typisierten Datei besteht also aus den Schlüsselworten "File of", gefolgt vom Typ der Datensätze.

Nachdem nun klar ist, wie die Datei strukturiert sein soll, müssen wir als erstes unsere Dateivariable f mit einer "realen" Datei verbinden, und zwar, indem wir den vollständigen Pfad zu dieser Datei angeben. Es muss sich dabei nicht unbedingt um eine schon existierende Datei handeln; allerdings muss der Pfad gültig sein, d.h. alle angegebenen Unterverzeichnisse müssen existieren. Die eigentliche Arbeit erledigt in Delphi die Prozedur AssignFile: der Aufruf
       AssignFile(f, 'D:\meinzeug\adressen\adress.dat');
verbindet die Dateivariable f mit der Datei "adress.dat", die auf dem Laufwerk "D:" im Unterverzeichnis "\meinzeug\adressen\" steht bzw. dort erzeugt werden wird. AssignFile führt übrigens keinerlei weitere Prüfungen durch, z.B. ob die Datei vorhanden ist oder ob sie erzeugt werden kann. Dies ist Sache des Benutzers (oder besser: des Programmierers!). Soll später in die Datei geschrieben werden, dann muss sichergestellt sein, dass der Benutzer dies auch kann und darf. Verbirgt sich hinter "D:" ein CD-ROM-Laufwerk, auf das konstruktionsbedingt nicht geschrieben werden kann, oder ein Netzlaufwerk, in dem der Benutzer keine Schreibrechte hat, dann wird beim ersten Schreibversuch ein entsprechender Laufzeitfehler auftreten.

Nehmen wir mal an, dass "D:" eine Daten-Partition unserer Festplatte ist und dass wir dort die unbeschränkte Herrschaft über die Bits und Bytes haben. Dann wollen wir zunächst einmal den Fall betrachten, dass wir Daten aus einer schon vorhandenen Datei lesen wollen. Dazu müssen wir die Datei zunächst "öffnen", was bedeutet, dass das Betriebssystem einen Informationskanal zwischen unserem Programm und der externen Datei auf der Festplatte einrichtet . Wir können uns diesen Informationskanal als eine "Datei-Lesemaschine" veranschaulichen, die aus der Datei Daten ausliest und sie bei unserem Programm abliefert. Wir öffnen die Datei dazu durch den Aufruf
       Reset(f);
Dadurch wird die "Datei-Lesemaschine" aktiviert und ihre Aufmerksamkeit auf den Anfang der Datei gerichtet. Die eigentlichen Leseoperationen werden durch die Standardprozedur Read() erledigt. Jeder Aufruf der Form
       Read(f, Adressen[ActAdr]);
liest die nächste komplette Adresse aus der Datei und legt die Daten im ActAdr-ten Eintrag unseres Adressen-Arrays ab. Dabei ist automatisch dafür gesorgt, dass sich die Aufmerksamkeit der "Datei-Lesemaschine" nach dem Lesen eines Datensatzes immmer automatisch auf den nächsten Datensatz richtet - die Datei wird also durch aufeinanderfolgende Aufrufe von Read() sequenziell von vorne nach hinten durchgelesen. Ob noch Daten da sind, kann mit Hilfe der Bool'schen Funktion EOF(f) (d.h.: "E"nd "O"f "F"ile) ermittelt werden: sie liefert genau dann TRUE, wenn in der durch f referenzierten Datei keine weiteren Datensätze mehr vorhanden sind.

Bevor wir aber eine schon vorhandene Datei lesen können, müsste sie ja zunächst einmal erzeugt und geschrieben werden. Wenden wir uns also nun der eigentlichen Dateierstellung zu: wenn wir eine Datei neu schreiben wollen, öffnen wir sie mit
       Rewrite(f);
Aber: Achtung! Sollte die Datei doch schon vorhanden sein, dann löscht dieser Aufruf ihren gesamten Inhalt: in die "Datei-Schreibmaschine" wird sozusagen ein leeres Blatt eingespannt, ein eventuell vorhandenes Blatt mit dem alten Inhalt wandert in den Papierkorb! Man sollte den Rewrite-Befehl also nur anwenden, wenn man genau weiß, was man tut. Die eigentlichen Schreiboperationen werden dann jeweils durch einen Aufruf der Form
       Write(f, Adressen[ActAdr]);
erledigt: der Inhalt des ActAdr-ten Eintrags in unserem Adressen-Array wird an das Ende der Datei angefügt. Direkt nach dem Rewrite-Aufruf ist die Datei leer, womit dann das Dateiende am Dateianfang ist. Die Daten werden also genau in der Reihenfolge in der Datei stehen, in der sie durch die einzelnen Write-Aufrufe angeliefert werden.

Wenn wir die Arbeit mit der Datei abgeschlossen haben, müssen wir dies dem Betriebssystem mitteilen. Dies geschieht durch den Befehl
       CloseFile(f);
Damit wird die Datei "geschlossen": der Informationskanal zwischen unserem Programm und der externen Datei wird wieder abgebaut.

Zur besseren Übersicht sind hier nochmals alle bisher beschriebenen Datei-Befehle zusammengestellt:

Operation Datei lesen Datei schreiben
Dateivariable f mit Datei
"filename" verbinden
AssignFile(f, filename)
Datei öffnen
Reset(f)
Rewrite(f)
Auf einen einzelnen
Datensatz zugreifen
Read(f, ds_var)
Write(f, ds_var)
Ist das Datei-Ende
erreicht?
EOF(f)
{Einsatz kaum sinnvoll:
EOF ist immer TRUE!}
Datei schließen
CloseFile(f)


Beachten Sie, dass wir hier nur zwei einfache Sonderfälle für den Umgang mit typisierten Dateien behandelt haben: wir können eine typisierte Datei entweder "in einem Rutsch" lesen oder aber komplett neu schreiben. Für die meisten typischen Anwendungssituationen werden diese beiden Fälle jedoch ausreichen. Sollten Sie jemals trickreichere Operationen benötigen (wie z.B. abwechselndes Lesen und Schreiben an verschiedenen Stellen einer Datei), dann finden Sie in der Delphi-Hilfe dazu weitere Informationen (mögliche Stichworte: "Reset", "FileMode", "Seek", "FilePos"). Für die folgenden Aufgaben sind solch schwierige Dinge aber auf keinen Fall nötig!




Aufgabe:

  1. Daten gehören in Dateien!

    Ergänzen Sie Ihr Programm so, dass es beim Starten den bisher vorliegenden Datenbestand aus einer Datei namens "adress.dat" einliest und beim Beenden wieder in diese Datei zurückschreibt. Die Datei soll sich im selben Verzeichnis befinden wie die EXE-Datei des Programms.
      Dazu zwei kleine Tips:
      In jedem Delphi-Programm enthält die Variable Application.ExeName zur Laufzeit den vollständigen Pfad der EXE-Datei. Und mit der Standardfunktion ExtractFilePath können Sie den Pfad alleine (also ohne den Dateinamen der EXE) erhalten.
    Sie können das Einlesen der Daten in der OnCreate-Prozedur des Formulars erledigen. Das abschließende Neuschreiben der Datei gehört entsprechend in eine OnClose-Prozedur des Formulars. Beachten Sie, dass Sie diese beiden Prozeduren vom Objektinspektor erstellen lassen müssen, damit sie intern korrekt verdrahtet werden!
    Lösungsvorschlag






7.4 Das Luxus-Adressbuch

Der bisher erreichte Zustand unseres Adressbuch-Projekts lässt die Vermutung nicht mehr völlig absurd erscheinen, dass daraus in absehbarer Zeit ein wirklich brauchbares und alltagstaugliches Programm hervorgehen könnte. Aber, es gibt da doch noch einige Unzulänglichkeiten! Um die gravierendsten wollen wir uns nun abschließend kümmern:


a) Suchen

b) Löschen

c) Sortieren




Aufgabe:

  1. Wer sucht, der findet...nicht immer!

    Kopieren Sie das Projekt aus der vorigen Aufgabe (um den bisher erreichten Stand zu sichern!). Ergänzen Sie das Programm um die Möglichkeit, nach Vornamen, Nachnamen und Orten zu suchen. (Die Auswahl können Sie mit einer "RadioGroup" realisieren. Details dazu finden Sie in der Delphi-Hilfe.)

    Bedenken Sie aber auch, dass das gesuchte Datenelement möglicherweise gar nicht gefunden werden kann! Wie kann Ihr Programm das bemerken? Lassen Sie in diesem Fall eine entsprechende Meldung ausgeben!
    Lösungsvorschlag


  2. Das Ende einer Freundschaft: Löschen der Adresse!

    Ergänzen Sie das Programm um die Möglichkeit, den aktuell angezeigten Datensatz zu löschen. Sie sollten aber bei jeder solchen Aktion, bei der wirklich Daten vernichtet werden, Ihrem Benutzer eine Sicherheitsabfrage gönnen. Geeignet dafür ist z.B. die Delphi-Funktion MessageDlg, deren Parameterliste Sie in der Delphi-Hilfe finden.

    Wenn der Benutzer den Datensatz wirklich löschen will, dann müssen alle folgenden Datensätze um einen Platz nach vorne rücken, also den jeweiligen Vorgänger im Array ersetzen. Achten Sie darauf, dass Sie den letzten Eintrag danach nicht doppelt haben!
    Lösungsvorschlag


  3. Und wieder mal: Sortieren!

    Ergänzen Sie das Programm so, dass die Adressen alphabetisch nach dem Nachnamen sortiert vorliegen.

    Die einfachste Möglichkeit ist: Sie fügen einen "Sortieren"-Knopf hinzu, dessen Klick-Prozedur das ganze Adressen-Array nach den Nachnamen alphabetisch sortiert.

    Eine trickreichere Möglichkeit: Sie sorgen beim Einfügen eines jeden neuen Datensatzes dafür, dass er gleich korrekt alphabetisch eingeordnet wird. Diese Methode hat aber den Nachteil, dass ein schon vorliegender ungeordneter Datenbestand nicht mehr nachträglich sortiert werden kann. Wenn Sie aber noch nicht gar so viele Adressen eingegeben haben, sollten Sie eher diese Möglichkeit wählen, denn Sie hat den Vorteil, dass dem Benutzer der "Sortieren"-Knopf erspart bleibt und er automatisch ein alphabetisch sortiertes Adressbuch erhält!

    Damit diese Aufgabe eine echte Herausforderung für Sie ist, gibt's diesmal keinen Lösungsvorschlag!





Zum vorigen Kapitel Zum Inhaltsverzeichnis Zum nächsten Kapitel