ImageCollection and VirtualImageList

How to work with it at runtime

Publish date:

Overview 

Not so long ago, Delphi added such components as TImageCollection and TVirtualImageList that make it possible to develop applications with HiDPI support. As practice has shown, these components can demonstrate excellent results in both developing new applications and migrating old applications to newer versions of Delphi.

The components themselves feature robust editors that, after a brief analysis of the documentation, allow developers to make all the required settings at DesignTime. This approach is quite convenient and makes it possible to easily resolve 90% of tasks associated with these components in typical applications. However, there are still situations where working with them at Runtime is necessary. For instance, if your application allows users to customize images themselves, or if they are stored in the file system, a database, a remote server, or elsewhere.

To better understand the code, let’s consider the simplified structure of these components.

For compatibility with legacy solutions based on the use of regular TImageList, the new TVirtualImageList also inherits from TCustomImageList. The difference is that while TImageList stores the images themselves, TVirtualImageList has a property called ImageCollection, which is a reference to an object of type TImageCollection from which the images are retrieved. Additionally, there is a collection called Images: TVirtualImageListItems, where TVirtualImageListItem objects are stored. These objects contain only additional information about the images (such as index, name, name in the collection, etc.).

ImageList_DFM
VirtualImageList_DFM

During its work, the VirtualImageList turns to the ImageCollection and requests the necessary image at the size specified in the Width and Height properties. The ImageCollection either returns the original image (if the requested size matches the size of the original image) or selects the optimal size from the available options and scales it to the required size in the VirtualImageList. Since scaling operations can be resource-intensive, the VirtualImageList includes a caching buffer to avoid repeated accesses to the ImageCollection and scaling operations during each rendering.

HiDPI support in this component combination is ensured in the following way: The VirtualImageList gets DPI change events of its parent form, scales its Height and Width properties, and updates its cache from the ImageCollection with the new icon sizes. (NOTE: VirtualImageList scales only when a DPI change event occurs for the parent form specified in the Owner property. If another component or data module is set as the Owner, scaling will not occur. This should be taken into account when you create the component manually.)

When you know this structure, the way of solving the task of adding a new image to the list becomes obvious: you can either populate the Images collection directly or use one of the overloaded variants of the Add method.

1234567891011121314151617
procedure TForm2.SpeedButton1Click(Sender: TObject);
begin
var xItem := VirtualImageList1.Images.Add;
xItem.CollectionName := 'Rollback_pink';
xItem.Name := 'Rollback_pink';
SpeedButton2.ImageName := 'Rollback_pink';
end;
или
procedure TForm2.SpeedButton2Click(Sender: TObject);
begin
VirtualImageList1.Add('Rollback_pink', 'Rollback_pink');
SpeedButton1.ImageName := 'Rollback_pink';
end;

In both cases, we will obtain a new icon in VirtualImageList1 with the name ‘Rollback_pink’. Of course, at the time of adding the image to VirtualImageList1, the original source images ‘Rollback_pink’ must already be added to the ImageCollection, otherwise, we will encounter an error. This should be done either at DesignTime or at Runtime.

In all other aspects, working with the TVirtualImageList class is not much different from the regular TImageList: if you need to obtain a Bitmap, you call the GetBitmap method; if you want to draw the image on a Canvas, you use one of the overloaded Draw methods. What’s worth mentioning is that TVirtualImageList has methods for working with images not only by index but also by name.

1234
procedure TForm2.PaintBox1Paint(Sender: TObject);
begin
VirtualImageList1.Draw(PaintBox1.Canvas, 2, 2, 'Rollback_pink');
end;

Now let’s move on to adding images to an ImageCollection. The process of adding images through an IDE is straightforward: you need to run the editor and follow the instructions. Therefore, let’s analyze the structure of the ImageCollection to understand how to do it programmatically.

The main properties of the TImageCollection component and its internal objects are as follows:

·    Images: TImageCollectionItems – a collection of final images of the TImageCollectionItem type.

o   Items[Index: Integer]: TImageCollectionItem – an object representing a final image (the one we see in the editor when we are adding images to VirtualImage or VirtualImageList).

§  Name – the name of the image.

§  Description – the description of the image.

§  SourceImages: TImageCollectionItemSources – a collection of source images.

·     Items[Index: Integer]: TImageCollectionSourceItem – a collection object of a source image.

o   Image: TWICImage – the actual source image.

In the DFM format, it looks something like this:

ImageCollection_DFM

The illustration above clearly demonstrates that we are working with a collection of collections. The first level is the logical representation of an image, with its own name (user, Database-find, next, search, etc.). These objects are operated by VirtualImageList and VirtualImage. The second level of the hierarchy is also a set of pseudo-objects, from which we are interested only in the Image property. Here, all the source images of the same picture in different sizes are collected.

It’s important to remember that in the SourceImages collection, you don’t have the actual source images, but only objects whose Image property contains the actual source image.

Now when we fully understand the structure, let’s move on to populating the ImageCollection.

123456789101112131415161718192021
procedure TDataModule1.DataModuleCreate(Sender: TObject);
var
xImageCollectionItem : TImageCollectionItem;
xImageCollectionSourceItem : TImageCollectionSourceItem;
begin
//create "place" for new image
xImageCollectionItem := icIcons8.Images.Add;
xImageCollectionItem.Name := 'MyNewImage';
//create item to put source of new image
xImageCollectionSourceItem := xImageCollectionItem.SourceImages.Add;
xImageCollectionSourceItem.Image.LoadFromFile('C:\Work\Icons\icons8_opened_folder_16.png');
//and one more source with other size
xImageCollectionSourceItem := xImageCollectionItem.SourceImages.Add;
xImageCollectionSourceItem.Image.LoadFromFile('C:\Work\Icons\icons8_opened_folder_32.png');
//and one more source with other size
xImageCollectionSourceItem := xImageCollectionItem.SourceImages.Add;
xImageCollectionSourceItem.Image.LoadFromFile('C:\Work\Icons\icons8_opened_folder_48.png');
end;

This method of adding items directly to collections is simple and understandable, but it requires a lot of coding. Of course, Embarcadero has added methods to the ImageCollection that simplify routine operations. Here’s what the equivalent functionality would look like using the overloaded Add method:

123456789
procedure TDataModule1.DataModuleCreate(Sender: TObject);
begin
//create item to put source of new image
icIcons8.Add('MyNewImage', 'C:\Work\Icons\icons8_opened_folder_16.png');
//and one more source with other size
icIcons8.Add('MyNewImage', 'C:\Work\Icons\icons8_opened_folder_32.png');
//and one more source with other size
icIcons8.Add('MyNewImage', 'C:\Work\Icons\icons8_opened_folder_48.png');
end;

This method automatically checks if an ImageCollectionItem with the specified name already exists. If it doesn’t exist, it creates a new one; otherwise, it retrieves the existing one. Then, it creates a new item in SourceImages and loads the file with the name we provided into it.

If we handle all the work through the VirtualImageList component, by adding (using the Add method) or updating (using the UpdateImageList method) the appropriate images in it, we will no longer need to manually interact with the ImageCollection. However, there are still several useful methods in the ImageCollection that may be helpful:

  • Draw – Draw an image from the collection item 
  • GetBitmap – Get scaled to a specific size TBitmap from an item with a specific index
  • GetSourceImage – Get a source TWICImage, which is optimal for scaling to AWidth and AHeight sizes.

Practical Application

To demonstrate when the work with these components at runtime can be useful, let’s create our own component. To keep the demonstration simple, let’s make it a regular panel with the company logo always displayed in the bottom right corner.

To do this, let’s create our own package in the IDE (File->New->Package)

Create_New_Package

Let’s save it with the name ImageCollectionPanel.dproj.

We will create a new unit named “LogoPanel.pas” in it. We can immediately list all the necessary modules in the “uses” section

123
uses
Vcl.ExtCtrls, System.Classes, Vcl.VirtualImageList, Vcl.ImageCollection,
System.SysUtils, Vcl.Forms, System.Messaging;

Below, we will write the declaration of the type of our component. Let it be:

1234
type
TLogoPanel = class(TPanel)
end;

Next, we need to provide a place to store our logo. We’ll use TImageCollection for this purpose. However, if we simply declare a field of this type, it will mean that we need to load our logo in all sizes as many times as there will be panels in the application. And this approach is not efficient at all. Especially if there are many logo variations and hundreds of panels. Therefore, we’ll declare it as a class variable so that we will have only one instance of it. Let’s name the component InternalImageCollection. Just immediately, we will create a class constructor and class destructor to manage its lifecycle:

123456
TLogoPanel = class(TPanel)
protected
class var InternalImageCollection : TImageCollection;
class constructor Create;
class destructor Destroy;
end;

Let’s proceed to the implementation of the methods:

1234567891011
class constructor TLogoPanel.Create;
begin
inherited;
InternalImageCollection := TImageCollection.Create(nil);
InternalImageCollection.Add('Logo', HInstance, 'Logo_', ['48', '72', '96', '144'])
end;
class destructor TLogoPanel.Destroy;
begin
FreeAndNil(InternalImageCollection);
end;

During initialization, we will create InternalImageCollection and immediately load the logos into it. Since our TLogoPanel can be used in any application, we cannot directly load the logo from a file. Therefore, we need to use a different version of the overloaded Add method, which loads data from resources. In this version, the first parameter is the name under which we will store the image, the second parameter is the HInstance from which the resources will be loaded (depending on how our component is used, this is either a BPL file or an EXE file), the third parameter is the beginning of the resource name, and the last one includes the suffixes. Each of these suffixes, in their turn, will be added to the name, and resources will be searched for loading. In our case, resources with the following names will be searched for: ‘Logo_48’, ‘Logo_72’, ‘Logo_96’, and ‘Logo_144’.

Now let’s add our logo. Otherwise, the component will crash with an error during the stage of initialization. There are several ways to do this, but that’s a topic for another article. Here, we’ll use the simplest, most understandable, and universal method. We will move the logos to a subfolder named “logo” within our project directory. Next, create and add a “Resource file” to the project with the extension “*.rc”. This is a text file, each line of which has the following structure:

1
<ResourceName> <ResourceType> <FileName>

In our case, the text of the file will look as follows:

1234
Logo_48 RCDATA "logo\\logo-48.png"
Logo_72 RCDATA "logo\\logo-72.png"
Logo_96 RCDATA "logo\\logo-96.png"
Logo_144 RCDATA "logo\\logo-144.png"

During the building of our BPL, the compiler will create a file with the extension “*.res” and place there 4 corresponding PNG files in the RCDATA section. In order for the data from this file to be compiled into our BPL or into the project that will use our TLogoPanel component, it is necessary to include it in the LogoPanel.pas file with the following line:

1
{$R logos.res}

After these manipulations, our logo will be successfully loaded into the InternalImageCollection during the initialization of the type.

Let’s continue developing our component, as it currently doesn’t differ from a regular TPanel. For drawing, we will declare a field of the TVirtualImageList type, create a constructor for its initialization, and override the “Paint” method to use it.

1234567891011
TLogoPanel = class(TPanel)
protected
class var InternalImageCollection : TImageCollection;
class constructor Create;
class destructor Destroy;
protected
FLogos : TVirtualImageList;
procedure Paint; override;
public
constructor Create(AOwner: TComponent); override;
end;

Realization:

123456789101112131415161718
constructor TLogoPanel.Create(AOwner: TComponent);
begin
inherited;
FLogos := TVirtualImageList.Create(self);
FLogos.ImageCollection := InternalImageCollection;
FLogos.SetSize(ScaleValue(48), ScaleValue(48));
FLogos.Add('Logo', 'Logo');
end;
procedure TLogoPanel.Paint;
begin
inherited;
var xPos := Self.ClientRect.BottomRight;
xPos.Offset(-(FLogos.Width) – 2, -(FLogos.Height) – 2);
FLogos.Draw(Canvas, xPos.X, xPos.Y, 'Logo');
end;

In the constructor, we will create our FLogos and load the image from the global collection. Note that the size is set using the ScaleValue function. This method will take the current scaling of the component and apply it to the parameters for correct display on HiDPI monitors.

In the Paint method, we will invoke the functionality of our parent, and then calculate the bottom-right corner and draw our logo there.

That would have been it, but there is one particular aspect  that we mentioned earlier. During dynamic DPI changes, such as moving a window between monitors with different scaling, TVirtualImageList captures events only from the component specified in its Owner property, which in our case is TLogoPanel. Consequently, dynamic scaling of the form will simply be ignored.

To eliminate the impact of this peculiarity, we will need to write another method, “DPIChangedMessageHandler”, which will handle the DPI change message. Its implementation will be as follows:

123456789
procedure TLogoPanel.DPIChangedMessageHandler(const Sender: TObject;
const Msg: System.Messaging.TMessage);
begin
if (TChangeScaleMessage(Msg).Sender = Owner) then
begin
TMessageManager.DefaultManager.SendMessage(Self,
TChangeScaleMessage.Create(Self, TChangeScaleMessage(Msg).M, TChangeScaleMessage(Msg).D));
end;
end;

This method receives the message from the form and sends a similar one on its own behalf so that the standard scaling mechanism of our TVirtualImageList will work. However, since this is not a WinAPI message but a TMessageManager message, it’s important not to forget to subscribe/unsubscribe to receive it. We will declare the variable FDPIChangedMessageID and add it to the constructor and destructor accordingly.

In the final step, we will create a register function to register the component, install the package, and then test it.

FinalResult

The full text of the demo project:

ImageCollectionPanel.dpk file

12345678910111213141516171819202122232425262728293031323334353637
package ImageCollectionPanel;
{$R *.res}
{$IFDEF IMPLICITBUILDING This IFDEF should not be used by users}
{$ALIGN 8}
{$ASSERTIONS ON}
{$BOOLEVAL OFF}
{$DEBUGINFO OFF}
{$EXTENDEDSYNTAX ON}
{$IMPORTEDDATA ON}
{$IOCHECKS ON}
{$LOCALSYMBOLS ON}
{$LONGSTRINGS ON}
{$OPENSTRINGS ON}
{$OPTIMIZATION OFF}
{$OVERFLOWCHECKS ON}
{$RANGECHECKS ON}
{$REFERENCEINFO ON}
{$SAFEDIVIDE OFF}
{$STACKFRAMES ON}
{$TYPEDADDRESS OFF}
{$VARSTRINGCHECKS ON}
{$WRITEABLECONST OFF}
{$MINENUMSIZE 1}
{$IMAGEBASE $400000}
{$DEFINE DEBUG}
{$ENDIF IMPLICITBUILDING}
{$IMPLICITBUILD ON}
requires
rtl,
vcl,
vclwinx;
contains
LogoPanel in 'LogoPanel.pas';
end.

LogoPanel.pas file:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
unit LogoPanel;
interface
uses
Vcl.ExtCtrls, System.Classes, Vcl.VirtualImageList, Vcl.ImageCollection,
System.SysUtils, Vcl.Forms, System.Messaging;
type
TLogoPanel = class(TPanel)
protected
FDPIChangedMessageID: Integer;
class var InternalImageCollection : TImageCollection;
class constructor Create;
class destructor Destroy;
protected
FLogos : TVirtualImageList;
procedure Paint; override;
procedure DPIChangedMessageHandler(const Sender: TObject; const Msg: System.Messaging.TMessage);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;
procedure Register;
implementation
{$R logos.res}
procedure Register;
begin
RegisterComponents('Sample', [TLogoPanel]);
end;
{ TLogoPanel }
class constructor TLogoPanel.Create;
begin
inherited;
InternalImageCollection := TImageCollection.Create(nil);
InternalImageCollection.Add('Logo', HInstance, 'Logo_', ['48', '72', '96', '144'])
end;
class destructor TLogoPanel.Destroy;
begin
FreeAndNil(InternalImageCollection);
end;
destructor TLogoPanel.Destroy;
begin
TMessageManager.DefaultManager.Unsubscribe(TChangeScaleMessage, FDPIChangedMessageID);
inherited;
end;
procedure TLogoPanel.DPIChangedMessageHandler(const Sender: TObject;
const Msg: System.Messaging.TMessage);
begin
if (TChangeScaleMessage(Msg).Sender = Owner) then
begin
TMessageManager.DefaultManager.SendMessage(Self,
TChangeScaleMessage.Create(Self, TChangeScaleMessage(Msg).M, TChangeScaleMessage(Msg).D));
end;
end;
constructor TLogoPanel.Create(AOwner: TComponent);
begin
inherited;
FLogos := TVirtualImageList.Create(self);
FLogos.ImageCollection := InternalImageCollection;
FLogos.SetSize(ScaleValue(48), ScaleValue(48));
FLogos.Add('Logo', 'Logo');
FDPIChangedMessageID := TMessageManager.DefaultManager.SubscribeToMessage(TChangeScaleMessage, DPIChangedMessageHandler);
end;
procedure TLogoPanel.Paint;
begin
inherited;
var xPos := Self.ClientRect.BottomRight;
xPos.Offset(-(FLogos.Width) – 2, -(FLogos.Height) – 2);
FLogos.Draw(Canvas, xPos.X, xPos.Y, 'Logo');
end;
end.

Subscribe to our newsletter and get amazing content right in your inbox.

This field is required
This field is required Invalid email address
By submitting data, I agree to the Privacy Policy

Thank you for subscribing!
See you soon... in your inbox!

confirm your subscription, make sure to check your promotions/spam folder

Subscribe to our newsletter and get amazing content right in your inbox.

You can unsubscribe from the newsletter at any time

This field is required
This field is required Invalid email address

You're almost there...

A confirmation was sent to your email

confirm your subscription, make sure to check
your promotions/spam folder