For all of you eagerly waiting for the interesting part of this article – here it is. In case you don’t know what I’m talking about, please read the introductory article first.
Let’s recap the essence of what has been dealt with:
- We need a virtual image list in each form to make use of the automatic scaling feature.
- The controls on that form and any frame on that form have to be linked to that image list.
- We don’t want to maintain all these image lists in the forms during design time.
- We don’t want to create dependencies of frames to these forms just to make links to its image lists.
I started with a simple project covering the common uses cases of my application: static and dynamically created frames in one or more forms. For some rough testing a single image list would do.
Starting with a data module for the image collection (TdmImages) I added another data module with a TVirtualImageList instance linked to the image collection. To emphasize that it is meant for designing I named it TdmDesignImages. While an instance for TdmImages is auto-created, TdmDesignImages is not!
Now I added a toolbar and some buttons to the main form and linked them to the images in dmDesignImages. Then I created a frame with a similar toolbar and placed an instance in the main form, together with a button to add more frame instances at run time. Finally a second empty form was added also with a button to add frames dynamically.
As we don’t create an instance of TdmDesignImages at run time, the links are not resolved when the form and frames are instantiated and thus no images are displayed. So we need to add a virtual image list to the form at run time that mimics that in TdmDesignImages. Then all the image list links from the toolbars and later other controls have to be redirected to that image list.
Fortunately this is not as complicated as it sounds. When a component is loaded, it cannot be guaranteed that all references already exist when the linked properties are read. If you look at the DFM source you can see that all non-visual components are placed at the end. Thus any action of a button cannot be resolved when loading the button, but rather when the action is loaded. For that the Delphi streaming system has a bookkeeping of all unresolved references – called FixUps.
There are local fixups that can be resolved when the form has finished loading. For unresolved external links there are global fixups, which the system tries to resolve whenever a root component (form, frame or data module) is loaded.
So all we have to do to resolve these dangling image list fixups to dmDesignImages is to create an instance of TdmDesignImages. After that we can safely move the image list from the TdmDesignImages instance to the form keeping the references intact. The now empty instance can then be destroyed (actually it must be destroyed). We just have to make sure that the image list is scaled to the current DPI of the form if necessary. Otherwise the images will most likely look too small. For later reference we make a note of the data modules we have handled this way.
We do that inside AfterConstruction before calling inherited. That way the code is executed between DFM loading and the OnCreate event. Thus any code inside FormCreate can rely on all references being resolved just as usual.
While this already works pretty well for simple forms and forms with static frames, it fails for dynamically created frames. While the global fixups still exist after the frame is created, we cannot just resolve them by creating a new data module instance and move the image lists into the form – we already have them. Of course we could rename the image list and let it coexist with the previous one, but that means having similar but separate image lists for each dynamic frame.
We need some means to redirect the references to the existing image list in the form. Luckily there is an existing method to do that: RedirectFixupReferences (one of those Embarcadero Technologies does not currently have any additional information for). The method has three parameters:
- Root: the instance for which the references are to be redirected (the frame)
- OldRootName: the name of the original reference root (dmDesignImages)
- NewRootName: the name of the replacement root (the name of the form)
The method iterates all global fixups. If either Root is nil or it matches Root it compares the reference root name to OldRootName. If they match it changes the reference root name to NewRootName. Following the iteration it tries to resolve the fixups.
This is where the note we made earlier about the handled data module comes into play. In a real world app there can be more than one data module like TdmDesignImages, so the handled data module names are probably stored in a list, which is now iterated calling RedirectFixupReferences.
We have to take care that the current image sizes are suitable for the frame. Therefore the frame has to be parented to the form first to allow for scaling it before we resolve the fixups to the already scaled image list. This can best be achieved by overriding SetParent and calling the redirect code after calling inherited.
To have a more general solution we put all this code in classes derived from TForm respectively TFrame. As we don’t need DFM files for these, we can use a trick to keep the existing form and frame units almost as is and just add the corresponding general unit to the uses clause.
This approach can safely be used with forms, but will clash with the designer for frames and data modules. Simply adding a proper alias will fix that (see code).
The complete code can be found on GitHub: https://github.com/UweRaabe/CmonLib/tree/main/Examples/ImageLists
Unfortunately this works only when the IDE is designing with 96 dpi (aka highdpi:unaware), but IMHO that is the only reasonable way to work with it in the moment. As soon as this gets a bit more reliable I will extend the code to handle that, too.