Taking Action - Using and Writing Actions and Action Lists
Note
This article was originally written when Delphi 4 was first released. Quite some time has passed since then. However actions are still an important and useful Delphi feature.
Source Code
Delphi 4 Adds Actions As A New Feature
Are they really a feature? When the marketing information for Delphi 4 was first released, one of the features was Action Lists. "Great!", I thought at first. Then I immediately thought, "OK, they sound cool, but what do they do?" Here is what the marketing guys had to say about Action Lists:
Action lists reduce the complexity of building increasingly complex modern graphical user interfaces. Action lists allow the developer a convenient centralized location to handle the response to user commands. It provides control over user interface items such as menus and buttons and how they respond to user commands. Delphi again takes object-oriented development to the next level by allowing the developer to separate user interface activity from application logic, thereby reducing lines of code, code complexity, reusability, and robustness. Delphi 4 includes many predefined Standard Actions, thereby further decreasing the time required to build fully functional feature-rich Windows applications.
Wow, I guess that sounds cool. But I still did not understand what they did. The marketing guys obviously wanted to sell this to the executives, but did not really want to tell us techies how they worked. All I got out of the above paragraph is that it centralized code that responded to user actions. This did not help, because this could already be done via a variety of different methods in Delphi. What did Action Lists offer me then?
Delphi 4 Arrives Finally I Know Why I Should Take Action
Or so it would seem. Even after Delphi 4 arrived, it took me quite a while to figure out how Action Lists were advantageous to me. The manual says, "Action lists let you centralize the response to user commands (actions) for objects such as menus and buttons that respond to those commands". As usual, the documentation is lacking (The PDF manuals are better than the help files, which are often forgotten about as a resource by developers) and it certainly did not do a good job of explaining why I would want to use them.
Sure, the documentation explained how to use them, but I was still stuck with "Why?" But then as usual real work set in and I had to tend to it. One day it got worse, people began to Email Kudzu (Me, See http://www.hower.org/Kudzu) and ask, "When should I use Action Lists? What do they offer me?" Now I was stuck. I certainly did not want to look stupid <G>, so I had to find out.
There were currently a few ways to extract the code from a form into other sections. Events are merely pointers, and they can be set to call procedures in other units, or the events in the form can merely call other methods or procedures in other units. The latter is quite a common one. At first glance, this is all the functionality that Action Lists appear to have. But they really do much, much more as you will soon see.
What Action Lists Are Really For
Action lists do more than separate code from the user interface.
Action lists can:
- Contain their own logic
- Have inherent knowledge about the currently focused control
- Propagate default properties such as images, captions, short cut keys, etc&
- Provide a centralized point for user actions
Let us take the TEditCopy action as a quick example.
TEditCopy contains its own logic: TEditCopy will do the retrieval of text, and the copying of the text to the clipboard for you. You do not need to write code to perform this function.
TEditCopy has inherent knowledge about the focused control: TEditCopy will enable and disable the menu and buttons when text is available which is valid for copying to the clipboard. When no text is available, or is invalid, TEditCopy will automatically disable the linked menu and/or buttons.
TEditCopy will automatically propagate default properties:
If you do not override by setting the properties yourself, TEditCopy will set certain properties for you such as caption, imageindex, hint, and shortcut.
Provide a centralized point for user actions: TEditCopy itself does not perform this function, but the property editor for the action list does. Using the property editor allows you to easily see and work with all of the code that responds to a user interface in a centralized way.
Take Some Action
Let us create a small example, which uses action lists. Create a new application. Now add a TActionList component onto the main form, followed by a TEdit, TMainMenu, TToolBar, and finally a TImageList. Now set the Images property of the TActionList to ImageList1. NOTE: This step must be done before creating actions. If it is not done before, the images will not be copied into the TImageList. You must also set the Images property of MainMenu1 and Toolbar1 to the TImageList on the form. This second step can be done at any time, but is best done when the corresponding components are created.
Now, I will create a few actions. Double click on ActionList1 to invoke its property editor. The dialog should look like this:

Right click in the categories pane and select New Standard Action. Now it will bring up a list of registered standard actions. The dialog should look like this:

Add the following actions (you can use the shift/control keys to select more than one): TEditCopy, TEditCut, and TEditPaste. Exit the action list property editor.
I will finish the demo application step by step:
- Add a new menu item to the TMainMenu component with the caption &Edit.
- Add a submenu of Edit, but do not fill in any properties. Set its action property to Copy1.
- Create two more subitems and set their actions to Cut1 and Paste1 respectively.
- Add three buttons to Toolbar1. Note: You will see that each of the buttons will render individual images that match the actions. Do not be fooled by this. The toolbar merely increments the ImageIndex for each button when a TImageList is linked in the images property. The actual actions have not been linked.
- Give each of the buttons a unique value for their action property (Copy1, Cut1, and Paste1)
Now the application is ready to run and demonstrate what we have just built. Run the application. When you first run the application it should look something like this:

The copy and cut buttons are enabled. The paste button may or may not be enabled; depending whether or not there is valid text content on your clipboard. Now click in the text box so the text is no longer selected. Did you notice the copy and cut are now disabled? The menus are also updated accordingly. They are now disabled because the copy and cut are not valid actions for the currently selected control. As I discussed previously, this demonstrates that actions perform the following tasks:
- Contain their own logic
- Have inherent knowledge about the currently focused control
Each standard action also has events and properties, which you can use to override and/or supplement the functionality of each action.
Using Plain TAction
There is also a plain vanilla action called TAction which has no specific functionality but you can use by setting its properties and events. Using plain TActions by adding code to its events and modifying its properties is the easiest way to create custom actions, but it does not make them easily reusable. That being said, actions that you are not likely to reuse are perfect candidates for being implemented this way.
I will create a simple project, which has an action that prompts the user to select a file, and returns the value. (Available as Project1). Here is the step by step creation of the project:
- Create a new application
- Add a public string variable named sFilename to the main form class.
- Add a TImageList and insert one 16x16 image that represents an open folder (Available as open.bmp)
- Add a TActionList and activate it's property editor
- Set the Images property to ImageList1
- Add a plain TAction and select it
- Activate the property inspector and set the following properties
- Name = actnOpen
- Caption = &Open
- ShortCut = Ctrl+O
- ImageIndex = 0
- Add a TMainMenu and activate it's TMainMenu property editor
- Set the Images property to ImageList1
- Add "&File" menu item
- Add a sub menu item to the "&File" menu item, and set it's Action property to actnOpen
- Add a TToolBar
- Set the Images property to ImageList1
- Add a new button, and set it's action property to actnOpen
Now run the application. Open the file menu. You should notice that the Open menu item and the toolbar button are disabled. TActions by default will disable their linked controls if they do not have anything to execute. Since this is a plain TAction, and we have not created an OnExecute event, it is disabled.
Now an OnExecute event needs to be added. Open the TActionList property editor and select the actnOpen component. Now activate the property inspector, add an event for the OnExecute event, and fill in the following code:
procedure TformMain.actnOpenExecute(Sender: TObject); begin with TOpenDialog.Create(Self) do try if not Execute then Exit; sFilename := Filename; finally free; end; ShowMessage(sFilename); end;
Now run the project again. This time the Open menu item should be enabled and functional.
In addition to using the OnExecute event of individual actions, both the ActionList itself, and the Application object contain OnExecute and OnUpdate events that you should be aware of.
Not Enough Action
Unfortunately Inprise was not very imaginative in creating standard actions. There are only a dozen or so. The ones included perform clipboard functions, window arrangement and database navigation. To remedy this I am opening a new section of my web site (http://www.hower.org/Kudzu) called Area52 (Because some of the aspects of writing custom actions are about as secretive if not more so than Area 51) which will serve as a collection area for new standard actions. I will build the available standard actions up to as many as I can think of and I invite your ideas and submissions. Of course, full credit will be given to the original authors of each new action.
Writing Custom Actions
Now that you fully understand actions (right?) we can move on to creating your own standard actions. Writing custom actions is largely undocumented, and what is available is incorrect and incomplete in many vital areas. I will cover the corrections later in this article.
TFileOpenAction
The example above uses an action named TFileOpenAction, however it does not do anything yet. I will develop this into a functional custom action next.
TFileOpenAction will create and display a FileOpen dialog, and return the selected file in Filename, a new property that has been added to TFileOpenAction.
The following code is available as Step_2 (Remember to copy the files):
unit KudzuActions;
interface
Uses
ActnList
, Classes;
Type
TFileOpenAction = class(TAction)
private
FsFilename: String;
FOnFilenameSelected: TNotifyEvent;
protected
public
constructor Create(AOwner: TComponent); override;
procedure ExecuteTarget(Target: TObject); override;
function HandlesTarget(Target: TObject): Boolean; Override;
published
property Filename: String read FsFilename;
property OnFilenameSelected: TNotifyEvent read FOnFilenameSelected write FOnFilenameSelected;
end;
// Procs
Procedure Register;
implementation
Uses
Dialogs
, Menus;
procedure Register;
begin
RegisterActions('File', [TFileOpenAction], nil);
end;
constructor TFileOpenAction.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Caption := '&Open';
Hint := 'Open a file';
ShortCut := Menus.ShortCut(Word('O'), [ssCtrl]);
end;
procedure TFileOpenAction.ExecuteTarget(Target: TObject);
begin
with TOpenDialog.Create(Self) do try
if not Execute then Exit;
FsFilename := Filename;
finally free; end;
if assigned(FOnFilenameSelected) then FOnFilenameSelected(Self);
end;
function TFileOpenAction.HandlesTarget(Target: TObject): Boolean;
begin
Result := True;
end;
end.
There are three new methods covered here: Create, ExecuteTarget, and HandlesTarget. I will cover them one by one, in addition to UpdateTarget which I did not include in the above example.
Create
Create is the standard constructor used in all objects. In a TAction descendant Create, the default values are set.
ExecuteTarget
ExecuteTarget is called when the action is invoked by a control that it has been linked to. This is the method where the action's functionality is placed. In my example, the Target parameter is not needed, but it can be used to determine which control the action was performed against. ExecuteTarget essentially performs the same function as the OnExecute event. Understand however that use of the event will cause this method not to be executed.
HandlesTarget
HandlesTarget needs to be overridden and is used to inform linked controls whether or not the current target is supported for this action. In my example, it does not matter so True is always returned.
UpdateTarget
UpdateTarget is called at idle points in the application. UpdateTarget should be used to enable/disable controls, or perform other such tasks.
Testing TFileOpen
By now you should know how to test this newer version of TFileAction. The one addition is a new event to our TAction. For your convenience I have included it as Project2.
Writing action list editors
Under the topic in Delphi help by the same name, you will find:
You may want to write your own component editor for action lists. If you do, you can assign your own procedures to the four global procedure variables in the ActionList unit:
CreateActionProc: function (AOwner: TComponent; ActionClass: TBasicActionClass): TBasicAction = nil;
EnumRegisteredActionsProc: procedure (Proc: TEnumActionProc; Info: Pointer) = nil;
RegisterActionsProc: procedure (const CategoryName: string; const AClasses: array of TBasicActionClass; Resource: TComponentClass) = nil;
UnRegisterActionsProc: procedure (const AClasses: array of TBasicActionClass) = nil;
You only need to reassign these if you want to manage the registration, unregistration, creation, and enumeration procedures of actions differently from the default behavior. If you do, write your own handlers and assign them to these variables within the initialization section of your design-time unit.
This would be great if it actually worked. However the standard actions that Inprise included are registered in a unit called StdReg, for which source code is not available. Furthermore, the pointers for the above four procedures are initialized in the dclstd40 package, which also contains StdReg. For various reasons, dclstd40 will always be loaded before you can change the hooks, so the Inprise standard actions will always call these procedures before you can hook them! This effectively makes this documented feature absolutely useless.
Registering actions
Under this title the documentation says:
You can register and unregister your own actions with the IDE by using the global routines in the ActnList unit:
procedure RegisterActions(const CategoryName: string; const AClasses: array of TBasicActionClass);
Use these routines the same way you would when registering components
RegisterComponents. For example, the following code registers the standard actions with the IDE in the StdReg unit:
RegisterActions('', [TAction]);
{ Standard action registration }
RegisterActions('', [TAction]);
RegisterActions('Edit', [TEditCut, TEditCopy, TEditPaste]);
RegisterActions('Window', [TWindowClose, TWindowCascade, TWindowTileHorizontal, TWindowTileVertical, TWindowMinimizeAll, TWindowArrange]);
But the problem is that RegisterActions actually takes three parameters! Not two as shown here. So if we check the documentation for RegisterActions we shall see:
procedure RegisterActions(const CategoryName: string; const AClasses: array of TBasicActionClass; Resource: TComponentClass);
Description
RegisterActions enables a class to appear in the action list editor as a selection when you choose New Action. At design-time the StdReg unit calls RegisterActions. If you want to install your own actions so that they can be used with the action list editor, call RegisterActions to register them.
Notice they completely omit discussion of the third parameter Resource? Now a little more research will lead us to CreateAction, which mentions:
Calling this function is essentially the same as:
ActionClass.Create(AOwner);
Except that the CreateAction function uses the Resource parameter of the RegisterActions procedure to initialize the values of the action object based upon the Resource
However this still does not tell us what we actually pass for the Resource parameter. I even tried hooking into this procedure and waiting for the property editor to call it, but I could not get it to work. Apparently you can call it (although I havent tested it, and not much works so far <G>) at design time to create a new action, the property editor does not use it when creating actions. I was hoping that if I could hook into it, I could do some exploring around and find out what this mysterious Resource parameter contains. I also tried the four procedure pointers above to explore, but since that does not work either, I did not find the answers I needed. As far as I can tell the resource parameter is not documented anywhere, and no examples exist. I do however know how it is used, and will convey this information to you after we build a basic action of our own.
Action Hierarchy

Above is the hierarchy for non-database actions. To add our own actions, it can be concluded that we should inherit from TAction in most cases. Here is a brief overview of the purpose of each of the four base functions: TBasicAction, TContainedAction, TCustomAction, and TAction
TBasicAction
Every action descends from the base action class TBasicAction. You should inherit from TBasicAction if you need to create an action for an object that is not a menu item or a control.
TContainedAction
TContainedAction descends directly from TBasicAction and adds functionality that allows the actions to belong to a category, and be used in TActionLists.
TCustomAction
TCustomAction descends directly from TContainedAction and adds functionality for use with menu items and controls. Keeping in line with the rest of the VCL, TCustomAction does not publish its properties, it only implements them. This allows you to control which ones become published in your descendants. If you do not need such control, use Taction instead.
TAction
TAction merely publishes properties that were implemented in TCustomAction. It does not introduce any new functionality. Unless you need to keep certain properties hidden, your custom actions should descend from TAction.
Basic Shell
I will first create a basic custom action with no additional functionality so that I can explain the basics first and also illustrate one more problem.
Creation, Installation and Registration
The following code is the minimum required to create a new standard action.
interface
Uses
ActnList;
Type
TFileOpenAction = class(TAction)
end;
// Procs
Procedure Register;
implementation
procedure Register;
begin
RegisterActions('File', [TFileOpenAction], nil);
end;
end.
This unit is available along with a package wrapper in the directory Step_1. Create a directory on your hard drive and copy the files into that directory. It is important that you copy the files and install from the new location, even if the files already exist on your hard drive as Step_1. Do not install from the Step_1 directory. This is important as some of the step directories contain duplicate (more advanced versions) file names, and when you install a package, Delphi adds the directory to your library path. If you install the package from Step_1 now, and then attempt to install a newer version of the package from another Step directory, you will encounter problems.
Now that the new standard action has been installed, I will explain what is involved. Create a new project, and drop a TActionList on the main form. Now double click on the TActionList and then right click and select "New Standard Action". You should see a new standard action named TFileOpenAction in a new category called File. Select TFileOpenAction and click OK.
The way in which a standard action is registered is controlled by the parameters passed in the RegisterActions procedure, which can be seen in the listing above.
Next Problem
If you make changes to the action you will certainly need to recompile the package. This poses yet another small problem with packages. Each time you recompile, the package will be reloaded and the RegisterActions procedure will be called. However, the RegisterActions procedure does not replace the registration, but adds an additional one. To illustrate this, merely recompile the package, and bring up the add standard action dialog. You will now see multiple TFileOpenAction actions. The good news is that you do not need to guess at which one is valid, as they all work. There is an UnRegisterActions procedure, however it does not remedy this situation for some reason. Each time you restart Delphi however, this list will be cleared, so this problem only exists during development of the actions.
What does the Resource parameter contain?
The resource parameter allows you to create the defaults for your Action's properties visually, and also allows you to embed and specify images with them.
To do this, a form is created, and TActions are added to a TActionList upon the form. However, since the TActions that you are creating, must be added to this form for the same TActions which are being created, you need to install them first without a resource form. It is essentially a multi step process:
- Create and install your actions
- Modify the package by adding a resource form
- Recompile and reinstall your actions
In this example, open the package (finished version available as Step_3) and add a new form to it. Name it TKudzuActionsRes. Now add a TImageList, TActionList and add a TFileOpenAction to the TActionList. Finally load the bitmap into the TImageList, and set the properties to their defaults (remember to set Images and ImageIndex).
The usage of a resource parameter allows the elimination of some code in our original example. Since the resource parameter will initialize all the properties to the settings specified on the resource TActionList, I have removed the constructor, which was previously used for this purpose.
To complete the changes, change the resource parameter of the RegisterActions call from nil to TKudzuActionsRes, and add the form unit to the implementation uses clause.
Finally recompile the package. If everything has been done correctly, recreate a test project as in Project2. This time the images should be included automatically without the need to load them onto your form (A TImageList is still needed)
Conclusion
Actions are a great new addition to Delphi 4. However they are largely misunderstood, and also much is not documented. Actions will become more prolific in the near future, and I hope this article has helped you understand both their usage and creation. Be sure to check out Area 52 and contribute your own new actions.

