Delphi - Strumienie - TResourceStream
Wed, 8 September 2004
#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.
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:
- Za pomocą programu "Kreator Zasobów" dołączonego do artykułu "Delphi - Zasoby" tworzymy zasób, który zawiera w sobie obraz typu JPEG.
- Otwieramy Delphi'ego i przenosimy się do Edytora Kodu (F12).
- 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}
- Po skompilowaniu programu, w EXE'cu naszego programu znajdzie się stworzony zasób.
- 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;
- 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:
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 ;)





Podobne artykuly:
- Delphi - Zasoby
- Delphi - Strumienie - Wstęp
- Delphi - Strumienie - TFileStream
- Delphi - Strumienie - TStringStream
- Delphi - Strumienie - TMemoryStream
- Delphi - Strumienie - Informacje dodatkowe
- KillAd
Skomentuj
Komentarze czytelników
-
- delphi_programm
- Mon, 26 May 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ść.