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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With