Step 05 - Filehandling (1/2)

Spendenaufruf des DRK Ortsvereins Spielberg e.V.

16. April 2023

 

Hallo liebe Besucherin, hallo lieber Besucher!

In einem ganz anderen Kontext möchte ich hier nun bewerben:
Der Verein, für den ich auch einstehe, benötigt Spenden.
Ich habe vergangenes Jahr über fünf Monate versucht für delphi-lernen.de Spenden zu sammeln - es blieb leider bei 20 Euro von zwei Spendewilligen.
Nun hoffe ich, da es nicht um mich geht, dass sich mehr entschließen und für eine gute Sache zu spenden:

Alle Informationen - auch für die Überweisung den Verwendungszweck beachten!

Der neuste Stand, mindestens einmal im Monat hier zu sehen!

Jeder von uns kann etwas tun, um die Welt ein bisschen besser zu machen. (John F. Kennedy)

 

 

Filehandling Basics: Wichtige Dateioperationen

Im folgenden einige gegebene Funktionen, die in Delphi direkt eingesetzt schon den Zugriff auf Dateien geben können
 
file
Reserviertes Wort. Leitet die Typisierung zum Zugriff auf eine Datei ein. Das Beispiel zeigt, wie eine Datei benutzt werden kann, die aus einzelnen Bytes besteht.
var
  f : file of Byte;
AssignFile(f: Zeiger auf die Datei; Dateiname: String)
Hier wird der Name der Datei festgelegt und an den Zeiger den man mit f typisiert hat, gebunden. myFileName hat in diesem Beispiel schon dem absoluten Pfad zur Datei inklusive dem Dateinamen, so dass nun mit f weitergearbeitet werden kann.
  //...
  AssignFile(f, myFileName);
  //...
Reset(f: File..)
Setzt den Lese-Zeiger an den Anfang zurück. Hierbei ist wichtig, dass die Datei bereits existieren muss. In früheren Delphi-Versionen war der Reset zunächst unabdingbar, da ansonsten der "Zeiger" auf die Datei beliebig verschieden sein konnte, da eben nicht initialisiert.
  //...
  Reset(f);
  //...
Rewrite(f: File..)
Setzt die Dateigröße auf 0 und erstellt eine Datei, falls sie noch nicht existiert.
  //...
  Rewrite(f);
  //...
Read(f:File..; data)
Lest spezifizierte Daten ein. Wenn wir f wie oben im Beispiel deklariert hätten, wäre "data" vom Typ Byte, so dass Byte für Byte ausgelesen werden könnte.
  //...
  Read(f, data);
  //...
Write(f:File..; data)
Schreibt Daten in die Datei. Hier auch wieder, "data" vom Typ Byte.
  //...
  Write(f, data);
  //...
CloseFile(f:File...)
Schließt einen offenen Dateizeiger. Geöffnete Dateien können nicht von anderen Programmen geöffnet werden. Daher sollte man Dateien nur so kurz wie möglich öffnen.
  //...
  CloseFile(f);
  //...
eof(f:File...):Boolean
eof = End Of File. Wird meist in While-schleifen benutzt um zu schauen, ob man schon am Ende der Datei angekommen ist. Wichtig hierbei ist das Zurücksetzen (Reset) des Lesezeigers der Datei.
  //...
  while not eof(f) do
  begin
    //...
  end;
  //...
seek(f:File...;l:LongInt);
An eine Position innerhalb der Datei springen. Dies ist dann sinnvoll, wenn man Daten ab einer Position z.b. einfach "nachladen" möchte. posInFile ist hierbei die Position, ab der gelesen oder etwas geschrieben werden soll.
  //...
  seek(f, posInFile);
  //...
FileExists(FileName : String);
Gibt an, ob eine Datei existiert.
  //...
  if FileExists(myFileName) then
  //...
 
Diese Dateioperationen sind auch mit der Arbeit mit Datenströhmen wichtig. Die s.g. Streams können mittels Read/Write/Seek-Methoden quasi wie Dateien behandelt werden.
 

Speichern und Laden von eigenen Dateitypen

Zunächst müssen wir hierbei erstmal genau definieren: Was ist eine Datei und was ein Dateityp?
Nun ja, was eine Datei ist dürfte eigentlich jedem klar sein. Eine Datei speichert Daten, die man eingegeben hat, oder Daten die von bestimmten Programmen benötigt werden. Aber auch Programme sind Dateien. Und genau hier setzt der begriff Dateityp an. Programme sind Dateien vom Typ Programm. Es wird auch oft als ausführbare Datei bezeichnet. Diese haben unter Windows beispielsweise die Endungen .exe, .com, .bat und .cmd . Die letzteren Zwei, die so genannten Batch-Dateien, sind in Text-form gespeichert und können eigenhändig in einem beliebigen Editor modifiziert werden.
Als Beispiel zu unserem eigenen Dateiformat werden wir nun das Projekt von Step 07 - Arrays und Records weiterentwickeln.
Hierbei ging es bereits um (statische) Arrays und Records um Datenbestände in einer Datei zu speichern. Wer das nicht neu erstellen möchte, kann aus dem Kapitel den Source einfach kopieren und hier entsprechend weiterentwickeln. (Wer allerdings von Grundauf neu anfangen möchte, kann das auch - dann allerdings bitte nur einmal die Eingabe von Nick bis UIN in das Formular und beim zweiten Unterpunkt bei der GUI weitermachen) Das Formular sollte dann nach dem öffnen wie folgt aussehen:
Wir werden die Beispielanwendung nun folgendermaßen modifizieren:
 
Die GUI
  • 3 der 4 Eingabeblöcke müssen entfernt werden
  • der nun noch vorhandene Eingabeblock sollte zentriert werden, und bei der Benennung keine Zahlen mehr enthalten (also EdNick1 wird zu EdNick)
  • Zudem im Objectinspector alle Edits auf Enabled := false;
  • Die Buttons "Eingabe übernehmen" und "Eingabe anzeigen" links und rechts des Blockes positionieren, den Namen auf BtnPrevious bzw. auf BtnNext und die Aufschrift auf Zurück bzw. Weiter ändern.
  • Ein Label lblPos, welches die aktuelle Position anzeigen soll, wird hinzugefügt und unter dem Eingabeblock positioniert.
  • Ein weiterer Button "BtnNew" soll das Hinzufügen von Daten realisieren und ein Button "BtnDelete" das Löschen eines Datensatzes.
Die GUI sollte dann in etwa so aussehen:
Screenshot des Formular-Aufbaus der Demo-Anwendung
 
Doch bevor es lauffähig wird:
Korrektur des Quellcodes:
  • Ein Integer-Feld deklarieren, welches "fCurrentIndex" heisst und bei OnFormCreate auf 0 initialisiert wird.
  • Die fehlerhaften Zeilen aus BtnNextClick und entfernen.
  • Die Methoden BtnPreviousClick sowie BtnNextClick können jetzt geleert werden. Einmal Strg+S und schon sollten die Methoden verschwinden (Falls eine bleiben sollte, was in meinem Versuch mit Delphi 10.4.2 so der Fall war, dann lasst euch davon nicht verwirren -> weiter im Programm ;) )
Und nun laufen lassen... ;)
 
Nun läuft es schonmal, d.h. es hat keine Fehler.
 
Als nächstes sollen Daten hinzugefügt werden können. Dafür definieren wir den Typ T4ICQKontaktBuch um zum dynamischen Array.
Kleine Erinnerung: Eine Deklaration eines dynamisches Array ist einfach eine Array-Deklaration ohne Angabe der Größe. Bisher haben wir fest hinterlegt, dass wir genau 4 Felder benötigt haben. Das kommt einfach weg ;)
Da ein dynamisches Array kein Dateityp sein kann, muss beim Laden und Speichern das "file of T4ICQKontaktBuch" zu "file of TICQKontakt" geändert werden. Damit wir es nachher auch gleich ausführen können, geben wir bei Read und Write einen pseudo-Array-Index an, da wir ja mit Read und Write jetzt nur ein Element des Arrays speichern und nicht mehr das ganze Array auf einmal (Bsp: Z. 61: Read(f,fICQKontaktBuch[0]); )
Nun erstellen wir die BtnNewClick-Methode (Doppelklick im Formular) in der folgendes gemacht werden muss:
  • Sichern, falls es bereits ein Datensatz gibt, der verändert worden sein könnte
  • Vergrößern des dynamischen Arrays fICQKontaktBuch,
  • fCurrentIndex auf das letzte Element setzen,
  • Da das Feld leer ist - alle Edits leeren,
  • Da es evtl. das erste Mal ist, dass Daten geschrieben werden - alle Edits wieder Enabled := true
  • BtnSaveFile wieder aktivieren, da ja mindestens jetzt Daten zum Speichern vorhanden sind.
Falls ich hier und da ein bisschen unklar war, was gemacht werden soll, hier kurz der Zwischenstand als Source:
procedure TMainForm.BtnNewClick(Sender: TObject);
begin    
  // falls es bereits ein Datensatz gibt, der verändert worden sein könnte
  if (fCurrentIndex >= 0) and (fCurrentIndex <= High( fICQKontaktBuch)) then
  begin
    // Sichern:
    fICQKontaktBuch[fCurrentIndex].Nick     := EdNick1.Text;
    fICQKontaktBuch[fCurrentIndex].Vorname  := EdVorname1.Text;
    fICQKontaktBuch[fCurrentIndex].Nachname := EdNachname1.Text;
    fICQKontaktBuch[fCurrentIndex].UIN      := StrToInt( EdIcqNummer1.Text);
  end;

  //Vergrößern des dynamischen Arrays fICQKontaktBuch
  SetLength( fICQKontaktBuch, Length( fICQKontaktBuch) + 1);

  // fCurrentIndex auf das letzte Element setzen,
  fCurrentIndex := High( fICQKontaktBuch);

  // Da das Feld leer ist - alle Edits leeren,
  EdNick1.Text      := '';
  EdVorname1.Text   := '';
  EdNachname1.Text  := '';
  EdIcqNummer1.Text := '0'; // Aufgrund IntToStr fürs Auslesen muss hier eine 0 stehen
  // Da es evtl. das erste Mal ist, dass Daten geschrieben werden -> Edits freigeben
  EdNick1.Enabled      := true;
  EdVorname1.Enabled   := true;
  EdNachname1.Enabled  := true;
  EdIcqNummer1.Enabled := true;

  // BtnSaveFile wieder aktivieren, da ja jetzt Daten zum Speichern vorhanden sind.
  BtnSave.Enabled := true;
end;
 
Das Durchscrollen der hiermit erstellten Daten ist genauso einfach. Hierzu müssen wir bei BtnPreviousClick lediglich ...
  • die Felder wieder erstmal in die aktuelle Position im Array speichern,
  • den Index runterzählen, sofern es weiter runter geht,
  • die entsprechenden Werte aus dem Array in die Felder wieder eintragen,
  • und den Status aktualisieren (das schließt auch ein, dass BtnPrevious und BtnNext deaktiviert werden, falls es kein weiteres Element im Array gibt).
Wieder - Das bedeutet, wir können das obige in eine Methode kapseln und auch wieder verwenden:
 
interface
// ...
type
    // ...
    TMainForm = class(TForm)
    // ...
    private
    { Private-Deklarationen }
        procedure FelderInsArray;
//...
implementation
//...
procedure TMainForm.FelderInsArray;
begin
  // die Felder erstmal in die aktuelle Position im Array speichern
  fICQKontaktBuch[fCurrentIndex].Nick     := EdNick1.Text;
  fICQKontaktBuch[fCurrentIndex].Vorname  := EdVorname1.Text;
  fICQKontaktBuch[fCurrentIndex].Nachname := EdNachname1.Text;
  fICQKontaktBuch[fCurrentIndex].UIN      := StrToInt( EdIcqNummer1.Text);
end;
 
Der Punkt mit dem Aufpassen ist auch wichtig, dass nicht außerhalb des Index-bereiches etwas ausgelesen oder geschrieben wird. Der Index-bereich eines dynamischen Arrays geht immer von 0 bis high(Array), jeweils inklusive.
Die Anleitung gilt nun für den "Zurück"-Button. Wir können uns einfach auch den "Weiter"-Button ausdenken, indem wir die Logik leicht anpassen. Das lass ich erstmal euch weiter denken, im folgenden könnt ihr den Beispiel-Code als Zwischenstand anzeigen lassen.
 
 
Durchscrollen funktioniert also. Dann geht es um das wesentliche hier: das Speichern und Laden der Daten.
Beim Speichern müssen wir jedes Record speichern, also das ganze Array durchlaufen. Hier bietet sich eine for-schleife an, welche von 0 bis zum letzten Index-Eintrag des Arrays hochzählt. Im Schleifenrumpf können wir dann pro Datensatz die Write-Methode ausführt und so das i-te Element des Arrays in die Datei schreiben.
Da wir für Aktualisierungen erstmal einen Button sparen möchten, werden wir beim Speichern den momentan aktiven Datensatz auch gleich im Array aktualisieren. Den Code dazu haben wir ja schon zum "wiederholten" Male.
 
 
Das Laden ist etwas komplizierter, da wir nicht wissen, wie viele Elemente in der Datei gespeichert wurden. Es bietet sich also eine while-Schleife an.
Wer möchte, kann nun versuchen, das Laden direkt zu programmieren. Hier ist es ähnlich, wie beim Speichern. wir brauchen einen Zeiger auf die Datei um dann mit Read alles auslesen zu können - Datensatz für Datensatz.
 
 
Und jetzt? Naja, wer ICQ-Nummern noch hat, kann hier wirklich was anfangen. Alle Anderen, können das auch Nutzen um irgendetwas anderes zu speichern. Dazu einfach die Felder entsprechend erweitern und anpassen.
Viel Spaß ;)
 

 
Die Sources für dieses kleine Progrämmchen liegen unter Github zum Download bereit!
Viel Spaß :)