Динамическое назначение анонимных функций в pascal

Я хотел бы иметь возможность динамически генерировать всплывающие меню в pascal.

Я также хотел бы иметь возможность динамически назначать обработчики OnClick каждому пункту меню.

это то, что я привык делать в C#, это моя попытка в pascal.

элемент меню обработчик событий onClick должен принадлежать объекту (of Object) поэтому я создаю объект контейнера для этого.

вот мой код:

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

при нажатии пункта меню номер 6

программа показывает ожидаемое сообщение

Menu6

однако следующее сообщение не показывало ожидаемого результата.

вместо 6 он показывает пункт 10

Menu10

независимо от того, какой элемент в списке я нажимаю, все они, похоже, запускают обработчик событий для последнего элемента в списке (10).

Это был предложил мне, что NE процедура-член объекта Event - Это один и тот же адрес памяти для всех экземпляров этого объекта.

какой бы пункт меню я ни нажал, адрес памяти MI.OnClick то же самое.

1 ответов


ключ к пониманию этого-понять, что захват переменной захватывает переменные, а не значения.

ваши методы anon захватывают одну и ту же переменную F. Существует только один экземпляр этой переменной с FormCreate выполняется только один раз. Это объясняет поведение. Когда ваши методы anon выполняют переменную F имеет значение, присвоенное ему в последней итерации цикла.

что вам нужно для каждого другой метод anon для захвата другой переменной. Вы можете сделать это, создав новый кадр стека при генерации каждого другого метода anon.

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

потому что аргумент функции GetWrapper является локальной переменной в кадре стека этой функции, каждый вызов GetWrapper создает новый экземпляр этой локальной переменной.

вы можете разместить GetWrapper куда вам угодно. Как вложенная функция в FormCreate, или как частный метод,или в единичной области.

затем создайте свои меню следующим образом:

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

связанные чтения: