views:

274

answers:

3

i have a method:

procedure Frob(Sender: TObject);

that i want to call when i click a menu item.

The method comes to me though an interface:

animal: IAnimal;

IAnimal = interface
   procedure Frob(Sender: TObject);
end;

The question revolves around what to assign to the OnClick event handler of a menu item (i.e. control):

var
   animal: IAnimal;
   ...
begin
   ...
   menuItem := TMenuItem.Create(FileMenu)
   menuItem.Caption := 'Click me!';
   menuItem.OnClick :=  <-------- what to do
   ...
end;

The obvious choice, my first attempt, and the wrong answer is:

   menuItem.OnClick := animal.Frob;

So how can i call a method when user clicks a control?

See also

+3  A: 

Have whatever object you're in hold the animal in a private field, then set up a method that calls it. Like so:

procedure TMyClass.AnimalFrob(Sender: TObject);
begin
   FAnimal.Frob(sender);
end;

Then the solution becomes easy:

menuItem.OnClick := self.AnimalFrob;

Mason Wheeler
In my case i have n items - populated from a supplied `IInterfaceList` of interfaces. Which i guess then would mean store the n-items in n-private fields (aka a TList of objects).
Ian Boyd
@Ian: Well, how you adapt it is up to you, but this is the basic idea. Make a method that matches the signature and knows which interface to call the method on, and have it forward the call for you.
Mason Wheeler
If you have packs of animals froging at you from a interface list I assume you're also creating your TMenuItems at runtime. So I'suggest declaring your own TMenuItem descendent that holds it's own interface and overrides the Click method so you don't need to assign the OnClick event handler. Code looks bad in comments so I'm not including any in here.
Cosmin Prund
+1  A: 

Another, slightly hackier approach would be to store a reference to the IAnimal in the Tag property of the TMenuItem.

This could be the index of the IAnimal in a TList as you suggested:

if Sender is TMenuItem then
  IAnimal(FAnimals[TMenuItem(Sender).Tag]).Frob;

Or you could cast the interface to Integer.

MenuItem.Tag := Integer(AnAnimal);

Then cast back to IAnimal in the event handler:

if Sender is TMenuItem then
  IAnimal(TMenuItem(Sender)).Frob;

This works well with Object references, some care may be required with interfaces due to the reference counting.

Note that Delphi 7 also has a TInterfaceList in Classes.pas

Gerry
i don't know who downvoted without explaining why; but they did. i had tried something like that; although wrapping the interface in an object (to maintain the reference count), and stuffing *that* in the tag. But stuffing Pointers into Integers is generally frowned upon - although works without problems.
Ian Boyd
I don't like the idea of stuffing interface objects in the Tag property, probably because I think the reference counters would get lost. But I'm putting TObjects in Tags all the time! That will work as long as Delphi is an 32 bit compiler, once that changed I'm actually expecting Delphi to also changed the Tag to 64 bits because it's common practice. So I'm giving an up-vote because I don't want to see this at -1
Cosmin Prund
I suspect the Embarcardero wouldn't/couldn't not make Tag the same size as a pointer, as it would break far too much code if they did.I'm not certain what would happen with interfaces, as I've never tried it.
Gerry
Typecasting interfaces to pointers/integers is an invitation to desaster if you don't know exactly what you are doing. Reference counting will come back to bite you sooner or later and you will get access violations and spend hours and hours trying to figure out why. You have been warned ;-)
dummzeuch
@dummzeuch - I agree with regard to interfaces. This technique does work well for objects. Ian's idea to create an object to hold the interface, or saving an index to an interface in a list would be preferable. It's perfectly OK to create a list solely for this purpose.
Gerry
@Cosmin, Gerry: Long time ago a created a helper class `TInterfaceBox`, who's sole member is an IUnknown. That way the reference count is kept alive as long as the wrapper object still exists. In the end i created a wrapper object that had a procedure matching the TNotifyEvent signature; the method simply turns around and calls the interface's method. Then i can assign the OnClick to the wrapper object's method, since you cannot assign event handlers to an interface's method.
Ian Boyd
+1  A: 

Hi,

I know you tagged the question as answered, but here are some other suggestions :

  type
    IClicker = Interface
      function GetOnClickProc : TNotifyEvent;
    End;

  type
    TBlob = class( TInterfacedObject, IClicker )
      procedure OnClick( Sender : TObject );
      function GetOnClickProc : TNotifyEvent;
    end;

{ TBlob }

function TBlob.GetOnClickProc : TNotifyEvent;
begin
  Result := Self.OnClick;
end;

procedure TBlob.OnClick(Sender: TObject);
begin
  MessageDlg('Clicked !', mtWarning, [mbOK], 0);
end;

{ MyForm }
  var
    clicker : IClicker;

  begin
    ...
    menuItem.OnClick := clicker.GetOnClickProc;
  end;

Of course, you have to be careful about the lifetime of the "clicker" object...

If you can manipulate your objects as objects (and not only as interfaces), try adding a common subclass :

type
  TClicker = class
    procedure OnClick( Sender : TObject ); virtual;
  end;

var
  lClicker : TClicker;
...
menuItem.OnClick := lClicker.OnClick;

I would also go for Cosmin Prund's comment : make a specialized TMenuItem subclass.

LeGEC
In the end i used a variation of this idea; although you have it sort of inside-out. The object must call the interface's method - the code that needs to be called is exposed by the interface. In your variation you have the handler code inside the object's OnClick method.
Ian Boyd
Indeed, to stick with your example, I should have left "Frob", instead of "OnClick", and maybe "GetNotifyEventProc" instead of "GetOnClickProc".
LeGEC