artykuły

Delphi - Strumienie - TFileStream

20:36
śro, 9 czerwiec 2004
Artykuł opisuje na przykładach działanie i zastosowania strumieni typu TFileStream. W prosty sposób wyjaśnia jak uniknąć częstych błędów związanych ze strumieniami.

#2 TFileStream

Wstęp

W pierwszej części zajmiemy się strumieniem obsługującym operacje na plikach -TFileStream. Dzięki temu strumieniowi możemy zapisywać dowolne dane do pliku. Zamiast prawić długi wstęp, na który pozwoliłem sobie w części I, przechodzę od razu do rzeczy.
Pierwszym zadaniem, jakie musimy wykonać uzyskując dostęp do pliku przez strumienie to ich utworzenie. Aby utworzyć strumień piszemy:

var strumien : TFileStream; begin strumien := TFileStream.Create('C:\plik.txt',fmCreate);

Jak widzimy, najpierw deklarowana jest zmienna typuTFileStream, do której potem będziemy się odwoływać np. przy zapisywaniu. Potem następuje utworzenie strumienia za pomocą:

TFileStream.Create(1,2)

gdzie:

1 - ścieżka dostępu do pliku
2 - parametr określający typ dostępu do tego pliku. Może on przybrać 4 podstawowe formy:

fmCreate- po zastosowaniu tego parametru plik określony w pierwszym parametrze zostanie utworzony jeśli go jeszcze nie ma lub wyczyszczony gdy już istnieje (coś jak ReWrite). Będziemy też mieli uprawnienia do zapisu.
fmOpenRead- otwiera plik jedynie do odczytu. Próba zapisu z tym parametrem wywoła błąd.
fmOpenWrite- otwiera plik jedynie do zapisu. Próba odczytu z tym parametrem spowoduje błąd.
fmOpenReadWrite- otwiera plik do odczytu i zapisu. Jednym słowem mamy pełną kontrolę nad treścią ;)

Po utworzeniu pliku, przydało by się jeszcze coś zrobić, dlategoTFileStreamwyróżniamy 4 (a właściwie 2 pary) bardzo przydatne funkcje zapisu i odczytu, mianowicieWrite,Read,WriteBuffer,ReadBuffer.

Write

Funkcja ta pozwala zapisać dane do pliku. Jej budowa prezentuje się następująco:

function Write(const Buffer; Count: Longint): Longint; override;

Widzimy, że jej pierwszym parametrem jest stała która przechowuje w sobie dane przeznaczone do zapisania. Następny parametr "mówi" funkcji ile danych ma zapisać. Na końcu zadeklarowany jest typ danej zwracanej przez funkcję. A funkcja ta zwraca ilość danych które udało jej się zapisać do pliku.
Przyjrzyjmy się teraz dokładniej parametrom funkcjiWrite. Kiedy do pliku próbujemy zapisać jakiś tekst - przykładowo:

Ala ma kota.

... liczący 12 znaków, a Ty wprowadzisz jako drugi parametr funkcjiWritecyfrę 6, funkcja zapisze do pliku jedynie urywek:

Ala ma

Zapis tekstu przeważnie przeprowadza się w całości, dlatego aby nie liczyć ręcznie ilości danych do zapisania (już nie mówię o przypadkach, w których jest to niewykonalne) stosujemy funkcjeSizeOf(zmienna), która zwraca wielkość zmiennej i w ten sposób gwarantuje, że wszystkie znaki zostaną zapisane:

SizeOf(buff)

(gdzie "buff" jest zmienną przechowującą tekst przeznaczony do zapisania) co daje nam zapisanie wszystkiego co chcieliśmy zapisać. Nasza funkcjaWrite, po wszystkich tych przejściach wygląda zatem następująco:

zapisanych := Write(buff, SizeOf(buff));

Zmienna "zapisanych" będzie w tym wypadku (po wykonaniu funkcjiWrite) zawierała liczbę bajtów, które funkcja zdołała zapisać do pliku. Jeden bajt, jak wiemy, charakteryzuje się tym, że możemy za jego pomocą zapisać dowolny znak ASCII, tak więc wartość zmiennej "zapisanych" będzie w rzeczywistości liczbą poprawnie zapisanych znaków (bajtów).

Read

Funkcja ta pozwala odczytać dane z pliku. Jej budowa prezentuje się następująco:

function Read(var Buffer; Count: Longint): Longint; override;

Widzimy, że jej pierwszym parametrem jest zmienna, która przechowuje w sobie odczytane z pliku dane. Następny parametr "mówi" funkcji, ile danych ma odczytać. Na końcu zadeklarowany jest typ danej zwracanej przez funkcje. Funkcja ta zwraca ilość danych, które udało jej się odczytać pliku.
Przyjrzyjmy się teraz dokładniej parametrom funkcjiRead. Gdy w pliku znajduje się jakiś tekst - przykładowo:

Ala ma kota.

... liczący 12 znaków, a Ty wprowadzisz jako drugi parametr funkcjiReadcyfrę 6, funkcja zwróci Ci jedynie:

Ala ma

Bezsensem jednak byłoby podawanie za każdym razem ilości danych do odczytania. Możemy przecież nie mieć pojęcia, ile znaków znajduje się w pliku i tym samym ile znaków my mamy odczytać. Dlatego przeważnie jako drugi parametr podajemy:

SizeOf(buff)

gdzie "buff" jest zmienną do której odczytujemy tekst. Nasza funkcjaReadwygląda zatem następująco:

odczytanych := Read(buff, SizeOf(buff));

Zmienna "odczytanych" będzie w tym wypadku (po wykonaniu funkcjiRead) zawierała liczbę bajtów zapisanych do pliku. Jeden bajt, jak wiemy, charakteryzuje się tym, że możemy za jego pomocą zapisać dowolny znak ASCII, tak więc wartość zmiennej "odczytanych" będzie w rzeczywistości liczbą odczytanych znaków (bajtów).

WriteBuffer i ReadBuffer

W zasadzie procedury te nie różnią się zbytnio od funkcjiWriteiRead, poza jednym wyjątkiem - nie zwracają one żadnych danych. Tak więc proceduraWriteBuffernie zwróci nam ilości zapisanych danych do pliku, tak jak to miało miejsce przy funkcjiWrite, a proceduraReadBuffernie zwróci nam ilości danych odczytanych z pliku (jak to miało miejsce przyRead).

Zwolnienie pamięci

Po zakończeniu korzystania ze strumieni wypadałoby zwolnić pamięć, którą one zajmują. Dlatego przeważnie po zakończeniu zapisu/odczytu zwalnia się pamięć zajmowaną przez strumień. Robimy to następująco:

strumien.Free;

Gwarantuje nam to brak błędów w przypadku, gdy coś zapisaliśmy, a teraz chcemy to odczytać.

Trochę kodu

Aby zapisać tekst wystarczą jedynie trzy wiersze kodu właściwego, tzn. kodu który służy jedynie do zapisu!

var Strumien : TFileStream; tekst: string; begin tekst := 'Ala ma kota.'; // Zapis zwykłego tekstu Strumien := TFileStream.Create('C:\plik.txt', fmCreate); Strumien.WriteBuffer(tekst, SizeOf(tekst)); Strumien.Free;

Widzicie? Utworzenie strumienia, zapisanie danych, zwolnienie pamięci po strumieniu.
Teraz tak zapisany tekst spróbujemy odczytać:

var Strumien : TFileStream; tekst: string; begin // Odczytanie zwykłego tekstu Strumien := TFileStream.Create('C:\plik.txt', fmOpenRead); Strumien.ReadBuffer(tekst,SizeOf(tekst)); Strumien.Free;

I również 3 linijki. Odczytany tekst siedzi nam w zmiennej "tekst".
Ale dlaczego mówiłem o zwalnianiu pamięci po zakończonej operacji? Ponieważ  gdybyśmy chcieli zapisać i odczytać coś w jednej procedurze bez zwalniania pamięci, uzyskalibyśmy piękny błąd:"String read error"lub"String write error". Szczególnie chciałbym to zaznaczyć, ponieważ kiedyś zajęło mi dużo czasu dojście dlaczego ten błąd występuje i jak sobie z nim poradzić.
Jeśli chcielibyśmy teraz napisać procedurę najpierw zapisującą, potem odczytującą dany tekst (bez sensu ;) to najpierw musimy:

  1. utworzyć strumień
  2. zapisać
  3. zwolnić pamięć
  4. utworzyć strumień
  5. odczytać
  6. zwolnić pamięć

Wyglądałoby to następująco:

var Strumien: TFileStream; tekst: string; begin tekst := 'Ala ma kota.'; // Zapis zwykłego tekstu Strumien := TFileStream.Create('C:\plik.txt', fmCreate); Strumien.WriteBuffer(tekst, SizeOf(tekst)); Strumien.Free; tekst := ''; // Odczytanie zwykłego tekstu Strumien := TFileStream.Create('C:\plik.txt', fmOpenRead); Strumien.ReadBuffer(tekst,SizeOf(tekst)); Strumien.Free; ShowMessage('Odczytałem taki tekst:'+tekst);

Bez większego sensu, ale powinno działać. Gdybyśmy jednak nie zwolnili pamięci przed odczytem i nie utworzyli na nowo strumienia, system poinformowałby nas o tym wyświetlając stosowny komunikat ;)

var Strumien: TFileStream; tekst: string; begin tekst := 'Ala ma kota.'; // Zapis zwykłego tekstu Strumien := TFileStream.Create('C:\plik.txt', fmCreate); Strumien.WriteBuffer(tekst, SizeOf(tekst)); tekst := ''; // Odczytanie zwykłego tekstu Strumien.ReadBuffer(tekst,SizeOf(tekst)); ShowMessage('Odczytałem taki tekst:'+tekst);

Taki kod nie będzie działał - zresztą możesz spróbować. Owszem, kod skompiluje się i kompilator nie poinformuje nas o zaistniałym błędzie. Dopiero podczas testowania programu wykryjesz błąd.
Myślę, że powyższe kody zostały już opanowane - przejdźmy do ciekawszych rzeczy, jak na przykład - zapis całego komponentu do pliku.
Zapis ustawień komponentu do pliku - WriteComponent()
Strumienie posiadają bardzo użyteczną w wykorzystaniu funkcję, o której wspomniałem już w części I - zapis komponentu. Nie widzę żadnych przeszkód, dla których miałbym przedłużać teorię - przejdźmy więc do praktyki ;)
Aby zapisać komponent, musisz najpierw ustawić go na formie. W tym celu z palety komponentów, wybierz komponentTEdit(paletaStandard) i połóż go na formie. Potrzebne będą jeszcze dwaTButton, które również znajdziesz na palecie Standard. Dla pierwszego przycisku (Button1) ustaw właściwość "Caption" na "Zapis" (zresztą jak uważasz), drugiemu nadaj etykietę "Odczyt".
Po wykonaniu czynności kosmetycznych, przechodzimy do kodu, klikając dwukrotnie na przycisk "Zapis". W nowoutworzonej procedurze wpisujemy taki oto kod:

var strumien : TFileStream; begin strumien := TFileStream.Create('C:\plik.txt',fmCreate); strumien.WriteComponent(Edit1); strumien.Free;

Już tłumaczę...
Pierwsza linia
Słówkovar- bez zbędnych objaśnień ;)
Druga linia
Zadeklarowanie zmiennej "strumien" jako nowego strumienia typuTFileStream
Czwarta linia
Utworzenie nowego strumienia (fmCreate) do(?) pliku 'C:\plik.txt'.
Piąta linia
Tutaj zatrzymamy się trochę dłużej. ProceduraWriteComponent()przeprowadza zapis podanej jako jej parametr kontrolki. Ma ona następującą budowę:
procedure WriteComponent(Component: TComponent);

Jak widzimy jako parametr procedury podajemy nazwę kontrolki (czyli w naszym przypadkuEdit1). Dzięki temu procedura nie pyta się programisty o dodatkowe właściwości komponentu, który właśnie zapisuje, tylko pobiera sobie je sama (sama odwołuje się do podanego komponentu i bierze co jej potrzebne). My również możemy pisać takie procedury, aczkolwiek częściej podajemy konkretne parametry, jak tekst komponentu czy jego wysokość. OK.

Odbiegłem trochę od tematu - przepraszam i biorę się ponownie do roboty.
Szósta linia: zwolnienie pamięci strumienia. Jeśli udało Wam się zapisać komponent do pliku (a na pewno Wam się udało :) to przechodzimy do odczytywania.

Odczyt ustawień komponentu z pliku - ReadComponent()

Procedura jest bardzo podobna, jak nie identyczna z WriteComponent(). W sumie, nie musimy nic zmieniać, oprócz praw dostępu do pliku. Musimy uzyskać prawa otworzenia/odczytu. Dlatego zamiast parametru "fmCreate" stosujemy "fmOpenRead".
Wygenerujcie teraz drugą procedurę - tj. po naciśnięciu przycisku "Odczyt". W tym celu dwukrotnie klikamy na przycisk "Odczyt" i w nowoutworzonej procedurze piszemy:

var strumien : TFileStream; begin strumien := TFileStream.Create('C:\plik.txt', fmOpenRead); strumien.ReadComponent(Edit1); strumien.Free;

Jak widzicie - w powyższej procedurze odczytującej zmienione zostały tylko dwie rzeczy. Gdybyśmy zamiast "fmOpenRead" zastosowali "fmCreate" bądź "fmOpenWrite" to system odwdzięczyłby się nam wyświetlając komunikat o błędzie. W rzeczywistości system domaga się praw odczytu i nie pozwoli nam odczytać z pliku jeśli mamy jedynie prawo zapisu, tak samo jak nie pozwoli nam zapisać do pliku, gdy mamy jedynie prawa odczytu. Lekarstwem na to jest "fmOpenReadWrite", która pozwala zapisywać i odczytywać plik - daje nam prawa odczytu i zapisu. Jest ona połączeniem "fmOpenRead" z "fmOpenWrite".

fmOpenReadWrite = fmOpenRead + fmOpenWrite

Nie pozwala nam natomiast tworzyć pliku, tak jak to robi "fmCreate". W zasadzie moglibyśmy używać jedynie dwóch praw "fmCreate" i "fmOpenReadWrite" (która pozwala odczytywać i zapisywać do pliku) i racja. Jednak czasami będziesz musiał ograniczyć prawa dostępu do pliku np. do samego czytania. Wtedy przyda Ci się znajomość wszystkich parametrów.
Podsumowując "fmCreate" używamy chcąc utworzyć (na przykład po raz pierwszy) nie istniejący plik i zapisać do niego lub gdy chcemy wyczyścić już istniejący plik. Następnie w zależności od sytuacji używamy "fmOpenRead", bądź "fmOpenWrite", lub też "fmOpenReadWrite" by uzyskać odpowiednie prawa dostępu. Sprzeczne prawa dostępu w stosunku do wykonywanej operacji (np. zapisu) spowodują błąd.

Uruchomcie teraz program (F9) i sprawdźcie, czy działa.

Poznaliśmy już podstawowe prawa rządzące strumieniami i nauczyliśmy się zapisywać i odczytywać dane za pomocą strumieni. Ale strumienie danych potrafią znacznie więcej. Przekonacie się o tym w dalszych częściach. Na razie zostańmy przy tym (nie chcę Wam za bardzo mieszać). Umiemy już to wszystko, więc wypadałoby napisać jakiś program. Napiszemy prostą książkę adresową, która będzie przeprowadzała zapis i odczyt danych właśnie za pomocą strumieni - co wy na to? Fajnie? No to do dzieła:

Piszemy program

"Kosmetyczna obróbka programu"

  1. Z palety "Additional" wybieramy komponent o nazwie "ValueListEditor" i umieszczamy go na formie. Zmieniamy jego właściwość "Name" (w inspektorze obiektów - po lewej stronie) z "ValueListEditor1" na "lista"
  2. Teraz wybieramy z palety "Standard" komponentTLabeli umieszamy go na formie. Czynność tę powtarzamy jeszcze trzykrotnie, dopóki na formie nie znajdą się 4 komponentyTLabel.
  3. Potrzebne będą nam jeszcze 4 pola tekstowe typuTEdit. Wybieramy je z palety "Standard" i wstawiamy na formę. Czynność tę powtarzamy jeszcze trzykrotnie tak, aby na formie figurowały ostatecznie 4TEdit;).
  4. Teraz kolej na przyciski. Wybieramy z palety "Standard" przycisk i wstawiamy go na formę. Powtarzamy tę czynność jeszcze dwa razy, tak aby na formie widniały w sumie trzy przyciski.
  5. Ustawiamy wszystkie komponenty w sposób podany na rysunku (szczególnie ważne jest ustawienie kontrolekTEdit- np.Edit1musi być polem na wpisanie imienia,Edit2musi być polem na wpisanie nazwiska itd.):
    Screen książki adresowej - przykład zastosowania strumieni danych.
  6. Zmieniamy właściwość "Caption" wszystkich komponentów - tak jak na obrazku (gdzie etykietaButton1to "Dodaj", dlaButton2to "Usuń", zaś dlaButton3- "Zakończ")

Trochę kodu

  1. Dwukrotnie klikamy na przycisk "Dodaj" (czyliButton1). Powoduje to wygenerowanie procedury odpowiadającej za zdarzenie - w tym wypadku kliknięcie.
  2. Kod procedury uzupełniamy następująco:

    (* Dodanie do komponentu 'lista' Pierwsza kolumna - Edit1.Text+' "'+Edit3.Text+'" '+Edit2.Text czyli imię + " + nick + " + nazwisko Druga kolumna - Edit4.Text czyli e-mail *) lista.InsertRow(Edit1.Text + ' "' + Edit3.Text + '" ' + Edit2.Text, Edit4.Text, True);
  3. Z powrotem przechodzimy do formularza (F12) i tym razem dwukrotnie klikamy przycisk "Usuń" (Button2).
  4. Uzupełniamy jego kod następująco:

      lista.DeleteRow(lista.Row);

    Co spowoduje usunięcie aktualnie zaznaczonego wiersza z danymi.
  5. Ponownie przechodzimy do formularza (F12) i dwukrotnie klikamy na przycisk "Zakończ"
  6. W nowo wygenerowanej procedurze piszemy bez większej filozofii:

      Close;
     
  7. Zaraz, zaraz - pozostała nam jeszcze najważniejsza część przedstawienia - zapis i odczyt. Napiszemy program w taki sposób, aby automatycznie przy zamykaniu programu zapisywał zawartość komponentu "lista", a przy uruchamianiu wczytywał jego zawartość.
  8. W tym celu przechodzimy do formularza (F12) i klikamy dwukrotnie na wolne miejsce na formie.
  9. Pojawia się edytor kodu wraz z automatycznie wygenerowaną procedurą "OnCreate". Instrukcje tej procedury będą wykonywane przy uruchamianiu programu. Piszemy więc w procedurze:

    var dane : TFileStream; begin // Jeśli plik nie istnieje to... If not FileExists('C:\baza.luk') Then // Zakończ wykonywanie tej procedury (nie mylić z wyjściem z programu) Exit; // Te instrukcje zostaną wykonane tylko wtedy gdy plik istnieje // Otwarcie strumienia do odczytu dane := TFileStream.Create('C:\baza.luk', fmOpenRead); // Odczytanie zawartości i ustawień komponentu lista.Strings.LoadFromStream(dane); // Zwolnienie strumienia dane.Free;
  10. OK. Teraz przydało by się zrobić procedurę, która by zapisywała dane do pliku przy zamykaniu programu. Można to zrobić w procedurze przycisku "Zakończ", ale tamte instrukcje wykonają się tylko wtedy, gdy użytkownik kliknie na przycisk aby wyjść z programu. A jak użytkownik zamknie program korzystając z krzyżyka? - całkowita klapa!
    Co więc zrobić aby wykonywać jakieś instrukcje przy zamykaniu samego programu? Należy skorzystać z "Inspektora Obiektów". W tym celu:
  11. Przechodzimy do formularza (F12). Zaznaczamy go - klikając nań raz (nie dwukrotnie!) lewym przyciskiem myszy. Przechodzimy do "Inspektora Obiektów", wybierając zakładkę "Events". Na tej zakładce znajdują się wszystkie zdarzenia obsługiwane przez zaznaczony komponent. Odszukujemy zdarzenie "OnClose" i klikamy dwukrotnie na białe pole obok. Zostaje wygenerowana procedura, której instrukcje będą wykonywane przy zamykaniu programu.
  12. Uzupełniamy ją następująco:
    var dane : TFileStream; begin // Utworzenie strumienia (jeśli plik istnieje to go czyści ;) dane := TFileStream.Create('C:\baza.luk', fmCreate); // Zapisz komponent lista.Strings.SaveToStream(dane); // Zwolnij strumień. dane.Free;
  13. Dotarłeś wreszcie do końca. Sprawdź czy program działa.

Screen książki adresowej - przykład zastosowania strumieni danych.
Ochrona danych osobowych, że się tak wyrażę ;)

Jak pewnie zauważyłeś nie stosujemy tu typowych procedurWriteComponent()iReadComponent(), ale procedury własne komponentu, które zapisują dane do strumienia. Zastosowanie tutaj np.ReadComponent()spowodowałoby błąd przy odczycie.
Trochę namieszałem, więc spróbuje to teraz odkręcić. Jeśli nie wiesz, kiedy masz stosować proceduryReadComponent()iWriteComponent(), a kiedy procedury zapisu strumieni należące do danego komponentu - wyjaśniam prostą regułą.

"Jeżeli dany komponent ma "swoje" procedury umożliwiające zapis lub odczyt (typuSaveToStream,LoadFromStream) to nie należy specjalnie kombinować tylko z nich korzystać, w przeciwnym przypadku korzystaj zWriteComponent()iReadComponent()".

Jeśli będziesz się trzymał tej prostej zasady, na pewno nie będziesz miał problemów. Takie kontrolki jakRichEdit,ValueListEditoritp. mają własne procedury zapisu, zarówno do strumieni, jak i "normalnego" pliku np. tekstowego. 

Zakończenie

Poznaliście właśnie strumienie typuTFileStream. Na razie oswójcie się z nowym kodem, potem będzie reszta. Większe programy, jakie pisaliśmy w tym artykule:

Pozostały nam jeszcze takie rzeczy, jak poleceniaSeek,CopyFromitd. Są to w miarę proste sprawy, ale również bardzo przydatne, więc nie mogę ich zostawić na pastwę losu. Postaram się o takie dopełnienie wiadomości w ostatniej części tego cyklu. A póki co - EEEDIS ;)

12345
Delphi - Strumienie - TFileStream Autor opinii: Czytelnicy, data przesłania: 0

Podobne artykuly:

Skomentuj

Aby zamieścić komentarz, proszę włączyć JavaScript - niestety roboty spamujące dają mi niezmiernie popalić.






Komentarze czytelników

    • big_zygi
    • pon, 30 sierpień 2010, 10:19
    • Wybacz nie czytałem do konca tekstu ale jak sam napisałeś odczytywanie strumieniowe zapisanego pliku jest "głupie".
      TAK. Bo jezeli nie wiesz to czytanie lub zapisywanie strumieniowe jednocześnie przesuwa położenie wskaźnika, więc aby błąd nie pojawial się w w twoim przykładzie należy wpisać dodatkowo jedną linijkę po zapisaniu do strumienia.
      strumien.Position:=0;
      i tera mozna odczytywać bez najmniejszego błędu.
    • jd61
    • nie, 12 kwiecień 2009, 13:47
    • OK, z wykorzystaniem strumienia do wczytania pliku tekstowego i wykorzystaniem tego odczytu - to jest jasne.Ale jak uruchomić odczytany ze strumienia dowolny plik z odpowiednim rozszerzeniem, do uruchomienia poprzez WINSHELL, bez tworzenia kopii tymczasowej na dysku. Chodzi mi o wykorzystanie możliwości zapisu dowolnego w bazie danych, odczycie poprzez stream i jego uruchomienie bezpośrednio ze zmiennej w programie?

Dexter