Reputation: 47
I am having a problem getting an Action assigned to a custom component's inherited Action property to work when the code is entirely created at run time (i.e. no form designer components). If I use an ActionList in the form designer and then use the same code things work fine.
Here is my constructor of a component derived from TCustomControl
:
self.FButtonSCActionList := TActionList.Create( self.Parent );
self.FButtonSCActionList.Name := 'ButtonSCActionList';
self.FButtonSCAction := TAction.Create( self.FButtonSCActionList );
self.FButtonSCAction.Name := 'ClickShortcutAction';
self.FButtonSCAction.OnExecute := self.ExecuteButtonShortcut;
self.FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
self.FButtonSCAction.Enabled := TRUE;
self.FButtonSCAction.Visible := TRUE;
self.FButtonSCAction.ActionList := self.FButtonSCActionList;
self.Action := FButtonSCAction;
If I create the custom control with this code, add it to the toolbar, place it on a form in a new VCL Forms application and then run the application, when I press the shortcut key nothing happens. If I create the control without this code, place it on a form and assign an Actionlist to the form, and then put the code lines just involving creating an action and assigning it to the component's Action property into an onclick event handler for the button, it then responds to the shortcut keypress correctly. For the life of me I can't see what is different, but hopefully you Actions Delphi gurus can...
The purpose of this Action is to allow the developer to assign a custom shortcut to the button in the Object Inspector via a property. I would like to assign directly to the "built in" Action but cannot find out how to access its Shortcut Property. (Obviously I could do this via the other HotKey delphi functionality and will if I have to but I also want to understand Actions and this seems a good place to start...)
Upvotes: 3
Views: 3494
Reputation: 47
Thanks so much for all the help! For those who will use this question for later google-fu (I live in google these days when not in the Delphi IDE...) here is the final fully functional code for a custom component:
unit ActionTester;
interface
uses
Winapi.windows,
Vcl.ExtCtrls,
System.Types,
System.SysUtils ,
System.Classes,
Vcl.Controls,
Vcl.Forms,
Vcl.Graphics,
Messages,
Vcl.Buttons,
System.Variants,
System.UITypes,
Dialogs,
Vcl.ExtDlgs,
Generics.Collections,
System.Actions,
Vcl.ActnList,
Clipbrd,
TypInfo,
Rtti,
Menus;
type
TActionTester = class(TCustomControl)
private
{ Private declarations }
protected
{ Protected declarations }
FButtonSCActionList: TActionList;
FButtonSCAction: TAction;
procedure ExecuteButtonShortcut(Sender: TObject);
procedure Notification(AComponent: TComponent; Operation: TOperation);
override;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
Procedure Paint; override;
Destructor Destroy; Override;
published
{ Published declarations }
Property OnClick;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TActionTester]);
end;
{ TActionTester }
constructor TActionTester.Create(AOwner: TComponent);
var
Form: TCustomForm;
function GetOwningForm(Component: TComponent): TCustomForm;
begin
result := NIL;
repeat
if Component is TCustomForm then
Result := TCustomForm(Component);
Component := Component.Owner;
until Component = nil;
end;
begin
inherited Create(AOwner);
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
FButtonSCAction.SetSubComponent(true);
if not (csDesigning in ComponentState) then
begin
Form := GetOwningForm(Self);
if Form <> nil then
begin
FButtonSCActionList := TActionList.Create(Form);
FButtonSCActionList.FreeNotification(Self);
FButtonSCAction.ActionList := FButtonSCActionList;
end;
end;
end;
destructor TActionTester.Destroy;
begin
FreeAndNil( self.FButtonSCAction );
inherited;
end;
procedure TActionTester.ExecuteButtonShortcut(Sender: TObject);
begin
if assigned( self.OnClick ) then self.OnClick( self );
end;
procedure TActionTester.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (AComponent = FButtonSCActionList) and (Operation = opRemove) then
FButtonSCActionList := nil;
end;
procedure TActionTester.Paint;
begin
inherited;
self.Canvas.Brush.Color := clGreen;
self.Canvas.Brush.Style := bsSolid;
self.Canvas.FillRect( self.GetClientRect );
end;
end.
works like a charm! Major kudos to NGLN, David and Dalija!
Upvotes: 0
Reputation: 43649
There is no built-in Action component in TControl
. It is an Action property that is unassigned by default. The user of the control can assign the property with whatever Action is desired. The designer of the control (you) does not have to provide an Action nor ActionList.
I would like to assign directly to the "built in" Action but cannot find out how to access its Shortcut Property.
That built-in Action is by default just an unassigned TAction
property. And if the property is not assigned, i.e. the property does not point to an Action component, then its ShortCut property does not exist.
The purpose of this Action is to allow the developer (red. the user of your component/control) to assign a custom shortcut to the button in the Object Inspector via a property.
If that is your sole goal, then simply publish the Action property and do nothing further:
type
TMyControl = class(TCustomControl)
published
property Action;
end;
This will result in the appearance of the property in the developer's Object Inspector. The developer simply has to assign one of his own actions to it, and to set the ShortCut property of thát action. Thus the actual solution is to get rid of all your current code.
self.FButtonSCActionList := TActionList.Create( self.Parent );
Self.Parent
is nil
during the constructor. Two things about that:
First, some well-intentioned comments on your code:
Self
is implicit and is not needed, nor customary.Name
property set.Visible
and Enabled
properties of an action are True by default.Secondly, as Dalija Prasnikar already said, the ActionList is not needed at design time. And the ActionList has to be indirectly owned by the form that the control owns. So the control can own the ActionList too (XE2).
constructor TMyControl.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
Action := FButtonSCAction;
if not (csDesigning in ComponentState) then
begin
FButtonSCActionList := TActionList.Create(Self);
FButtonSCAction.ActionList := FButtonSCActionList;
end;
end;
Somehere before XE2, at least still in D7, the ActionList had to be registered by the form that the control owns. (There is more to it, but since it is unlikely that the control is parented by another form nor that the action is invoked when another form is focussed, this simplification can be made). Registration could be done by making the form the owner of the ActionList. Since you give ownership of the ActionList beyond the control, let the ActionList notify its possibly destruction to the control with FreeNotification
. (Ok, this is far-fetched, since typically the control then will be destroyed as well, but this is how it strictly should be done).
type
TMyControl = class(TCustomControl)
private
FButtonSCActionList: TActionList;
FButtonSCAction: TAction;
protected
procedure ExecuteButtonShortcut(Sender: TObject);
procedure Notification(AComponent: TComponent; Operation: TOperation);
override;
public
constructor Create(AOwner: TComponent); override;
end;
constructor TMyControl.Create(AOwner: TComponent);
var
Form: TCustomForm;
function GetOwningForm(Component: TComponent): TCustomForm;
begin
repeat
if Component is TCustomForm then
Result := TCustomForm(Component);
Component := Component.Owner;
until Component = nil;
end;
begin
inherited Create(AOwner);
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
Action := FButtonSCAction;
if not (csDesigning in ComponentState) then
begin
Form := GetOwningForm(Self);
if Form <> nil then
begin
FButtonSCActionList := TActionList.Create(Form);
FButtonSCActionList.FreeNotification(Self);
FButtonSCAction.ActionList := FButtonSCActionList;
end;
end;
end;
procedure TMyControl.ExecuteButtonShortcut(Sender: TObject);
begin
//
end;
procedure TMyControl.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (AComponent = FButtonSCActionList) and (Operation = opRemove) then
FButtonSCActionList := nil;
end;
Note that when GetOwningForm
returns False
(when the developer creates the control without owner), the ActionList is not created because it cannot resolve the owning form. Overriding SetParent could fix that.
Because transfering ownership to another component feels unnecessary (and could give problems with the IDE's streaming system when the code is run if csDesigning in ComponentState
), there is another way to register the ActionList to the form by adding it to the protected FActionLists
field:
type
TCustomFormAccess = class(TCustomForm);
constructor TMyControl.Create(AOwner: TComponent);
var
Form: TCustomForm;
function GetOwningForm(Component: TComponent): TCustomForm;
begin
repeat
if Component is TCustomForm then
Result := TCustomForm(Component);
Component := Component.Owner;
until Component = nil;
end;
begin
inherited Create(AOwner);
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
Action := FButtonSCAction;
if not (csDesigning in ComponentState) then
begin
Form := GetOwningForm(Self);
if Form <> nil then
begin
FButtonSCActionList := TActionList.Create(Self);
FButtonSCAction.ActionList := FButtonSCActionList;
if TCustomFormAccess(Form).FActionLists = nil then
TCustomFormAccess(Form).FActionLists := TList.Create;
TCustomFormAccess(Form).FActionLists.Add(FButtonSCActionList)
end;
end;
end;
TControl.Action
is a public property, and TControl.SetAction
is not virtual. This means that the user of the control can assign a different Action, rendering this Action useless, and you cannot do anything about nor against it. (Not publishing is not enough). Instead, declare another Action property, or - again - offer a separate Action component.Upvotes: 2
Reputation: 28516
You don't need to create ActionList at design time. Use following code in your Create method:
FButtonSCAction := TAction.Create(Self);
FButtonSCAction.SetSubComponent(true);
FButtonSCAction.OnExecute := ExecuteButtonShortcut;
FButtonSCAction.ShortCut := TextToShortCut('CTRL+K');
FButtonSCAction.Enabled := TRUE;
FButtonSCAction.Visible := TRUE;
Action := FButtonSCAction;
if not (csDesigning in ComponentState) then
begin
FButtonSCActionList := TActionList.Create(aOwner);
FButtonSCAction.ActionList := FButtonSCActionList;
end;
During run-time creation of control, you can have situation where aOwner passed to your control will not be form itself, but another control. In that case instead of creating action list with aOwner you would have to call function that will give you the form from the aOwner parameter.
function GetOwnerForm(Component: TComponent): TComponent;
begin
Result := Component;
while (Result <> nil) and (not (Result is TCustomForm)) do
begin
Result := Result.Owner;
end;
end;
FButtonSCActionList := TActionList.Create(GetOwnerForm(aOwner));
Upvotes: 3