方法指针和常规过程不兼容。

20

我有一个应用程序,其中有多个表单。所有这些表单都有一个弹出菜单。我通过编程方式构建菜单项,所有菜单项都在一个公共根菜单项下方。我想让所有菜单项调用同一个程序,并且菜单项本身基本上是作为参数。

当我只有一个表单执行此功能时,我已经将其工作。现在我有多个需要执行此操作的表单。我正在将所有代码移动到一个公共单元中。

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添加项目。

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

编译时我得到了一个错误信息。具体来说,OnClick行在抱怨:

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

我已经像在单个窗体上做这件事情时那样准确地定义了BrowseCategories1Click。唯一的区别是它现在是在一个公共单元中定义的,而不是作为窗体的一部分。

它被定义为

procedure BrowseCategories1Click(Sender: TObject);
begin
//
end;

有什么最简单的方法来解决这个问题吗?

谢谢 GS

5个回答

31

一些背景...

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所建议的那样,可以通过欺骗编译器来解决这个问题,但如果这些事件处理程序在同一单元中,则它们应该已经相关了,因此您可以将它们封装到一个类中。

我还有一个未见过的额外建议是稍微回退一下。让每个窗体实现自己的事件处理程序,但是让该处理程序将责任委托给在您的新单元中声明的函数。

TForm1.BrowseCategoriesClick(Sender:TObject)
begin
  BrowseCategories;
end;

TForm2.BrowseCategoriesClick(Sender:TObject)
begin
  BrowseCategories;
end;

unit CommonUnit

interface
procedure BrowseCategories;
begin
//
end;

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

选择哪个方向最终取决于您,但我建议您专注于哪个选项将使未来的可维护性更加容易,而不是在当前情况下最快捷的选项。


匿名方法

匿名方法完全是另一回事。匿名方法指针可以指向独立的函数、方法或在内联声明的未命名函数。它们得名自最后一种函数类型中的“匿名”。匿名函数/方法有捕获其范围外声明的变量的独特能力。

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;

这使得它们成为过程性指针类型中最灵活的,但也可能有更多的开销。变量捕获会消耗额外的资源,内联声明也是如此。编译器使用一个隐藏的引用计数接口来处理内联声明,这会增加一些轻微的开销。


20

你可以将自己的过程封装在一个类中。这个类可能看起来像这样,在一个单独的单元里:

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.

要将操作指派给不同单元中的菜单项,只需使用此代码:

uses
  CommonUnit;

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

更新:

根据David的建议,已更新为使用类过程(而不是对象方法)。对于那些需要使用对象实例的对象方法的人,请参考this version


6
将它变成一个类过程,你就不需要实例化一个对象。 - David Heffernan
дҪҶжҳҜеҰӮжһңжҲ‘жғіеңЁеҚ•еҮ»еӨ„зҗҶзЁӢеәҸеҶ…и®ҝй—®selfеҸӮж•°пјҲеҚіжӢҘжңүзҡ„иЎЁеҚ•пјүпјҢиҜҘжҖҺд№ҲеҠһпјҹ - Johan

12

「过程」和「对象过程」之间的区别如下:

OnClick 被定义为 TNotifyEvent 类型:

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

你不能将一个普通过程赋值给 OnClick,因为类型不对。它需要是一个对象过程。


11

您可以选择以下其中之一:

  1. 从一个共同的祖先派生您的表单,并在其中声明该方法,以便后代可以使用
  2. 使用所有表单共享的类的全局实例(例如数据模块)
  3. 像这样使用伪方法的过程:

procedure MyClick(Self, Sender: TObject);
begin
  //...
end;

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

3
不要踩下投票按钮,因为从技术上讲,它们 都会 生效。然而,在我看来,@TLama的解决方案比这些任何一个都更加简洁,特别是如果添加了David的建议,使用class procedure之后,甚至不需要创建实例就可以使用它。 - Ken White
3
我完全同意你的观点。我只是想列出其他人没有提到过的选项。(我忘记了类方法。) - Ondrej Kelle
这种方法有其用途...假设您没有对象来托管事件处理程序。虽然不经常发生,但偶尔也很有用! - X-Ray

3
一种解决方法是将OnClick方法放置在一个TDatamodule中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接