[Previous][Up][Next] (#fcl-res)

How to implement a new resource class

Remark: This chapter assumes you have some experience in using this library.

Some considerations

Usually, a specific resource class is needed when resource data is encoded in a specific binary format that makes working with RawData uncomfortable.

However, there aren't many reasons to design a new binary format requiring a specific resource class: the classes provided with this package exist for compatibility with Microsoft Windows, but in the general case one should avoid such approach.

In Microsoft Windows, some resource types have a specific format, and the operating system supports them at runtime making it easy to access that kind of data: e.g. icons and bitmaps are stored in resources in a format that is slightly different from the one found in regular files, but the operating system allows the user to easily load them using LoadImage function, without having to deal with their internal format. Other resource types are used to obtain information about the executable: RT_VERSION resource type and RT_GROUP_ICON contain version information and program icon that can be displayed in Windows Explorer, respectively.

Using this kind of resources in a cross-platform perspective doesn't make much sense however, since there is no support by other operating systems for these types of resources (and for resources in general), and this means that it's up to you to provide support at runtime for these binary formats. So if you need to store images as resources it's better to use TGenericResource and store an image in PNG format (for instance), which can be read by existing libraries at runtime, instead of creating a RT_BITMAP resource with TBitmapResource, since libraries that read BMP files can't handle that resource contents.

New resource classes thus make sense when you want to add support for existing Windows-specific resources, e.g. because you are writing a resource compiler or editor, but in the general case they should be avoided.

Now that you've been warned, let's start with the topic of this chapter.

How to implement a new resource class

A resource class is a descendant of TAbstractResource, and it's usually implemented in a unit named typeresource, where type is resource type.

If you are implementing a new resource class, you are doing it to provide additional methods or properties to utilize resource data. You resource class must thus be able to read its RawData stream format and write data back to it when it is requested to do so.

Generally, your class shouldn't create private objects, records or memory buffers to provide these specific means of accessing data until it's requested to do so (lazy loading), and it should free these things when it is requested to write back data to the RawData stream.

We'll see these points in more detail, using TAcceleratorsResource as an example.

TAcceleratorsResource holds a collection of accelerators. An accelerator is a record defined as follows:

type
  TAccelerator = packed record
    Flags : word;
    Ansi : word;
    Id : word;
    padding : word;
  end;

The resource simply contains accelerators, one immediately following the other. Last accelerator must have $80 in its flags.

To provide easy access to the elements it contains, our accelerator resource class exposes these methods and properties in its public section:

procedure Add(aItem : TAccelerator);
procedure Clear;
procedure Delete(aIndex : integer);
property Count : integer read GetCount;
property Items[index : integer] : TAccelerator read GetItem write SetItem; default;

We must also implement abstract methods (and an abstract constructor) of TAbstractResource:

protected
  function GetType : TResourceDesc; override;
  function GetName : TResourceDesc; override;
  function ChangeDescTypeAllowed(aDesc : TResourceDesc) : boolean; override;
  function ChangeDescValueAllowed(aDesc : TResourceDesc) : boolean; override;
  procedure NotifyResourcesLoaded; override;
public
  constructor Create(aType,aName : TResourceDesc); override;
  procedure UpdateRawData; override;

The protected methods are very easy to implement, so let's start from them. For GetType and GetName, we need to add two private fields, fType and fName, of type TResourceDesc. We create them in the constructor and destroy them in the destructor. Type will always be RT_ACCELERATOR. We make the parameterless constructor of TAbstractResource public, using 1 as the resource name, while in the other constructor we use the name passed as parameter, ignoring the type (since it must always be RT_ACCELERATOR).

So, GetType, GetName, the constructors and the destructor are implemented as follows:

function TAcceleratorsResource.GetType: TResourceDesc;
begin
  Result:=fType;
end;

function TAcceleratorsResource.GetName: TResourceDesc;
begin
  Result:=fName;
end;

constructor TAcceleratorsResource.Create;
begin
  inherited Create;
  fType:=TResourceDesc.Create(RT_ACCELERATOR);
  fName:=TResourceDesc.Create(1);
  SetDescOwner(fType);
  SetDescOwner(fName);
end;

constructor TAcceleratorsResource.Create(aType, aName: TResourceDesc);
begin
  Create;
  fName.Assign(aName);
end;

destructor TAcceleratorsResource.Destroy;
begin
  fType.Free;
  fName.Free;
  inherited Destroy;
end;

Note that we used SetDescOwner to let type and name know the resource that owns them.

Now ChangeDescTypeAllowed and ChangeDescValueAllowed come. As we said, resource type must be RT_ACCELERATOR, always. Thus, we only allow name to change value or type:

function TAcceleratorsResource.ChangeDescTypeAllowed(aDesc: TResourceDesc): boolean;
begin
  Result:=aDesc=fName;
end;

function TAcceleratorsResource.ChangeDescValueAllowed(aDesc: TResourceDesc): boolean;
begin
  Result:=aDesc=fName;
end;

NotifyResourcesLoaded is called by TResources when it finishes loading all resources. Since we are not interested in this fact, we simply leave this method empty. This method is useful for resources that "own" other resources, like TGroupIconResource and TGroupCursorResource (note: you should not implement resource types that depend on other resources: it complicates things a lot and gives you a lot of headaches).

Now, let's see the more interesting - and more difficult - part: providing an easy way to deal with accelerators.

As we said earlier, we must implement some methods and properties to do so. Surely we'll need a list to hold pointers to accelerator records, but we must think a little bit about how this list is created, populated, written to RawData and so on.

When the object is created, we don't have to create (yet) single accelerator records, as said before; but if the user tries to access them we must do it. If the object is created and its RawData contains data (e.g. because a reader has created the resource when loading a resource file) and the user tries to access an accelerator, we must create accelerators from RawData contents. So, until a user tries to access accelerators our class doesn't do anything, while when the user does so it "lazy-loads" data, or creates empty structures if RawData is empty.

When data is loaded, RawData contents aren't considered anymore. When, however, our resource is requested to update RawData (that is, when UpdateRawData method is invoked), our "lazy-loaded" data must be freed. In fact, a user could ask our resource to update raw data, then he/she could modify it directly and then could use our resource's specialized methods and properties to do further processing: for this reason, when RawData is written, other buffered things must be freed, so that they'll created again from RawData if needed.

Note that other resource classes could behave differently: e.g. TBitmapResource uses a copy-on-write mechanism on top of RawData to insert a BMP file header at the beginning of the stream, but it doesn't copy RawData contents whenever possible.

Coming back to our TAcceleratorsResource example, let's see how to implement this behaviour.

Let's add a fList field in the private section of our class:

fList : TFPList;

In the constructor, we set this field to nil: we use it to check if data has been loaded from RawData or not. Consequently in the destructor we'll free the list only if it's not nil:

destructor TAcceleratorsResource.Destroy;
begin
  fType.Free;
  fName.Free;
  if fList<>nil then
  begin
    Clear;
    fList.Free;
  end;
  inherited Destroy;
end;

(we did not implement Clear method yet: we'll see it later).

We said that our resource must load data only when needed; to do this we add a private method, CheckDataLoaded that ensures that data is loaded. This method is called by whatever method needs to operate on the list: if data has already been loaded it will quickly return, otherwise it will load data.

procedure TAcceleratorsResource.CheckDataLoaded;
var acc : TAccelerator;
    tot, i : integer;
    p : PAccelerator;
begin
  if fList<>nil then exit;
  fList:=TFPList.Create;
  if RawData.Size=0 then exit;
  RawData.Position:=0;
  tot:=RawData.Size div 8;
  for i:=1 to tot do
  begin
    RawData.ReadBuffer(acc,sizeof(acc));
    GetMem(p,sizeof(TAccelerator));
    p^:=acc;
    fList.Add(p);
  end;
end;	

If fList is not nil data is already loaded, so exit. Otherwise, create the list. If RawData is empty we don't need to load anything, so exit. Otherwise allocate room for accelerators, read them from the stream, and add them to the list.

Note that if we are running on a big endian system we should swap the bytes we read, e.g. calling SwapEndian function, but for simplicity this is omitted.

The counterpart of CheckDataLoaded is UpdateRawData. When it is called, data from the list must be written back to RawData, and the list and its contents must be freed:

procedure TAcceleratorsResource.UpdateRawData;
var acc : TAccelerator;
    i : integer;
begin
  if fList=nil then exit;
  RawData.Size:=0;
  RawData.Position:=0;

  for i:=0 to fList.Count-1 do
  begin
    acc:=PAccelerator(fList[i])^;
    // $80 means 'this is the last entry', so be sure only the last one has this bit set.
    if i=Count-1 then acc.Flags:=acc.Flags or $80
    else acc.Flags:=acc.Flags and $7F;
    RawData.WriteBuffer(acc,sizeof(acc));
  end;
  Clear;
  FreeAndNil(fList);
end;	

If fList is nil data hasn't been loaded, so RawData is up to date, so exit. Otherwise, write each accelerator (ensuring that only last one has $80 flag set), clear the list, free it and set it to nil. Again, if we are on a big endian system we should swap each accelerator contents before writing it, but for simplicity this is omitted.

Other methods we named earlier (Add, Delete, Clear) are trivial to implement: remember only to call CheckDataLoaded before doing anything. The same is true for accessor methods of relevant properties (Count, Items).

If you are curious, you can check the full implementation of TAcceleratorsResource looking at source code.


Documentation generated on: Jun 22 2020