Method pointer and regular procedure incompatible

35,039

Solution 1

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.

Solution 2

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.

Solution 3

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.

Solution 4

You could choose one of these:

  1. Derive your forms from a common ancestor and declare the method in it so it's available to descendants
  2. Use a global instance of a class (e.g. data module) shared by all forms
  3. Use a procedure as a fake method like this:

procedure MyClick(Self, Sender: TObject);
begin
  //...
end;

var
  M: TMethod;
begin
  M.Data := nil;
  M.Code := @MyClick;
  MyMenuItem.OnClick := TNotifyEvent(M);
end;

Solution 5

One solution is to place the OnClick method into a TDatamodule.

Share:
35,039
user1009073
Author by

user1009073

Updated on March 15, 2020

Comments

  • user1009073
    user1009073 about 4 years

    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

  • David Heffernan
    David Heffernan almost 12 years
    Make it a class procedure and you don't need to instantiate an object
  • Ken White
    Ken White almost 12 years
    Ugh! Not downvoting, because technically they will all work. However, @TLama's solution is much cleaner than any of these, IMO, especially if David's suggestion of a class procedure is added so that an instance doesn't even have to be created before using it.
  • Sam
    Sam almost 12 years
    @KenWhite I agree with you completely. I was only trying to list options not already mentioned by others. (I forgot the class method.)
  • Johan
    Johan over 8 years
    But what if I want to access the self parameter (i.e. the owning form) inside the click handler?
  • byrakham
    byrakham about 7 years
    This method has its uses...supposing you don't have an object to host the event handler. Doesn't happen often but it is occasionally useful!