Actions, Action Lists And Action Managers

(updated for Delphi XE3)

Brian Long (www.blong.com)

Delphi

Table of Contents

Click here to download the files associated with this article.

If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.


Introduction

Actions and action lists were introduced into the VCL by Delphi 4 in June 1998 (in all flavours of the product) and were apparently considered so important that they were the first addition to the Standard page of the Component Palette since Delphi was released back in February 1995. Action managers were later added in Delphi 6 (May 2001).

In 2012 Delphi XE3, C++Builder XE3 and RAD Studio XE3 have added the first iteration of action support into the FireMonkey cross-platform framework. FireMonkey is often generally abbreviated to FMX, but the second FireMonkey release in the XE3 products seems also to be often referred to as FM2. It is FM2, specifically, that introduces actions to the cross-platform world. Initially FM2 supports actions and action lists but does not support action managers yet. 

It would appear that at the time of writing, over 14 years after the introduction of actions, these potentially very useful components have been much under-used by the Delphi community. Maybe this is just because people don't know much about them. Maybe the TActionList component should have been placed at the beginning of the Standard page rather than the end.

Anyway, for those who have never had the time or inclination to look into what actions do or how they work, this article will explore their purpose, usage and internal operation, making sure to take a look at action bands and action managers as introduced back in Delphi 6 (May 2001).

The article closes with a look at how to install new, reusable actions into the IDE. Where possible sample applications are provided twice, once using the VCL and once using FMX.

What Is An Action?

Often in applications, there are several UI mechanisms to trigger the same functionality (or command). For example a button, a menu item and maybe a tool button on a tool bar or a speedbutton on a speedbar. Normally you set up this type of arrangement by sharing OnClick event handlers between the various objects. Of course you must set up the captions and various other properties of each control individually and this also applies when you need to disable all the controls that can invoke the command.

An action is a non-visual component that represents a user-generated command. It allows you to set up all the UI properties related to that command in one central place, along with the code required to execute the command and also code that can control if the command is available to the user or not. Actions are managed either through action lists, which are also non-visual components, or through an action manager (also non-visual).

You connect actions to various trigger controls which can invoke the action. The UI properties and command functionality are both automatically propagated from the action to these controls. If any property of the action is modified at any point, these changes are also immediately propagated. So for example, if the action gets disabled at any point then all the related controls are disabled in turn. Code that controls whether the actions are available or not is automatically called during idle time, meaning that UI controls that can invoke the action are automatically enabled and disabled as appropriate.

The trigger controls that are designed to invoke the action's code are called action clients. The controls affected by the action are described as action targets or simply targets. Normally, when you create actions in the IDE you do not explicitly specify action targets (there is no place to do so). Instead, the action's code affects various action targets. We will see how action targets gain more significance later.

Why Should We Use Actions?

The answer to this question is simply because they are easier to deal with. They allow application code to be modularised and defined independently of the controls that will invoke the code. Actions are also automatically updated, thereby updating the action clients. The Delphi IDE (from Delphi 4 onwards) is positively chock full of action objects. Actions are responsible for all the tool button/menu controls that become enabled and disabled as circumstances change in the IDE.

Let's try implementing a simple application a few times, firstly without actions in the normal way, then using actions. Hopefully, you will see that actions simplify the development of application functionality and the management of a smooth UI. The application has an edit control (edtEntry), a button (btnAddString) and a listbox (lstEntries), as shown in Figure 1. The button's job is to add the edit's contents into the listbox. However, it only does this if the edit control does not contain a blank string or a string already contained in the list.

Figure 1: The application without actions

The application without actions

The first attempts will be without actions. There are two approaches we can choose from. One is to implement the button's OnClick handler as shown in Listing 1. As you can see, the basic job of adding the text into the listbox is done here, as is the validation of whether to perform the job at all. In this case the button is always enabled, but sometimes pressing it has no effect.

Listing 1: Functionality and validation in one place
procedure TForm1.btnAddStringClick(Sender: TObject);
begin
  if ( Length( Trim( edtEntry.Text ) ) > 0 ) and
     ( lstEntries.Items.IndexOf( Trim( edtEntry.Text ) ) = -1 ) then
    lstEntries.Items.Add( Trim( edtEntry.Text ) );
  //Give focus back to edit
  edtEntry.SetFocus;
  //Highlight edit contents so it can be replaced by overtyping
  edtEntry.SelectAll
end;

In more complex situations it could prove advantageous to split the actual implementation of the job from the validation code, as in Listing 2. Here, the validation is performed when the edit content is changed, and also after adding a string into the list. Invalid input in the list is avoided by ensuring the button is disabled when invalid data is in the edit, which perhaps gives a more intuitive user interface (see Figure 1). However to achieve this, the button must also be disabled in the Object Inspector, since the edit will start its life empty. This code can be found in the ActionLessApp.dpr project in the files that accompany this article (there is a VCL and an FMX version).

Listing 2: The functionality split from the validation

procedure TForm1.btnAddStringClick(Sender: TObject);
begin
  lstEntries.Items.Add( Trim( edtEntry.Text ) );
  //Give focus back to edit
  edtEntry.SetFocus;
  //Highlight edit contents so it can be replaced by over-typing
  edtEntry.SelectAll;
  //Trigger edit's OnChange to ensure button
  //is enabled or disabled as appropriate
  edtEntry.OnChange( edtEntry )
end;

procedure TForm1.edtEntryChange(Sender: TObject);
begin
  btnAddString.Enabled :=
    ( Length( Trim( edtEntry.Text ) ) > 0 ) and
    ( lstEntries.Items.IndexOf( Trim( edtEntry.Text ) ) = -1 )
end;

Figure 2: Prototype number 2

Prototype number 2

As you can see, the validation is almost automatic, but not quite. When the text is added to the list, the edit's OnChange event must be explicitly invoked to ensure that the button is disabled whilst the edit contains a string contained in the list box.

Now think about what would be needed if more controls could invoke the string adding behaviour. You would need to share the button's OnClick handler with all the other controls. You would also need to set the Enabled property of all the other controls in the edit's OnChange event handler. This would quickly get messy to manage and maintain. This is where actions become very useful, although they are just as appropriate when only a single control invokes some behaviour.

How Actions Are Used

You typically set up actions before putting the action client controls on the form. Once the actions are defined, it is then easy to add client controls and associate them with the actions.

In order to rebuild this application with actions and see how things change, let's get some basic background first. The full details will be covered later.

Actions are managed either by action lists (TActionList components in Delphi 4 and later) or by action managers (TActionManager components in Delphi 6 and later). You can use as many action lists as you like, perhaps using multiple instances to keep actions in different logical groups. Each action list can be associated with a TImageList that contains small images that can optionally be used to represent each action. If you have an image list set up, use the action list's Images property to make the connection.

An action manager can act as a more functional action list, and can also link with existing action lists so that it can manage all actions in an application.

Creating Actions

Once you place an action list on a form you can add actions using the Action List Editor available by right-clicking or double-clicking on it (see Figure 3). This allows you to create new actions and new standard actions (see later for details on standard actions).

Figure 3: The Action List Editor

The Action List Editor

Clicking the yellow button (or pressing Insert) makes a new action. Whilst selected, the Object Inspector can be used to set up the UI properties that represent the action. Figure 4 shows a new action with a number of properties including a shortcut key (ShortCut) and an index into the action list's image list (ImageIndex), although action images are not yet supported in FMX. The action's image is shown in the VCL action list editor.

Figure 4: Editing an action's properties

Editing an action's properties

One property that warrants description is Category. All actions in an action list can have a category (a string) but normal actions default to having none (a blank string). You can choose categories for each of your actions and the action list editor will list all categories in its left list box. When any given category is selected, only the actions from that category are listed in the right list box. As your use of actions increases, separating different actions into different action lists and then having actions in a given list grouped by their categories, helps manage them.

The Object Inspector also allows you to set up a number of events for each action, the most important of which are OnExecute and OnUpdate. OnExecute should perform the job represented by the action. OnUpdate should verify whether the action is still valid.

Suitable action event handlers for an action that can work in our application are shown in Listing 3 (they can be found in the VCL or FMX ActionApp.dpr project that accompanies this article). Notice that this time, unlike with Listing 2, the string adding code does not need to trigger the validation code. Instead it relies on the code being automatically called, which it will be (again, the details are coming).

Listing 3: Action event handlers

procedure TForm1.actAddStringExecute(Sender: TObject);
begin
  lstEntries.Items.Add( Trim( edtEntry.Text ) );
  //Give focus back to edit
  edtEntry.SetFocus;
  //Highlight edit's content so it can be replaced by over-typing
  edtEntry.SelectAll;
end;

procedure TForm1.actAddStringUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled :=
    ( Length( Trim( edtEntry.Text ) ) > 0 ) and
    ( lstEntries.Items.IndexOf( Trim( edtEntry.Text ) ) = -1 )
end;

At this stage we have not specified any action clients, although the code implicitly identifies action targets of the edit control and list box. However, these are hard-coded in source and so do not quite fit the normal definition of action targets (more on action targets later).

Invoking Actions

At this stage we need a way to invoke the action. Normal procedure would involve adding action clients and connecting the action to them, but this is not strictly necessary. The action has already defined a shortcut (Ctrl+A). At run time, pressing Ctrl+A will automatically invoke the action if it is available (in other words is enabled), which frankly, is rather clever.

In this application though, action clients are required. Drop a button on the form and use the Action property to connect it to the only available action, actAddString. It will immediately absorb all appropriate properties and events from the action, which are Caption, Enabled, HelpContext, Hint, Visible and OnExecute (assigned to OnClick). These make the button look and behave sensibly. When the button is clicked, the action will be invoked.

Note: You use the Action property to choose a pre-defined action object to hook up to your action client control. However, if an action list exists on the form you can also use one of the two additional items added to the drop-down menu for the property editor in this same Action property to either create a new action or choose a standard action (see later).

Notice that the action's properties/events do not necessarily tie up with like-named properties/events in the client (for example, the action's OnExecute event handler is assigned to the button's OnClick handler). This allows more flexibility in associating actions with a whole variety of action clients.

If the default action client invocation mechanism is not suitable (say for example you want a control double-clicked to invoke the action, or the control has no Action property) this is no problem. You can either make the control's favoured event share the action's OnExecute event handler, if compatible, or alternatively make an explicit call to the action's Execute method in the control's event handler.

The action's OnUpdate event is regularly called during application idle time (full details later) and so if any of the validation conditions fail, the action's Enabled property is set to False. This change immediately propagates to the button causing it to become disabled, meaning that when the action is disabled the action client cannot trigger the action. The program running looks much like Figure 2 so another screenshot is pointless.

ActionApp2.dpr is another project, much like ActionApp.dpr but with additional controls. In the VCL app a TBitBtn is used instead of a TButton (which allows a small bitmap to be rendered on its surface). Also, it employs a popup menu for the list box, a main menu and a toolbar with a button on it. You will not be surprised to learn that each of these controls is hooked up to the action to prove a point. The action's properties propagate to all the action clients (see Figure 5) and when the action is disabled, all the action clients instantly disable.

Figure 5: The application with actions

The application with actions

Also, these other controls use more of the action's properties. For example, when a TMenuItem in the Menu Designer is connected to the action, the ShortCut property is copied along with Checked, ImageIndex and GroupIndex. In the VCL app, to make sure the menu item's ImageIndex had something to index into both the popup menu and main menu were initially connected to the same image list as the action list.

In the case of a VCL TToolButton, the action's Checked property value is copied to the Down property but most of the others go through to the correspondingly named property. Tool buttons also have an ImageIndex property so the toolbar was also connected to the image list in advance of connecting the toolbutton to the action. The FMX TSpeedButton does not have a Down property and doesn't seem to absorb the action's Checked property and, again, FMX actions do not yet support images.

One important point about a VCL tool button is worth making. Having connected the toolbar to the image list, the first tool button added to the toolbar will automatically display the action's image. This is just coincidental. The first toolbutton gets an ImageIndex of 0 automatically, the second gets an ImageIndex of 1 and so on. The toolbutton will still need to be connected to the action like any other action client.

You should be able to see the main point of action components now. Being automatically validated they give a smooth, reactive user interface with consistency amongst controls that invoke the same functionality.

When writing shared event handlers in applications that do not use actions, it is common to use the Sender parameter as a means of identifying which UI control triggered the event, so that specific code can execute when some specified control triggers it. When using actions, the Sender parameter of the OnExecute event handler that is propagated around will be the action itself, not the UI control. Delphi 6 added the ActionComponent property to all actions, which is set to the action client just before OnExecute is fired and set back to nil afterwards.

Action Managers And ActionBands

The final iteration of this project is ActionApp3.dpr. This project shows the more modern way of building up a UI like the one in Figure 5 taking advantage of ActionBands. This is a VCL-only project as, at the time of writing, ActionBands are restricted to the VCL; they are not supported by FM2.

From Delphi 6 you can dispense with action list components altogether and simply use an action manager, which allows you to create action components. However in this case we will start from the position we were at with ActionApp.dpr and make use of the existing action list.

These additional action components are sat at the end of the Additional page of the Component Palette. The TActionManager component is the enhanced action list replacement that we will use here. You can also find the Action Band components, TActionMainMenuBar, TPopupActionBar and TActionToolBar.

As you can probably guess, the TActionMainMenuBar and TActionToolBar are specialised versions of a menu bar and a tool bar respectively (the TPopupActionBar is a descendant of the regular TPopupMenu). They allow you to very easily set up the UI for your actions using a drag and drop approach between the action manager's customisation dialog and the action band components. They also allow you to easily specify the style that the components will use; you can choose between standard (the Windows 98/2000 style) and XP (the Windows XP style). Another component you will find on the Additional page is TCustomizeDlg, which provides users with a runtime equivalent of the action manager's design-time configuration dialog, thereby making it very straightforward for users to customise their menu and toolbars.

To build our action application with the new components, proceed as follows. Drop a TActionManager on the form and connect it to the image list already on the form using its Images property. Double-clicking on the action manager invokes its customisation dialog where new actions can be added (amongst other features), but before we go there, we need to hook up to the existing action list component.

The property LinkedActionLists is a TLinkedActionListCollection object that represents a collection of references to action lists in your application. Press the button next to this property to invoke the collection editor and then either press the Insert key or the yellow button to add a TActionListItem item to the collection. The Object Inspector will show that this collection item has just two properties. The ActionList property allows you to connect it to the action list component, and the Caption property allows you to give it a user-friendly name, as shown in Figure 6.

Figure 6: Connecting an action manager to an existing action list

Connecting an action manager to an existing action list

With this step done, you can double-click the action manager component to see its customisation dialog, where you can see all the actions defined in our action list (which in this example amounts to a single action) as shown in Figure 7. At this point, it might be wise to specify a category for our sole action to make things more intuitive later. Select the action in the action manager dialog, and on the Object Inspector set the Category property to List.

Figure 7: Seeing all the actions under the action manager's control

Seeing all the actions under the action manager's control

With the action manager prepared, it is time to add one of the action bands to the form.

Get a TActionMainMenuBar from the Additional page of the Component Palette and place it on the form. To make the action manager fully aware of it, select the action manager component, then select its ActionBars property on the Object Inspector. Press the ellipsis button to bring up the ActionBars collection editor and press the Insert key to make a new ActionBar collection item. Use the ActionBar property to link it to your action menu bar component. However this step is actually not required, as the link will be made implicitly as soon as you start adding items to the Action Bar (action main menu bar or action toolbar) as described below.

To get an action-aware toolbar, you can pick a TActionToolBar from the Component Palette and use the same approach to tell the action manager about it. Alternatively, you can switch to the action manager dialog's Toolbars tab and press the New button.

Note: this page of the dialog also allows you to customise the tooltip settings used on the toolbars, although there was a bug in this area prior to Delphi 6 Update Pack 2.

To set up the menu bar and the toolbar you drag the single action from the action manager dialog's Actions tab onto the toolbar. You can also drag the whole List category of actions onto the menu bar. This adds a List menu on the menu bar, with menu items for each action in the category hanging off it. Figure 8 shows the action main menu bar being used, thereby temporarily obscuring the action toolbar just below it.

Figure 8: An ActionBand in use

An ActionBand in use

Styles and Colour Maps

In Delphi 7 and later you can readily switch the style that the Actionbands components display with. You do this with the action manager's Style property, which by default offers three values these days: Standard, XP Style and the more recent Platform Default. It defaults to XP Style and so the menus take on an XP look and feel. The sample Actionbands application in Delphi's Demos\ActionBands is called WordPad.dpr and this has a Style menu that allows you to dynamically switch between the styles, as well as enable or disable shadows drawn behind the menus.

You can see the difference between the styles here. Figure 9 shows this sample application using the standard style:

Figure 9: The standard ActionBands style

The standard ActionBands style

Figure 10 shows the effect of enabling the XP style. You can see the more colourful menu with the column for the menu icons. You can also see that toolbuttons have a different appearance when they are selected.

Figure 10: The XP ActionBands style

The XP ActionBands style

The general Actionbands style can be enhanced further using colour maps (introduced in Delphi 7). Each ActionBand component has a ColorMap property (a subcomponent property of type TCustomActionBarColorMap) that allows you to customise any of the colours used in drawing various parts of it. You can also make use of three colour map components that are supplied on the Additional page of the Component Palette: TStandardColorMap, TXPColorMap and TTwilightColorMap. These components define the default colours used by the standard and XP styles as well as a set of colours that give your application the same look and feel as the now defunct Microsoft Encarta encyclopedia.

Note: I'd approach the colour maps with caution as my simple test in Delphi XE3 didn't have good results. The action bars currently seem to ignore the colour map font colour when running on my Windows 8 and so I don't see any text. [BUG?]

Figure 11: The twilight (Encarta) colour map (compiled and running in Delphi 7)

The twilight (Encarta) colour map

Aesthetic Sugar

Making life easy for setting up menus and toolbars full of action clients is not the end of what Action Bands offer. Over and above the general UI style and colours that we saw above that we can change, you can customise the appearance of an action toolbar or any of its toolbuttons, as well as the main menu bar, and also each individual menu that hangs off it. For example, if you have an action menu bar with a File and a Help menu, you can customise the main menu bar, as well as the File menu and the Help menu.

The customisation on offer is quite impressive, matching the sorts of things Microsoft once did in their own applications, though it's probably safe to say that much of these capabilities make things look a little dated now. You can change the colour or supply a background bitmap to start with, which can optionally be stretched or tiled. This bitmap can also be used as a banner, laid down the right side (of a menu for example) or up the left side (rather like the Windows XP Start menu).

For example, Figure 12 shows the same application as before with the main menu bar and sporting a background bitmap.

Note: I did actually try and set the action toolbar to use a different background colour, but it failed to work correctly at runtime, a bit like the text colour. While it looked ok in the form designer, it reverted to the default menu colour at runtime. [BUG?]

Figure 12: Customised Action Bands

Customised Action Bands

Setting up these modifications is straightforward as they are all attributes of the action manager's ActionBars collection items (TActionBarItem objects). Bring up the collection editor for the ActionBars property and select the top level action bar you are interested in. The Object Inspector will show you the properties you need to use, including Background, BackgroundLayout and Color (though as previously noted, you'll need to test carefully to ensure things work at runtime as expected). To get to individual menus or submenus, use the Items property, which is a TActionClients collection, containing TActionClientItem objects. This has a similar set of properties, including Items for going down to further levels.

Note: as well as Items, there is also a ContextItems collection property. You can add new items to this collection (using the collection property editor or the Object TreeView) and connect the up to any action you choose using the Object Inspector. These context items will automatically appear in a popup menu when you right click on the relevant Action Bar.

Figure 13 shows the Borland demo project for Action Bands with a modified menu bar and Edit menu. As you can see, it uses a bitmap as a left-hand menu banner and also uses a tiled bitmap as a menu bar background. This was achieved as follows:

Figure 13: The Action Bands demo project with a menu banner bitmap

The Action Bands demo project with a menu banner bitmap

Usage-Aware Menus (aka Personalized Menus)

Personalized menus (sometimes known as IntelliMenus) were introduced in Windows 2000 and were also supported in Windows XP and versions of Office from around the same time. They involve having the more frequent menu items in a menu brising to the top of the menu and the less frequently used menu items being hidden away in a collapsed menu section, but being accessible by clicking the collapsed section on the double arrow that represents it.

Action bands support these usage-aware as you can see in Figure 14.

Figure 14: A menu with some items hidden through lack of use

A menu with some items hidden through lack of use

Figure 15 shows what you get if you hold your mouse over that menu item, or click on it.

Note: this usage support is automatic, and operates through a number of properties. To determine when to display or hide a UI element that is rendered by the action band for a specific TActionClientItem, the action manager maintains a count for each item of the total number of sessions it has been used in (TActionClientItem.UsageCount), the last session number it was used in (TActionClientItem.LastSession), and the current session number (TActionBars.SessionCount). The number of sessions is defined as the number of times the application has been launched.

This information is then tallied against a reference table, stored in the action manager's PrioritySchedule property. To determine if an item should be displayed, the program looks up the value of UsageCount in the left hand column in PrioritySchedule. The corresponding value in the right hand column is the number of sessions an item can be unused before it becomes hidden. If it has not been used in that many sessions, it becomes hidden.

Figure 15: All menu items are visible now

All menu items are visible now

Setting the UsageCount property of a TActionClientItem to -1 will ensure it is always shown. To disable this facility altogether, clear all the values from the PrioritySchedule property.

Note: Windows Vista dropped support for personalized menus and Office also followed by removing support for them. The appeal of them apparently wasn't so great after all. Consequently making use of this menu functionality is probably not wise.

Note: It would appear that at some point in time between Delphi 7 and Delphi XE2 the VCL broke its support for them. All the properties and methods are still there. However the option to enable or disable them on the Options tab of the Action Manager customisation dialog stays disabled (see Figure 16). This appears to be because it is only enabled if there is an action menu bar in the list of toolbars on the Toolbars tab of the dialog. However you can only add action toolbars in the list. Additionally, and perhaps more importantly, I found some disturbing issues when running the WordPad action bands sample (from Delphi XE3) on Windows XP. On first view of the menus all looks well, but when moving the mouse back over the menus most of the menu items had completely disappeared! I haven't spent too much time folloing this up, given the suggestion to ignore usage-aware menus, but be warned! [BUG?]

Figure 16: The personalized menus option is disabled

The personalizd menus option is disabled

End-User ActionBands Customisation

It has been mentioned that the action manager dialog can be made available at runtime. This can be done in one of two ways. You can add a TCustomizeDlg to a form, set its ActionManager property appropriately and call its Show method. Alternatively you can use the pre-supplied TCustomizeActionBars standard action. We will look more at how standard actions are accessed and used in the next section.

Saving Settings Across Sessions

As your users make modifications to their menus and toolbars using the customisation dialog, you will almost certainly want all their settings to be saved to disk between sessions. This is easily accommodated using the action manager's FileName property. When the application starts up, the action manager will read all the settings from the file, and when the application is being terminated, all the settings are written back to it.

Miscellanea

There is a lot of nice functionality on offer in the ActionBands classes that evades observation on first glances. One example can be found in the ActionBars property, which is an object that manages all the action bar items. Recall that an action bar item links the Action Manager to an Action Bar and also manages the action client items on the Action Bars). Whilst the help doesn't directly mention it, there is a useful method called IterateClients available in ActionBars (inherited from the ancestor TActionClientsCollection class).

This routine allows you to recursively execute a specified callback method against every action client item in a specified action client collection. This means you can have code execute against every item that sits on an Action Bar connected to an Action Manager using simple code such as in Listing 4. IterateClients takes a TActionClientsCollection object (such as an Action Manager's ActionBars property) and proceeds to iterate through all the action clients (TActionClientItem or TActionBarItem objects) it finds.

Note: IterateClients will locate action clients found in Items action client collections but does not look in the ContextItems properties.

Listing 4: Iterating over all action clients

procedure TForm1.ActionCallBack(AClient: TActionClient);
begin
  if AClient is TActionClientItem then
    ListBox1.Items.Add(TActionClientItem(AClient).Caption)
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  ListBox1.Clear;
  ActionManager1.ActionBars.IterateClients(
    ActionManager1.ActionBars, ActionCallBack);
end;

If you take the time to browse through the ActionBands source you will find plenty of other goodies. You can get a good demonstration of all the general ActionBands options by opening up the demo projects that come with Delphi, which can be found in Delphi\VCL\ActionBands and its subdirectories under the Samples directory. Figure 13 shows that demo running after I added a left banner image to one of the menus. Other demo apps (added in Delphi 7) to be found there show you how to build an MRU (most recently used) menu (like Delphi's File | Reopen menu), how to dynamically create ActionBand components, and how to use alpha-blending in ActionBand menus.

Standard Actions

The actions that we have been manually setting up are sometimes called custom actions, since the Delphi developer customises their behaviour and properties. Delphi also comes with quite a number of pre-defined actions with built-in behaviour and property values, which are referred to as standard actions, so named since their behaviour and attributes are supplied with Delphi as standard.

These standard actions provide commonly useful behaviour, such as clipboard interaction, MDI window commands and help commands. Table 1 shows the standard VCL actions that are supplied with Delphi and Table 2 shows the standard FMX actions.

Table 1: Standard VCL actions available in Delphi

Standard action class name Defined in Category Purpose
TEditCut Vcl.StdActns.pas Edit cuts highlighted text from the target to the Clipboard
TEditCopy Vcl.StdActns.pas Edit copy highlighted text to the Clipboard
TEditPaste Vcl.StdActns.pas Edit pastes text from the Clipboard to the target and ensures that the Clipboard is enabled for the text format
TEditSelectAll Vcl.StdActns.pas Edit selects all the text in the target edit control
TEditUndo Vcl.StdActns.pas Edit undoes the last change made to the target edit control
TEditDelete Vcl.StdActns.pas Edit deletes the highlighted text
TRichEditBold Vcl.ExtActns.pas Format toggles the bold attribute of the currently selected text in a rich edit control
TRichEditItalic Vcl.ExtActns.pas Format toggles the italic attribute of the currently selected text in a rich edit control
TRichEditUnderline Vcl.ExtActns.pas Format toggles the underline attribute of the currently selected text in a rich edit control
TRichEditStrikeOut Vcl.ExtActns.pas Format toggles the strike-out attribute of the currently selected text in a rich edit control
TRichEditBullets Vcl.ExtActns.pas Format toggles whether the current paragraph in a rich edit control is bulleted
TRichEditAlignLeft Vcl.ExtActns.pas Format left-justifies the text in the current paragraph of a rich edit control
TRichEditAlignRight Vcl.ExtActns.pas Format right-justifies the text in the current paragraph of a rich edit control
TRichEditAlignCenter Vcl.ExtActns.pas Format centres the text horizontally in the current paragraph of a rich edit control
THelpContents Vcl.StdActns.pas Help brings up the Help Topics dialog on the tab (Contents, Index or Find) that was last used
THelpTopicSearch Vcl.StdActns.pas Help brings up the Help Topics dialog on the Index tab
THelpOnHelp Vcl.StdActns.pas Help brings up the Microsoft help file on how to use Help
THelpContextAction Vcl.StdActns.pas Help brings up the help topic for the active control
TWindowClose Vcl.StdActns.pas Window closes the active MDI child form
TWindowCascade Vcl.StdActns.pas Window cascades the MDI child forms
TWindowTileHorizontal Vcl.StdActns.pas Window arranges MDI child forms so that they are all the same size, tiled horizontally
TWindowTileVertical Vcl.StdActns.pas Window arranges MDI child forms so that they are all the same size, tiled vertically
TWindowMinimizeAll Vcl.StdActns.pas Window minimises all of the MDI child forms
TWindowArrange Vcl.StdActns.pas Window arranges the icons of minimised MDI child forms
TFileOpen Vcl.StdActns.pas File displays a file open dialog
TFileOpenWith Vcl.StdActns.pas File display dialog that lets users choose what application to user for opening a file
TFileSaveAs Vcl.StdActns.pas File displays a file save as dialog
TFilePrintSetup Vcl.StdActns.pas File displays a print setup dialog
TFilePageSetup Vcl.StdActns.pas File displays a page setup dialog
TFileRun Vcl.ExtActns.pas File launches an application, or performs some other registered file operation
TFileExit Vcl.StdActns.pas File terminates the application
TBrowseForFolder Vcl.StdActns.pas File displays the folder browse dialog
TSearchFind Vcl.StdActns.pas Search displays a find dialog
TSearchFindFirst Vcl.StdActns.pas Search displays a find dialog that looks for the first string
TSearchReplace Vcl.StdActns.pas Search displays a find-and-replace dialog
TSearchFindNext Vcl.StdActns.pas Search locates the next instance of a string in an appropriate control
TPreviousTab Vcl.ExtActns.pas Tab moves a page/tab control to the previous tab
TNextTab Vcl.ExtActns.pas Tab moves a page/tab control to the next tab
TListControlCopySelection Vcl.StdActns.pas List copies selected items in a list control (listbox or list view) to another list control
TListControlDeleteSelection Vcl.StdActns.pas List deletes all selected items in a list control
TListControlSelectAll Vcl.StdActns.pas List selects all items in a list control
TListControlClearSelection Vcl.StdActns.pas List deselects all items in a list control
TListControlMoveSelection Vcl.StdActns.pas List moves selected items in a list control (listbox or list view) to another list control
TStaticListAction Vcl.ListActns.pas List provides items to client list controls
TVirtualListAction Vcl.ListActns.pas List provides items to client list controls
TOpenPicture Vcl.ExtActns.pas Dialog displays the open picture dialog
TSavePicture Vcl.ExtActns.pas Dialog displays the save picture dialog
TColorSelect Vcl.StdActns.pas Dialog displays the colour section dialog
TFontEdit Vcl.StdActns.pas Dialog displays the font edit dialog
TPrintDlg Vcl.StdActns.pas Dialog displays the print dialog
TBrowseURL Vcl.ExtActns.pas Internet launches the default browser to display a specified URL
TDownLoadURL Vcl.ExtActns.pas Internet saves contents of a specified URL to a file
TSendMail Vcl.ExtActns.pas Internet sends an email message
TDataSetFirst Vcl.DbActns.pas Dataset sets the current record to the first record in the dataset
TDataSetPrior Vcl.DbActns.pas Dataset sets the current record to the previous record
TDataSetNext Vcl.DbActns.pas Dataset sets the current record to the next record
TDataSetLast Vcl.DbActns.pas Dataset sets the current record to the last record in the dataset
TDataSetInsert Vcl.DbActns.pas Dataset inserts a new record before the current record, and sets the dataset into dsInsert state so it can be modified
TDataSetDelete Vcl.DbActns.pas Dataset deletes the current record and makes the next record (if there is one, otherwise the previous record) the current record
TDataSetEdit Vcl.DbActns.pas Dataset puts the dataset into dsEdit state so that the current record can be modified
TDataSetPost Vcl.DbActns.pas Dataset writes changes in the current record to the dataset
TDataSetCancel Vcl.DbActns.pas Dataset cancels the edits to the current record, restores the record display to its condition prior to editing, and turns off dsInsert or dsEdit states if they are active
TDataSetRefresh Vcl.DbActns.pas Dataset refreshes the buffered data in the associated dataset by calling its Refresh method
TClientDataSetApply Vcl.DbClientActns.pas DataSnap Client applies the updates in a client dataset's change log
TClientDataSetRevert Vcl.DbClientActns.pas DataSnap Client back out all changes in a client dataset's current record
TClientDataSetUndo Vcl.DbClientActns.pas DataSnap Client back out the last edit to a client dataset's current record
TBindNavigateFirst Vcl.Bind.Navigator.pas LiveBindings sets current record to be first record in LiveBindings data source
TBindNavigatePrior Vcl.Bind.Navigator.pas LiveBindings sets current record to be previous record in LiveBindings data source
TBindNavigateNext Vcl.Bind.Navigator.pas LiveBindings sets current record to be next record in LiveBindings data source
TBindNavigateLast Vcl.Bind.Navigator.pas LiveBindings sets current record to be last record in LiveBindings data source
TBindNavigateInsert Vcl.Bind.Navigator.pas LiveBindings insert a new record in LiveBindings data source
TBindNavigateDelete Vcl.Bind.Navigator.pas LiveBindings delete record from LiveBindings data source
TBindNavigateEdit Vcl.Bind.Navigator.pas LiveBindings put LiveBindings data source into edit mode
TBindNavigatePost Vcl.Bind.Navigator.pas LiveBindings write changes in current record to LiveBindings data source
TBindNavigateCancel Vcl.Bind.Navigator.pas LiveBindings cancel edits in current record of LiveBindings data source and leave edit/insert mode
TBindNavigateRefresh Vcl.Bind.Navigator.pas LiveBindings refresh current record from LiveBindings data source
TBindNavigateApplyUpdates Vcl.Bind.Navigator.pas LiveBindings apply all pending updates to LiveBindings data source
TBindNavigateCancelUpdates Vcl.Bind.Navigator.pas LiveBindings cancel all pending updates to LiveBindings data source
TCustomizeActionBars Vcl.BandActn.pas Tools causes the Action Bands customisation dialog to appear
THintAction Vcl.StdActns.pas None First documented in Delphi 6 as a way of displaying custom hints in the same way as done by TStatusBar.AutoHint.

Table 2: Standard FMX actions available in Delphi

Standard action class name Defined in Category Purpose
TVirtualKeyboard FMX.StdActns Edit shows an on-screen keyboard
TWindowClose FMX.StdActns Window close the active window
TFileExit FMX.StdActns File exit app
TFileHideApp FMX.StdActns File  
THideAppOthers FMX.StdActns File  
TViewAction FMX.StdActns View  
TFMXBindNavigateFirst Fmx.Bind.Navigator LiveBindings sets current record to be first record in LiveBindings data source
TFMXBindNavigatePrior Fmx.Bind.Navigator LiveBindings sets current record to be previous record in LiveBindings data source
TFMXBindNavigateNext Fmx.Bind.Navigator LiveBindings sets current record to be next record in LiveBindings data source
TFMXBindNavigateLast Fmx.Bind.Navigator LiveBindings sets current record to be last record in LiveBindings data source
TFMXBindNavigateInsert Fmx.Bind.Navigator LiveBindings insert a new record in LiveBindings data source
TFMXBindNavigateDelete Fmx.Bind.Navigator LiveBindings delete record from LiveBindings data source
TFMXBindNavigateEdit Fmx.Bind.Navigator LiveBindings put LiveBindings data source into edit mode
TFMXBindNavigatePost Fmx.Bind.Navigator LiveBindings write changes in current record to LiveBindings data source
TFMXBindNavigateCancel Fmx.Bind.Navigator LiveBindings cancel edits in current record of LiveBindings data source and leave edit/insert mode
TFMXBindNavigateRefresh Fmx.Bind.Navigator LiveBindings refresh current record from LiveBindings data source
TFMXBindNavigateApplyUpdates Fmx.Bind.Navigator LiveBindings apply all pending updates to LiveBindings data source
TFMXBindNavigateCancelUpdates Fmx.Bind.Navigator LiveBindings cancel all pending updates to LiveBindings data source

You create standard actions from either the action list editor or the action manager's customisation dialog. However, instead of pressing the yellow button, you should drop down the arrow next to it and choose New Standard Action... from the drop down menu (or press Ctrl+Insert). This takes you to a dialog that lists all available standard actions (see Figure 17).

Figure 17: The standard action choice dialog

The standard action choice dialog

If an action list or action manage exists on the form, you can also create a new standard action that is automatically associated with an action client control by going to the control in question and choosing New Standard Action from the Action property's dropdown editor. In Figure 18 a new standard action is being added to an action list called ActionList on the form (it could equally be added to the action manager called ActionManager1). This new action will be connected to the currently selected control.

Figure 18: Another way to create a standard action

Another way to add a standard action to an action list or action manager

Assuming you have an image list associated with your action list, when a standard action is created it will add its associated image into your image list and set its ImageIndex property to the position of it, assuming it has one. It also sets up its other property values to pre-defined values (see Figure 19). However I think it's fair to say that the set of default action images is long overdue for an overhaul to make to look current and up to date.

Figure 19: Instances of each of Delphi's standard actions

Instances of each of Delphi's standard actions

Some of these standard actions have an additional property that can be used to specify a dedicated target control. For example, all the dataset actions have a published DataSource property that appears on the Object Inspector. You can optionally use this property to connect the actions to one specific data source component but, again, this is not required. If you leave the property blank, the magic of action handling will allow the action to find the first data source on the active form at run-time.

Similarly all the standard edit actions have a public Edit property and the standard window actions have a Form public property. You can therefore programmatically tie an edit action to a fixed edit control, or a window action to one specific form. But if you do not, the edit action will act on the active edit control (if there is one) and the window action will act on the active MDI form (if there is one).

Some action components manage their own extra components on your behalf, without requiring you to set up your own. For example, the TFileOpen standard action manages its own internal open dialog component, whose properties you can see by expanding its Dialog property.

Classes Involved With Actions

There are a lot of classes associated with actions and so, rather than simply listing them out, I will try and give an overview that encompasses them all. If you have no desire to know more about the internal workings of actions or of how to make reusable standard actions, you should perhaps skip the rest of this article. It is dirty-hand territory from here on in.

Action Classes

The action class hierarchy (see Figure 20) starts with TBasicAction from System.Classes (see Listing 5) which can be used in conjunction with an action client that is neither a menu nor a control. TContainedAction (from System.Actions) adds support to allow an action to appear in an action list. It also adds the published Category property to allow actions to be categorised and the UI properties that can be propagated to action clients such as menus and controls, which are not published. TCustomAction (Vcl.ActnList or Fmx.ActnList) adds in the image list support (in the case of the VCL) and adds in the awareness of action lists and TAction (also in Vcl.ActnList and Fmx.ActnList) publishes all the interesting properties of TCustomAction.

Listing 5: The TBasicAction base action class

TBasicAction = class(TComponent)
private
  FClients: TList;
  [Weak] FActionComponent: TComponent;
  FOnChange: TNotifyEvent;
  FOnExecute: TNotifyEvent;
  FOnUpdate: TNotifyEvent;
  function GetClientCount: Integer;
  function GetClient(Index: Integer): TBasicActionLink;
  procedure SetActionComponent(const Value: TComponent);
protected
  procedure Change; virtual;
  procedure SetOnExecute(Value: TNotifyEvent); virtual;
  property OnChange: TNotifyEvent read FOnChange write FOnChange;
  procedure Notification(AComponent: TComponent; Operation: TOperation); override;
  property ClientCount: Integer read GetClientCount;
  property Clients[Index: Integer]: TBasicActionLink read GetClient;
public
  constructor Create(AOwner: TComponent); override;
  destructor Destroy; override;
  function HandlesTarget(Target: TObject): Boolean; virtual;
  procedure UpdateTarget(Target: TObject); virtual;
  procedure ExecuteTarget(Target: TObject); virtual;
  function Execute: Boolean; dynamic;
  procedure RegisterChanges(const Value: TBasicActionLink);
  procedure UnRegisterChanges(const Value: TBasicActionLink);
  function Update: Boolean; virtual;
  property ActionComponent: TComponent read FActionComponent write SetActionComponent;
  property OnExecute: TNotifyEvent read FOnExecute write SetOnExecute;
  property OnUpdate: TNotifyEvent read FOnUpdate write FOnUpdate;
end;

Figure 20: The action class hierarchy

The action class hierarchy

You will notice that THintAction is also sitting in the hierarchy (and was listed in Table 1). This class has been around since Delphi 4, but was first documented in Delphi 6. You can use THintAction to display component hints as the mouse moves around a form. TStatusBar uses this approach in order to implement its AutoHint property.

Action Link Classes

Whilst apparently changing the subject, but not really, I will talk briefly about data aware controls. Data aware controls and data source components appear to be directly connected through the data aware controls' DataSource property. However this is not actually the case. Instead, data link objects are employed in any data aware control that implements a DataSource property to act as the liaison officer, to represent the link to the dataset and to respond to data events. Similarly, action clients that implement an Action property use action link objects to connect action components to their properties (such as Caption, Hint and ShortCut).

Action links exist as various classes in a mini-hierarchy (see Figure 21) with TBasicActionLink at the root (see Listing 6). This class takes the client object as a constructor parameter (although it does not store it) and the related action is available as the Action property. It sets up the basic structure of a connection between an action and a client. It defines virtual Execute and Update methods which call the associated action's Execute and Update methods. If the action has an OnExecute event handler, Execute returns True and if it has an OnUpdate handler, Update returns True. It also has an OnChange event triggered when the properties of the action change.

Figure 21: The action link class hierarchy

The action link class hierarchy

Listing 6: The TBasicActionLink base action link class

TBasicActionLink = class(TObject)
private
  FOnChange: TNotifyEvent;
  [Weak] FAction: TBasicAction;
protected
  procedure AssignClient(AClient: TObject); virtual;
  procedure Change; virtual;
  function IsOnExecuteLinked: Boolean; virtual;
  procedure SetAction(Value: TBasicAction); virtual;
  procedure SetOnExecute(Value: TNotifyEvent); virtual;
public
  constructor Create(AClient: TObject); virtual;
  destructor Destroy; override;
  function Execute(AComponent: TComponent = nil): Boolean; virtual;
  function Update: Boolean; virtual;
  property Action: TBasicAction read FAction write SetAction;
  property OnChange: TNotifyEvent read FOnChange write FOnChange;
end;

TContainedActionLink adds in basic support for managing the connection between an action's properties and the action client properties (see Listing 7). It has elementary support for Caption, Checked, Enabled, GroupIndex, HelpContext, HelpKeyword, Hint, ImageIndex (in the VCL), ShortCut and Visible. The IsXXXXLinked methods all return True if Action has been assigned a TCustomAction or descendant whilst the SetXXXX methods do nothing. TActionLink is a shallow descendant of TContainedActionLink that adds nothing to it.

A TContainedActionLink or a TActionLink can be used as a base class for an action link that can be used when the action client is neither a control nor a menu (which are catered by descendant action link classes).

Listing 7: The TActionLink class

///  This class is designed to communicate with some of the object. 
///  It implements to work with common properties for all platforms (FMX, VCL).
TContainedActionLink = class(TBasicActionLink)
protected
  procedure DefaultIsLinked(var Result: Boolean); virtual;
  function IsCaptionLinked: Boolean; virtual;
  function IsCheckedLinked: Boolean; virtual;
  function IsEnabledLinked: Boolean; virtual;
  function IsGroupIndexLinked: Boolean; virtual;
  function IsHelpContextLinked: Boolean; virtual;
  function IsHelpLinked: Boolean; virtual;
  function IsHintLinked: Boolean; virtual;
  function IsImageIndexLinked: Boolean; virtual;
  function IsShortCutLinked: Boolean; virtual;
  function IsVisibleLinked: Boolean; virtual;
  function IsStatusActionLinked: Boolean; virtual;
  procedure SetAutoCheck(Value: Boolean); virtual;
  procedure SetCaption(const Value: string); virtual;
  procedure SetChecked(Value: Boolean); virtual;
  procedure SetEnabled(Value: Boolean); virtual;
  procedure SetGroupIndex(Value: Integer); virtual;
  procedure SetHelpContext(Value: THelpContext); virtual;
  procedure SetHelpKeyword(const Value: string); virtual;
  procedure SetHelpType(Value: THelpType); virtual;
  procedure SetHint(const Value: string); virtual;
  procedure SetImageIndex(Value: Integer); virtual;
  procedure SetShortCut(Value: System.Classes.TShortCut); virtual;
  procedure SetVisible(Value: Boolean); virtual;
  procedure SetStatusAction(const Value: TStatusAction); virtual;
end;

TMenuActionLink adds specific support for menu item clients by overriding AssignClient (which stores the client in a private data field) and IsOnExecuteLinked from Listing 6 as well as all the virtual methods in Listing 7. This allows actions to map their UI properties to equivalent menu item properties and their OnExecute to menu items' OnClick events. TControlActionLink does a similar job for generic controls, although it only skips some of the action properties, leaving those to descendant classes.

The other action link classes add support for various other specific properties of their indented client controls. For example TToolButtonActionLink works with tool buttons, adding a link between the action's Checked property and the tool button's Down property (note the difference in name).

Being aware of action links and how they fit in is generally useful, however getting down to the nitty-gritty of how they operate is only important if you wish to write interesting new components with new properties that you want controlled by actions. Given that we will not be covering that subject in this article, it is safe to leave action links alone now.

How The Action Architecture Works

Now that we have seen the basic use of actions and had an overview of the classes involved, let's look in more detail at how they work inside a Delphi application. On our travels you will see that the engineers who developed actions have provided many points that can be used to hook into action functionality. The execution path of actions presented here will be quite detailed, to give a full understanding of what goes on.

Action is defined as a public property in TControl (it is published by a number of descendant classes) and a published property in TMenuItem. When you assign an action component to an action client's Action property the following sequence of events occurs.

In the case of a control, csActionClient is added to its ControlStyle set property. Then if there is no action link object present already it creates an action link using the class reference returned by the protected dynamic GetActionLinkClass method. This returns an appropriate action link class of which an instance is created.

The action link is given the action object and its OnChange event is handled by a method that copies the key action properties to the action client properties. Since the action has just been set, this routine (a protected dynamic procedure called ActionChange) is triggered to get the current action properties copied across. At this point, the client has got the action properties and an appropriate action link object.

How Actions Are Invoked

Now we need to see what happens when an action is invoked. Remember this can happen by an action client invoking it, such as a button being clicked, or by any piece of code calling the action's Execute method. It can also happen by the user pressing the shortcut key associated with an action, which need not be connected to any action client. By the end of this section we should be able to see how each of these possibilities works.

We start by taking the case of an action client invoking the action, using an example of a button hooked up to an action. When you look at a button set up as an action client you can see the Object Inspector showing the OnClick event connected to the action's OnExecute event handler. You might therefore understandably think that clicking the button will simply call the action's OnExecute handler. But it is not as simple as that. Oh no.

Instead, assuming the OnClick event has not been changed, the action link's virtual Execute method is called (see Listing 8) with a parameter equating to the action client (this becomes the action's ActionComponent property for the duration of the OnExecute handler). The default implementation of this in TBasicActionLink (which is not overridden in any of the descendant classes) calls the action's dynamic Execute method. So at this point the action client invoking the action now looks just the same as the some code explicitly invoking an action by calling the Execute method.

Listing 8: The implementation of TControl.Click

procedure TControl.Click;
begin
  { Call OnClick if assigned and not equal to associated action's OnExecute.
    If associated action's OnExecute assigned then call it, otherwise, call
    OnClick. }
  if Assigned(FOnClick) and (Action <> nil) and not DelegatesEqual(@FOnClick, @Action.OnExecute) then
    FOnClick(Self)
  else if not (csDesigning in ComponentState) and (ActionLink <> nil) then
    ActionLink.Execute(Self)
  else if Assigned(FOnClick) then
    FOnClick(Self);
end;

The base action class (TBasicAction) implementation of Execute tries to call the action's OnExecute event handler. Both Execute methods return True if the handler exists and False if not. However TCustomAction overrides Execute to perform more interesting logic. Since custom actions can be managed by action lists, they broaden the potential for response to an action by a wide margin.

Firstly, the action defers to its action list (if it has one) by calling its ExecuteAction method and passing itself as a parameter. The action list's ExecuteAction method is implemented to call its OnExecute event handler if present. If the event handler sets its Handled parameter to True, the story ends here. Otherwise it goes further.

Note: all the actions in an action list will trigger the action list's OnExecute event handler and so some generic handling or action tracking can be implemented there if needed.

Next, the Application object's OnActionExecute event is triggered if present. Similarly, if the Handled parameter is set to True, processing ends there, otherwise it carries on.

Note: the Application object's OnActionExecute event will be triggered for all actions in the application that are not handled by their corresponding action list, providing an option for completely general action handling, or alternatively a way to intercept all actions not handled by their action list.

If the action still has momentum, it at last tries to execute its own OnExecute event handler. If no handler was set up, which will be the case for standard actions, it goes still further. It packages itself up in a CM_ACTIONEXECUTE message which gets sent to the Application object's window as a cry for help in trying to find a target to execute against. The Application object's message handler calls its DispatchAction method which tries to locate a target on the active form and, failing that, the main form.

It does this by sending the same message to the active form and then potentially the main form. Each form then verifies that it is visible and if so, tries to find a target control. Firstly, it checks whether the active control is a suitable target by passing the action object to the control's ExecuteAction method. If not, the form itself is tested to see if it might be a target, passing the action to its own ExecuteAction method. If there is still no joy it calls ExecuteAction for every visible control on the form, stopping if it finds a match.

The implementation of a component's ExecuteAction method typically involves the component passing itself to the action's HandlesTarget method. If this returns True, we have a suitable target and so the target is passed to the action's ExecuteTarget method.

This way, an action target can be found for an action without the target knowing anything about the action in advance. However, ExecuteAction can be overridden to allow any control to pick up specific actions of interest, if needed, without the action knowing about the target. It works both ways.

If the main form does not successfully handle the message then the final step is reached. If the action is a TCustomAction (or inherited from that class), is currently enabled, has no OnExecute handler and its DisableIfNoHandler property is True, then the action is disabled. DisableIfNoHandler is a public property defined by TContainedAction which defaults to True.

TCustomAction also calls the virtual Update method to update the action's state before setting off on this possibly lengthy trek to execute the action. This ensures the action's state is up-to-date based upon the immediately current state of the application before being executed.

As a bit of a contrast the FireMonkey action execution implementation is a little more straightforward. There are various things that can't be done in the same way with FireMonkey thanks to a lack of a message pasing model underlying the framework. Consequently the TCustomAction.Execute has a rather simpler implementation. After calling Update it checks its Enabled property is True and then calls the Execute inherited from TBasicAction. The implication of this is that FireMonkey will not manage to locate an action target without it being connected to it in some way. This presumably explains why there is a smaller set of standard actions available for FireMonkey at present.

The case with actions we have not looked at yet is where an action's shortcut key is pressed, causing the action to be invoked, regardless of whether an action client has been set up or not. Let's run through the VCL's logic first:

  1. Whenever a keystroke is pressed and is not handled by the active control or a suitable popup menu item, it is passed to the underlying form's IsShortCut method.
  2. The form tries to handle the keystroke through its OnShortCut event or, failing that, through its main menu.
  3. If nothing wants it, all action lists owned by the form are checked for a matching shortcut. The action list checks each of its actions and if a match is found, the action's Execute method is called.
  4. If no suitable action is found on the current form a CM_APPKEYDOWN message is sent to the Application object which calls its own IsShortCut method.
  5. This tries to handle the keystroke in its own OnShortCut event and if that fails it calls the main form's IsShortCut method.

This way a shortcut key can be picked up by an action on the active form or the main form, assuming a menu item or OnShortCut event does not handle it first.

Now let's consider the FMX logic for locating and executing an action that is not connected to an action client:

  1. When a keystroke is picked up by a form in TCommonCustomForm.KeyDown then it tries to find something to handle it in the following order.
  2. Firstly the focused control is tested, and then if necessary it checks the popup menu of the focused control.
  3. If not handled it looks at the form's main menu and then any other popup menus on the form.
  4. If still not handled it looks at other controls on the form.
  5. At this point it will then start looking at the actions in any action lists on the form, seeking something to handle the keystroke.
  6. If it still fails to find anything it finally looks for a response from menu items in main menus or actions in action lists on other forms, looking at the main form first.

As you can see, the VCL goes to a lot of trouble to service actions if they do not have an OnExecute event or even an action client, which standard actions tend not to. It is this concerted search effort that enables standard actions to work without necessarily being connected to an action client. They can typically operate on the active control (or some other suitable control) on the active form thanks to the VCL's in-built target-searching logic.

How Actions Are Updated

As mentioned, TCustomAction updates an action just before trying to execute it. However, actions also get updated at another point in time. When a VCL application has processed all of its pending messages it transitions into an idle state. Windows wakes it up when another message arrives. The same basic behaviour happens in FireMonkey applications.

In the case of the VCL, this is the sequence of events that occurs:

  1. Just before going idle the Application object's Idle method calls its OnIdle event handler and then calls the DoActionIdle method.
  2. DoActionIdle loops through all enabled forms on-screen calling UpdateActions.
  3. UpdateActions calls the virtual InitiateAction method for the form itself, all top level, visible menu items and then all visible controls with csActionClient in ControlStyle (in other words all action clients).
  4. InitiateAction calls the Update method of the action link, if there is one, which calls the action's Update method.
  5. The shared TBasicAction implementation of Update either calls OnUpdate if it exists and returns True, otherwise it returns False.
  6. The VCL-specific TCustomAction overrides Update to do much the same sort of thing as with Execute. It checks to see if the action list or Application object wishes to deal with updating the action in their OnActionUpdate events. Then it tries its own OnUpdate event handler.
  7. If there is no handler it asks the Application object to help find a target control to update itself against. The action is passed to each possible target's UpdateAction method which calls HandlesTarget. If the action claims to handle the target then the action's UpdateTarget method is called.

In an FMX app the sequence looks like this:

  1. The Application object's Idle method calls DoIdle
  2. DoIdle triggers the OnIdle event, if present, passing True as the value of the var parameter Done
  3. If Done is set to False then no action updating is performed, but if Done is still True then the actions will be updated
  4. Actions are either executed immediately or upon the expiration of a timer, depending on the value of the application's ActionUpdateDelay property:
  5. Action updates are initiated through the application's DoUpdateActions method. This calls UpdateActions on all active forms and then on all inactive forms.
  6. TCommonCustomForm.UpdateActions gathers up a list of menu items and controls on the form and calls their virtual TFmxObject.InitiateAction method.
  7. InitiateAction calls the Update method of the action link, if there is one, which calls the action's Update method.
  8. The shared TBasicAction implementation of Update either calls OnUpdate if it exists and returns True, otherwise it returns False.
  9. The FMX-specific TCustomAction overrides Update to check if the action list or Application object wishes to deal with updating the action in their OnUpdateAction events. Then finally it calls the inherited code and so tries its own OnUpdate event handler.

This all means that every time a user's input (key presses, mouse clicks, and so on) have been serviced and the program goes idle waiting for the next message, all actions connected to action clients are updated. Consequently, this means that the action clients always have an up-to-date representation of the action properties. Additionally, all actions (regardless of whether they are connected to clients or not) are updated just before they execute.

Because the CPU is so fast, the application will go idle between each key press and mouse click meaning that actions are updated very regularly. The implication of this is that you must ensure your action update code is not time-intensive to avoid having a sluggish application.

How Standard Actions Are Made

We have seen how to make custom actions and how to use standard actions, so now we turn our attention to making new standard actions of our own. The general idea is fairly straightforward although there are a few twists and turns here and there.

The goal is to inherit a class from TAction and override three methods. HandlesTarget decides whether we handle a given target control. UpdateTarget should check appropriate criteria and update the action properties if needed. ExecuteTarget contains the code represented by the standard action. These methods have all been mentioned before and are all defined as virtual in TBasicAction where they do nothing except HandlesTarget, which returns False, indicating nothing is handled.

If needed, the action can define a property to link it to a specific target component. If this is done you must be careful to hook into the standard notification mechanism so you are informed if the linked component is destroyed.

The standard action we will develop will be called THelpAbout and will invoke an application About box. It will have a public property that allows it to be connected to any form class in an application, which will be displayed modally as the About dialog. Now, the job of this action is clearly not too ambitious; it can readily be done in program code. However, wrapping functionality like this into an action means that it can easily be invoked from both a menu and a toolbutton.

Incidentally, the reason the property allows connecting to a form class, rather than a form object is so that the About form does not have to be auto-created, or programmatically created in advance. The action can create the About form, display it and then free it. This also means that unlike when connecting to an object, we will not need to hook into the notification system to spot if the form gets destroyed behind our back.

Also, the reason this property is public rather than published is that the Object Inspector does not display class reference properties.

Note: if this property has not been assigned a form class, the action will use the standard Windows About box, as used by Windows Explorer, Notepad, Calculator, etc. Listing 9 shows the action's code.

Listing 9: The common base class for the standard actions

type
  THelpAbout = class(TAction)
  private
    FAboutClass: TCustomFormClass;
  protected
    procedure SetAboutClass(Value: TCustomFormClass);
  public
    function HandlesTarget(Target: TObject): Boolean; override;
    procedure UpdateTarget(Target: TObject); override;
    procedure ExecuteTarget(Target: TObject); override;
    property AboutClass: TCustomFormClass read FAboutClass write SetAboutClass;
  end;
...
uses
  ShellAPI;

{ THelpAbout }

procedure THelpAbout.ExecuteTarget(Target: TObject);
var
  About: String;
begin
  if Assigned(AboutClass) then
    with AboutClass.Create(Application) do
      try
        ShowModal
      finally
        Free
      end
  else
  begin
    About := 'Windows ' + Application.Title;
    ShellAbout(Application.MainForm.Handle,
      PChar(About), nil, Application.Icon.Handle)
  end
end;

function THelpAbout.HandlesTarget(Target: TObject): Boolean;
begin
  //This action does not operate on any target controls,
  //so all proposed targets are acceptable
  Result := True
end;

procedure THelpAbout.SetAboutClass(Value: TCustomFormClass);
begin
  if Value <> FAboutClass then
    FAboutClass := Value;
end;

procedure THelpAbout.UpdateTarget(Target: TObject);
begin
  //Whether a custom about form class has been assigned or not,
  //this action will work (it uses the Windows About dialog if
  //no form class has been assigned)
  Enabled := True
end;

This code can be found in AboutAction.pas, which has been added into the AboutAction.dpk run-time package. Don't forget to place the compiled package (the BPL file) in a directory on the path to allow Delphi to see it.

Initialising Standard Actions

Apart from initialising its properties, the action is now ready to be registered. Typically, components perform property initialisation in their constructors. This would work fine for this action as well except for its associated image.

You may recall that standard actions can copy their image into the image list associated with their action list at design-time (see Figure 18 for a reminder). In order to allow our action to be just as friendly, we initialise its properties (image index, shortcut and so on) in a different way to normal components.

Registering Standard Actions

To register standard actions in the IDE, you call RegisterActions passing a category, a list of action classes and optionally, a data module class. The data module is used to pre-initialise the action properties and image. It works like this.

You make a data module, then drop an image list onto it, which you fill with images for your standard actions. Next, you place an action list on the data module and hook it up to the image list. The idea is that you then use the Action List Editor to create instances of your new standard actions whose properties you can initialise as you like. This data module class is then passed as the third parameter to RegisterActions.

However, if you think about this, in order to get your standard actions created through the action list editor they must first be registered so the IDE knows about them. So what we can do is ignore the data module issue to start with and register the action on its own (we can worry about the data module later).

This is done with a normal IDE registration routine in a registration unit (AboutActnReg.pas), as shown in Listing 10. Notice that nil is passed in place of the data module class. The registration unit is added to a design-time package (DCLAboutAction.dpk) which is compiled and installed.

Listing 10: First time registration of an action

procedure Register;
begin
  //The first-time registration to get the actions in the IDE,
  //but without initialised properties or an associated image
  //(which is the ImageIndex property)
  RegisterActions('Help', [THelpAbout], nil)
end;

Standard Actions And Data Modules

At this point the IDE knows about the new standard actions so the data module can now be set up. A suitable data module called AboutActionModule is in the unit AboutActionRes.pas, which should be added to the design-time package. The image list can now be set up to contain appropriate images and the action list can be added and connected to it.

The Action List Editor can now be used to create an instance of our new standard action, and its properties can be set as required. Figure 22 shows the action fully set up on the data module, with all its custom properties on the Object Inspector.

Figure 22: New standard actions being set up

New standard actions being set up

Now all that is left is to modify the registration routine in the design-time package to refer to the data module class. Listing 11 shows the final version. One final compile of the design-time package and the job is done. A new fully-fledged standard action is available for use.

Listing 11: Registering the actions

procedure Register;
begin
  //The first-time registration to get the actions in the IDE,
  //but without initialised properties or an associated image
  //(which is the ImageIndex property)
  //RegisterActions('Help', [THelpAbout], nil)

  //The second registration call, which refers to a data module
  //that holds an instance of the action with all its properties
  //set, and an image list containing its image
  RegisterActions('Help', [THelpAbout], TAboutAction)
end;

You are now free to make a new application using this new standard action to test it out. A sample project accompanies this paper called AboutActionTest.dpr and can be seen running in Figure 23.

Figure 23: A hand-crafted standard action in use

A hand-crafted standard action in use

The application has set the Application object's Title property to some descriptive string (SuperApp by Oblong) and you can see how this affects the Windows About dialog, used because I have not given the action a specific About box form class, in Figure 24.

Figure 24: The about action doing its thing

The about action doing its thing

Note: these steps have also been followed for a new action in the FMX framework. The action is called THelpAbout, defined in the FMXAboutAction.pas unit. This unit has been added to the FMXAboutAction.dpk package, whilst the action is registered in the DCLFMXAboutAction.dpk package through the FMXAboutActionReg.pas unit and the data module in the FMXAboutActionRes.pas unit.

The action is identical in all respects except that it does not call the Windows About box (that API might not be available, depending on the target platform the app is running on). Figure 25 shows a test program that has assigned the class type of an about box form to the new standard action's AboutClass property.

Figure 25: Testing the new FMX standard action

Testing the new CLX standard action

Summary

Actions represent a very convenient and manageable way to implement user-driven functionality that can be invoked in a variety of ways. Whilst unfortunately still under-used by developers, it is hoped that they might at some point become commonplace aspects of Delphi development, especially when used in conjunction with action bands.

This article is an updated version of a conference paper I originally delivered back in 2002 at BorCon in Anaheim, California. The paper was updated and came on the conference CD for BorCon 2003 in San Josť, California. Those conference papers were evolutions of an article of mine called Actions and Action Lists that appeared in Issue 61 of The Delphi Magazine way back in September 2000, before the introduction of action bands and action managers, and indeed before many of the standard actions were added to the VCL.

About Brian Long

Brian Long has spent the last 1.7 decades as a trainer, trouble-shooter and mentor focusing on the Delphi, Oxygene, C# and C++ languages, and the Win32, .NET and Mono platforms, recently adding iOS and Android onto the list. In his spare time, when not exploring the Chiltern Hills on his mountain-bike or pounding the pavement in his running shoes, Brian has been re-discovering and re-enjoying the idiosyncrasies and peccadilloes of Unix-based operating systems. Besides writing a Pascal problem-solving book in the mid-90s he has contributed chapters to several books, written countless magazine articles, spoken at many international developer conferences and acted as occasional Technical Editor for Sybex. Brian has a number of online articles that can be found at http://blong.com and a blog at http://blog.blong.com.

© 2012 Brian Long Consulting and Training Services Ltd. All Rights Reserved.