关于delphi:方法指针与常规过程不兼容

Method pointer and regular procedure incompatible

我有一个应用程序,它具有多种形式。所有这些形式都有一个PopupMenu。我以编程方式构建菜单项,所有这些都在一个公共的根菜单项下。我希望所有菜单项都调用相同的过程,并且菜单项本身基本上充当参数...。

当我只有一种形式执行此功能时,我就可以使用此功能。我现在有多种形式需要执行此操作。我将所有代码移到一个通用单元。

1
2
3
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.

当我需要从每种形式调用我的弹出窗口时,我调用顶层过程(在CommonUnit单元中),将顶层菜单项的名称从每种形式传递到通用单元中的顶层过程。

我将代码添加到我的PopupMenu中。

1
2
3
4
5
M1 := TMenuItem.Create(TopMenuItem);
M1.Caption := FieldByName('NAME').AsString;
M1.Tag := FieldByName('ID').AsInteger;
M1.OnClick := BrowseCategories1Click;
TopMenuItem.Add(M1);

编译时出现错误信息。具体来说,OnClick行抱怨

不兼容的类型:"方法指针和常规过程"。

我已经定义了BrowseCategories1Click,与在单个表单上执行此操作时完全一样。唯一的区别是,它现在是在公共单位中定义的,而不是作为表单的一部分定义的。

定义为

1
2
3
4
procedure BrowseCategories1Click(Sender: TObject);
begin
//
end;

解决此问题的最简单方法是什么?

谢谢
GS


一点背景...

Delphi有3种程序类型:

  • 声明如下的独立或单元作用域的函数/过程指针:

    var Func: function(arg1:string):string;
    var Proc: procedure(arg1:string);

  • 方法指针声明如下:

    var Func: function(arg1:string):string of object;
    var Proc: procedure(arg1:string) of object;

  • 而且,从Delphi 2009开始,匿名函数(方法如下)的声明如下:

    var Func: reference to function(arg1:string):string;
    var Proc: reference to procedure(arg1:string);

独立指针和方法指针不可互换。这样做的原因是方法中可以访问的隐式Self参数。 Delphi的事件模型依赖于方法指针,这就是为什么您不能将独立函数分配给对象的事件属性的原因。

因此,必须将您的事件处理程序定义为某些类定义的一部分,以定义任何可以使编译器满意的类定义。

正如TOndrej所建议的那样,您可以修改编译器,但是如果这些事件处理程序位于同一单元中,则它们应该已经相关,因此您最好继续将它们包装到一个类中。

我还没有看到的另一个建议是稍微回溯一下。让每种表单实现其自己的事件处理程序,但让该处理程序将责任委托给新单元中声明的函数。

1
2
3
4
5
6
7
8
9
TForm1.BrowseCategoriesClick(Sender:TObject)
begin
  BrowseCategories;
end;

TForm2.BrowseCategoriesClick(Sender:TObject)
begin
  BrowseCategories;
end;
1
2
3
4
5
6
7
unit CommonUnit

interface
procedure BrowseCategories;
begin
//
end;

这具有将对用户操作的响应与触发该操作的控件分开的额外好处。您可以轻松地将工具栏按钮的事件处理程序和弹出菜单项委托给同一函数。

您最终选择哪个方向取决于您,但是我提醒您,专注于哪个选项将使将来的可维护性更加容易,而不是目前最方便的选择。


匿名方法

匿名方法是完全不同的野兽。匿名方法指针可以指向内联声明的独立函数,方法或未命名函数。最后一种函数类型是他们从中获取匿名名称的地方。匿名函数/方法具有捕获超出其范围声明的变量的独特能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

这使它们成为过程指针类型中最灵活的,但它们也可能具有更多的开销。变量捕获和内联声明一样消耗更多资源。编译器将隐藏的引用计数接口用于内联声明,这会增加一些次要开销。


您可以将过程包装到一个类中。此类在单独的单元中看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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.

并将操作分配给其他单位中的菜单项足以使用此功能:

1
2
3
4
5
6
7
uses
  CommonUnit;

procedure TForm1.FormCreate(Sender: TObject);
begin
  PopupMenuItem1.OnClick := TMenuActions.BrowseCategoriesClick;
end;

更新:

根据David的建议,已更新为使用类过程(而不是对象方法)。对于那些想要在需要对象实例的情况下使用对象方法的人,请遵循文章的this version


这是"过程"和"对象过程"之间的区别

OnClick定义为TNotifyEvent

type TNotifyEvent = procedure(Sender: TObject) of object;

您不能为OnClick分配过程,因为它是错误的类型。它必须是对象的过程。


您可以选择以下之一:

  • 从共同祖先派生表单并在其中声明方法,以便后代可以使用
  • 使用所有表单共享的类(例如数据模块)的全局实例
  • 使用过程作为伪造方法,如下所示:
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    procedure MyClick(Self, Sender: TObject);
    begin
      //...
    end;

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


    一种解决方案是将OnClick方法放入TDatamodule中。