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.
On clicking menu item number 6
The program shows the expected message
However the next message was not showing the expected result.
Instead of 6 it shows item 10
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.
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: