Einleitung
Top Next

Dieses Tutorial soll eine kurze Einführung in die Streams-Klassen der VCL geben.

Was ist ein Stream?

Da das Wort Streams häufig in Zusammenhang mit Dateien fällt, könnte man auf die Idee kommen, dass ein Stream nichts anderes als eine Datei sei. Stream ist aber ein allgemeinerer Begriff. Bei einem Stream geht es eigentlich nur darum dass eine Information nach der anderen geschrieben oder gelesen wird.
In der Regel arbeitet man mit Daten, die nicht zusammenhängend im Speicher vorliegen. Will man diese Daten z.B. in einer Datei speichern oder in die Zwischenablage von Windows bringen, dann muss man diese über den Speicher verstreuten Daten irgendwie zusammen packen. Eine Möglichkeit dies zu machen ist sie als Strom von Daten zu übermitteln.
Beim Speichern von Dateien könnte man das z.B. mit den Funktionen FileRead und FileWrite aus der Unit SysUtils machen. Wir wollen uns in diesem Tutorial aber nur mit den von TStream abgeleiteten Klassen der VCL beschäftigen.

Mit diesen Klassen kann man Daten schreiben und lesen. Es spielt keine Rolle, ob die Daten in eine Datei, in den Hauptspeicher oder sonst irgendwohin geschrieben werden. Für das Schreiben und Lesen der Daten schreibt man nur einmal Code. Erst durch die Auswahl einer bestimmten Stream-Klasse wird festgelegt, wo sich die Daten tatsächlich befinden.

Wie speichert TStream Daten?

Das ist so einfach, dass es schon wieder schwierig wird. TStream speichert und liest einfach ein Byte nach dem anderen.
TStream weiß nichts über die Bedeutung der Bytes. Wenn man z.B. einen Longint mit TStream speichert, dann sind das für TStream einfach vier Bytes. TStream hat keine Ahnung, dass es sich dabei um einen Integer-Wert handelt. TStream will einfach nur eine Speicherstelle wissen und wie viele Bytes sie von dort aus lesen oder schreiben soll. TStream führt keinerlei Prüfungen durch, ob die Daten den richtigen Typ haben. Man hat also alle Freiheiten und ist somit aber auch selbst verantwortlich, dass man mit den richtigen Daten arbeitet.

Wofür kann man die Stream-Klasen konkret verwenden?

· Zum Speichern und Laden von Dateien
· Zum Austausch von Daten mit BLOB-Datenbankfeldern
· Um eigene Daten-Formate mit der Windows-Zwischenablage zu verwenden
· Um Daten zwischen zwei Anwendungen mit WM_COPYDATA auszutauschen
· und vieles mehr

Damit keine Missverständnisse aufkommen - alles was man mit den Stream-Klassen machen kann, lässt sich irgendwie auch anders machen. Der Einsatz von den Stream-Klassen erleichtert einiges, besonders, wenn man die gleichen Daten für verschiedene Zwecke benötigt, z.B. wenn sich ein Objekt in einer Datei speichern und man das Objekt auch über die Zwischenablage austauschen können soll.


Daten in einer Datei speichern
Previous Top Next

Als erstes werden wir Daten mit Hilfe eines Streams in einer Datei speichern. Dafür stellt uns die VCL die Stream-Klasse TFileStream zur Verfügung.

Wir werden gleich mit etwas komplizierteren Daten arbeiten, damit das Beipsiel nicht zu trivial wird.
Wir werden mit einem Array eines Records arbeiten.

type
  TDatensatz = record
    ID: integer;
    Name: AnsiString;
  end;

  TDatenArray = array of TDatensatz;

Bevor wir Daten mit einem Stream in einer Datei speichern können, müssen wir erst einmal Daten haben.

procedure TFrmMain.BtnSaveClick(Sender: TObject);
var
  Daten: TDatenArray;
  Stream: TStream;
  I: integer;
  Len: Longint;
begin
  SetLength(Daten, 2);
  Daten[0].ID := 1;
  Daten[0].Name := 'Ein Name';
  Daten[1].ID := 2;
  Daten[1].Name := 'Ein anderer Name';

Jetzt haben wir zwei Datensätze, die wir sofort in einer Datei speichern wollen. Zum Speichern verwenden wir einen TFileStream.

  Stream := TFileStream.Create('c:\Temp\StreamTest.Datei', fmCreate);
  try

Das fmCreate sorgt dafür, dass die Datei neu angelegt wird, falls sie noch nicht existiert. Wenn es eine solche Datei bereits geben sollte, dann wird sie einfach überschrieben. Wir müssten hier eigentlich noch einigen Überprüfungscode schreiben. Es könnte z.B. sein, dass der Ordner schreibgeschützt ist oder überhaupt nicht existiert. Zwecks Übersichtlichkeit sparen wir uns das hier.

Als erstes werden wir abspeichern, wie viele Datensätze wir haben.

    Len := Length(Daten);
    Stream.Write(Len, SizeOf(Len));

Zuerst versorgen wir die Variablen Len mit der Anzahl der Datensätze. Dann schreiben wir den Wert von Len in den Stream. Zu den Umweg über die Variable Len zwingt uns die Methode Write.

function TStream.Write(const Buffer; Count: Longint): Longint; virtual; abstract;

Das const Buffer; ohne Typangabe bedeutet, dass einfach nur mit einem Zeiger auf den Speicher der übergebenen Variablen gearbeitet wird. Der Stream hat also keine Ahnung, um welchen Typ es sich handelt. Er muss wissen, wieviele Bytes geschrieben werden sollen. Das wird an den Parameter Count übergeben.
Wir übergeben SizeOf(Len) an Count. Da Len den Typ Longint hat, ist SizeOf(Len) = 4. Ein Longint belegt vier Bytes im Hauptspeicher. Es werden also vier Bytes in den Stream geschrieben.

Nachdem wir die Anzahl der Datensätze gespeichert haben, kommen jetzt die Datensätze selbst an die Reihe.

    for I := 0 to Length(Daten) - 1 do
    begin

Da wir keine Annahmen darüber machen wollen, wie ein dynamisches Array intern aufgebaut ist, speichern wir einen Datensatz nach dem anderen.

Wir haben in unserem Record TDatensatz ein Element vom Typ AnsiString. AnsiStrings werden von Delphi intern mit Zeigern verwaltet. Bei Bedarf, wird der Inhalt eines AnsiStrings an einen anderen Ort im Speicher kopiert. Die Daten eines Records verteilen sich also beliebig über den Hauptspeicher. Wir können demnach den Record nicht als Ganzes in dem Stream speichern, sondern müssen jedes Element des Records einzeln speichern.

      Stream.Write(Daten[I].ID, SizeOf(Daten[I].ID));

Mit ID geht das genauso einfach wie vorhin mit Len.
Bei AnsiStrings muss man etwas mehr Aufwand treiben.

      Len := Length(Daten[I].Name);
      Stream.Write(Len, SizeOf(Len));
      Stream.Write(PChar(Daten[I].Name)^, Len);

Zuerst müssen wir die Länge des Strings in den Stream schreiben. Das geht genauso, wie mit der Anzahl der Datensätze.
Das Schreiben des eigentlichen Strings ist dagegen etwas tricky. Wie schon gesagt, ist ein AnsiString intern eigentlich nur ein Zeiger auf den Speicherbereich, wo sich die Daten des Strings befinden.
Würden wir Stream.Write(Daten[I].Name, Len); schreiben, dann würden wir nicht den String, sondern den Zeiger auf den String und alles was sich zufällig dahinter im Speicher befindet, in den Stream schreiben.
Die Typumwandlung nach PChar liefert uns den Zeiger auf den ersten Buchstaben des Strings. Da wir aber nicht den Zeiger auf den Buchstaben in dem Stream schreiben wollen, müssen wir den erst dereferenzieren. Dies wird mit dem "^" gemacht. Jetzt sieht es für Delphi so aus, als ob wir eine Variable vom Typ Char übergeben würden. Da wie bereits gesagt, der Typ überhaupt keine Rolle spielt, sondern nur die Speicherstelle, ist es dem Stream egal, ob wir das erste Zeichen des Strings oder den ganzen String übergeben.

Damit haben wir einen Datensatz in den Stream geschrieben.

    end;
  finally
    Stream.Free;
  end;
end;

Wir dürfen natürlich nicht vergessen den Speicher, den das Stream-Objekt belegt, wieder freizugeben.

Wenn Sie die Erklärungen zwischen den Codestücken heraus löschen, dann haben Sie die Methode, mit der wir Daten in eine Datei schreiben.


Daten aus einer Datei lesen

Die Daten, die wir in die Datei geschrieben haben, wollen wir natürlich auch wieder herauslesen.

Zuerst brauchen wir einem Stream, mit dem wir die Datei auslesen.

procedure TFrmMain.BtnLoadClick(Sender: TObject);
var
  Daten: TDatenArray;
  Stream: TStream;
  I: integer;
  Len: LongInt;
begin
  Stream := TFileStream.Create('c:\Temp\StreamTest.Datei', fmOpenRead);
  try

Dieses mal übergeben wir fmOpenRead, damit der FileStream weiß, dass er die Datei nur zum Lesen öffnen soll. Den Überprüfungscode haben wir hier zwecks der Übersichtlichkeit wieder weggelassen.

Dann lesen wir in der gleichen Reihenfolge wie beim Speichern die Daten wieder ein.
Als erstes lesen wir die Anzahl der Datensätze ein

    Stream.Read(Len, SizeOf(Len));

Die Methode Read funktioniert genauso wie die Methode Write, nur dass jetzt die Daten eingelesen werden.
Mit den eingelesenen Daten initialisieren wir die Größe des dynamischen Arrays.

    SetLength(Daten, Len);

Jeder Datensaz wird einzeln eingelesen, da sie auch einzeln abgespeichert wurden.

    for I := 0 to Len - 1 do
    begin

Das Einlesen von ID ist einfach.

      Stream.Read(Daten[I].ID, SizeOf(Daten[I].ID));

Mit Name müssen wir wieder den gleichen Aufwand treiben, wie beim Speichern.

      Stream.Read(Len, SizeOf(Len));
      SetLength(Daten[I].Name, Len);
      Stream.Read(PChar(Daten[I].Name)^, Len);

Zuerst lesen wir ein, wie viele Zeichen der String hat. Dementsprechend setzen wir seine Länge. Dann wird der eigentliche String eingelesen.

    end;
  finally
    Stream.Free;
  end;
  ShowData(Daten);
end;

Nachdem wir das Stream-Objekt freigegeben haben, lassen wir zur Kontrolle die eingelesenen Daten ausgeben. Es bleibt Ihnen überlassen, wie Sie die Methode ShowData implementieren.


MemoryStream

Wie bereits in der Einleitung gesagt, bestimmt alleine die Stream-Klasse wohin die Daten tatsächlich geschrieben werden. Indem wir TFileStream durch TMemoryStream austauschen, können wir bewirken, dass unsere Daten nicht in einer Datei, sondern im Hauptspeicher landen.

procedure TFrmMain.BtnSaveClick(Sender: TObject);
var
  Daten: TDatenArray;
  Stream: TStream;
  I: integer;
  Len: LongInt;
begin
  SetLength(Daten, 2);
  Daten[0].ID := 1;
  Daten[0].Name := 'Ein Name';
  Daten[1].ID := 2;
  Daten[1].Name := 'Ein anderer Name';

  Stream := TMemoryStream.Create;
  try
    Len := Length(Daten);
    Stream.Write(Len, SizeOf(Len));

    for I := 0 to Length(Daten) - 1 do
    begin
      Stream.Write(Daten[I].ID, SizeOf(Daten[I].ID));

      Len := Length(Daten[I].Name);
      Stream.Write(Len, SizeOf(Len));
      Stream.Write(PChar(Daten[I].Name)^, Len);
    end;

    // Hier könnten wir z.B. die Daten des MemoryStreams an die Windows-Zwischenablage übergeben

  finally
    Stream.Free;
  end;
end;

Die Methode ist bis auf zwei Stellen identisch mit der Methode, die wir in "Daten in einer Datei speichern" entwickelt haben.
Hier wird eine Instanz von TMemoryStream erzeugt und nicht eine von TFileStream.
Der Kommentar müsste noch durch irgendeinem Code ersetzt werden, der etwas sinnvolles mit dem Stream anfängt. Nachdem der Stream freigegeben wurde, sind alle Daten weg.


Die Basisklasse TStream

allgemeinere Methoden

Da in den vorherigen Seiten der Code für TMemoryStream und TFileStream fast identisch war, liegt der Gedanke nahe, dass man dafür einen gemeinsamen Code schreibt.
Da sowohl TMemoryStream als auch TFileStream von der Klasse TStream abgeleitet sind, fällt das nicht schwer. Wenn wir von den Konstruktoren absehen, dann haben wir bisher nur Methoden genutzt, die bereits durch TStream zur Verfügung gestellt werden.
Wir können also große Teile des Codes in eine eigene Methode zusammenfassen, die nicht wissen muss, ob es sich um einen TMemoryStream oder TFileStream handelt.

Zuerst das Speichern:

procedure TFrmMain.WriteToStream(Stream: TStream; Daten: TDatenArray);
var
  I: integer;
  Len: LongInt;
begin
  Len := Length(Daten);
  Stream.Write(Len, SizeOf(Len));

  for I := 0 to Length(Daten) - 1 do
  begin
    Stream.Write(Daten[I].ID, SizeOf(Daten[I].ID));

    Len := Length(Daten[I].Name);
    Stream.Write(Len, SizeOf(Len));
    Stream.Write(PChar(Daten[I].Name)^, Len);
  end;
end;

Unsere neue Methode WriteToStream erwartet die zu speichernden Daten und eine Referenz auf den Stream, in dem die Daten geschrieben werden sollen. Wir können der Methode also jeden Stream übergeben, der von TStream abgeleitet ist. Dies kann TMemoryStream, TFileStream oder irgendeine andere Stream-Klasse sein.
Der restliche Code ist einfach aus der Methode BtnSaveClick geklaut. Es musste nichts umgeschrieben werden.

Dieser Code fehlt dann natürlich in BtnSaveClick.

procedure TFrmMain.BtnSaveClick(Sender: TObject);
var
  Daten: TDatenArray;
  Stream: TStream;
begin
  SetLength(Daten, 2);
  Daten[0].ID := 1;
  Daten[0].Name := 'Ein Name';
  Daten[1].ID := 2;
  Daten[1].Name := 'Ein anderer Name';

  Stream := TFileStream.Create('c:\Tmp\StreamTest.Datei', fmCreate);
  try
    WriteToStream(Stream, Daten);
  finally
    Stream.Free;
  end;
end;

Neu hinzugekommen ist der Aufruf von WriteToStream.

Dementsprechend könnten wir auch eine ReadFormStream-Methode schreiben. Da klar sein dürfte, was da zu machen ist, ersparen wir uns das hier.


Ausblick
Previous Top

Dieses Tutorial sollte nur ein erstes Gefühl für den Einsatz von Streams vermitteln.

Wir haben uns nicht bemüht das Ganze objektorientiert aufzuziehen. Wir haben einfach einen Record und ein dynamisches Array für das Halten unserer Daten genommen. Besser wäre es gewesen, wir hätten dafür eigenen Klassen geschrieben, denen wir beigebracht hätten sich in Streams zu speichern und aus Streams zu lesen. Dann wäre allerdings der Code deutlich umfangreicher geworden und wir hätten evtl. den Blick auf das Wesentliche verloren.

Für den einfachen Hausgebrauch dürfte das hier erlernte reichen. Oft wird man aber schon bald auf Probleme stoßen, die hier nicht angesprochen wurden.
· Programme werden mit der Zeit weiterentwickelt. Die Datenstrukturen erweitern sich. Man möchte aber trotzdem noch in der Lage sein, die Dateien von älteren Programmversionen zu lesen. Toll wäre es natürlich, wenn alte Programmversionen sogar mit den Dateien der neuen zurecht kämen.
· Wie erkennt man, dass sich in einem Stream wirklich Daten des eigenen Programms befinden? Wir haben einfach den Stream ausgelesen und vorausgesetzt, dass sich darin Daten für unser Programm befinden.
· Wir mussten hier schon etwas Aufwand treiben, damit wir AnsiStrings korrekt schreiben und lesen konnten. Wenn wir mit Bitmaps oder anderen Multimediaformaten arbeiten wollen, wird es noch aufwendiger.

Wenn Sie Fragen zu dem Tutorial haben, dann können Sie diese per Email (richard@kasparsoft.de) an den Autor senden.