4

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.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
Wodzu
  • 6,932
  • 10
  • 65
  • 105

1 Answers1

7

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.   
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • The final paragraph assumes runtime packages. If you aren't using these then it is a little simpler but you are still subject to the redistribution constraints. – David Heffernan Apr 02 '12 at 23:06
  • 1
    Even if runtime packages are disable in the application that uses the component, you still have to (and should) split runtime code and designtime code into separate packages. The IDE will load the designtime package (so it should be as small as possible) and will decide whether the app links to the runtime code statically or dynamically. If the designtime code is in the runtime package, and that runtime package gets linked statically, then the designtime code would also get processed and potentially linked in, which is not allowed. Runtime/designtime separation has been enforced since D6. – Remy Lebeau Apr 02 '12 at 23:56
  • Thank you Remy. One more question, how does work GetComponent(0). I was reading documentation and it says that component is taken from a clipboard but how does it get to the clipboard on the first place? – Wodzu Apr 03 '12 at 05:20
  • 2
    @Wodzu `GetComponent` gives the component(s) on which the property is invoked. As long as `paMultiSelect` isn't added in `GetAttributes`, index 0 is the only component on which you are working. Selecting multiple `MenuItemSelector` components will disable the `MenuItem` property in the property editor. The clipboard is not used. – NGLN Apr 03 '12 at 05:54