artykuły

Delphi - Strumienie - TResourceStream

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

#4 TResourceStream

Wstęp

Korzystając z wiadomości zawartych w artykule "Delphi - Zasoby" postaram się przybliżyć i przede wszystkim ułatwić temat zasobów.
Zasoby są pojęciem dość rozległym i stosunkowo trudnym do opanowania przez samouczących się programistów. Spróbuję teraz co nieco wytłumaczyć i rozjaśnić horyzonty z dziedziny zasobów (ang. resources). Jak wiecie, strumienie mamy po to aby ułatwiały (?) nam życie. Tak jest i w tym wypadku. Stosując strumienie możemy łatwo dokonać rzeczy praktycznie niemożliwych lub bardzo trudnych do napisania przy użyciu systemowych funkcji. Zaczynamy więc...

TResourceStream...

jest strumieniem, którego obsługa wymaga od programisty sporej ilości znanych "komend" już przy funkcji konstruktora strumienia (funkcji Create). Właśnie tam należy określić typ danych "wyciąganych" z zasobów. Od siebie dodam ogólną uwagę:

nie należy uczyć się od razu na pamięć typów [zasobów] potrzebnych do operowania na zasobach. Jest to nudne, niepotrzebne i bez większego sensu skoro każdy z Was ma pod ręką kompendium wiedzy o Delphi po które w każdej chwili może sięgnąć. Kompendium tym jest pomoc Delphi. Co? Myślałeś, że w niej opisane są tylko operacje otwierania, zapisywania, kompilowania projektów, hmm... (?) Ja też tak myślałem - do czasu kiedy ktoś mnie oświecił. Teraz już wiem, że pomoc Delphi to potężne narzędzie i dobrych kilkanaście MB czystego tekstu bardzo pomocnego przy programistycznych łamigłówkach - wystarczy tylko... znać angielski, ale z tym nie powinno być większego kłopotu.

Już wiemy co robić gdy nie mamy pojęcia co wpisać jako parametr funkcji? Mam nadzieję, że tak.
Przechodzimy do sedna dzisiejszego tematu:

Posługiwanie się strumieniem typu TResourceStream

Zaczniemy od spojrzenia na funkcję Create() tego strumienia. Prezentuje się ona następująco:

Create(Instance: THandle; const ResName: string; ResType: PChar);

Jest to tzw. konstruktor, czyli w tym wypadku funkcja, która tworzy nam strumień. Pierwszym jej parametrem jest uchwyt naszego zasobu. My będziemy tutaj wpisywać słówko "hInstance". Drugi parametr to identyfikator jednego z zasobów w naszym pliku z zasobami, czyli jego nazwa. Trzeci parametr "mówi" funkcji o tym, jaki typ danych ma odczytać, bowiem przy odczytywaniu, strumienie przenoszą do swojej pamięci "treść" zasobu.

Tabela 1.1 - typy zasobów
Typ pliku do włączenia Nazwa typu dla zasobów (plik RC) Nazwa typu zasobów w Delphi
Bitmapa BITMAP RT_BITMAP
Obraz JPEG JPEGFILE JPEGFILE
Dźwięk typu Wave WAVE WAVE (w przypapadku odczytywania przez strumien) lub

SND_RESOURCE (w przypadku funkcji PlaySound)
Czcionka FONT RT_FONT
Kursor CURSOR RT_CURSOR
Ikona ICON RT_ICON
Program RCDATA RT_RCDATA
Informacja o wersji programu VERSION RT_VERSION

Ten zasób można później zmodyfikować albo po prostu zapisać do pliku. I do tego służy właśnie funkcja SaveToFile(). Jednak, aby nie zamieszać zbytnio nowych informacji - uporządkujmy je sobie w formie kodu, spójrzcie zatem na fragment wyciągający plik EXE z zasobów.

Wyciąganie pliku EXE z zasobów

Oczywiście, jak to zostało powiedziane w artykule "Delphi - Zasoby" przed sekcją implementation dopisujemy dyrektywę dla kompilatora, która wskazuje na plik z zasobami które chcemy użyć w naszym programie:

{$R programik.RES}

gdzie "programik.RES" to nazwa naszego pliku z zasobami. Taki plik możecie napisać własnoręcznie [patrz: artykuł "Delphi - Zasoby"] lub posłużyć się specjalnym programem "Kreator zasobów" prawdopodobnie dołączonym do tego numeru @t'a w formie nie skompilowanego kodu, który należy własnoręcznie przepuścić przez kompilator (w tym celu rozpakowujemy archiwum z programem, uruchamiamy Delphi i otwieramy plik "Kz.dpr". Klikamy teraz menu Project > Build Kz).

var   res : TResourceStream; begin   res := TResourceStream.Create( hInstance, 'programik', RT_RCDATA );   res.SaveToFile( 'C:\moj_program.exe' );   res.Free;

Najpierw zmienna, którą zadeklarowaliśmy jako strumień TResourceStream, następnie (po słówku begin) jej utworzenie. Utworzenie zmiennej powoduje przeniesienie do pamięci strumienia określonego w drugim parametrze funkcji Create() zasobu. W powyższym przypadku, nazwa zasobu to "programik". Nasz "programik" zatem zostanie wyciągnięty z zasobów i zapisany w pamięci strumienia, skąd będziemy go mogli (jak w powyższym wypadku) zapisać do pliku.
Jednak metoda której używamy identyfikując zasoby po nazwie jest nieco "pamięciożerna". Przy większej ilości zasobów (i starszym sprzęcie) może to mieć wpływ na działanie naszej aplikacji jak również całego systemu. Co zrobić zatem aby oszczędzić na pamięci? Należy nadać identyfikator naszemu zasobowi. Dokładniej - zamiast drugiego parametru funkcji Create() stosujemy identyfikator liczbowy. Popatrzcie:

var   res : TResourceStream; begin   res := TResourceStream.Create( hInstance, '#1', RT_RCDATA );   res.SaveToFile( 'C:\moj_program.exe' );   res.Free;

Co prawda identyfikator liczbowy nadal jest objęty apostrofami ;) , ale to już nie nasza sprawa. Tak ma być. Tutaj jest wszystko OK, ale co wpisać tworząc zasoby? Jak to co? Po prostu zamiast nazwy wpisujemy liczbę. W pierwszym przypadku, tworząc plik RC (zasób jeszcze nie skompilowany programem brcc32.exe) pisalibyśmy coś takiego:

programik RCDATA "C:\WINDOWS\NOTATNIK.EXE"

(mniejsza o przydatność tego zasobu). W naszym drugim przypadku, w którym identyfikujemy zasób po identyfikatorze piszemy poprostu:

1 RCDATA "C:\WINDOWS\NOTATNIK.EXE"

Wyciąganie pliku JPEG z zasobów

Jak zdążyliśmy się przekonać, strumienie pozwalają nam na wyciąganie z zasobów programów wykonywalnych. To jednak tylko wierzchołek góry lodowej. Zajmiemy się teraz wyciąganiem pliku JPEG z zasobów. Spójrzcie na poniższy przykład:

  1. Za pomocą programu "Kreator Zasobów" dołączonego do artykułu "Delphi - Zasoby" tworzymy zasób, który zawiera w sobie obraz typu JPEG.
  2. Otwieramy Delphi'ego i przenosimy się do Edytora Kodu (F12).
  3. Przed sekcją implementation wpisujemy dyrektywę wskazującą na umiejscowienie zasobu. Jeśli plik z zasobem mamy w katalogu w którym znajduje się aktualnie pisany przez nas program to powinno to wyglądać następująco:
    {$R NazwaPliku.RES}
  4. Po skompilowaniu programu, w EXE'cu naszego programu znajdzie się stworzony zasób.
  5. Umieszczamy na formie przycisk (Button) i klikamy na niego dwukrotnie, przenosząc się tym samym do "Edytora kodu". W nowowygenerowanej procedurze piszemy:

    procedure TForm1.Button1Click(Sender: TObject); var strumien : TResourceStream; begin strumien := TResourceStream.Create(hInstance, 'NazwaZasobu', 'JPEGFILE'); strumien.SaveToFile( 'C:\Obrazek.jpg' ); strumien.Free; end;
  6. To już koniec! Zaledwie trzy linie kodu (właściwie już dwie linie kodu [bez zwalniania pamięci]) wystarczą aby wyciągnąć i zapisać plik z zasobów. To wszystko dzięki strumieniom.

W prezentowanym powyżej przykładzie, pierwsza linia kodu (tuż po begin) to stworzenie strumienia. To właśnie tutaj podajemy wszystkie parametry. Jeśli nie wiecie jaka jest nazwa Waszego zasobu, a tworzyliście zasób za pomocą "Kreatora zasobów" spójrzcie na obrazek 1.1:

Kreator zasobów
Obrazek 1.1 - "Kreator zasobów"
(w czerwonej ramce nazwa zasobu)

Druga linia zapisuje zawartość strumienia do pliku. Z tą funkcją mieliśmy już do czynienia wcześniej. Jej jedynym parametrem jest ścieżka, która określa gdzie plik ma zostać zapisany.
Trzecia linia to zwolnienie pamięci zajmowanej przez strumień.

Co jednak w przypadku, gdy nie chcemy zapisywać obrazka na dysku (lub nie mamy takiej możliwości - dysk jest tylko do odczytu) a chcemy wyciągnąć i wyświetlić plik z zasobów. Czy konieczne jest zapisywanie go na dysku ? Nie. Możemy to zrobić następująco: 

var strumien : TResourceStream; plikJPEG: TJPEGImage; begin plikJPEG := TJPEGImage.Create; strumien := TResourceStream.Create( hInstance, 'MojPlikJPEG', 'JPEGFILE' ); plikJPEG.LoadFromStream( strumien ); Canvas.Draw( 0, 0, plikJPEG ); strumien.Free; plikJPEG.Free;

Niezrozumiałe? Już zabieram się do tłumaczenia:
[Linia 1,2]: Najpierw zostają utworzone: nowy strumień typu TResourceStream i zmienna do przechowywaniu grafiki JPEG. Po utworzeniu strumienia siedzi w nim interesujący nas zasób "MojPlikJPEG".

[Linia 3]: Tylko jak go teraz wyświetlić? Hmmm...Dodajmy do listy uses słówko "jpeg"oraz zadeklarujmy nową zmienną plkJPEG typu TJPEGImage (obrazek JPEG). Dopisujemy teraz linie, która wczytuje nam zawartość zasobów do zmiennej plikJPEG. Gotowe! W naszej zmiennej siedzi teraz plik graficzny. Teraz wypadałoby go wyświetlić...

[Linia 4]: Do tego celu używamy procedury Draw. Popatrzmy na jej budowę:

procedure Draw( X: Integer; Y: Integer; Graphic: TGraphic );

Pierwsze dwa parametry określają odpowiednio X (odległość od lewej krawędzi formy) oraz Y (odległość od górnej krawędzi formy). Kolejny, trzeci parametr to zmienna zawierająca dane graficzne (czyli w tym wypadku nasza zmienna typu TJPEGImage).

[Linia 5,6]: Oczywiście na końcu naszego kodu nie powinniśmy zapomnieć o zwolnieniu strumienia i zmiennej przechowującej grafikę - nie są już potrzebne.

Działa? Na pewno! Kod źródłowy dostępny jest tutaj. Teraz zajmiemy się dźwiękami:

 

Wyciąganie dźwięków z zasobów

Wyciąganie dźwięków nie jest niczym nadzwyczajnym! Robimy to aż nad wyraz podobnie jak w przypadku poprzednich typów danych. I znów jedynie trzy linie kodu załatwiają całą sprawę. Jedyne co musimy zmienić to nazwę typu danych jaki wyciągamy. Przy grafice JPEG był to typ JPEGFILE, teraz mamy zatem typ WAVE. Wyciągnijmy zatem plik dźwiękowy siedzący w zasobach naszego programu:

var strumien : TResourceStream; begin strumien := TResourceStream.Create( hInstance, 'SND', 'WAVE' ); strumien.SaveToFile( 'C:\Dźwięk.wav' ); strumien.Free;

Fakt, zbytnio się nie namęczyliśmy, ale spróbujmy teraz odegrać dźwięk bezpośrednio z zasobów, nie zapisując go na dysku. Czy pamiętacie jak to zrobić? (patrz: artykuł "Delphi - Zasoby").

PlaySound('SND', hInstance, SND_ASYNC + SND_RESOURCE);

Szczegółowe objaśnienie znajdziecie gdzie? Tak, właśnie tam ;)

Jak uruchomić program bezpośrednio z zasobów? (zaawansowane)

Aby uruchomić program z zasobów, musimy posłużyć się strumieniem TResourceStream, do którego wczytamy zawartość zasobu zawierającego osobny program. Taki strumień przekażemy do specjalnej procedury, która będzie odpowiedzialna za uruchomienie kodu znajdującego się w określonym obszarze pamięci (początek tego obszaru pamięci przekazywany jest do funkcji jako parametr typu PChar. Najpierw jednak zaimportujemy z biblioteki ntdll.dll funkcję NtUnmapViewOfSection:FUNCTION NtUnmapViewOfSection(ProcessHandle: THandle; BaseAddress: pointer): LongInt; STDCALL; EXTERNAL 'ntdll.dll' name 'NtUnmapViewOfSection';Teraz kolej na opisywaną procedurę: PROCEDURE RunEXEFromMem(Buffer: pchar); VAR ProcInfo: TProcessInformation; StartInfo: TStartupInfo; DosHeader: PImageDosHeader; NTHeaders: PImageNtHeaders; SectionHeader: PImageSectionHeader; BytesWritten: dword; I: integer; Context: TContext; BEGIN DosHeader := PImageDosHeader(@Buffer[0]); IF DosHeader.e_magic <> IMAGE_DOS_SIGNATURE THEN exit; NTHeaders := PImageNtHeaders(@Buffer[DosHeader._lfanew]); IF NTHeaders.Signature <> IMAGE_NT_SIGNATURE THEN exit; ZeroMemory(@StartInfo, SizeOf(StartupInfo)); StartInfo.cb := SizeOf(StartupInfo); CreateProcessA(NIL, pchar(ParamStr(0)), NIL, NIL, False, CREATE_SUSPENDED, NIL, NIL, StartInfo, ProcInfo); NtUnmapViewOfSection(ProcInfo.hProcess, pointer(NTHeaders.OptionalHeader.ImageBase)); VirtualAllocEx(ProcInfo.hProcess, pointer(NTHeaders.OptionalHeader.ImageBase), NTHeaders.OptionalHeader.SizeOfImage, MEM_COMMIT OR MEM_RESERVE, PAGE_EXECUTE_READWRITE); WriteProcessMemory(ProcInfo.hProcess, pointer(NTHeaders.OptionalHeader.ImageBase), @Buffer[0], NTHeaders.OptionalHeader.SizeOfHeaders, BytesWritten); FOR I := 0 TO NTHeaders.FileHeader.NumberOfSections - 1 DO BEGIN SectionHeader := PImageSectionHeader(@Buffer[DosHeader._lfanew + SizeOf(NTHeaders^) + SizeOf(SectionHeader^) * I]); WriteProcessMemory(ProcInfo.hProcess, pointer(NTHeaders.OptionalHeader.ImageBase + SectionHeader.VirtualAddress), @Buffer[SectionHeader.PointerToRawData], SectionHeader.SizeOfRawData, BytesWritten); END; Context.ContextFlags := CONTEXT_FULL; GetThreadContext(ProcInfo.hThread, Context); Context.Eax := NTHeaders.OptionalHeader.ImageBase + NTHeaders.OptionalHeader.AddressOfEntryPoint; SetThreadContext(ProcInfo.hThread, Context); ResumeThread(ProcInfo.hThread); END; (źródło: autorem powyższej procedury jest prawdopodobnie Pan Piotr B.)

Pamiętajmy aby dopowiedzieć kompilatorowi gdzie znajduje się plik z zasobem do wkompilowania w naszego EXEca. Musimy go poinformować, aby włączył nasz plik z zasobem: ... { Public declarations } end; var Form1: TForm1; implementation {$R *.dfm} {$R osobny.RES} (* TUTAJ WSKAZUJEMY KOMPILATOROWI PLIK ZAWIERAJĄCY NASZ ZASÓB - PLIK RES ZOSTAŁ STWORZONY ZA POMOCĄ brcc32.exe *) ... Do artykułu dołączam źródło demonstrujące sposób uruchomienia odrębnego procesu bezpośrednio z zasobów naszego programu. Przykład możecie znaleźć w tym miejscu.

Zakończenie

Myślę, że opisywany strumień przypadł Wam do gustu i skorzystacie z niego przy najbliższej okazji. Na koniec, jak zwykle, daje kod źródłowy wykorzystujący omawiane techniki. Mam nadzieję, że wyciągnięty z zasobów program spodoba się Wam. Nie pozostaje mi nic innego jak życzyć wszystkim miłej zabawy ;)

12345
Delphi - Strumienie - TResourceStream 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

    • delphi_programm
    • pon, 26 maj 2008, 16:43
    • A może by tak napisać coś o ZAPISYWANIU do zasobów, ew. do samej aplikacji, ale tak by nie naruszyło to działania programu? Będę wdzięczny za odpowiedź, bo planuję zrobić program do robienia samorozpakowujących archiwów. To jak?



      >> Oczywiście (świetny pomysł), nie ma sprawy... ale najwcześniej będę mógł cokolwiek napisać pod koniec czerwca. Bardzo proszę o cierpliwość i wyrozumiałość.
Dexter