These classes support read and writing 32 bit binary resource files. Some routines are also included that assist in manipulating resource identifiers.
The code encapsulates the high-level structure of a resource file and its resources. It deals only with raw resource data - the actual format of the raw data depends on the resource type. This code does not provide any support for specific resource types or their data formats.
This document is divided into various sections:
The classes included in this release are as follows:
Class | Description |
---|---|
TPJResourceFile | Encapsulates a 32 bit binary resource file and exposes the entries within it. The class allows resources within the file to be accessed, created, read and searched for. |
TPJResourceEntry | Encapsulates a single resource within a resource file. Permits access to the resource type, name and language. Other header data can be modified. Provides read/write access to the raw resource data through a TStream. |
EPJResourceFile | Class of exception raised by the above classes to report errors. |
Although we use the word "file" in these notes, this term also covers binary resource data stored in a stream.
Detailed descriptions of the classes now follow.
This class is used to encapsulate a 32 bit resource file, to find which resources in contains and to add new and delete existing resources. The number of resources in the file is given by the EntryCount property while the Entries[] property provides access to them. Resource entries are not created directly by the user but via the AddEntry() method of this class.
Resource entries accessed via this class are TPJResourceEntry objects and have methods and properties that can be used to interogate and update them.
constructor Create;
Class constructor. Creates a new empty resource file object.
destructor Destroy;
Class destructor. Usually called via the Free method. Destroying the object frees all the resource entries currently in the file.
procedure Clear;
Clears all resources from the file object. The resources are freed.
function DeleteEntry(const Entry: TPJResourceEntry): Boolean;
Deletes a resource entry from the resource file object if it exists. The entry object is not freed and must be freed by the user since it will no longer be freed by the Clear or Destroy methods. The recommended way to delete and free a resource entry is to free the entry object since this automatically unlinks it from the resource.
function IndexOfEntry(const Entry: TPJResourceEntry): Integer;
Gets the index number of the given resource entry in the resource file's Entries[] property.
procedure LoadFromFile(const FileName: TFileName);
Loads a resource file from the named file, replacing any existing resource.
procedure LoadFromStream(const Stm: TStream);
Loads a resource "file" from the current location in the given stream.
procedure SaveToFile(const FileName: TFileName);
Saves data as a 32 bit resource file.
procedure SaveToStream(const Stm: TStream);
Saves data in resource file format on the given stream at the current location.
class function IsValidResourceStream(const Stm: TStream): Boolean;
Checks if the given stream contains data representing a valid 32 bit resource file starting at the current location. This method checks that a 32 bit resource file header is present but does not validate the whole of the file. Note that the stream is not rewound to the starting position after the check is made.
function AddEntry(const ResType, ResName: PChar; const LangID: Word = 0): TPJResourceEntry;
Adds a new, empty, resource to the current "file" object.
function AddEntry(const Entry: TPJResourceEntry; const ResName: PChar; const LangID: Word = 0): TPJResourceEntry;
Adds a copy of the given resource entry to the current "file" object with a new resource name and language id. The new entry has the same resource type as the one being copied.
function FindEntry(const ResType, ResName: PChar; const LangID: WORD = $FFFF): TPJResourceEntry;
Finds a resource entry with the given type, name and language id. The search can ignore the resource name and or language id in which case first entry that matches the provided information is found.
function FindEntryIndex(const ResType, ResName: PChar; const LangID: WORD = $FFFF): Integer;
Finds the Entries[] property index of a resource entry with the given type, name and language id. The search can ignore resource name or language id in which case first entry that matches the provided information is found.
function EntryExists(const ResType, ResName: PChar; const LangID: WORD = $FFFF): Boolean;
Checks whether a resource entry matching given search criteria exists.
property EntryCount: Integer;
Read-only property that provides the number of resources in the resource file (i.e. the number of entries in the Entries[] property).
property Entries[Idx: Integer]: TPJResourceEntry;
Read-only property that provides access to all the resources in the resource file by index.
Encapsulates a resource within a resource file. Object of this class must not be directly instantiated. TPJResourceFile automatically creates the required objects when they are read from a file or can create new instances in its AddEntry methods. TPJResourceFile is actually an abstract class that provides the required interface for manipulating resource entries. The actual concrete class that implements the resource entry is private. Therefore all valid resource objects are accessed via TPJResourceFile.
TPJResourceEntry objects are used to manipulate and interogate resource entries. This is done mainly via properties which give access to resource header information, allow some header information to be set and give read/write access to the raw resource data via a TStream.
Care should be taken not to use a resource entry after the resource file object has been cleared or destroyed or a new resource file has been read since all these actions free previous resource entries. A TPJResourceEntry object can be freed directly - doing so removes the entry from any resource file object it belongs to.
function IsMatching(const ResType, ResName: PChar; const LangID: Word = $FFFF): Boolean;
Checks if the resource entry has the given type, name and
language id. The resource name and/or language id can be ignored in which case
only the values provided will be matched. For example to match only a resource
type use IsMatching(MyResType, nil)
;
function IsMatching(const Entry: TPJResourceEntry): Boolean;
Checks if the resource entry has the same type, name and language ID as the given resource entry.
property DataSize: DWORD;
Read-only. Provides the size of the resource data (same as calling Data.Size).
property HeaderSize: DWORD;
Read-only. Provides the size of the resource header (which varies depending on the type and size of the resource type and name).
property DataVersion: DWORD;
Gets or sets the predefined data resource version information.
property MemoryFlags: Word;
Gets or sets the attribute bitmask that specifies the state of resource.
property LanguageID: Word;
Read-only. Gets the language used by the resource (value of 0 is language-neutral).
property Version: DWORD;
Gets or sets the user specified version number for resource data.
property Characteristics: DWORD;
Gets or sets the user specified characteristics of the resource.
property ResName: PChar;
Read-only. Gets the name of the resource. This value is either a pointer to a zero-terminated string or an ordinal value as returned from MakeIntResource().
property ResType: PChar;
Read-only. Gets the type of the resource. This value is either a pointer to a zero-terminated string or an ordinal value as returned from MakeIntResource().
property Data: TStream;
Read-only. Gets a reference to the stream that contains the resource's raw data. The stream can be used to read or write the data. Any padding bytes that follow the resource's data are not included in the stream.
This is the class of exceptions that are raised directly by TPJResourceFile and TPJResourceEntry. Note that some methods may raise exceptions of other classes.
The class defines no new methods or properties.
The helper routines provided with this code can be used to assist in manipulating resource identifiers. They can be useful for working with Windows API routines as well as the classes presented here. Here are the routines.
function IsIntResource(const ResID: PChar): Boolean;
A clone of the IS_INTRESOURCE macro defined on
MSDN that checks if a resource identifier is ordinal or a string. Complements
the MakeIntResource "macro" defined in Windows.pas
.
function IsEqualResID(const R1, R2: PChar): Boolean;
Checks for equality of two resource identifiers. To be equal the identifiers either be ordinal and have the same value or must both point to strings that have the same text when case is ignored.
function ResIDToStr(const ResID: PChar): string;
Converts a resource identifier into its string representation as defined on MSDN.
Some constants are defined to assist in setting some of the class properties.
The following flags are used to form the bitmask in a resource entry's MemoryFlags property. The first four constants in the table can or ORd together to form the bitmask. The final three constants are complements of the first three and are ANDed againgst the bitmask to remove their complement from the bitmask.
Constant | Value | Description |
---|---|---|
RES_MF_MOVEABLE |
$0010 |
The system can move the resource in memory. If this flag is not present the resource is fixed in memory. |
RES_MF_PURE |
$0020 |
The resource contains DWORD aligned data so that padding is not required. If this flag is not present the resource is not DWORD aligned and must be padded. |
RES_MF_PRELOAD |
$0040 |
The resource is to be loaded in memory just after the application itself has been loaded. If not present then loading of the resource may be deferred until required by the application. |
RES_MF_DISCARDABLE |
$1000 |
If set then on low memory conditions, the resource can be removed from memory and then reloaded when the application needs it, otherwise the resource must remain in memory. |
RES_MF_FIXED |
$FFEF |
Complement of RES_MF_MOVEABLE : used
to remove this flag from the bitmask. |
RES_MF_IMPURE |
$FFDF |
Complement of RES_MF_IMPURE : used
to remove this flag from the bitmask. |
RES_MF_LOADONCALL |
$FFBF |
Complement of RES_MF_LOADONCALL :
used to remove this flag from the bitmask. |
Note that Windows NT ignores RES_MF_MOVEABLE, RES_MF_IMPURE and RES_MF_PRELOAD.
Delphi's Windows unit defines all the predefined resources types known at the
time of writing except RT_HTML
and RT_MANIFEST
. Therefore
these two type identifiers are defined in this unit for convenience:
RT_HTML = MakeIntResource(23);
RT_MANIFEST = MakeIntResource(24);
See Appendix 1 for a description of all the predefined resource types.
In this first example we demonstrate how to create a resource file object and how to load a file into it. The following code fragment shows how this is done.
var ResFile: TPJResourceFile; ... begin ResFile := TPJResourceFile.Create; try ResFile.LoadFromFile('MyResFile.res'); ... // Do something with resource file object ... finally ResFile.Free; end; end;
First we create a TPJResourceFile object and then use its LoadFromFile method read a file from disk. We then process the file in some way and once we are finished we free the resource file object. That's all there is to it. Note that if the given file does not contain a valid 32 bit resource file an exception will be raised.
We can also read resource data from a stream rather than loading from file by using the LoadFromStream method of TPJResourceFile in place of LoadFromFile.
In our next example we show how to scan through all the resources in a file and how to list some information about each one. The following example assumes we have created a resource files object ResFile and have loaded a resource file into it (as in example 1). We also assume that the form contains a memo named Memo1. Here is the code:
var ResFile: TPJResourceFile; ResEntry: TPJResourceEntry; EntryIdx: Integer; begin ... // Assume ResFile contains a loaded resource file ... Memo1.Clear; for EntryIdx := 0 to Pred(ResFile.EntryCount) do begin ResEntry := ResFile.Entries[EntryIdx]; Memo1.Lines.Add( Format( 'Type: "%s" Name: "%s" LanguageID: %0.4X', [ResIDToStr(ResEntry.ResType), ResIDToStr(ResEntry.ResName), ResEntry.LanguageID] ) ); end; ... // Don't forget to free ResFile at some stage. end;
This code uses both TPJResourceFile and TPJResourceEntry objects. The resource file's EntryCount property tells us how many resources there are in the file. Each resource is represented by a TPJResourceEntry object made available from the integer indexed Entries[] array property. So we loop through all the valid indexes in Entries[] and store a reference to each resource entry in turn. Having got the resource entry object we now access its ResType, ResName and LanguageID properties to get the information we want to display.
The details of each entry are formatted by Delphi's Format function and added to Memo1. Note that we use the ResIDToStr helper function to get a string representation of the resource type and name. We display the language ID as a four digit hex number since the value is a Word.
You may have noticed that we have not freed any of the resource entry objects. This is not necessary since they are all freed automatically when the resource files object is freed (as shown in example 1).
To find a resource we use either the FindEntry or FindEntryIndex methods. The difference is that FindEntry returns the TPJResourceEntry object for the entry (or nil if not found) while FindEntryIndex returns the index of the entry in the Entries[] property.
Let's assume a resource file is loaded into the TPJResourceFile variable
ResFile. We want to find a RT_HTML
resource named INDEX_HTML
.
The following code checks if such a resource exists and displays its data size
in a message box, or a message saying the reosurce doesn't exist. This first
version of the code uses FindEntry:
// Version using FindEntry var ResFile: TPJResourceFile; Entry: TPJResourceEntry; begin ... // Assume ResFile contains a loaded resource file ... Entry := ResFile.FindEntry(RT_HTML, 'INDEX_HTML', $0809); if Assigned(Entry) then ShowMessageFmt( 'Data size for INDEX_HTML is %d', [Entry.DataSize] ) else ShowMessage('Can''t find resource'); ... // Don't forget to free ResFile at some stage. end;
This second version of the code shows how the same result is obtained with FindEntryIndex:
// Version using FindEntryIdex var ResFile: TPJResourceFile; Entry: TPJResourceEntry; Idx: Integer; begin ... Idx := fResFile.FindEntryIndex(RT_HTML, 'INDEX_HTML'); if Idx >= 0 then begin Entry := fResFile.Entries[Idx]; ShowMessageFmt( 'Data size for INDEX_HTML is %d', [Entry.DataSize] ); end ... end;
Note that we have used the "short form" of the FindEntry
and FindEntryIndex methods above: they find the first resource with
the given type and name, irrespective of language. The long version of the methods
finds a specific resource type, name and language. For example the following
code finds a RT_HTML
resource named INDEX_HTML
with
language $0809:
// "Full" version of FindEntry var ResFile: TPJResourceFile; Entry: TPJResourceEntry; begin ... Entry := ResFile.FindEntryIndex( RT_HTML, 'HTMLRES_HTML', $0809 ); if Assigned(Entry) then ... etc ... end;
We can use the IsMatching method of TPJResourceEntry to check if a specific resource matches given criteria. IsMatching can match just a resource type, and resource type and name or can uniquely identify a resource in a file by matching its type, name and language. Like TPJResourceFile.FindEntry, the language ID parameter is optional. The resource name parameter can be nil if we don't want to specify the name in the match.
Given the above description of IsMatching, we can list all the RT_HTML
resources in a resource file in a TMemo with this code:
var ResFile: TPJResourceFile; Entry: TPJResourceEntry; Idx: Integer; begin ... // Assume ResFile contains a loaded resource file ... for Idx := 0 to Pred(ResFile.EntryCount) do begin Entry := ResFile.Entries[Idx]; if Entry.IsMatching(RT_HTML, nil) then Memo1.Lines.Add( Format('%s', [ResIDToStr(Entry.ResName)]) ); end; ... end;
To list only all the different language versions of the RT_HTML
resource named INDEX_HTML
we simply change the IsMatching
method call in the for loop to:
Entry.IsMatching(RT_HTML, 'INDEX_HTML')
While the code in this unit does not understand the various resource data formats it does assist in reading, adding, updating and deleting the raw resource data. The TPJResourceEntry class's Data object exposes the resource data as a TStream which means that we can use normal stream handling techniques to access the data.
The following code fragment shows how to read all the data from the resource to a buffer.
var Entry: TPJResourceEntry; Buf: PByte; begin ... // Make sure Entry references a resource object ... // Create buffer of required size GetMem(Buf, Entry.DataSize); try // Make sure resource data stream at start Entry.Data.Position := 0; // Read all resource data into buffer Entry.Data.ReadBuffer(Buf^, Entry.DataSize); // Rewind data stream again Entry.Data.Position := 0; ... // Do something with Buf ... finally // Release buffer FreeMem(Buf); end; end;
We first set the buffer to the required size using TPJResourceEntry's DataSize property (The Data property's own Size property also gives this information). We now ensure the data stream is positioned at the start (you can't assume this!) and then read al the data into the buffer using TStream's ReadBuffer method, and finally reposition the stream again ready for the next use. Having processed the data in the buffer in some way we finally free the buffer.
It may be more convenient to copy the data stream to another stream. The next example illustrates this by storing the resource data in a file named 'ResEntry.dat':
var Entry: TPJResourceEntry; FS: TFileStream; begin ... // Make sure Entry references a resource object ... // Open stream onto new file FS := TFileStream.Create('ResEntry.dat', fmCreate); try // Copy resource data to file FS.CopyFrom(Entry.Data, 0); Entry.Data.Position := 0; finally // Close the file FS.Free; end; end;
Here we first open a stream onto a new file. We then use TStream's CopyFrom method to copy the whole of the resource data to the file stream. Note by specifying a size of 0 to the CopyFrom method, TStream automatically positions the resource data stream to the start and copies the whole stream, so we don't need to position it first. Once again we reset the resource data stream once we are done.
It is very easy to delete all the data in a resource: simply set the data stream's size to 0 as follows.
var Entry: TPJResourceEntry; begin ... // Make sure Entry references a resource object ... Entry.Data.Size := 0; ... end;
Note that you must set the Size property of the entry's Data property here: you can't set the DataSize property since it is read only.
We can add data to an exisiting resource simply. Let's first look at how to overwrite the existing data and then show how to append data to an existing resource. For the purposes of this example, assume we have a user defined resource that stores some plain text. Again, Entry is a TPJResourceEntry object that references our resource. We will replace any existing data with the text "Hello World".
var Entry: TPJResourceEntry; Text: string; begin ... // Make sure Entry references a resource object ... // Delete any existing data Entry.Data.Size := 0; // Write the required text Text := 'Hello World'; Entry.Data.WriteBuffer(Text[1], Length(Text)); // Position data ready for reading Entry.Data.Position := 0; ... end;
The only point of note here is the use of one of the normal idioms is writing
a string to a stream: we can't just do WriteBuffer(Text, Length(Text))
,
but must either specify Text[1]
or PChar(Text)^
. Since
this is an attribute of TStream rather than the resource file code,
it is outside the scope of this document to discuss why!
Now let's look at how we add more text to the end of the resource data:
var Entry: TPJResourceEntry; Text: string; begin ... // Make sure Entry references a resource object ... // Move to end of existing data Entry.Data.Seek(0, soFromEnd); // Write new text Text := ' From DelphiDabbler'; Entry.Data.WriteBuffer(Text[1], Length(Text)); Text := #13#10'www.delphidabbler.com'; Entry.Data.WriteBuffer(Text[1], Length(Text)); // Reposition stream to start Entry.Data.Position := 0; ... end;
Here we move the stream pointer to the end of the stream so the text we write is appended to any existing data. Given the two examples above the data ends up holding the following two lines of text:
Hello World from DelphiDabbler www.delphidabbler.com
We can add a new resource to an existing file using TPJResourceFile's AddEntry method. The new resource must be unqiuely named within the resource file (i.e. its combined resource type, name and language id must be unique) otherwise an exception will be raised. There are two versions of the AddEntry method - the first simply adds a new empty resource with zeroed header properties while the second version adds a renamed copy of an existing resource. Both versions of AddEntry take an optional language identifier. If this is not specified the resource is language neutral (LanguageID = 0).
The following code snippet adds four new resource to an existing resource file object:
RCDATA
resource with ordinal name
42. The MemoryFlags property is then set to "Discardable"
and its data is set to "Hello World"RCDATA
resource that is a copy of the
first entry but named FORTYTWO
. This resource has the same MemoryFlags
and Data.RCDATA
resource also named FORTYTWO
but
with language ID of $0809. Again this resource is a copy of the first resource.RCDATA
resource named 42 with language ID of $0809.var ResFile: TPJResourceFile; Entry1, Entry2, Entry3, Entry4: TPJResourceEntry; const s42 = 'FORTYTWO'; ord42 = MakeIntResource(42); sHello = 'Hello World'; begin ... // Assume ResFile references a valid object ... // Create 1st entry: empty Entry1 := fResFile.AddEntry(RT_RCDATA, ord42); // now set mem flags and data Entry1.MemoryFlags := RES_MF_DISCARDABLE; Entry1.Data.WriteBuffer(PChar(sHello)^, Length(sHello)); Entry1.Data.Position := 0; // Create 2nd entry as copy of entry 1 Entry2 := fResFile.AddEntry(Entry1, s42); // Create 3rd entry as copy of entry 1 with language id Entry3 := fResFile.AddEntry(Entry1, s42, $0809); // Create 4th entry: empty with language id Entry4 := fResFile.AddEntry(RT_RCDATA, ord42, $0809); ... // Do something with the entries ... end;
The entries we have created have the following properties:
Entry 1 | Entry 2 | Entry 3 | Entry 4 | |
---|---|---|---|---|
Type | RT_RCDATA |
RT_RCDATA |
RT_RCDATA |
RT_RCDATA |
Name | 42 | FORTYTWO |
FORTYTWO |
42 |
LanguageID | 0 | 0 | $0809 | $0809 |
MemoryFlags | $0100 | $0100 | $0100 | 0 |
Data | "Hello World" | "Hello World" | "Hello World" | <empty> |
We noted above that an exception is raised in a duplicate resource is added to a file. To prevent this we may need to check if a resource exists and we can do this with the EntryExists method of TPJResourceFile as follows:
var ResFile: TPJResourceFile; Entry: TPJResourceEntry; begin ... // Assume ResFile references a valid object ... if not ResFile.EntryExists(RT_RCDATA, 'FORTYTWO', $0809) then Entry := ResFile.AddEntry(RT_RCDATA, 'FORTYTWO', $0809); ... end;
Note that the language id parameter to EntryExists is optional and
if ommitted the function checks if any resource with the given type and name
exists. Furthermore, specifying nil for the resource name makes the routine
check if any resource of the given type exists. So to check if a resource file
contains any RCDATA
resources use:
if ResFile.EntryExists(RT_RCDATA, nil) then // We have RCDATA resources in the file
We can delete all resources from a resource file simply by calling the Clear method of TPJResourceFile. In addition to deleting the resources it also frees all the TPJResourceEntry instances.
var ResFile: TPJResourceFile; begin ... // Assume ResFile is a valid resource file object ... // Delete all resources ResFile.Clear; ... end;
A single resource can be deleted from the resource file using the TPJResourceFile.DeleteEntry method. This checks if the file contains the resource and deletes it if so. However, the resource entry object is not freed. While this behaviour may be useful, it is not recommended. The preferred method is simply to free the resource entry instance. Freeing a TPJResourceEntry object autmatically removes it from the resource file.
So, to remove a single resource ResEntry from the resource file use the following code:
var Entry: TPJResourceEntry; begin ... // Assume ResEntry references a valid object ... // Delete the object from its resource file Entry.Free; ... end;
Now that we have learned how to modify a resource file, it's time to know how to save it. We simply use the SaveToFile or SaveToStream methods of TPJResourceFile. The following code shows how to use SaveToFile:
var ResFile: TPJResourceFile; begin // Assume ResFile references a valid object ... // Save the file to 'MyResource.res' ResFile.SaveToFile('MyResource.res'); end;
Having given some contrived examples of most of the functionality in the PJResFile unit, let us close by presenting a useful example that uses several of the methods we have reviewed.
We will create a routine that takes a list of HTML and related files and create a resource file which has a unique resource HTML resource for each file. Such resources can be used for display in Internet Explorer, using the res:// protocol. See my article "How to create and use HTML resource files" for more information on this subject.
Here is the code of the routine:
procedure BuildHTMLResFile(const Files: TStrings; const ResFileName: string); var ResFile: TPJResourceFile; // res file object Entry: TPJResourceEntry; // a resource entry ResName: string; // a resource name SrcFileName: string; // a source file name SrcStm: TFileStream; // source file stream FileIdx: Integer; // loops thru Files begin // Create new empty resource file object ResFile := TPJResourceFile.Create; try // Loop thru all source files for FileIdx := 0 to Pred(Files.Count) do begin // Record source file name SrcFileName := Files[FileIdx]; // Get resource name from source name ResName := ExtractFileName(SrcFileName); // Ensure res name is not a duplicate while ResFile.EntryExists( RT_HTML, PChar(ResName), $0809 ) do ResName := '_' + ResName; // Create new resource Entry := ResFile.AddEntry( RT_HTML, PChar(ResName), $0809 ); // Copy source file into resource data SrcStm := TFileStream.Create(SrcFileName, fmOpenRead); try Entry.Data.CopyFrom(SrcStm, 0); Entry.Data.Position := 0; finally SrcStm.Free; end; end; // Save resource file ResFile.SaveToFile(ResFileName); finally // Free resource file object ResFile.Free; end; end;
This routine is passed a list of files that are to be included in the resource file (as a string list). The name of the output file is also provided. We first create a new resource file object to store the HTML resources. We then loop through all the files in the list and add a new resource for each file. The resource is named with the name of the file (file name only, no path). To ensure the resource names are not duplicated we repeatedly append underscore characters to duplicate names until they are unique. Having got a unique resource name we create a new entry with the required name and then copy the file's contents into the resource data. Finally we save the resource file and free the resource file object.
This user guide and the software it refers to are copyright © Peter Johnson,2004. http://www.delphidabbler.com/.