8

I would like to be able to dynamically generate popup menus in pascal.

I would also like to be able to dynamically assign OnClick handlers to each menu item.

This is the sort of thing that I am used to being able to do in C#, this is my attempt in pascal.

The menu item onClick event handler needs to belong to an object (of Object) so I create a container object for this.

Here is my code:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.Menus;

type
  TForm1 = class(TForm)
    PopupMenu1: TPopupMenu;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

  TFoo = class
    public
      Bar : String;
      Val : Integer;
  end;

  TNotifyEventWrapper = class
    private
      FProc: TProc<TObject>;
      I : Integer;
    public
      constructor Create(Proc: TProc<TObject>);
    published
      procedure Event(Sender: TObject);
  end;

var
  Form1: TForm1;
  NE : TNotifyEventWrapper;

implementation

{$R *.dfm}

constructor TNotifyEventWrapper.Create(Proc: TProc<TObject>);
begin
    inherited Create;
    FProc := Proc;
end;

procedure TNotifyEventWrapper.Event(Sender: TObject);
begin
    ShowMessage(IntToStr(I));
    FProc(Sender);
end;

procedure TForm1.FormCreate(Sender: TObject);
var
    F : TFoo;
    I: Integer;
    mi : TMenuItem;
begin
    if Assigned(NE) then FreeAndNil(NE);

    for I := 1 to 10 do
    begin
        F := TFoo.Create;
        F.Bar := 'Hello World!';
        F.Val := I;
        NE := TNotifyEventWrapper.Create
        (
            procedure (Sender :TObject)
            begin
               ShowMessage(F.Bar + ' ' + inttostr(F.Val) + Format('  Addr = %p', [Pointer(F)]) + Format('Sender = %p, MI.OnClick = %p', [Pointer(Sender), Pointer(@TMenuItem(Sender).OnClick)]));
            end
        );
        NE.I := I;

        mi := TMenuItem.Create(PopupMenu1);

        mi.OnClick := NE.Event;

        mi.Caption := inttostr(F.Val);

        PopupMenu1.Items.Add(mi);
    end;
end;

end.

MenuItems

On clicking menu item number 6

The program shows the expected message

Menu6

However the next message was not showing the expected result.

Instead of 6 it shows item 10

Menu10

No matter which item in the list I click on, they all seem to fire the event handler for the last item in the list (10).

It has been suggested to me that the NE object's member procedure Event is the same memory address for all instances of that object.

Whichever menu item I click on, the memory address MI.OnClick is the same.

12
  • I'm wondering whether I have found a bug/limitation of delphi Commented Mar 20, 2015 at 5:22
  • 1
    No you have not. You just haven't fully understood a nuance of variable capture yet. It captures variables rather than values. Commented Mar 20, 2015 at 5:25
  • Note that your code leaks. I assume you know this and have a plan to tackle that later. Commented Mar 20, 2015 at 6:23
  • The code address of the OnClick event is always the same. It's the address of TNotifyEventWrapper.Event. The handler's data address varies though. Commented Mar 20, 2015 at 6:30
  • @DavidHeffernan yea I know this leaks, its actually a small dummy example I created when trying to do this in a larger program. I've incorporated your solution into my program now. I use FastMM4 to spot leaks and so far it hasn't complained. Commented Mar 20, 2015 at 6:58

1 Answer 1

7

The key to understanding this is to understand that variable capture captures variables rather than values.

Your anon methods all capture the same variable F. There's only one instance of that variable since FormCreate only executes once. That explains the behaviour. When your anon methods execute the variable F has the value assigned to it in the final loop iteration.

What you need is for each different anon method to capture a different variable. You can do this by making a new stack frame when generating each different anon method.

function GetWrapper(F: Foo): TNotifyEventWrapper;
begin
  Result := TNotifyEventWrapper.Create(
    procedure(Sender: TObject)
    begin
      ShowMessage(F.Bar + ...);
    end
  );
end;

Because the argument to the function GetWrapper is a local variable in that function's stack frame, each invocation of GetWrapper creates a new instance of that local variable.

You can place GetWrapper where you please. As a nested function in FormCreate, or as a private method, or at unit scope.

Then build your menus like this:

F := TFoo.Create;
F.Bar := 'Hello World!';
F.Val := I;
NE := GetWrapper(F);
NE.I := I;

Related reading:

Sign up to request clarification or add additional context in comments.

4 Comments

Since I put the constructor F:= TFoo.Create in a loop, I would have expected there to be 10 instances of TFoo. What have I missed
There are 10 instances of TFoo objects but only one instance of the variable F. Remember that F is simply a pointer. Your anon methods capture the variable. When the anon methods execute then F points to the last created TFoo.
Oh, I think I get it. It using the same object reference on the stack.
Pretty much. Except that when a variable is captured the compiler "lifts" it from the stack and stores it on the heap. It has to because it can outlive the stack frame. And it does in your code. F lives beyond the end of FormCreate. That's implementation detail that you don't need to know though. It can help understanding but you don't need to know how it is implemented.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.