I want to have a custom StringGrid element.
I created a class:
type
TClassStringGrid = class(TCustomControl)
...
with
constructor TClassStringGrid.Create(AOwner: TForm);
begin
inherited Create(nil);
myGroupBox1 := TGroupBox.Create(AOwner);
myGroupBox1.Parent := AOwner;
myStringGrid1 := TStringGrid.Create(self);
myStringGrid1.Parent := myGroupBox1;
myStringGrid1.Options := myStringGrid1.Options + [goEditing];
end;
destructor TClassStringGrid.Destroy;
begin
if myStringGrid1 <> nil then begin
FreeAndNil(myStringGrid1);
end;
if myGroupBox1 <> nil then begin
DestroyComponents;
FreeAndNil(myGroupBox1);
end;
// Call the parent class destructor
inherited;
end;
I created a class in Form1 and show it. It works. But if I put some value into the StringGrid (Form1) and then try to close Form1 I get an exception "the element has no parent window" in FreeAndNil(myStringGrid1);.
What is wrong by Destroy?
I would be thankfull for any information you can provide me.
Assuming you want to show a String grid in a Group box on this control, then this is how it should look like:
type
TMyStringGrid = class(TCustomControl)
private
FGroupBox: TGroupBox;
FStringGrid: TStringGrid;
public
constructor Create(AOwner: TComponent); override;
end;
constructor TMyStringGrid.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FGroupBox := TGroupBox.Create(Self);
FGroupBox.Parent := Self;
FStringGrid := TStringGrid.Create(Self);
FStringGrid.Parent := FGroupBox;
end;
In this manner, your newly designed control is owner and parent of the sub controls. Destruction is done automatically because of that.
Related
What I'm basically trying to create is a component that inherits from TScrollBox. That component has a TGroupBox and inside it a TFlowPanel. What I need is when I double click this component, a TCollection-like editor appears where I can add components (TFiltros) that will be children of that TFlowPanel. The problem is that I want those components to be named, such that I can directly access them via code, kinda like a TClientDataSet, where you add fields and they appear in your code.
I've managed to make it almost work by overriding GetChildren and making it return the children of the TFlowPanel. That also required me to make TFiltros's owner be the Form which they are in. It shows in the Structure panel as children (even tho they are not direct children) and also saves it in the DFM, but when I close the form and open it again, it fails to load the data back from the DFM, throwing an Access Violation. I have no idea how to override the loading to properly set the children.
Any help in how I can fix that, or even different ideas would be really nice. I'm new to creating Delphi components.
My current code which is heavily inspired in this question:
unit uFiltros;
interface
uses
System.SysUtils, System.Classes, Vcl.Controls, Vcl.ExtCtrls, Forms, StdCtrls,
ClRelatorio, Math, DesignEditors, DesignIntf, System.Generics.Collections;
type
TFiltrosEditor = class(TComponentEditor)
procedure ExecuteVerb(Index: Integer); override;
function GetVerb(Index: Integer): String; override;
function GetVerbCount: Integer; override;
end;
TFiltros = class(TScrollingWinControl)
private
FChilds: TList<TComponent>;
FGroupBox: TGroupBox;
FFlowPanel: TFlowPanel;
FWidth: Integer;
procedure OnFlowPanelResize(Sender: TObject);
procedure SetWidth(AWidth: Integer);
public
procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
function GetChildOwner: TComponent; override;
constructor Create(AOwner: TComponent); override;
property Childs: TList<TComponent> read FChilds;
published
property Width: Integer read FWidth write SetWidth;
end;
TClFiltro = class(TFiltro)
private
FFiltros: TFiltros;
protected
procedure SetParent(AParent: TWinControl); override;
public
constructor Create(AOwner: TComponent; AFiltros: TFiltros); reintroduce;
function GetParentComponent: TComponent; override;
function HasParent: Boolean; override;
property Parent: TWinControl write SetParent;
end;
TFiltroItem = class(TCollectionItem)
private
FFiltro: TClFiltro;
protected
function GetDisplayName: String; override;
public
constructor Create(Collection: TCollection); override;
destructor Destroy; override;
published
property Filtro: TClFiltro read FFiltro write FFiltro;
end;
TFiltrosCollection = class(TOwnedCollection)
private
FDesigner: IDesigner;
public
property Designer: IDesigner read FDesigner write FDesigner;
end;
procedure Register;
implementation
uses Dialogs, ClFuncoesBase, Vcl.Graphics, ColnEdit;
procedure Register;
begin
RegisterClass(TClFiltro);
RegisterNoIcon([TClFiltro]);
RegisterComponents('Cl', [TFiltros]);
RegisterComponentEditor(TFiltros, TFiltrosEditor);
end;
{ TFiltroItem }
constructor TFiltroItem.Create(Collection: TCollection);
begin
inherited;
if Assigned(Collection) then
begin
FFiltro := TClFiltro.Create(TFiltros(Collection.Owner).Owner, TFiltros(Collection.Owner));
FFiltro.Name := TFiltrosCollection(Collection).Designer.UniqueName(TClFiltro.ClassName);
FFiltro.Parent := TFiltros(Collection.Owner).FFlowPanel;
FFiltro.Margins.Top := 1;
FFiltro.Margins.Bottom := 1;
FFiltro.AlignWithMargins := True;
//FFiltro.SetSubComponent(True);
end;
end;
destructor TFiltroItem.Destroy;
begin
FFiltro.Free;
inherited;
end;
function TFiltroItem.GetDisplayName: String;
begin
Result := FFiltro.Name;
end;
{ TFiltros }
constructor TFiltros.Create(AOwner: TComponent);
begin
inherited;
FChilds := TList<TComponent>.Create;
// Configurações ScrollBox
Align := TAlign.alRight;
AutoScroll := False;
AutoSize := True;
//Configurações GroupBox
FGroupBox := TGroupBox.Create(Self);
FGroupBox.Parent := Self;
FGroupBox.Caption := ' Fil&tros ';
FGroupBox.Font.Style := [fsBold];
//Configurações FlowPanel
FFlowPanel := TFlowPanel.Create(FGroupBox);
FFlowPanel.Parent := FGroupBox;
FFlowPanel.Top := 15;
FFlowPanel.Left := 2;
FFlowPanel.AutoSize := True;
FFlowPanel.FlowStyle := TFlowStyle.fsRightLeftTopBottom;
FFlowPanel.Caption := '';
FFlowPanel.OnResize := OnFlowPanelResize;
FFlowPanel.BevelOuter := TBevelCut.bvNone;
end;
function TFiltros.GetChildOwner: TComponent;
begin
Result := FFlowPanel;
end;
procedure TFiltros.GetChildren(Proc: TGetChildProc; Root: TComponent);
var I: Integer;
begin
// inherited;
for I := 0 to FChilds.Count - 1 do
Proc(TComponent(FChilds[I]));
end;
procedure TFiltros.OnFlowPanelResize(Sender: TObject);
begin
FGroupBox.Width := FFlowPanel.Width + 4;
FGroupBox.Height := Max(FFlowPanel.Height + 17, Height);
VertScrollBar.Range := FGroupBox.Height;
FWidth := FFlowPanel.Width;
end;
procedure TFiltros.SetWidth(AWidth: Integer);
begin
FFlowPanel.Width := AWidth;
FWidth := FFlowPanel.Width;
OnFlowPanelResize(Self);
end;
{ TFiltrosEditor }
procedure TFiltrosEditor.ExecuteVerb(Index: Integer);
var LCollection: TFiltrosCollection;
I: Integer;
begin
LCollection := TFiltrosCollection.Create(Component, TFiltroItem);
LCollection.Designer := Designer;
for I := 0 to TFiltros(Component).Childs.Count - 1 do
with TFiltroItem.Create(nil) do
begin
FFiltro := TClFiltro(TFiltros(Component).Childs[I]);
Collection := LCollection;
end;
ShowCollectionEditorClass(Designer, TCollectionEditor, Component, LCollection, 'Filtros');
end;
function TFiltrosEditor.GetVerb(Index: Integer): String;
begin
Result := 'Editar filtros...';
end;
function TFiltrosEditor.GetVerbCount: Integer;
begin
Result := 1;
end;
{ TClFiltro }
constructor TClFiltro.Create(AOwner: TComponent; AFiltros: TFiltros);
begin
inherited Create(AOwner);
FFiltros := AFiltros;
end;
function TClFiltro.GetParentComponent: TComponent;
begin
Result := FFiltros;
end;
function TClFiltro.HasParent: Boolean;
begin
Result := Assigned(FFiltros);
end;
procedure TClFiltro.SetParent(AParent: TWinControl);
begin
if Assigned(AParent) then
FFiltros.FChilds.Add(Self)
else
FFiltros.FChilds.Remove(Self);
inherited;
end;
end.
I've finally managed to do it. It required a combination of TOwnedCollection and overriding GetChildren and GetParentComponent.
Basically what I've learned (and you can correct me if I'm wrong), is the following:
For a component to be shown in the Structure tab at all, the Owner of that component has to be the form. So the first thing was to create TFiltro with that owner.
GetParentComponent defines where in the Structure tree the component is going to reside in, it doesn't necessarily have to be the actual parent. So the second thing was to make GetParentComponent of the TFiltro return the TScrollBox but set the actual parent to be the TFlowPanel.
Now, as the parent of TFiltro no longer is the form, it won't save it to the DFM, because TFlowPanel is the actual parent but is not defined as a subcomponent. Overriding GetChildren in the TScrollBox and making it return every TFiltro solves this, and it is now saved in the DFM as a child.
But now, for the TFiltro to be properly read back from the DFM and be set again accordingly, it has to be a published value in an item inside the TOwnedCollection, which itself is a published value in the TScrollBox. Then, make the TCollectionItem published value's set function define the parent of the TFiltro to be the TFlowPanel.
The article which helped me the most in achieving this is available in the WayBack machine.
I'm writing a component that include few components.
TMyComponent = class(TPanel)
private
fGrid : TExCustomDBGrid;
fOnCellClick : TDBGridClickEvent;
public
constructor Create(AOwner: TComponent); override;
published
property OnCellClick: TDBGridClickEvent read FOnCellClick write FOnCellClick;
End;
...
constructor TMyComponent .Create(AOwner: TComponent);
begin
inherited;
fGrid := TExCustomDBGrid.Create(self);
fGrid.parent := self;
fGrid.Align := alClient;
end;
I want to be able to propagate the Event from the component (TPanel), to the fGrid included.
How can I reach that goal ?
I guess I should declare an Event with the same type on the TPanel (as container component). Then how to propagate into the fGrid ?
It's a bit unclear what you're asking, but based on the code I see, write an event handler and assign it to the grid...
procedure TMyComponent.DBGridCellClicked(Column: TColumn);
begin
if Assigned(fOnCellClick) then
fOnCellClick(Column);
end;
constructor TMyComponent.Create(AOwner: TComponent);
begin
inherited;
fGrid := TExCustomDBGrid.Create(self);
fGrid.Parent := self;
fGrid.Align := alClient;
fGrid.OnCellClick := DBGridCellClicked;
end;
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
end;
TForm2 = class(TForm)
private
FAppWindow: Boolean;
protected
procedure CreateParams(var Params: TCreateParams); override;
public
property AppWindow: Boolean read FAppWindow write FAppWindow;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Form2 := TForm2.Create(Self);
Form2.AppWindow := True;
Form2.Show;
end;
procedure TForm2.CreateParams(var Params: TCreateParams);
begin
inherited;
if FAppWindow then begin
Params.Style := Params.Style or WS_EX_APPWINDOW;
Params.WndParent := 0;
end;
end;
This doesn't work, because the window handle is created during the constructor of TForm, so CreateParams is run too early and FAppWindow is always False.
Writing a custom constructor also doesn't work since you have to eventually call the inherited constructor which creates the handle before you can save any data to the instance:
constructor TForm2.CreateAppWindow(AOwner: TComponent);
begin
inherited Create(AOwner);
FAppWindow := True;
end;
Is there a way to:
Delay the creation of the window handle?
Alter the window style after creation of the window handle?
Recreate the window handle after the constructor has run?
Some other option I haven't thought of, yet?
How can I change the style of a form from the "outside" of the class?
The simplest solution is to pass the parameter to the form in its constructor, rather than wait until it has finished being created.
That means you need to introduce a constructor for TForm2 that accepts as parameters whatever information you need to pass on in CreateParams.
Make a note of any state before you call the inherited constructor. Also, there's no need to set WS_EX_APPWINDOW when you are setting the owner to be zero.
The nice thing about Delphi is that a derived constructor DOES NOT have to call the inherited constructor as its first statement. So you can set your FAppWindow member first, THEN call the inherited constructor to stream the DFM and create the window, eg:
procedure TForm1.FormCreate(Sender: TObject);
begin
Form2 := TForm2.CreateAppWindow(Self);
Form2.Show;
end;
constructor TForm2.CreateAppWindow(AOwner: TComponent);
begin
FAppWindow := True;
inherited Create(AOwner);
end;
This seems to work to recreate the handle, I got the idea from the RecreateAsPopup VCL method:
procedure TForm2.SetAppWindow(const Value: Boolean);
begin
FAppWindow := Value;
if HandleAllocated then
RecreateWnd
else
UpdateControlState;
end;
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...)
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));
Summary
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.
The actual problem
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.
Why your current code doesn't work
self.FButtonSCActionList := TActionList.Create( self.Parent );
Self.Parent is nil during the constructor. Two things about that:
Unless you destroy the ActionList yourself in de destructor, you have a memory leak.
For default ShortCut processing, the application traverses all ActionLists which are (indirectly) owned by the currently focussed form or by the MainForm. Your ActionList has no owner, thus its ShortCuts are never evaluated.
Solution for the current code
First, some well-intentioned comments on your code:
Self is implicit and is not needed, nor customary.
Runtime made components do not need a Name property set.
The 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;
Reflection on this solution:
This approach is not desirable. You should not create action components within your custom control. If you have to, offer them seperately so that the user of your control can decide to which ActionList the custom Action will be added. See also: How do I add support for actions in my component?
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.
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!
In my custom component I created some TAction-s as subcomponents. They're all published, but I could not assign them at design time since they were not available through object inspector.
How do you make them "iterable" by the object inspector? I have tried to set the Owner of the actions to the Owner of the custom component (which is the hosting Form) to no success.
EDIT: It looks like Embarcadero changed Delphi IDE behaviour related with this problem. If you are using Delphi versions prior XE, you should use solution from my own answer. For XE and above, you should use solution from Craig Peterson.
EDIT: I've added my own answer that solves the problem, i.e. by creating a TCustomActionList instance in my custom component and setting its Owner to the hosting form (owner of the custom component). However I am not too happy with this solution, since I think the instance of TCustomActionList is kind of redundant. So I am still hoping to get better solution.
EDIT: Add code sample
uses
.., ActnList, ..;
type
TVrlFormCore = class(TComponent)
private
FCancelAction: TBasicAction;
FDefaultAction: TBasicAction;
FEditAction: TBasicAction;
protected
procedure DefaultActionExecute(ASender: TObject); virtual;
procedure CancelActionExecute(ASender: TObject); virtual;
procedure EditActionExecute(ASender: TObject); virtual;
public
constructor Create(AOwner: TComponent); override;
published
property DefaultAction: TBasicAction read FDefaultAction;
property CancelAction : TBasicAction read FCancelAction;
property EditAction : TBasicAction read FEditAction;
end;
implementation
constructor TVrlFormCore.Create(AOwner: TComponent);
begin
inherited;
FDefaultAction := TAction.Create(Self);
with FDefaultAction as TAction do
begin
SetSubComponent(True);
Caption := 'OK';
OnExecute := DefaultActionExecute;
end;
FCancelAction := TAction.Create(Self);
with FCancelAction as TAction do
begin
SetSubComponent(True);
Caption := 'Cancel';
OnExecute := Self.CancelActionExecute;
end;
FEditAction := TAction.Create(Self);
with FEditAction as TAction do
begin
SetSubComponent(True);
Caption := 'Edit';
OnExecute := Self.EditActionExecute;
end;
end;
As far as I can tell you're not supposed to do it that way.
The easy way to do what you want is to create new standalone actions that can work with any TVrlFormCore component and set the target object in the HandlesTarget callback. Take a look in StdActns.pas for examples. The actions won't be available automatically when sommeone drops your component on the form, but they can add them to their action list manually using the New Standard Actions... command. There's a good article on registering standard actions here.
If you really want to auto-create the actions you need to set the action Owner property to the form and you need to set the Name property. That's all that's necessary, but it does introduce a bunch of issues you need to work around:
The form owns the actions so it will add them its declaration's published section and will auto-create them as part of the streaming process. To work around that you can just disable streaming by overwriting the action's WriteState method and skip the inherited behavior.
Since you aren't writing the state, none of the properties will be persisted. To avoid confusing your users you should switch make the actions descend from TCustomAction instead of TAction, so it doesn't expose anything. There may be way to make the action stream properly, but you didn't say whether it was necessary.
You need to register for free notifications in case the form frees the action before you can.
If someone drops more than one of your component on the action names will conflict. There's multiple ways to handle that, but the cleanest would probably be to override the component's SetName method and use its name as a prefix for the actions' names. If you do that you need to use RegisterNoIcon with the new class so they don't show up on the form.
In the IDE's Structure pane the actions will show up directly under the form, rather than nested like ActionList shows. I haven't found a way around that; none of SetSubComponent, GetParentComponent/HasParent, or GetChildren have any effect, so this may be hard-coded behavior. You can delete the action from the structure pane, separate from the component, too.
I'm sure it can be improved, but this works without any custom property editors:
type
TVrlAction = class(TCustomAction)
protected
procedure WriteState(Writer: TWriter); override;
end;
TVrlFormCore = class(TComponent)
private
FDefaultAction: TVrlAction;
protected
procedure DefaultActionExecute(ASender: TObject); virtual;
procedure Notification(AComponent: TComponent;
Operation: TOperation); override;
procedure SetName(const NewName: TComponentName); override;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
public
property DefaultAction: TVrlAction read FDefaultAction;
end;
procedure Register;
implementation
// TVrlAction
procedure TVrlAction.WriteState(Writer: TWriter);
begin
// No-op
end;
// TVrlFormCore
constructor TVrlFormCore.Create(AOwner: TComponent);
begin
inherited;
FDefaultAction := TVrlAction.Create(AOwner);
with FDefaultAction do
begin
FreeNotification(Self);
Name := 'DefaultAction';
Caption := 'OK';
OnExecute := DefaultActionExecute;
end;
end;
destructor TVrlFormCore.Destroy;
begin
FDefaultAction.Free;
inherited;
end;
procedure TVrlFormCore.DefaultActionExecute(ASender: TObject);
begin
end;
procedure TVrlFormCore.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if Operation = opRemove then
if AComponent = FDefaultAction then
FDefaultAction := nil;
end;
procedure TVrlFormCore.SetName(const NewName: TComponentName);
begin
inherited;
if FDefaultAction <> nil then
FDefaultAction.Name := NewName + '_DefaultAction';
end;
procedure Register;
begin
RegisterComponents('Samples', [TVrlFormCore]);
RegisterNoIcon([TVrlAction]);
end;
EDIT: Use this solution for Delphi versions prior to Delphi XE. For XE and later, use Craig Peterson answer (which does not require redundant TCustomActionList instance).
After meddling around and using information from Craig Peterson's answer, I've decided to instantiate a TCustomActionList in my custom component. So far it is the only way to get list of actions in Object Inspector.
Here is the code:
uses
..., ActnList, ...;
type
TVrlAction=class(TCustomAction)
protected
procedure WriteState(Writer: TWriter); override;
published
property Caption;
end;
TVrlActionList=class(TCustomActionList)
protected
procedure WriteState(Writer: TWriter); override;
end;
TVrlFormCore = class(TVrlItemSource)
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
procedure SetName(const NewName: TComponentName); override;
public
constructor Create(AOwner: TComponent); override;
end;
implementation
{ TVrlAction }
procedure TVrlAction.WriteState(Writer: TWriter);
begin
end;
{ TVrlActionList }
procedure TVrlActionList.WriteState(Writer: TWriter);
begin
end;
{ TVrlFormCore }
constructor TVrlFormCore.Create(AOwner: TComponent);
begin
inherited;
FActions := TVrlActionList.Create(AOwner);
FDefaultAction := TVrlAction.Create(AOwner);
with FDefaultAction as TVrlAction do
begin
FreeNotification(Self);
Caption := 'OK';
OnExecute := DefaultActionExecute;
end;
FActions.AddAction(TContainedAction(FDefaultAction));
FCancelAction := TVrlAction.Create(AOwner);
with FCancelAction as TVrlAction do
begin
FreeNotification(Self);
Caption := 'Cancel';
OnExecute := Self.CancelActionExecute;
end;
FActions.AddAction(TContainedAction(FCancelAction));
FEditAction := TVrlAction.Create(AOwner);
with FEditAction as TVrlAction do
begin
FreeNotification(Self);
Caption := 'Edit';
OnExecute := Self.EditActionExecute;
end;
FActions.AddAction(TContainedAction(FEditAction));
end;
procedure TVrlFormCore.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if Operation=opRemove then
begin
if AComponent = FMaster then
FMaster := nil
else if (AComponent is TVrlFormCore) then
FDetails.Remove(TVrlFormCore(AComponent))
else if AComponent=FDefaultAction then
FDefaultAction := nil
else if AComponent=FCancelAction then
FCancelAction := nil
else if AComponent=FEditAction then
FEditAction := nil;
end;
end;
procedure TVrlFormCore.SetName(const NewName: TComponentName);
begin
inherited;
if FActions<>nil then
FActions.Name := NewName + '_Actions';
if FDefaultAction <> nil then
FDefaultAction.Name := NewName + '_DefaultAction';
if FCancelAction <> nil then
FCancelAction.Name := NewName + '_CancelAction';
if FEditAction <> nil then
FEditAction.Name := NewName + '_EditAction';
end;
You cannot assign them because they are read only by design:
property DefaultAction: TBasicAction read FDefaultAction;
property CancelAction : TBasicAction read FCancelAction;
property EditAction : TBasicAction read FEditAction;
You should change your class' interface to:
property DefaultAction: TBasicAction read FDefaultAction write FDefaultAction;
property CancelAction : TBasicAction read FCancelAction write FCancelAction;
property EditAction : TBasicAction read FEditAction write FEditAction;
or write appropriate setter for each action.
Edit:
What you need is then
to implement your 3 custom actions as Predefined Actions (See StdActns.pas for samples).
to register them by calling ActnList.RegisterActions. (See RAD Studio documentation)
to add to the form a TActionList and/or TActionManager to allow your Predefined Actions appear in the list of predefined actions in the action list editor of every TControl's descendent.
You may do extensive search on google for the topic and find some concrete example.