Overview
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.
TForm1.Edit_CopyClick(Sender: TObject) TForm1.Copy_UIButtonClick(Sender: TObject) TForm1.Popup_Menu_CopyClick(Sender: TObject)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.
TForm1.CopyText(Sender: TObject)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
Adding Pre-Defined Actions to Your Application
There are minor variations for other Delphi versions.
How Actions Work
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)
function TComponent.ExecuteAction(Action: TBasicAction): Boolean; begin if Action.HandlesTarget(Self) then begin Action.ExecuteTarget(Self); Result := True; end else Result := False; end; function TComponent.UpdateAction(Action: TBasicAction): Boolean;//! begin if Action.HandlesTarget(Self) then begin Action.UpdateTarget(Self); Result := True; end else Result := False; end;
These are some of the default virtual methods for TBasicAction (in classes.pas).
function TBasicAction.HandlesTarget(Target: TObject): Boolean; begin Result := False; end; procedure TBasicAction.ExecuteTarget(Target: TObject); begin end; procedure TBasicAction.UpdateTarget(Target: TObject); begin end;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.
procedure TAction_mcFileSave.UpdateTarget (Target: TObject); begin if (Control <> nil) and (Control.FFilename <> '') then Enabled := true else Enabled := false; end;
If you want control over the default menu caption, you can override Create and set Caption.
constructor Create(AOwner: TComponent); override; constructor TAction_mcFileSaveAs.Create(AOwner: TComponent); begin inherited Create(AOwner); Caption := 'Save&As...'; end;Or you can set defaults using an *.dfm file.
TAction is defined in actnlist.pas. Several examples are defined in stdactns.pas.
OnExecute
This is the relevant code (from forms.pas).
function TApplication.DispatchAction(Msg: Longint; Action: TBasicAction): Boolean; var Form: TCustomForm; begin Form := Screen.ActiveForm; Result := (Form <> nil) and (Form.Perform(Msg, 0, Longint(Action)) = 1) or (MainForm <> Form) and (MainForm <> nil) and (MainForm.Perform(Msg, 0, Longint(Action)) = 1); { Disable action if no "user" handler is available } if not Result and (Action is TCustomAction) and TCustomAction(Action).Enabled and TCustomAction(Action).DisableIfNoHandler then TCustomAction(Action).Enabled := Assigned(Action.OnExecute); end;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.DispatchActionIntermixed 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).
function TBasicAction.Execute: Boolean; begin if Assigned(FOnExecute) then begin FOnExecute(Self); Result := True; end else Result := False; end;
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
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.
// from classes.pas // It just disables the Loading bit in the State flag (aka a Set) procedure TComponent.Loaded; begin Exclude(FComponentState, csLoading); end; // from controls.pas procedure TControl.Loaded; begin inherited Loaded; if Action <> nil then ActionChange(Action, True); UpdateAnchorRules; end; procedure TControl.ActionChange(Sender: TObject; CheckDefaults: Boolean); begin if Sender is TCustomAction then with TCustomAction(Sender) do begin if not CheckDefaults or (Self.Caption = '') or (Self.Caption = Self.Name) then Self.Caption := Caption; if not CheckDefaults or (Self.Enabled = True) then Self.Enabled := Enabled; if not CheckDefaults or (Self.Hint = '') then Self.Hint := Hint; if not CheckDefaults or (Self.Visible = True) then Self.Visible := Visible; if not CheckDefaults or not Assigned(Self.OnClick) then Self.OnClick := OnExecute; end; end; procedure TWinControl.ActionChange(Sender: TObject; CheckDefaults: Boolean); begin inherited ActionChange(Sender, CheckDefaults); if Sender is TCustomAction then with TCustomAction(Sender) do if not CheckDefaults or (Self.HelpContext = 0) then Self.HelpContext := HelpContext; end; // from comctrls.pas procedure TToolButton.ActionChange(Sender: TObject; CheckDefaults: Boolean); begin inherited ActionChange(Sender, CheckDefaults); if Sender is TCustomAction then with TCustomAction(Sender) do begin if not CheckDefaults or (Self.Down = False) then Self.Down := Checked; if not CheckDefaults or (Self.ImageIndex = -1) then Self.ImageIndex := ImageIndex; end; end; // from stdctrls.pas procedure TButtonControl.ActionChange(Sender: TObject; CheckDefaults: Boolean); begin inherited ActionChange(Sender, CheckDefaults); if Sender is TCustomAction then with TCustomAction(Sender) do begin if not CheckDefaults or (Self.Checked = False) then Self.Checked := Checked; end; end;
Creating Your Own "Pre-Defined" Actions
TAction_mcFileExit = class(TAction) public constructor Create(AOwner: TComponent); override; function HandlesTarget(Target: TObject): Boolean; override; procedure ExecuteTarget(Target: TObject); override; end; procedure Register; implementation procedure Register; begin RegisterActions('mcFileExit', [TAction_mcFileExit], nil); end;
Basically, there are 2 ways to add the unit (*.pas file) to an existing package.
Predefined Edit Actions
TEditCopy | Copies highlighted text to the Clipboard |
TEditCut | Cuts highlighted text from the target to the Clipboard |
TEditPaste | Pastes text from the Clipboard to the target |
TEditDelete | Deletes the highlighted text |
TEditSelectAll | Selects all the text in the target edit control |
TEditUndo | Undoes the last edit made to the target edit control. |
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
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
Menu_Edit_Check.Checked := not Menu_Edit_Check.Checked; UISpeedButton.Down := Menu_Edit_Check.Checked;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.)
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.
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).
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,
Note:
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
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:
Notes
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
if Ctrl <> nil then Ctrl.FreeNotification(Self);
Also override the TControl.Notification method.
procedure TEditAction.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (Operation = opRemove) and (AComponent = Control)
then Control := nil;
end;
This way, if the control is deleted, your control will be notified
so that you can set the pointer to nil.
Images
The rest is automatic.
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.
Initializing Registered Actions
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
This is working code from mcFile_IO.pas.
procedure Register;
begin
RegisterComponents('mc', [TmcFile_IO]);
unRegisterActions( [TAction_mcFileOpen, TAction_mcFileSave,
TAction_mcFileSaveAs,
TAction_mcFileNew, TAction_mcFileSaveAll]);
RegisterActions('mcFile_IO', [TAction_mcFileOpen, TAction_mcFileSave,
TAction_mcFileSaveAs,
TAction_mcFileNew, TAction_mcFileSaveAll],
TmcActionImages);
end;
ToolBars
I have many more details
here.
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
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).