Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Method pointer and regular procedure incompatible

I have an app, which has multiple forms. All these forms have a PopupMenu. I build the menu items programatically, all under a common root menu item. I want ALL the menu items to call the same procedure, and the menu item itself is basically acting as an argument....

I had this working when I just had one form doing this functionality. I now have multiple forms needing to do this. I am moving all my code to a common unit.

Example.
Form A has PopupMenu 1.  When clicked, call code in Unit CommonUnit.
Form B has PopupMenu 2.  When clicked, call code in unit CommonUnit.

When I need to call my popup from each form, I call my top level procedure (which is in unit CommonUnit), passing the name of the top menu item from each form to the top level procedure in the common unit.

I am adding items to my PopupMenu with with code.

M1 := TMenuItem.Create(TopMenuItem);
M1.Caption := FieldByName('NAME').AsString;
M1.Tag := FieldByName('ID').AsInteger;
M1.OnClick := BrowseCategories1Click;
TopMenuItem.Add(M1);

I am getting an error message when I compile. Specifically, the OnClick line is complaining about

Incompatible types: 'method pointer and regular procedure'.

I have defined BrowseCategories1Click exactly like it was before when I was doing this on a single form. The only difference is that it is now defined in a common unit, rather than as part of a form.

It is defined as

procedure BrowseCategories1Click(Sender: TObject);
begin
//
end;

What is the easiest way to get around this?

Thanks GS

like image 538
user1009073 Avatar asked Jul 03 '12 15:07

user1009073


3 Answers

A little background...

Delphi has 3 procedural types:

  • Standalone or unit-scoped function/procedure pointers declared like so:

    var Func: function(arg1:string):string;
    var Proc: procedure(arg1:string);

  • Method pointers declared like so:

    var Func: function(arg1:string):string of object;
    var Proc: procedure(arg1:string) of object;

  • And, since Delphi 2009, anonymous(see below) function/method pointers declared like so:

    var Func: reference to function(arg1:string):string;
    var Proc: reference to procedure(arg1:string);

Standalone pointers and method pointers are not interchangeable. The reason for this is the implicit Self parameter that is accessible in methods. Delphi's event model relies on method pointers, which is why you can't assign a standalone function to an object's event property.

So your event handlers will have to be defined as part of some class definition, any class definition to appease the compiler.

As TOndrej suggested you can hack around the compiler but if these event handlers are in the same unit then they should already be related anyway so you may as well go ahead and wrap them into a class.

One additional suggestion I have not seen yet is to backtrack a little. Let each form implement its own event handler but have that handler delegate responsibility to a function declared in your new unit.

TForm1.BrowseCategoriesClick(Sender:TObject)
begin
  BrowseCategories;
end;

TForm2.BrowseCategoriesClick(Sender:TObject)
begin
  BrowseCategories;
end;

unit CommonUnit

interface
procedure BrowseCategories;
begin
//
end;

This has the added benefit of separating the response to the user's action from the control that triggered the action. You could easily have the event handlers for a toolbar button and a popup menu item delegate to the same function.

Which direction you choose is ultimately up to you but I'd caution you to focus on which option will make maintainability easier in the future rather than which is the most expedient in the present.


Anonymous methods

Anonymous methods are a different beast all together. An anonymous method pointer can point to a standalone function, a method or a unnamed function declared inline. This last function type is where they get the name anonymous from. Anonymous functions/methods have the unique ability to capture variables declared outside of their scope

function DoFunc(Func:TFunc<string>):string
begin
  Result := Func('Foo');
end;

// elsewhere
procedure CallDoFunc;
var
  MyString: string;
begin
  MyString := 'Bar';
  DoFunc(function(Arg1:string):string
         begin
           Result := Arg1 + MyString;
         end);
end;

This makes them the most flexible of the procedural pointer types but they also have potentially more overhead. Variable capture consumes additional resources as does inline declarations. The compiler uses a hidden reference counted interface for inline declarations which adds some minor overhead.

like image 103
Kenneth Cochran Avatar answered Nov 23 '22 13:11

Kenneth Cochran


You can wrap your procedures into a class. This class might look like this in a separate unit:

unit CommonUnit;

interface

uses
  Dialogs;

type
  TMenuActions = class
  public
    class procedure BrowseCategoriesClick(Sender: TObject);
  end;

implementation

{ TMenuActions }

class procedure TMenuActions.BrowseCategoriesClick(Sender: TObject);
begin
  ShowMessage('BrowseCategoriesClick');
end;

end.

And to assign the action to a menu item in a different unit is enough to use this:

uses
  CommonUnit;

procedure TForm1.FormCreate(Sender: TObject);
begin
  PopupMenuItem1.OnClick := TMenuActions.BrowseCategoriesClick;
end;

Update:

Updated to use class procedures (instead of object methods) by David's suggestion. For those who want to use the object methods with the need of object instance, follow this version of the post.

like image 24
TLama Avatar answered Nov 23 '22 13:11

TLama


This is the difference between a "procedure" and a "procedure of object"

The OnClick is defined as a TNotifyEvent:

type TNotifyEvent = procedure(Sender: TObject) of object;

You cannot assign a procedure to the OnClick as it is the wrong type. It needs to be a procedure of object.

like image 41
GDF Avatar answered Nov 23 '22 15:11

GDF