16

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

当我只有一个表单执行此功能时,我就可以进行此工作。我现在有多种表格需要这样做。我正在将所有代码移动到一个公共单元。

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

4

5 回答 5

26

一点背景...

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;

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

于 2012-07-03T19:21:59.050 回答
19

您可以将您的过程包装到一个类中。此类在单独的单元中可能如下所示:

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帖子。

于 2012-07-03T16:13:05.600 回答
12

这是“程序”和“对象程序”之间的区别

OnClick定义为TNotifyEvent

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

您不能将过程分配给 ,OnClick因为它是错误的类型。它需要是一个对象的过程。

于 2012-07-03T16:11:01.583 回答
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;
于 2012-07-03T16:13:10.577 回答
3

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

于 2012-07-03T16:09:52.663 回答