Step 06 - Filehandling (2/2)

Was sind Streams?

Streams sind erstmal ins deutsche übersetzt "Ströme". Gemeint sind hierbei aber Datenströme. Diese Datenströme können Daten von verschiedenen Datentypen speichern. Es wird meist eine einfache Listenform benutzt, falls die Daten keine konstante Größe haben.
Zum Beispiel könnten die Daten in unserem letzten Step auch einfach in einem solchen Datenstrom gespeichert werden. Dies geschieht in dem wir zunächst die Größe der Daten, welche in diesen Datenstrom geschrieben werden sollen, in den Datenstrom schreibt - was meist ein 32-Bit-Integerwert ist und somit 4 Byte belegt - und hiernach dann die Daten schreiben. Bei unserem Beispiel würden wir für den Vornamen einer Person zunächst die Integer-Zahl, welche die länge des Namens angibt, in den Datenstrom schreiben: "5" und dannach den Namen selbst: "Marco" .
Ein Vorteil ist die dadurch entstehende Dynamik. Zudem wird durch diese Art der Speicherung enorm an Platz gespart. Wenn man beispielsweise annimmt, dass ein Vorname maximal 15 Buchstaben hat, und man will n Vornamen speichern, dann wäre die Datei bei der in Step04 vorgestellten Taktik immer n*15 Bytes groß. Da die Länge jedoch variiert, wird die Datei wesentlich kleiner sein.
Natürlich gibt es bei kleinen Dateien kein Problem. Daher wollen wir ein kleines Tool entwickeln, welches beliebige Dateien in eine zusammenführt. Wir werden dieses Tool, genauso wie die Endung der entstehenden Dateien "paked" nennen.
 

Daten in FileStreams speichern

Um die hier nicht den Rahmen zu sprengen, sollte das folgende zur Anpassung der Oberfläche ausreichen: Wir brauchen Zwei Buttons - einer zum Packen, und einer zum Auseinanderführen. Zum Laden und Speichern verwenden wir die Komponenten OpenDialog und SaveDialog, wobei der SaveDialog nur paked-Dateien anzeigen soll. Beim vergeben des Dateinamens soll auch überprüft werden, ob die Endung .paked heist (mit der Methode ExtractFileExt kann dies einfach verglichen werden). (OpenDialog, vom Typ TOpenDialog, kann direkt aus der Palette auf das Formular gezogen werden!)
Wir werden nun beim Betätigen des "Dateien zusammenführen"-Buttons zwei Szenarien durcharbeiten:
  1. Die Datei ist die erste Datei des Paketes, also die gewählte paked-Datei existiert noch nicht, und
  2. Die paked-Datei existiert und die Datei soll nun eingefügt werden.
In Delphi gesprochen sieht das ganze dann so aus:
procedure TForm1.BtnPakClick(Sender: TObject);
begin
  if OpenDialog.Execute then
  begin
    if SaveDialog.Execute then
    begin
      // Sicherstellen, dass Dateiendung stimmt:
      if ExtractFileExt(SaveDialog.FileName) <> '.paked' then
        SaveDialog.FileName := SaveDialog.FileName+'.paked';
      // Fallunterscheidung:
      if not FileExists(SaveDialog.FileName) then
      begin
        // Neue Datei
      end
      else
      begin          
        // Datei existiert bereits
      end;
    end;
  end;
end;
 
Kümmern wir uns zuerst erstmal um das Erstellen einer Datei:
Der benötigte Objekttyp ist TFileStream. Wir deklarieren also eine lokale Variable targetStream und instanziieren die Klasse. In der Create-Methode haben wir einen Parameter "Mode". Hier eine kleine Liste mit den möglichen Werten:
  • fmCreate - erstellt die Datei und öffnet den Stream zum Schreiben. Vorhandene Daten werden geleert.
  • fmOpenRead - öffnet lediglich den Lesemodus. Hierbei kann die Datei während dessen auch von anderen Programmen lesend benutzt werden.
  • fmOpenWrite - öffnet lediglich den Schreibmodus. Die Datei ist für andere Programme gesperrt. Es ist nur Schreiben möglich.
  • ...
Weitere Modi können unter der Delphi Hilfe nachgelesen werden.
Da wir die Information, wie die Datei einst hies, nicht verlieren wollen, werden wir zunächst den Dateinamen in den Stream schreiben. Den Dateinamen bekommen wir durch die Funktion ExtractFileName(OpenDialog.FileName). OpenDialog.Filename beinhaltet auch den Pfad zur Datei, welchen wir nicht speichern möchten.
Bisher sieht es also so aus:
          targetStream := TFileStream.Create( SaveDialog.FileName, fmCreate);
          fName := ExtractFileName( OpenDialog.FileName);
          fNameLength := Length( FName);
          targetStream.Write( fNameLength, SizeOf(fNameLength));
          // Char für Char eintragen, ganze Strings gehen nicht. Wieso?
          for i := 1 to fNameLength do
            targetStream.Write( fName[i], SizeOf(fName[i]));
Nun stellt sich die Frage: Wie schaffen wir es, eine andere, uns unbekannte Datei, hier reinzupacken?
Und die Antwort ist einfach: Wir öffnen einen weiteren (ReadOnly-)Stream und kopieren mittels CopyFrom dessen Inhalt in unser targetStream rein ;)
          sourceStream := TFileStream.Create( OpenDialog.FileName, fmOpenRead);
          fileSize := sourceStream.Size;
          targetStream.Write(fileSize, SizeOf(fileSize));
          targetStream.CopyFrom(sourceStream, sourceStream.Size);
          sourceStream.Free;
Also kümmern wir uns um das weitere Hinzufügen von Dateien.
Wir wissen, vom vorherigen Kapitel, dass man eine geöffnete Datei immer zuerst resetten muss, damit man am Anfang der Datei ist. Wir brauchen jedoch das Ende des Streams, wo wir dann weitere Daten anhängen können. Also versuchen wir es einfach mal, dass wir den oberen Quellcode übernehmen, mit dem Unterschied, dass wir anstatt fmCreate, fmOpenWrite benutzen. Wenn wir es jedoch bei dieser Änderung belassen, werden wir kein Erfolg haben, da beim öffnen des Streams sofort auf den Anfang gezeigt wird. Wir setzen also per Seek-Methode den Zeiger auf das Ende des Streams:
          targetStream := TFileStream.Create( SaveDialog.FileName, fmOpenWrite);
          targetStream.Seek( 0, soEnd);
Hier sehen wir, dass die Methode Seek wunderlicherweise zwei Parameter hat. Einmal ein "Offset" und dazu noch ein "Origin". Das erklärt sich so, dass das Offset angibt, wie weit der Lesezeiger verschoben werden soll. Origin wiederum gibt den Ursprung an und kann somit am Anfang des Streems sein ("soBeginning"), an der momentanen Position ("soCurrent"), oder wie wir es brauchen, am Ende des Streams mit "soEnd". ("so" steht hierbei als Abkürzung für "StreamOrigin")
Und hiermit ist das komplette Erstellen einer paked-Datei abgeschlossen. Also müssen wir nur noch:
 

Daten aus FileStreams laden

Damit wir nun kein weiteren OpenDialog brauchen, werden wir den vorhandenen während der Laufzeit modifizieren. Wir müssen lediglich den Filter neu setzen. Beim Öffnen einer beliebigen Datei ist er also 'Alle Dateien|*.*' und hier brauchen wir nur die '*.baked'-Dateien.
Nach dieser Modifikation können wir - kurzgefasst - wie folgt fortfahren:
  • Wir öffnen den paked-Stream lesend.
  • Solange wir nicht am Ende des Streams sind,
    • lesen wir den Dateinamen fn aus,
    • öffnen mit fmCreate einen weiteren Stream mit dem Dateinamen fn,
    • kopieren die Daten der Datei in den neuen Dateistream,
    • schließen ('befreien') den Stream wieder.
  • Wenn dies fertig ist, schließen wir den paked-Stream.
Wenn ihr beim Lesen der Daten aufgepasst habt und euch auch mal die Methoden, welche mit TFileStream mitgeliefert werden, angeschaut habt, dann sollte dies hier ein Kinderspiel werden.
Falls doch etwas nicht ganz geklappt hat - kein Thema, lest euch nochmal in Ruhe durch, was zu machen ist und es wird hierzu bald den Source unten verlinkt geben...
Wenn alles geklappt hat, können noch einige Verbesserungen durchgeführt werden. Zum einen könnte es ja mal sein, dass zwei Dateien den gleichen Namen besitzen. Oder man möchte nicht in dem Ordner 'entpacken' in dem die paked-Datei liegt, sondern irgendwo anders. Oder man möchte einen kompletten Ordner mit allen Unterordnern in die paked-Datei stecken. Oder oder oder... ;-)
Also, ich hoffe ihr habt hierbei viel gelernt und hattet Spaß beim nachprogrammieren ;-)
 

 
Wer das nachprogrammiert hat, darf gerne seinen Sourcecode an mich zusenden. Falls es für dich in Ordnung geht, publiziere ich es mit deiner Einwilligung auch auf Github und verlinke dies hier. Ansonsten bleibt das aktuell leider pure Theorie ohne Code-Beispiel.