Václav Kadlec 28.6.2005
Dnešní článek se bude zabývat praktickou stránku datových proudů neboli streamů. Ukážeme si, jakým způsobem streamy prakticky použít k ukládání informací na disk a k jejich opětovnému načítání z disku. Poukážeme také na některá úskalí, které je nutno mít na paměti při práci s textovými řetězci.
Dnešní díl seriálu naváže na to, co jsme začali probírat před týdnem. V minulém článku jsme otevřeli problematiku tzv. datových proudů neboli streamů. Řekli jsme si úplné teoretické základy týkající se toho, co to streamy vlastně jsou, k čemu slouží, jaké druhy streamů existují a jaké základní operace se streamy máme k dispozici.
Víme už tedy, že stream funguje jako jakési vodovodní potrubí: na jedné straně do něho lijeme vodu, na straně druhé jej kamsi připojíme. V důsledku toho voda jednoduše protéká z nádrže do jejího cílového umístění. Podobně funguje stream: na jedné straně jej kamsi připojíme (např. souborový stream do diskového souboru), na druhé straně do něho lijeme data. Po úvodním nakonfigurování streamu („připojení trubky“) data jednoduše protékají tak, jak chceme.
Pomocí streamů lze přistupovat k načítání a ukládání informací do celé řady umístění, například souborový stream (TFileStream) umožňuje pracovat se soubory, paměťový stream (TMemoryStream) umožňuje pracovat s operační pamětí, řetězcový stream (TStringStream) umožňuje zapisovat do řetězců apod. Existují i streamy pro zapisování do databázových BLOB polí, existují streamy pro zapisování do síťových socketů apod.
Výhodou streamů je, že práce se všemi druhy je do určité míry unifikovaná, jinak řečeno – metody použité pro ukládání informací na disk lze bez velkých modifikací použít i pro ukládání informací do operační paměti apod. Vždy je pouze nutné učinit rozhodnutí týkající se použitého streamu a zvolit odpovídající třídu pro práci s odpovídajícím umístěním (paměť, disk).
Třídy pro práci se streamy poskytují sadu metod umožňujících zapisovat a načítat data. Dostupných „streamových“ tříd existuje celá řada, mají však jedno společné: všechny jsou odděleny od společného předka – od třídy TStream. Z tohoto faktu také plyne to, co jsem si uvedli před okamžikem - všechny streamy mají do určité míry podobné chování a ovládání.
V minulém článku jsme si ještě ukázali základní obecné metody pro práci takřka se všemi druhy streamů. Pojďme si jen v rychlosti zopakovat jejich funkční prototypy:
function Read(var Buffer; Count: Longint): Longint;
function Write(const Buffer; Count: Longint): Longint;
procedure ReadBuffer(var Buffer; Count: Longint);
procedure WriteBuffer(const Buffer; Count: Longint);
Tolik k zopakování toho, co jsme si řekli minule. Náplň dnešního dílu bude ryze praktická: ukážeme si, jak prostřednictvím streamu realizovat několik jednoduchých, avšak ryze praktických operací.
Pojďme společně vytvořit aplikaci, která demonstruje základy práce se streamy. Pro ukázku využijeme souborový stream, budeme tedy pracovat s diskovým souborem.
Nejprve se podívejme na to, jak stream použít k jednoduchému zapisování na disk, resp. ke čtení informací z disku.
Dnešní aplikace se zaměří na specifickou formu práce se streamy, a to na ukládání/načítání řetězců. Skutečností je, že pro ukládání řetězců se streamy zase až tak často nepoužívají: k tomu existují specializované metody jednak proto, že řetězce jsou jedním z nejčastěji používaných datových formátů a pak také proto, že řetězce mají všemožná specifika, na něž je nutné při jejich zpracování vzít zřetel.
Přesto však řetězci začneme, především proto, že jsem si to tak naplánoval :-) a pak také proto, že na následující aplikaci uvidíme hned několik úskalí práce se streamy.
Vytvořme tedy novou aplikaci. Na formulář umístíme dvě tlačítka (komponenty Button) a jednu komponentu Memo. Funkce aplikace bude následující:
Je mi jasné, a proto mi na toto téma nemusíte posílat rozhořčené emaily :-), že podobného výsledku by bylo možné dosáhnout s nesrovnatelně menším úsilím použitím nativních metod komponenty Memo, například LoadFromFile nebo SaveToFile. Použití streamů je zde zvoleno z čistě demonstračních důvodů, abyste viděli, jak do streamu „nacpat“ řetězec a abyste viděli, jak řetězec ze streamu zase „vydolovat“. Neříkám, že zvolené řešení je optimální, neříkám ani, že ne nejlepší možné, neříkám dokonce ani, že je příliš elegantní.
Pojďme se však vrátit k tvorbě aplikace. Na formulář jsme umístili potřebné komponenty, nyní pojďme ošetřit několik málo událostí.
Začneme událostí OnCreate hlavního formuláře. V její obsluze pouze nastavíme titulky komponent:
procedure TForm1.Button1Click(Sender: TObject);
var x: TFileStream;begin
x := TFileStream.Create(`info.txt`, fmCreate or fmOpenWrite);
// otevreme stream
if (x.Write(PChar(Memo1.Lines.Text)^, Length(Memo1.Lines.Text)) < Length(Memo1.Lines.Text)) then
// nacteme ulozime do nej udaje z Memo
ShowMessage(`Nebyly zapsany vsechny znaky`)
else
ShowMessage(`Udaje byly uspesne zapsany`);Memo1.Lines.Clear; // vymazeme obsah <emo
x.Free; // uvolnime streamend;
Uvedený zdrojový kód je poměrně samovysvětlující, proto popisek bude jen velmi kusý:
nejprve vytvoříme stream (procedura TFileStream.Create),
následně do vytvořeného streamu zapíšeme pomocí funkce Write obsah vlastnosti Memo1.Lines.Text, což je řetězcová reprezentace obsahu komponenty Memo, vypíšeme hlášení o výsledku operace, vymažeme obsah Memo a uvolníme stream.
Pokud tento kód napíšete nebo zkopírujete do Delphi, můžete si aplikaci rovnou vyzkoušet: na diskovém souboru v aktuálním adresáři po kliknutí na tlačítko Button1 nepochybně najdete soubor info.txt, který obsahuje doslovný přepis původního obsahu komponenty Memo.
Nyní se pojďme podívat na obsluhu tlačítka Button2. Stisk tohoto tlačítka nezpůsobí nic jiného, než načtení souboru info.txt z disku a jeho zobrazení v komponentě Memo. Zdrojový kód je mírně komplikovanější: na vině jsou specifika řetězců a také určitá neelegance, s níž streamy v tomto kontextu používáme:
procedure TForm1.Button2Click(Sender: TObject);
var x: TFileStream; // stream
f: File of byte; // soubordelka: integer; // delka souboru
precteno: integer; // pocet prectenych znakubuf: PChar; // pomocny buffer
ret: string; // precteny retezecbegin
AssignFile(f, `info.txt`); // otevreme soubor
Reset(f); // jen abychom mohli
Delka := FileSize(f); // zjistit jeho delku = pocet znaku
CloseFile(f); // soubor zase pekne zavreme
GetMem(buf, 1); // vyhradime si 1bytovy buffer
Precteno := 0;
x := TFileStream.Create(`info.txt`, fmOpenRead);
// otevreme si stream pro cteni
while (precteno < delka) do begin
// cteme az do konce souboru
if (x.Read(buf^, 1) = 0) then break;
// pekne znak po znaku
ret := ret + buf[0]; // a vytvarime vysledny retezec
Inc(Precteno);
end;if (precteno < delka) then
ShowMessage(`Nektere udaje nebyly nacteny`)
else
ShowMessage(`Udaje byly uspesne nacteny`);Memo1.Lines.Text := ret; // retezec zobrazime v Memo
FreeMem(buf); // uvolnime buffer
x.Free; // uvolnime streamend;
Vidíte, že zdrojový kód je trochu kostrbatý a vyžaduje některé „úkroky stranou“, které ve zdrojácích neradi vidíme. Po pravdě řečeno, mám-li být upřímný, nemyslím si, že by tento kód vyjadřoval nejlepší možné řešení: pakliže některý z pozorných čtenářů zná elegantnější řešení, budu rád, když mi jej zašle na email nebo případně rovnou zveřejní ve formě diskusního příspěvku.
Nevím, má-li smysl detailně popisovat funkci uvedené procedury. Možná jen velmi stručně:

Ať už je řešení hezké nebo ne, skutečnost je taková, že funguje a splňuje stanovené zadání: aplikace je schopna ukládat a znovu načítat obsah komponenty Memo z diskového souboru.
Protože jsme se v tomto okamžiku už prudce přiblížili ke konci článku, přerušíme pro tento okamžik tok jeho textu a na týden se rozloučíme.
Za týden bezprostředně navážeme a ukážeme si to, k čemu vlastně celý dnešní článek směřoval:
Závěrem si znovu předvedeme kompletní zdrojový kód dnešní aplikace.
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;type
TForm1 = class(TForm)
Button1: TButton;
Memo1: TMemo;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;var
Form1: TForm1;implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var x: TFileStream;begin
x := TFileStream.Create(`info.txt`, fmCreate or fmOpenWrite);
// otevreme stream
if (x.Write(PChar(Memo1.Lines.Text)^, Length(Memo1.Lines.Text)) < Length(Memo1.Lines.Text)) then
// nacteme ulozime do nej udaje z Memo
ShowMessage(`Nebyly zapsany vsechny znaky`)
else
ShowMessage(`Udaje byly uspesne zapsany`);Memo1.Lines.Clear; // vymazeme obsah <emo
x.Free; // uvolnime streamend;
procedure TForm1.Button2Click(Sender: TObject);
var x: TFileStream; // stream
f: File of byte; // soubordelka: integer; // delka souboru
precteno: integer; // pocet prectenych znakubuf: PChar; // pomocny buffer
ret: string; // precteny retezecbegin
AssignFile(f, `info.txt`); // otevreme soubor
Reset(f); // jen abychom mohli
Delka := FileSize(f); // zjistit jeho delku = pocet znaku
CloseFile(f); // soubor zase pekne zavreme
GetMem(buf, 1); // vyhradime si 1bytovy buffer
Precteno := 0;
x := TFileStream.Create(`info.txt`, fmOpenRead);
// otevreme si stream pro cteni
while (precteno < delka) do begin
// cteme az do konce souboru
if (x.Read(buf^, 1) = 0) then break;
// pekne znak po znaku
ret := ret + buf[0]; // a vytvarime vysledny retezec
Inc(Precteno);
end;if (precteno < delka) then
ShowMessage(`Nektere udaje nebyly nacteny`)
else
ShowMessage(`Udaje byly uspesne nactny`);Memo1.Lines.Text := ret; // retezec zobrazime v Memo
FreeMem(buf); // uvolnime buffer
x.Free; // uvolnime streamend;
procedure TForm1.FormCreate(Sender: TObject);
begin
Button1.Caption := `Uloz`;
Button2.Caption := `Nacti`;
Self.Caption := `Ukazka streamu`;
end;end.
Dnes jsme si ukázali, jakým způsobem lze použít souborové streamy pro ukládání souborů na disk a pro načítání souborů z disku. Dnešní článek sice končí, ale dokončený není: příště na něj bezprostředně navážeme a ukážeme si skutečné výhody, které streamy přinášejí.