How to publish a subcomponent's property in a compound component? - delphi

In a compound component derived from TPanel I'm trying to publish a property whose only pourpose is that to sets and gets a linkage property of a subcomponent. Each time I add my compound component to a form, an access violation is raised:
Access violation at address 12612D86 in module 'MyRuntimePackage.bpl'. Read of address 00000080.
I've prepared a simplified example using a TLabel and its PopupMenu property but I still have the same problem when placing the compound component on a form/frame.
Runtime package:
uses
StdCtrls, Menus, ExtCtrls, Classes;
type
TTestCompoundComponent = class(TPanel)
private
FSubCmp : TLabel;
function GetLabelPopupMenu() : TPopupMenu;
procedure SetLabelPopupMenu(AValue : TPopupMenu);
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
public
constructor Create(AOwner : TComponent); override;
destructor Destroy(); override;
published
property LabelPopupMenu : TPopupMenu read GetLabelPopupMenu write SetLabelPopupMenu;
end;
...
function TTestCompoundComponent.GetLabelPopupMenu() : TPopupMenu;
begin
Result := FSubCmp.PopupMenu;
end;
procedure TTestCompoundComponent.SetLabelPopupMenu(AValue : TPopupMenu);
begin
if(GetLabelPopupMenu() <> AValue) then
begin
if(GetLabelPopupMenu() <> nil)
then GetLabelPopupMenu().RemoveFreeNotification(Self);
FSubCmp.PopupMenu := AValue;
if(GetLabelPopupMenu() <> nil)
then GetLabelPopupMenu().FreeNotification(Self);
end;
end;
procedure TTestCompoundComponent.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited;
if((AComponent = GetLabelPopupMenu()) AND (Operation = opRemove))
then SetLabelPopupMenu(nil);
end;
constructor TTestCompoundComponent.Create(AOwner : TComponent);
begin
inherited;
FSubCmp := TLabel.Create(nil);
FSubCmp.Parent := Self;
end;
destructor TTestCompoundComponent.Destroy();
begin
FSubCmp.Free;
inherited;
end;
Designtime package:
procedure Register;
begin
RegisterComponents('MyTestCompoundComponent', [TTestCompoundComponent]);
end;

#kobik's answer explains the root cause of the AV (accessing the FSubCmp.PopupMenu property before FSubCmp is created). However, your entire component code is overly complicated for what you are trying to achieve.
You should be setting your component as the TLabel's Owner, then you can remove your destructor completely. And you should also be calling FSubCmp.SetSubComponent(True) in your constructor (especially if you ever intend to expose the TLabel in the Object Inspector at a later time, so the user can customize its properties at design-time):
constructor TTestCompoundComponent.Create(AOwner : TComponent);
begin
inherited;
FSubCmp := TLabel.Create(Self);
FSubCmp.SetSubComponent(True);
FSubCmp.Parent := Self;
end;
Your Notification() method should be setting FSubCmp.PopupMenu := nil directly in response to opRemove, instead of calling SetLabelPopupMenu(nil). You already know the PopupMenu is assigned and that it is in progress of being destroyed, so the extra code to retrieve the PopupMenu (repeatedly), check it for nil, and call RemoveFreeNotification(), is all overkill for an opRemove operation:
procedure TTestCompoundComponent.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited;
if (Operation = opRemove) and (AComponent = LabelPopupMenu) then
FSubCmp.PopupMenu := nil;
end;
And your SetLabelPopupMenu() method is just an eyesore in general, with all those redundant calls to GetLabelPopupMenu(). Call it only one time and store the returned object pointer to a local variable that you can then use as needed:
procedure TTestCompoundComponent.SetLabelPopupMenu(AValue: TPopupMenu);
var
PM: TPopupMenu;
begin
PM := LabelPopupMenu;
if (PM <> AValue) then
begin
if (PM <> nil) then
PM.RemoveFreeNotification(Self);
FSubCmp.PopupMenu := AValue;
if (AValue <> nil) then
AValue.FreeNotification(Self);
end;
end;
However, your Notification() method is actually completely redundant and should be removed altogether. TLabel already calls FreeNotification() on its own PopupMenu property, and has its own Notification() implementation that will set the PopupMenu property to nil if the TPopupMenu object is freed. You don't need to handle this manually at all. And so, all of the extra code in SetLabelPopupMenu() is redundant and should be removed:
procedure TTestCompoundComponent.SetLabelPopupMenu(AValue: TPopupMenu);
begin
FSubCmp.PopupMenu := AValue;
end;
This also means the fix proposed by #kobik is redundant and can be removed as well 1:
function TTestCompoundComponent.GetLabelPopupMenu: TPopupMenu;
begin
Result := FSubCmp.PopupMenu;
end;
1: Unless you want to handle the case where a user decides to free your TLabel directly (which is foolish, and no one would ever really do that in practice, but it is still technically possible), then you would need Notification() to handle that situation (assigning your component as the TLabel's Owner will call FreeNotificatio() for you):
function TTestCompoundComponent.Notification(AComponent: TComponent; Opration: TOperation);
begin
inherited;
if (Operation = opRemove) and (AComponent = FSubCmp) then
FSubCmp := nil;
end;
function TTestCompoundComponent.GetLabelPopupMenu: TPopupMenu;
begin
if FSubCmp <> nil then
Result := FSubCmp.PopupMenu
else
Result := nil;
end;
That being said, here is a simplified version of your code:
uses
StdCtrls, Menus, ExtCtrls, Classes;
type
TTestCompoundComponent = class(TPanel)
private
FSubCmp: TLabel;
function GetLabelPopupMenu: TPopupMenu;
procedure SetLabelPopupMenu(AValue: TPopupMenu);
public
constructor Create(AOwner: TComponent); override;
published
property LabelPopupMenu: TPopupMenu read GetLabelPopupMenu write SetLabelPopupMenu;
end;
...
constructor TTestCompoundComponent.Create(AOwner : TComponent);
begin
inherited;
FSubCmp := TLabel.Create(Self);
FSubCmp.SetSubComponent(True);
FSubCmp.Parent := Self;
end;
function TTestCompoundComponent.GetLabelPopupMenu: TPopupMenu;
begin
Result := FSubCmp.PopupMenu;
end;
procedure TTestCompoundComponent.SetLabelPopupMenu(AValue: TPopupMenu);
begin
FSubCmp.PopupMenu := AValue;
end;
Or even just this:
uses
StdCtrls, Menus, ExtCtrls, Classes;
type
TTestCompoundComponent = class(TPanel)
private
FSubCmp: TLabel;
public
constructor Create(AOwner: TComponent); override;
published
property SubLabel: TLabel read FSubCmp;
end;
...
constructor TTestCompoundComponent.Create(AOwner : TComponent);
begin
inherited;
FSubCmp := TLabel.Create(Self);
FSubCmp.SetSubComponent(True);
FSubCmp.Parent := Self;
end;

In GetLabelPopupMenu(), FSubCmp is nil when Notification() receives an opInsert notification during construction before FSubCmp has been created. If FSubCmp is nil, referring to its PopupMenu property will cause the AV. So, you need to check for that in GetLabelPopupMenu(), eg:
if FSubCmp = nil then
Result := nil
else
Result := FSubCmp.PopupMenu;
Otherwise, change the order of the and logic in Notification() to this instead:
if (Operation = opRemove) and (AComponent = GetLabelPopupMenu())
If the condition (Operation = opRemove) is false, the right side condition will not be evaluated (short-circuit).

Related

Named subcomponent inside compound(?) component

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.

Persistent Polymorphic Lists in Delphi

I need a list of polymorphic objects (different object classes, but with a common base class) that I can 'persist' as part of a form file.
TList isn't persistent, and TCollection isn't polymorphic.
I can probably roll my own but prefer not to reinvent the wheel. Ideas?
None of the standard library classes meet you needs. You need to roll your own, or find a third party library.
For using default streaming framework you have to create wrapper collection item that can hold and create object instances of different classes.
unit PolyU;
interface
uses
System.SysUtils,
System.Classes;
type
TWrapperItem = class(TCollectionItem)
protected
FObjClassName: string;
FObjClass: TPersistentClass;
FObj: TPersistent;
procedure SetObjClass(Value: TPersistentClass);
procedure SetObjClassName(Value: string);
procedure SetObj(Value: TPersistent);
function CreateObject(OClass: TPersistentClass): Boolean; dynamic;
public
property ObjClass: TPersistentClass read FObjClass write SetObjClass;
published
// ObjClassName must be published before Obj to trigger CreateObject
property ObjClassName: string read FObjClassName write SetObjClassName;
property Obj: TPersistent read FObj write SetObj;
end;
implementation
procedure TWrapperItem.SetObjClass(Value: TPersistentClass);
begin
if Value <> FObjClass then
begin
FObj := nil;
FObjClass := Value;
if Value = nil then FObjClassName := ''
else FObjClassName := Value.ClassName;
CreateObject(FObjClass);
end;
end;
procedure TWrapperItem.SetObjClassName(Value: string);
begin
if Value <> FObjClassName then
begin
FObj := nil;
FObjClassName := Value;
if Value = '' then FObjClass := nil
else FObjClass := FindClass(Value);
CreateObject(FObjClass);
end;
end;
procedure TWrapperItem.SetObj(Value: TPersistent);
begin
FObj := Value;
if Assigned(Value) then
begin
FObjClassName := Value.ClassName;
FObjClass := TPersistentClass(Value.ClassType);
end
else
begin
FObjClassName := '';
FObjClass := nil;
end;
end;
function TWrapperItem.CreateObject(OClass: TPersistentClass): Boolean;
begin
Result := false;
if OClass = nil then exit;
try
FreeAndNil(FObj);
if OClass.InheritsFrom(TCollectionItem) then FObj := TCollectionItem(TCollectionItemClass(OClass).Create(nil))
else
if OClass.InheritsFrom(TComponent) then FObj := TComponentClass(OClass).Create(nil)
else
if OClass.InheritsFrom(TPersistent) then FObj := TPersistentClass(OClass).Create;
Result := true;
except
end;
end;
end.
Classes that are going to be wrapped by TWrapperItem have to be registered with Delphi streaming system via RegisterClass or RegisterClasses methods.
Following test component contains base collection that can be edited and streamed through IDE. For more control it is possible that you may want to write custom IDE editors, but this is base to start from.
unit Unit1;
interface
uses
System.Classes,
PolyU;
type
TFoo = class(TPersistent)
protected
FFoo: string;
published
property Foo: string read FFoo write FFoo;
end;
TBar = class(TPersistent)
protected
FBar: integer;
published
property Bar: integer read FBar write FBar;
end;
TTestComponent = class(TComponent)
protected
FList: TOwnedCollection;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
published
property List: TOwnedCollection read FList write FList;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Test', [TTestComponent]);
end;
constructor TTestComponent.Create(AOwner: TComponent);
begin
inherited;
FList := TOwnedCollection.Create(Self, TWrapperItem);
end;
destructor TTestComponent.Destroy;
begin
Flist.Free;
inherited;
end;
initialization
RegisterClasses([TFoo, TBar]);
finalization
UnRegisterClasses([TFoo, TBar]);
end.
This is how streamed TTestComponent (as part of Form) can look like:
object TestComponent1: TTestComponent
List = <
item
ObjClassName = 'TFoo'
Obj.Foo = 'abc'
end
item
ObjClassName = 'TBar'
Obj.Bar = 5
end>
Left = 288
Top = 16
end
I am not sure why a TCollection can not hold TCats and TDogs ?
TAnimal = class(TCollectionItem)
end;
TCat = class(TAnimal)
end;
TDog = class(TAnimal)
end;
FCollection : TCollection;
FCollection := TCollection.Create(TAnimal);
cat : TCat
cat := TCat.Create(FCollection);
dog : TDog
dog := TDag.Create(FCollection);
var
i : integer;
begin
for I := 0 to FCollection.Count - 1 do
TAnimal(FCollection.Items[i]).DoSomething;
end;
FCollection will now hold 2 items, a cat and a dog
Or I am missing the point here ?

How to modify TComponentProperty to show only particular items on drop down list?

Please consider such scenerio:
I have component called TMenuItemSelector which has two published properties: PopupMenu - allows to pick an instance of TPopupMenu from the form and MenuItem which allows to pick any instance of TMenuItem from the form.
I would like to modify property editor for MenuItem property in a way that when PopupMenu is assigned then only menu items from this PopupMenu are visible in a drop down list.
I know that I need to write my own descendant of TComponentProperty and override GetValues method. The problem is that I do not know how to access the form on which TMenuItemSelector is lying.
Original TComponentProperty is using this method to iterate all available instances:
procedure TComponentProperty.GetValues(Proc: TGetStrProc);
begin
Designer.GetComponentNames(GetTypeData(GetPropType), Proc);
end;
However, Designer seems to be precompiled so I have no idea how GetComponentNames works.
This is what I have so far, I guess only thing which I am missing is the implementation of GetValues:
unit uMenuItemSelector;
interface
uses
Classes, Menus, DesignIntf, DesignEditors;
type
TMenuItemSelector = class(TComponent)
private
FPopupMenu: TPopUpMenu;
FMenuItem: TMenuItem;
procedure SetPopupMenu(const Value: TPopUpMenu);
procedure SetMenuItem(const Value: TMenuItem);
published
property PopupMenu: TPopUpMenu read FPopupMenu write SetPopupMenu;
property MenuItem: TMenuItem read FMenuItem write SetMenuItem;
end;
type
TMenuItemProp = class(TComponentProperty)
public
function GetAttributes: TPropertyAttributes; override;
procedure GetValues(Proc: TGetStrProc); override;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterPropertyEditor(TypeInfo(TMenuItem), TMenuItemSelector, 'MenuItem', TMenuItemProp);
RegisterComponents('Test', [TMenuItemSelector]);
end;
{ TMenuItemSelector }
procedure TMenuItemSelector.SetMenuItem(const Value: TMenuItem);
begin
FMenuItem := Value;
end;
procedure TMenuItemSelector.SetPopupMenu(const Value: TPopUpMenu);
begin
FPopupMenu := Value;
end;
{ TMenuItemProperty }
function TMenuItemProp.GetAttributes: TPropertyAttributes;
begin
Result := inherited GetAttributes + [paValueList, paSortList];
end;
procedure TMenuItemProp.GetValues(Proc: TGetStrProc);
begin
//How to filter MenuItems from the form in a way that only
//MenuItems which belong to TMenuItemSelector.PopupMenu are displayed? \
//And how to get to that form?
//inherited;
end;
end.
Anyone could help?
Thanks.
When TMenuItemProp.GetValues() is called, you need to look at the TMenuItemSelector object whose MenuItem property is currently being edited, see if that object has a PopupMenu assigned, and if so then loop through its items as neded, eg:
procedure TMenuItemProp.GetValues(Proc: TGetStrProc);
var
Selector: TMenuItemSelector;
I: Integer;
begin
Selector := GetComponent(0) as TMenuItemSelector;
if Selector.PopupMenu <> nil then
begin
with Selector.PopupMenu.Items do
begin
for I := 0 to Count-1 do
Proc(Designer.GetComponentName(Items[I]));
end;
end else
inherited GetValues(Proc);
end;
BTW, you need to implement TMenuItemSelector and TMenuItemProp in separate packages. With the exception of the RegisterComponents() function, (which is implemented in a runtime package), design-time code is not allowed to be compiled into run-time executables. It is against the EULA, and Embarcadero's design-time pacakges are not allowed to be distributed. You need to implement TMenuItemSelector in a runtime-only package, and then implement TMenuItemProp and Register() in a designtime-only package that Requires the runtime-only package and uses the unit that TMenuItemSelector is declared in, eg:
unit uMenuItemSelector;
interface
uses
Classes, Menus;
type
TMenuItemSelector = class(TComponent)
private
FPopupMenu: TPopUpMenu;
FMenuItem: TMenuItem;
procedure SetPopupMenu(const Value: TPopUpMenu);
procedure SetMenuItem(const Value: TMenuItem);
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
published
property PopupMenu: TPopUpMenu read FPopupMenu write SetPopupMenu;
property MenuItem: TMenuItem read FMenuItem write SetMenuItem;
end;
implementation
{ TMenuItemSelector }
procedure TMenuItemSelector.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited;
if Operation = opRemove then
begin
if AComponent = FPopupMenu then
begin
FPopupMenu := nil;
FMenuItem := nil;
end
else if AComponent = FMenuItem then
begin
FMenuItem := nil;
end;
end;
end;
procedure TMenuItemSelector.SetMenuItem(const Value: TMenuItem);
begin
if FMenuItem <> Value then
begin
if FMenuItem <> nil then FMenuItem.RemoveFreeNotification(Self);
FMenuItem := Value;
if FMenuItem <> nil then FMenuItem.FreeNotification(Self);
end;
end;
procedure TMenuItemSelector.SetPopupMenu(const Value: TPopUpMenu);
begin
if FPopupMenu <> Value then
begin
if FPopupMenu <> nil then FPopupMenu.RemoveFreeNotification(Self);
FPopupMenu := Value;
if FPopupMenu <> nil then FPopupMenu.FreeNotification(Self);
SetMenuItem(nil);
end;
end;
end.
.
unit uMenuItemSelectorEditor;
interface
uses
Classes, DesignIntf, DesignEditors;
type
TMenuItemSelectorMenuItemProp = class(TComponentProperty)
public
function GetAttributes: TPropertyAttributes; override;
procedure GetValues(Proc: TGetStrProc); override;
end;
procedure Register;
implementation
uses
Menus, uMenuItemSelector;
procedure Register;
begin
RegisterComponents('Test', [TMenuItemSelector]);
RegisterPropertyEditor(TypeInfo(TMenuItem), TMenuItemSelector, 'MenuItem', TMenuItemSelectorMenuItemProp);
end;
{ TMenuItemSelectorMenuItemProp }
function TMenuItemSelectorMenuItemProp.GetAttributes: TPropertyAttributes;
begin
Result := inherited GetAttributes + [paValueList, paSortList] - [paMultiSelect];
end;
procedure TMenuItemSelectorMenuItemProp.GetValues(Proc: TGetStrProc);
var
Selector: TMenuItemSelector;
I: Integer;
begin
Selector := GetComponent(0) as TMenuItemSelector;
if Selector.PopupMenu <> nil then
begin
with Selector.PopupMenu.Items do
begin
for I := 0 to Count-1 do
Proc(Designer.GetComponentName(Items[I]));
end;
end else
inherited GetValues(Proc);
end;
end.

How do I add support for actions in my component

What do I need to do for adding actions support to my component. It is a button component but I guess it is the same for whatever component type it is. Any information or how to will help.
That depends on how you define action support. There is two kinds:
A possibly customized Action property of your component, which is assignable by an Action component
The Action component itself.
An action property
Every TControl descendant has an Action property which execution is by default linked to a left mouse button click. This link is managed by an ActionLink. The default ActionLink is of the type TControlActionLink which takes care of the synchronization of the caption, the hint, the enabled state, etc... of both the Action and that of the Control. If this basis functionality is all that you want, then simply publish the Action property in your component type declaration and the Delphi framework takes care of all, like Serg and LU RD already answered.
If you want your own Action property to be linked to some other condition or event (i.e. other than Click), or if you want to implement an Action property for a specific sub element of your component (that is not a TControl descendant), then you can implement your own custom Action property by defining and implementing a custom ActionLink class.
Suppose your component is some kind of grid which has columns and you want every column to have an action property that should be invoked when the user clicks the title of a column. Since such columns are likely to be of a TCollectionItem type, the column type does not have an action property by default. So you have to implement one yourself. Consider the next example which links the action's caption to the column's title, links the action's enabled state inversely to the column's readonly property and so on...:
unit Unit1;
interface
uses
Classes, ActnList, SysUtils;
type
TColumn = class;
TColumnActionLink = class(TActionLink)
protected
FClient: TColumn;
procedure AssignClient(AClient: TObject); override;
function IsCaptionLinked: Boolean; override;
function IsEnabledLinked: Boolean; override;
function IsOnExecuteLinked: Boolean; override;
function IsVisibleLinked: Boolean; override;
procedure SetCaption(const Value: String); override;
procedure SetEnabled(Value: Boolean); override;
procedure SetOnExecute(Value: TNotifyEvent); override;
procedure SetVisible(Value: Boolean); override;
end;
TColumnActionLinkClass = class of TColumnActionLink;
TColumn = class(TCollectionItem)
private
FActionLink: TColumnActionLink;
FGrid: TComponent;
FOnTitleClick: TNotifyEvent;
FReadOnly: Boolean;
FTitle: String;
FVisible: Boolean;
function DefaultTitleCaption: String;
procedure DoActionChange(Sender: TObject);
function GetAction: TBasicAction;
function IsOnTitleClickStored: Boolean;
function IsReadOnlyStored: Boolean;
function IsVisibleStored: Boolean;
procedure SetAction(Value: TBasicAction);
protected
procedure ActionChanged(Sender: TObject; CheckDefaults: Boolean); dynamic;
procedure DoTitleClick; virtual;
function GetActionLinkClass: TColumnActionLinkClass; virtual;
property ActionLink: TColumnActionLink read FActionLink write FActionLink;
public
destructor Destroy; override;
procedure InitiateAction; virtual;
published
property Action: TBasicAction read GetAction write SetAction;
property OnTitleClick: TNotifyEvent read FOnTitleClick write FOnTitleClick
stored IsOnTitleClickStored;
property ReadOnly: Boolean read FReadOnly write FReadOnly
stored IsReadOnlyStored;
property Title: String read FTitle write FTitle;
property Visible: Boolean read FVisible write FVisible
stored IsVisibleStored;
end;
implementation
{ TColumnActionLink }
procedure TColumnActionLink.AssignClient(AClient: TObject);
begin
FClient := TColumn(AClient);
end;
function TColumnActionLink.IsCaptionLinked: Boolean;
begin
Result := inherited IsCaptionLinked and (Action is TCustomAction) and
(FClient.Title = TCustomAction(Action).Caption);
end;
function TColumnActionLink.IsEnabledLinked: Boolean;
begin
Result := inherited IsEnabledLinked and (Action is TCustomAction) and
(FClient.ReadOnly <> TCustomAction(Action).Enabled);
end;
function TColumnActionLink.IsOnExecuteLinked: Boolean;
begin
Result := inherited IsOnExecuteLinked and
(#FClient.OnTitleClick = #Action.OnExecute);
end;
function TColumnActionLink.IsVisibleLinked: Boolean;
begin
Result := inherited IsVisibleLinked and (Action is TCustomAction) and
(FClient.Visible = TCustomAction(Action).Visible);
end;
procedure TColumnActionLink.SetCaption(const Value: string);
begin
if IsCaptionLinked then
FClient.Title := Value;
end;
procedure TColumnActionLink.SetEnabled(Value: Boolean);
begin
if IsEnabledLinked then
FClient.ReadOnly := not Value;
end;
procedure TColumnActionLink.SetOnExecute(Value: TNotifyEvent);
begin
if IsOnExecuteLinked then
FClient.OnTitleClick := Value;
end;
procedure TColumnActionLink.SetVisible(Value: Boolean);
begin
if IsVisibleLinked then
FClient.Visible := Value;
end;
{ TColumn }
procedure TColumn.ActionChanged(Sender: TObject; CheckDefaults: Boolean);
begin
if Sender is TCustomAction then
with TCustomAction(Sender) do
begin
if not CheckDefaults or (Caption = DefaultTitleCaption) then
FTitle := Caption;
if not CheckDefaults or (not ReadOnly) then
ReadOnly := not Enabled;
if not CheckDefaults or not Assigned(FOnTitleClick) then
FOnTitleClick := OnExecute;
if not CheckDefaults or (Self.Visible = True) then
Self.Visible := Visible;
Changed(False);
end;
end;
function TColumn.DefaultTitleCaption: String;
begin
Result := 'Column' + IntToStr(Index);
end;
destructor TColumn.Destroy;
begin
FreeAndNil(FActionLink);
inherited Destroy;
end;
procedure TColumn.DoActionChange(Sender: TObject);
begin
if Sender = Action then
ActionChanged(Sender, False);
end;
procedure TColumn.DoTitleClick;
begin
if Assigned(FOnTitleClick) then
if (Action <> nil) and (#FOnTitleClick <> #Action.OnExecute) then
FOnTitleClick(Self)
else if FActionLink = nil then
FOnTitleClick(Self)
else if FActionLink <> nil then
if (FGrid <> nil) and not (csDesigning in FGrid.ComponentState) then
begin
if not FActionLink.Execute(FGrid) then
FOnTitleClick(Self);
end
else
if not FActionLink.Execute(nil) then
FOnTitleClick(Self);
end;
function TColumn.GetAction: TBasicAction;
begin
if FActionLink <> nil then
Result := FActionLink.Action
else
Result := nil;
end;
function TColumn.GetActionLinkClass: TColumnActionLinkClass;
begin
Result := TColumnActionLink;
end;
procedure TColumn.InitiateAction;
begin
if FActionLink <> nil then
FActionLink.Update;
end;
function TColumn.IsOnTitleClickStored: Boolean;
begin
Result := (FActionLink = nil) or not ActionLink.IsOnExecuteLinked;
end;
function TColumn.IsReadOnlyStored: Boolean;
begin
Result := (FActionLink = nil) or not FActionLink.IsEnabledLinked;
if Result then
Result := FReadOnly;
end;
function TColumn.IsVisibleStored: Boolean;
begin
Result := (FActionLink = nil) or not FActionLink.IsVisibleLinked;
if Result then
Result := not Visible;
end;
procedure TColumn.SetAction(Value: TBasicAction);
begin
if Value = nil then
FreeAndNil(FActionLink)
else
begin
if FActionLink = nil then
FActionLink := GetActionLinkClass.Create(Self);
FActionLink.Action := Value;
FActionLink.OnChange := DoActionChange;
ActionChanged(Value, csLoading in Value.ComponentState);
if FGrid <> nil then
Value.FreeNotification(FGrid);
end;
Changed(False);
end;
end.
Note that this code is stripped to only the applicable action parts.
Source: www.nldelphi.com.
An action component
An action component is assignable to the action property of an arbitrary component. But since explaining all that is involved with writing such an action component is pretty comprehensive, I will make it easy for myself in providing the example below.
Suppose you want to make a control that provides zoom capabilities and that you also want the corresponding ZoomIn and ZoomOut actions that can be assigned to toolbar buttons.
unit Zoomer;
interface
uses
Classes, Controls, ActnList, Forms, Menus, Windows;
type
TZoomer = class;
TZoomAction = class(TCustomAction)
private
FZoomer: TZoomer;
procedure SetZoomer(Value: TZoomer);
protected
function GetZoomer(Target: TObject): TZoomer;
procedure Notification(AComponent: TComponent; Operation: TOperation);
override;
public
destructor Destroy; override;
function HandlesTarget(Target: TObject): Boolean; override;
procedure UpdateTarget(Target: TObject); override;
published
property Caption;
property Enabled;
property HelpContext;
property HelpKeyword;
property HelpType;
property Hint;
property ImageIndex;
property ShortCut;
property SecondaryShortCuts;
property Visible;
property OnExecute; { This property could be omitted. But if you want to be
able to override the default behavior of this action
(zooming in on a TZoomer component), then you need to
assign this event. From within the event handler
you could invoke the default behavior manually. }
property OnHint;
property OnUpdate;
property Zoomer: TZoomer read FZoomer write SetZoomer;
end;
TZoomInAction = class(TZoomAction)
public
constructor Create(AOwner: TComponent); override;
procedure ExecuteTarget(Target: TObject); override;
end;
TZoomer = class(TCustomControl)
public
procedure ZoomIn;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('RoyMKlever', [TZoomer]);
RegisterActions('Zoomer', [TZoomInAction], nil);
end;
{ TZoomAction }
destructor TZoomAction.Destroy;
begin
if FZoomer <> nil then
FZoomer.RemoveFreeNotification(Self);
inherited Destroy;
end;
function TZoomAction.GetZoomer(Target: TObject): TZoomer;
begin
if FZoomer <> nil then
Result := FZoomer
else if (Target is TZoomer) and TZoomer(Target).Focused then
Result := TZoomer(Target)
else if Screen.ActiveControl is TZoomer then
Result := TZoomer(Screen.ActiveControl)
else
{ This should not happen! HandlesTarget is called before ExecuteTarget,
or the action is disabled }
Result := nil;
end;
function TZoomAction.HandlesTarget(Target: TObject): Boolean;
begin
Result := ((FZoomer <> nil) and FZoomer.Enabled) or
((FZoomer = nil) and (Target is TZoomer) and TZoomer(Target).Focused) or
((Screen.ActiveControl is TZoomer) and Screen.ActiveControl.Enabled);
end;
procedure TZoomAction.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (Operation = opRemove) and (AComponent = FZoomer) then
FZoomer := nil;
end;
procedure TZoomAction.SetZoomer(Value: TZoomer);
begin
if FZoomer <> Value then
begin
if FZoomer <> nil then
FZoomer.RemoveFreeNotification(Self);
FZoomer := Value;
if FZoomer <> nil then
FZoomer.FreeNotification(Self);
end;
end;
procedure TZoomAction.UpdateTarget(Target: TObject);
begin
Enabled := HandlesTarget(Target);
end;
{ TZoomInAction }
constructor TZoomInAction.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Caption := 'Zoom in';
Hint := 'Zoom in|Zooms in on the selected zoomer control';
ShortCut := Menus.ShortCut(VK_ADD, [ssCtrl]);
end;
procedure TZoomInAction.ExecuteTarget(Target: TObject);
begin
GetZoomer(Target).ZoomIn;
{ For safety, you cóuld check if GetZoomer <> nil. See remark in GetZoomer. }
end;
{ TZoomer }
procedure TZoomer.ZoomIn;
begin
{ implementation of zooming in }
end;
end.
Activating this action (with a click on a toolbar button, or choosing a menu item) calls in the following priority the ZoomIn routine of:
the Zoomer control that you manually have set in the relating property of the action, if done so, and if the action is enabled, otherwise:
the by the application requested Target, but only if that target is a focused Zoomer control, or otherwise:
the active control in the entire application, but only if that is an enabled Zoomer control.
Subsequently, the ZoomOut action is simply added:
type
TZoomOutAction = class(TZoomAction)
public
constructor Create(AOwner: TComponent); override;
procedure ExecuteTarget(Target: TObject); override;
end;
{ TZoomOutAction }
constructor TZoomOutAction.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
Caption := 'Zoom out';
Hint := 'Zoom out|Zooms out on the selected zoomer control';
ShortCut := Menus.ShortCut(VK_SUBTRACT, [ssCtrl]);
end;
procedure TZoomOutAction.ExecuteTarget(Target: TObject);
begin
GetZoomer(Target).ZoomOut;
end;
Note that action components require registration in the IDE for being able to use them design time.
Applicable read food in the Delphi help:
Writing action components,
How actions find their targets,
Registering actions,
What happens when an action fires,
Updating actions,
Setting up action lists.
Source: www.nldelphi.com.
Basic action support is implemented in TControl class, so in the most simple case all you have to do is to inherit your component from TControl descendant and declare Action property as published, ex:
type
TMyGraphicControl = class(TGraphicControl)
published
property Action;
end;
If your component has additional properties that should be linked to TAction properties you should also override ActionChange method.
If your component is already a descendant of TButton then the action support is inherited.
All you need to do is declare the action property as published.

How to use a TcxCustomDataSource in a TcxExtLookupComboBox?

I use a TcxExtLookupComboBox from Devexpress and try to implement a custom datasource. I have set the customdatasource like this:
procedure TMainForm.FormCreate(Sender: TObject);
begin
fDataSource := TMyDataSource.Create;
cbotestSearch.Properties.View.DataController.CustomDataSource := fDataSource;
end;
TMyDataSource is defined here:
unit Datasource;
interface
uses
Classes,
IBQuery,
SysUtils,
cxCustomData;
type
TSearchItem = class
private
BoldID: String;
Display: String
end;
TMyDataSource = class(TcxCustomDataSource)
private
fSearchList: TList;
protected
function GetRecordCount: Integer; override;
function GetValue(ARecordHandle: TcxDataRecordHandle; AItemHandle: TcxDataItemHandle): Variant; override;
public
constructor Create;
destructor Destroy; override;
procedure GetData;
end;
implementation
constructor TMyDataSource.Create;
begin
inherited Create;
fSearchList := TList.Create;
end;
destructor TMyDataSource.Destroy;
begin
FreeAndNil(fSearchList);
inherited;
end;
procedure TMyDataSource.GetData;
var
vItem: TSearchItem;
begin
fSearchList.Clear;
vItem := TSearchItem.Create;
vItem.BoldID := '1000';
vItem.Display := 'test';
fSearchList.Add(vItem);
vItem := TSearchItem.Create;
vItem.BoldID := '1100';
vItem.Display := 'test2';
fSearchList.Add(vItem);
DataChanged; // Don't do anything as provider is nil
end;
function TMyDataSource.GetRecordCount: Integer;
begin
// Is never entered
Result := fSearchList.Count;
end;
function TMyDataSource.GetValue(ARecordHandle: TcxDataRecordHandle;
AItemHandle: TcxDataItemHandle): Variant;
begin
// Is never entered
Result := 'Test';
end;
end.
The problem is that TMyDataSource.GetValue is never called. Any hint how to fix ?
Update 1: I have another hint here. If I single step in the DataChanged method that should cause GetValue to be called is looks like this:
procedure TcxCustomDataSource.DataChanged;
begin
if Provider = nil then Exit;
// Code using Provider
end;
and Provider is nil in this case. But I have assigned the Datasource in Forms oncreate as you see.
cxExtLookupComboBox can only work with DB~views. Such views cannot accept instances of the TcxCustomDataSource object as a DataSource. So, your code will not work :-(. There is a suggestion to implement this feature in the future and it is registered at:
http://www.devexpress.com/Support/Center/ViewIssue.aspx?issueid=AS10025

Resources