Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autorzy: 'Złośliwiec',
'NO NAME'
Biblioteki C++

Zasoby

[lekcja] Rozdział 20. Jak umieszczać dane różnego rodzaju (obrazy, dźwięki, teksty i inne) wewnątrz tworzonej aplikacji.

Zasoby

Zanim zabierzemy się za tworzenie zasobów, warto abyśmy wiedzieli, co to w ogóle jest. Zasoby aplikacji to dane znajdujące się w pliku wykonywalnym, które nie są jednak instrukcjami dla procesora. Mogą to być pliki, jak również łańcuchy znaków, czy nawet skrypty tworzenia menu, czy okien dialogowych.

Przed dodaniem pliku do programu należy się jeszcze zastanowić, czy jest to potrzebne. Równie dobrze można go przecież trzymać w folderze programu i stąd wczytywać. Umieszczanie czegoś w zasobach aplikacji ma kilka negatywnych skutków. Rozmiar pliku programu znacząco się zwiększa, co sprawia, że wolniej się on uruchamia i zajmuje więcej pamięci. W niektórych przypadkach warto jednak umieścić plik w zasobach:
  • gdy chcemy współdzielić zasoby np. między plikiem EXE i DLL
  • gdy chcemy, aby plik wykonywalny miał ikonę (widoczną np. w Eksploratorze)
  • gdy chcemy, aby program składał się wyłącznie z pliku wykonywalnego

Tworzenie pliku zasobów

Na początku musimy dodać do projektu specjalny plik o rozszerzeniu *.rc, w którym będzie znajdować się lista zasobów dodawanych do programu. W środowisku Dev-C++ należy w tym celu wybrać z menu Plik -> Nowy -> Plik zasobów. Jeśli używasz innego IDE bez możliwości edycji zasobów, możesz ściągnąć darmowy program ResEdit.

Kompilator zasobów, to nie ten sam program, który zajmuje się plikami *.cpp. Pliki *.rc są kompilowane oddzielnie do postaci plików *.res, a dopiero te są dołączane do reszty programu przez linker. Dawniej trzeba było ręcznie kompilować pliki *.rc, ale obecnie tą pracę wykonuje za nas środowisko.

W naszym projekcie możemy umieścić więcej plików zasobów, jednak po kompilacji otrzymamy tylko jeden plik *.res. Dzieje się tak dlatego, że nasz projekt zawiera jeszcze jeden, niewidoczny w menadżerze projektu plik *.rc, do którego pozostałe pliki są po prostu dołączane dyrektywą #include.

Dla każdego pliku zasobów warto utworzyć plik nagłówkowy (*.h), w którym będziemy przechowywać identyfikatory zasobów. Będzie o nich mowa za chwilę.

Dodawanie pliku do zasobów

Jeśli chcemy dołączyć do naszego programu jakiś plik, musimy dopisać do pliku zasobów następującą linijkę:

Składnia: nameID typeID filename

ArgumentZnaczenie
nameIDunikalny w skali aplikacji numer zasobu, który pozwoli nam się do niego dostać z poziomu kodu programu
typeIDliczba, określająca z jakim rodzajem zasobu mamy do czynienia
filenamełańcuch znaków z nazwą pliku, lub ścieżką do niego, jeśli nie znajduje się on w katalogu projektu

Poniżej znajduje się lista, zawierająca stałe najważniejszych typów zasobów:
NazwaWartośćZawartość
CURSOR1plik *.cur
BITMAP2plik *.bmp
ICON3plik *.ico
RCDATA10dowolny plik
MENU4skrypt menu
DIALOG5skrypt okna dialogowego
Teraz spróbujmy dołączyć sobie do naszego programu plik 'DarkCult.bmp', aby później móc wyświetlić logo ukochanej strony na honorowym miejscu w głównym oknie naszego programu ;-)

Dołączamy do projektu pliki 'logo.h' i 'logo.rc' (oczywiście nazwy mogą być dowolne). Następnie w pliku 'logo.h' definiujemy makro, które zastąpi nam liczbowy identyfikator zasobu:
C/C++
#define IDB_LOGO 1234
Pliku 'logo.rc' także nie zostawimy pustego. Dołączamy do niego plik nagłówkowy, żeby móc skorzystać z makra, a następnie piszemy instrukcje dołączenie do zasobów naszej bitmapy:
C/C++
#include "logo.h"

IDB_LOGO BITMAP "DarkCult.bmp"
Kompilator sam zamieni na odpowiednie liczby zarówno nasze makro IDB_LOGO, jak i stałą BITMAP. Gdybyśmy chcieli to napisać, używając wyłącznie liczb, byłby to rónież poprawne:
C/C++
1234 2 "DarkCult.bmp"

Zwykle nie musimy zawracać sobie głowy zapamiętywaniem wartości poszczególnych stałych. Wyjątek stanowi RCDATA, która w kompilatorze Dev-c++ musi być zapisana w formie liczby, gdyż inaczej pojawi się błąd kompilacji.

Dodawanie skryptu menu i okna dialogowego jest nieco trudniejsze. Omówiliśmy to już wcześniej w oddzielnych artykułach: » Kurs WinAPI, C++ » PodstawyMenu lekcja i » Kurs WinAPI, C++ » PodstawyOkna dialogowe, cz. 1 lekcja.
Dialogami nie będziemy już się zajmować w tej części kursu.

Użycie zasobów w programie

Tyle się namęczyliśmy, żeby dodać do zasobów naszą bitmapę (3 linijki kodu ;-)), a teraz nagle zdaliśmy sobie sprawę z tego, że nie umiemy jej wyświetlić. Nie ma się jednak czym martwić, ponieważ panowie z Microsoftu okazali nam łaskę i stworzyli wyjątkowo łatwe w użyciu funkcje:
Typ zasobuFunkcjaTyp zwracany
CURSORLoadCursorHCURSOR
BITMAPLoadBitmapHBITMAP
ICONLoadIconHICON
MENULoadMenuHMENU
Nie jest to trudne do zapamiętania :-) Wywołanie każdej z funkcji wygląda identycznie:

Składnia: LoadCośtam (hInstance, lpName)

ArgumentZnaczenie
hInstanceidentyfikator aplikacji, która zawiera zasób - w naszym przypadku pierwszy parametr funkcji WinMain.
lpNamemakro MAKEINTRESOURCE w identyfikatorem zasobu.

Spróbujmy więc wczytać sobie naszą bitmapkę. Pamiętajmy o dołączeniu pliku 'logo.h', abyśmy mogli skorzystać z makra IDB_LOGO:
C/C++
#include "logo.h"

HBITMAP hLogo = LoadBitmap( hInstance, MAKEINTRESOURCE( IDB_LOGO ) );

Zasoby typu RCDATA

Teraz omówimy metodę, która umożliwi nam odczytanie zawartości zasobów dowolnych typów, czyli także RCDATA. Będą nam potrzebne dwie funkcje:

Funkcja FindResource pozwala ona na uzyskanie uchwytu do zasobu:

Składnia: FindResource (hModule, lpName, lpType)

ArgumentZnaczenie
hModuleidentyfikator aplikacji, która zawiera zasób - w naszym przypadku pierwszy parametr funkcji WinMain.
lpNamemakro MAKEINTRESOURCE w identyfikatorem zasobu.
lpTypeliczba określająca typ zasobu.

Jak widzimy, bardzo przypomina ona funkcje z poprzedniego akapitu. Jedyne, co pojawiło się nowego, to ostatni parametr. Jest to ta sama liczba, którą umieściliśmy na drugim miejscu w pliku *.rc. Tutaj również mamy  do dyspozycji makra, które zwolnią nas z obowiązku zapamiętywania gołych wartości. Niestety, żeby nie było tak łatwo, mają one inne nazwy:
Plik *.rcPlik *.cpp
CURSORRT_CURSOR
BITMAPRT_BITMAP
ICONRT_ICON
MENURT_MENU
Jak widać, wystarczy że dodamy przedrostek RT_.

Funkcja FindResource zwraca nam liczbę typu HRSRC, czyli uchwyt do zasobu. Będziemy go później wykorzystywać do dalszych operacji na zasobie. Jeśli funkcja zwróci nam NULL, to znaczy, że jej działanie zakończyło się niepowodzeniem.

Funkcja LoadResource jest dla najważniejsza, ponieważ zwraca wskaźnik do miejsca w pamięci, w którym znajduje się dany zasób:

Składnia: LoadResource (hModule, hResInfo)

ArgumentZnaczenie
hModuleidentyfikator aplikacji, która zawiera zasób - wiemy już o co chodzi
hResInfouchwyt, który zwróciła nam funkcja FindResource

Adres pamięci, który otrzymamy, zawiera treść pliku, który dołączyliśmy do zasobów. Spróbujmy uzyskać wskaźnik do naszego 'DarkCult.bmp':
C/C++
#include "logo.h"

HRSRC hLogo = FindResource( hInstance, MAKEINTRESOURCE( IDB_LOGO ), RT_BITMAP );
if( hLogo != NULL )
{
    HGLOBAL pLogo = LoadResource( hInstance, hLogo );
}

Zapisywanie zasobu do pliku

Niestety czasem plik znajdujący się wyłącznie w pamięci komputera jest dla nas nieprzydatny. Przykładowo jest to plik wykonywalny (*.exe). Najlepiej unikać takich sytuacji i nie dołączać do zasobów tego typu plików. W razie potrzeby, można jednak łatwo zapisać zasób do pliku.

Potrzebna nam do tego funkcja SizeofResource, która zwraca nam ilość pamięci, zajmowanej przez zasób:

Składnia: SizeofResource (hModule, hResInfo)

ArgumentZnaczenie
hModuleidentyfikator aplikacji, która zawiera zasób - wiemy już o co chodzi
hResInfouchwyt, który zwróciła nam funkcja FindResource
Skoro mamy jego adres w pamięci, oraz długość pliku, możemy załatwić sprawę jednym wywołaniem funkcji WriteFile. Zapiszemy teraz nasz obrazek 'DarkCult.bmp', jako 'Logo.bmp':
C/C++
#include "logo.h"

HRSRC hLogo = FindResource( hInstance, MAKEINTRESOURCE( IDB_LOGO ), RT_BITMAP );
if( hLogo != NULL )
{
    HGLOBAL pLogo = LoadResource( hInstance, hLogo );
    DWORD dwDlugosc = SizeofResource( hInstance, hLogo );
   
    HANDLE hPlik = CreateFile( "Logo.bmp", GENERIC_WRITE, NULL, NULL, CREATE_NEW,
    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL );
   
    DWORD dwBajtyZapisane;
    if( !WriteFile( hPlik, pLogo, dwDlugosc, dwBajtyZapisane, NULL ) )
         return 0;
   
    if( dwBajtyZapisane != dwDlugosc )
         return 0;
   
    CloseHandle( hPlik );
} else
     return 0;

Wydaje się to na pierwszy rzut oka skomplikowane, ale jest bardzo proste, jak na Windows. Użyliśmy tu kilku funkcji, o których można przeczytać w artykule » Kurs WinAPI, C++ » PodstawyPliki lekcja.

Teraz wiemy już chyba wystarczająco dużo, aby posługiwać się podstawowymi typami zasobów.

Informacje o wersji programu

W plikach .rc można też dodać informacje o wersji programu. Generalnie łatwiej tu pokazać przykład, niż wytłumaczyć wszystkie szczegóły:
C/C++
1 VERSIONINFO
FILEVERSION 2, 0, 0, 0 // wersja pliku
PRODUCTVERSION 2, 0, 0, 0 // wersja produktu
FILETYPE VFT_APP
{
    BLOCK "StringFileInfo"
    {
        BLOCK "041504E4"
        {
            VALUE "CompanyName", "Dark Cult" // nazwa firmy
            VALUE "FileVersion", "2.0" // wersja pliku
            VALUE "FileDescription", "Edytor tekstowy" // opis programu
            VALUE "InternalName", "DarkCult.pl" // wewnętrzna nazwa pliku
            VALUE "LegalCopyright", "(C) Copyright by Kowalski" // prawa autorskie
            VALUE "LegalTrademarks", "" // prawne znaki towarowe
            VALUE "OriginalFilename", "DarkCult.exe" // oryginalna nazwa pliku
            VALUE "ProductName", "DarkCult" // nazwa pliku
            VALUE "ProductVersion", "2.0" // wersja produktu
        }
    }
    BLOCK "VarFileInfo"
    {
        VALUE "Translation", 0x0415, 1252
    }
}
Tak to mniej więcej powinno wyglądać, by działało (tj. aby informacje o wersji były widoczne, gdy oglądamy właściwości pliku EXE lub DLL). Warto jeszcze dodać, że wartość 0x0415 na końcu oznacza identyfikator języka polskiego, 1252 to nasza strona kodowa, zaś tajemnicze 041504E4 to po prostu te dwie liczby złożone w jedną, 32-bitową, szesnastkową liczbę.

Jeśli chcemy wyświetlać gdzieś w programie te informacje, najprościej zdefiniować poszczególne elementy za pomocą #define w pliku *.h, include'ować ten plik wewnątrz pliku zasobów i używać tych definicji zamiast bezpośrednio wartości, tzn.:
C/C++
// to idzie do pliku Version.h (nazwa dowolna)
// ...
#define PRODUCT_NAME "DarkCult"
// ...

C/C++
// to jest już plik *.rc
// ...

#include "Version.h"

// ...

VALUE "ProductName", PRODUCT_NAME
Poprzedni dokument Następny dokument
Łamacze komunikatów Zaawansowane