Delphi User Interface Design
Using Actions, Menus, and ToolBars

Overview | Adding Pre-Defined Actions to Your Application | How Actions Work
Creating Your Own "Pre-Defined" Actions | Predefined Edit Actions | Toggle Buttons
Actions that Reference Components | Images | Initializing Registered Actions | ToolBars
Delphi 6 | Summary | References


Overview

Normally, applications are controlled by a variety of user interface controls.

Typically, a particular action will be assigned to several related controls. For instance, Copy may be assigned to a menu selection, a toolbar button, and a pop-up menu selection (activated when you right click a control). By default, double clicking on the OnClick property of each control will create a separate method.

In order to simplify the code, your application might have only one routine which implements the Copy action and each of the related controls will simply call that routine. However, it is not quite that simple. For instance, you may want the Copy action disabled unless some text is selected. In that case, you will need a routine which is aware of which controls are associated with that action and set their Enabled properties to the appropriate state. Some properties that may be common to related controls are Delphi provides Action objects to simplify this type of control synchronization. In general, you use Action objects by In Delphi, every control derived from TControl has an Action property. However, only controls which do something when clicked have the property published (such as TButton and TMenuItem). Generally speaking, the Action property points to a component derived from TBasicAction.


Adding Pre-Defined Actions to Your Application

For Delphi 5 Note that this is in the Delphi 5 help under Contents / Quick Start Tutorial / Adding support for a menu and a toolbar and the following 2 pages.

There are minor variations for other Delphi versions.


How Actions Work

The built-in help describes the steps for writing and using Actions. However, I developed a much deeper understanding by reviewing several of the pre-defined Action components.

To begin, all Actions must be based on TBasicAction (an abstract class with many virtual methods), but they should be derived from TAction (which, itself, descends from TBasicAction).

The call sequence for executing an Action is pretty involved, and you should read the help for full details. What I am going to describe here is stripped down to the essentials.

When a menu option (or button or whatever) is clicked, Delphi looks in the ActionList for an Action with the same name as the Menu.Action property (this actually takes place at design time) and calls Action.Execute. The TContainedAction default for this calls many routines until one returns True. Eventually, the form calls the active controlís ExecuteAction method with the Action component as the parameter. (This example is from Classes.pas)

These are some of the default virtual methods for TBasicAction (in classes.pas).

From this, it is obvious that you need to define (override) HandlesTarget and ExecuteTarget for any Action that you define. In addition you may need to override UpdateTarget.

Basically, set Enabled and other parameters in UpdateTarget.

If you want control over the default menu caption, you can override Create and set Caption.

Or you can set defaults using an *.dfm file.

TAction is defined in actnlist.pas. Several examples are defined in stdactns.pas.


OnExecute

There is an anomaly - if the action's OnExecute is set, then button and menu items are enabled without overriding HandlesTarget. I spent many hours trying to understand how this works ...

This is the relevant code (from forms.pas).

It is called via
TApplication.Run / TApplication.HandleMessage / TApplication.Idle / TApplication.DoActionIdle / TCustomForm.UpdateActions / TControl.InitiateAction / TBasicActionLink.Update / FAction.Update (Assy) / SendAppMessage / SendMessage (Assy) / WndProc / TApplication.DispatchAction
Intermixed with this call string is some assembly code (Assy) which I have indicated.

It should be noted that FAction.Update is followed by calls to Assembly code, TApplication.UpdateAction and TBasicAction.Update before SendAppMessage is called. However, the way this works is not obvious to me.

Interestingly, UpdateTarget is called via one of the 2 Preform commands in TApplication.DispatchAction (I'm not sure which one).

TApplication.DispatchAction / TControl.Perform (Assy) / TCustomForm.CMActionUpdate (Assy) / TComponent.UpdateAction (calls Action.HandlesTarget) / TComponent.UpdateTarget

This is also relevant (from classes.pas).

In order to collect this information, I had to enable the use of Debug DCUs - Project / Options... / Compiler / Use Debug DCUs - which allows you to step through the VCL files.


Component Initialization

One of the methods needed by component designers, but poorly documented in the help, is Loaded. Normally, a form loads, it Creates all its components, it modifies the default parameters to the values that the designer placed in the Object Inspector, then it calls Loaded. From TControl.Loaded help
Loaded overrides the inherited method in order to initialize the control from its associated Action. To change the properties the control copies from its action, override the ActionChange method.
Here is the relevant code.


Creating Your Own "Pre-Defined" Actions

Simply writing an action is ok, but to make them re-usable, you need to register your actions so that they can be picked from the list of pre-defined actions. An action is a class. It is installed (registered) in a package the same as any other class (or component). The only difference is that you use the RegisterActions command instead of RegisterComponents. The following code fragments come from mcCommon.pas.

Basically, there are 2 ways to add the unit (*.pas file) to an existing package.


Predefined Edit Actions

Delphi provides a number of predefined Edit actions which operate on the currently selected edit control (any control based on TCustomEdit). If your application uses these actions, the associated controls are automatically enabled and disabled based on whether or not some text is selected (Copy and Delete) or there is text currently on the clipboard (Paste).

All of these actions are based on TEditAction which ensures that the target control is based on the TCustomEdit class and that it currently has the focus. (Implemented via the HandlesTarget method.)

For examples, see


Toggle Buttons

Toggle buttons are controls which are either Up or Down. In Delphi, these are implemented via the SpeedButton. Some other controls which represent one of 2 states are CheckBoxes and MenuItems (which allows a check mark).

In Windows programs, it is fairly common to provide a Toggle Button on the toolbar and to provide a second way to perform the same action - perhaps a menu option which allows a check mark. (When possible, there should always be a way to control a program using only the keyboard.) For instance, suppose that a toolbar has a toggle button. Also suppose that the menu has a related selection which contains a checkmark. However, because the button position is controlled via the Down property and the Menu check mark is controlled via the Checked property, you will need to provide extra code to synchronize the control states. Typical code to handle this might be

provided in the OnExecute method.

The TToolbar component provides a TToolButton which also implements this capability. Just set the Style property to tbsCheck.


In general, I have a problem with Toggle Buttons and menu options which allow check marks - basically, when the button is up (or the option is not checked), there is no visual indication that the control can display 2 states.

Checkboxes, on the other hand, always indicate that they can display one of 2 states. (Uh, some checkboxes can actually display one of 3 states.)


Notes

Actions provide a way to simplify synchronizing several "similar" controls.

When you create your own action, you must verify that the target components have a property (such as Enabled or Checked) before you set it.

Parts of the Action code are executed at design time. This way, if you set one of the linked properties, all realeated controls will have the same property value.

By default, if a new TAction is added without an OnExecute method, the associated components automatically have Enabled set to false.

FileAccessObject

To perform an action, xxx.FileAccess must be set
to an object of type TFileAccessObject (or a descendant).

If Object is set, then the default extension and read/write
actions are used, otherwise, OnRead and OnWrite must be set.

Any edit field, .txt .ini 
image, .bmp, .jpg


Actions that Reference Components

This is pretty important, whenever any control saves a pointer (reference) to another control, that reference must be set to nil if the referenced component is deleted.

This notifies the remote control that this control has a pointer (ie, that its Notification method must be called when the remote control is destroyed).

Also override the TControl.Notification method. This way, if the control is deleted, your control will be notified so that you can set the pointer to nil.


Images

Well, most of the stuff on this page I figured out myself. However, the Delphi 5 help and examples provided no information on associating Images with actions. (Actually, part of it is in the tutorial, but I found that after I wrote this.) Basically, I simply stumbled onto this Kudzu article. Additional information was provided by the Delphi 6 help.

Somehow (I still haven't found it in the source code), the Delphi standard actions add images to the ImageList (win32 / ImageList) associated with TActionList.Images. Then, when those actions are associated with various components, the component's image (or glyph) is automatically assigned.

For example, when the EditCut action is added to a SpeedButton, the Glyph property is automatically set to a pair of scissors. (Simply changing the action will not change the glyph. To automatically assign a new glyph, first delete (clear) the existing glyph, then set the new action.)

It is important to add the ImageList to the ActionList before any actions are added to the ActionList. You should also associate the same ImageList with your MainMenu component before any of the MenuItem actions are set.

Kudzu explains that the image, and other default values, are associated with actions by defining a form that contains your actions and a TImageList. Then you set the actions' default values, place images in the list, and assign the list index. To use this,

The rest is automatic.

Note:

According to the Delphi developer team (via Delphi by Design), you should actually use a TDataModule (File / New... / Data Module) to hold the ActionList and ImageList components. At any rate, both Forms and Data Modules work and both are stored as *.dfm files.

When creating your own images

According to the help file, Delphi 6 does not automatically load the images for the selected images - instead, with the Personal edition you must add each one manually, with the Enterprise and Professional editions, you need to manually copy a special ImageList component from Delphi6\Source\Vcl\ActnRes.pas and then you need to manually set the ImageIndex property for each action. Delphi 5's method seems much better to me.


Initializing Registered Actions

In general, the Caption, Hint, and ShortCut properties should have default values. The most obvious way to initialize a component is to override its Create constructor. However, Actions are a little weird because when you Register them, you have the option of associating an *.dfm file (either a Form or a Data Module, your choice). In this case, the properties set in the *.dfm file will be set AFTER the constructor is executed. (This is how *.dfm files normally work.)

Though the 3 properties mentioned above can be set either way - Create constructor or *.dfm file - the ImageIndex property should be set using an *.dfm file.

What's weird here is that if an image is associated with a registered Action (via an *.dfm file), and the ActionList in the current form is associated with an ImageList, then the image is copied from the *.dfm file to the form's ImageList and the Action's ImageIndex property is set to this image. (Notice that the two ImageIndex's - the one in the current form and the one used when the action was registered - will normally be different. Delphi performs this conversion automatically.)

In general, leave the Category property alone - it is initialized based on the parameter used to register the action.

Note:

According to the Delphi developer team (via Delphi by Design), when creating Actions that will be Registerd, you should use a TDataModule (File / New... / Data Module) to hold the ActionList and ImageList components. Because the author was not aware that images could be associated with registered Actions (perhaps Delphi 4 did not support this feature), he suggests that using the constructor makes more sense (because that is how other objects are initialized).


Hints

Making this work is actually a little bit tricky. This is working code from mcFile_IO.pas.


ToolBars

This is ONE way to produce a ToolBar. I have many more details here.


Delphi 6

Delphi 6 Professional provides additional action components that support drag and drop capabilities. For instance, dragging a group of actions to an ActionMainMenuBar will actually create the menu options. Dropping actions on an ActionToolBar will create the buttons.

There are also components to save the state when a user redefines the toolbars.

References:


Summary

As I said, even though this stuff is powerful, writing your own Actions is very complex. However, if you can get it to work, then Actions can make your code much easier to re-use.

I do not suggest writing Actions based on these instructions and the help alone. You really should refer to the Delphi source files (which do not ship with the Standard edition).


References


Author: Robert Clemenzi - clemenzi@cpcug.org
URL: http:// cpcug.org / user / clemenzi / technical / Languages / Delphi / UserInterfaceDesign.html