Delphi 7事件的多处理机制

Allen Tao

2007-08-19


  首先解释一下这个题目。在我使用Delphi 7的过程中发现,一个对象的事件只能被一个过程处理。如果多次给这个对象的事件赋给处理事件的过程,最后真正处理事件的将是最后赋值的那个过程。例如,有类TMyClass中定义了一个事件OnSomeFired,在类TClientClass中该类被实例化,它的事件被处理。如下所示:

constructor TClientClass.Create;

var

  myObj: TMyClass;

begin

  //…

  myObj:= TMyClass.Create;

  myObj.OnSomeFired:= SomeFired1;

  myObj.OnSomeFired:= SomeFired2;

  这里的SomeFired1与SomeFired2都是TClientClass中定义的处理过程。其最终的结果是当OnSomeFired事件发生时,只有SomeFired2被调用。

  但在编程的实际中,往往需要一个事件被多个方法所处理。为此,我参考了一些对这个问题的解决办法,总结得出了一个自己的方法,称为事件的多处理机制。

原理

  Delphi 7中的事件本质上是一种过程指针。但事件类型在定义时要比一般过程指针在最后多一个“of object”,如常用的TNotifyEvent的定义是:

TNotifyEvent = procedure(Sender: TObject) of object;

  因此,给一个事件属性赋值,也就是给一个类的过程指针类型的成员变量赋值,当然是最后一次的赋值才有效。要想多次赋值有效就必须有一个数据结构把每次赋值赋给的过程指针变量都记录下来,最合适的数据结构当然是列表TList。但如果在每一个有事件的类中都加一个记录事件赋值的列表对象,自然是不方便的,而且使用这个列表的代码在不同类中也差不多,应该抽取出来形成一个类。这个类就是事件多处理机制的核心内容。

做法

  要记录事件处理过程,就需要将过程指针变量放入列表对象中。但列表对象只能添加指针类型对象(也就是引用类型),而过程指针变量是指类型变量,不能直接添加。这就需要有一个类对过程指针变量进行包装,转化为指针类型对象。于是,先要定义包装类TCallMethod,如下所示:

TCallMethod = class

  private

    _callback: TMethod;

  public

    property Callback: TMethod read _callback write _callback;

  end;

  这里的TMethod是Delphi预定义的记录类型,任何过程指针变量都可以强制转化为这种类型。之后,再定义记录处理过程的类,如下所示:

  TEventObject = class

  private

    callList: TList;

    function GetItem(i: Integer): TMethod;

    function GetCount: Integer;

  public

    constructor Create;

    procedure Add(p: TMethod);

    procedure Remove(p: TMethod);

    property Count: Integer read GetCount;

    property Items[i: Integer]: TMethod read GetItem;  default;

  end;

  下面是实现部分:

constructor TEventObject.Create;

begin

  callList:= TList.Create;

end;

procedure TEventObject.Add(p: TMethod);

var

  a: TCallMethod;

begin

  a:= TCallMethod.Create;

  a.Callback:= p;

  callList.Add(a);

end;

procedure TEventObject.Remove(p: TMethod);

var

  i: Integer;

begin

  for i:= 0 to GetCount - 1 do

    if (TCallMethod(callList[i]).Callback.Code = p.Code) and

      (TCallMethod(callList[i]).Callback.Data = p.Data) then

      callList.Delete(i);

end;

function TEventObject.GetCount: Integer;

begin

  Result:= callList.Count;

end;

function TEventObject.GetItem(i: Integer): TMethod;

var

  a: TCallMethod;

begin

  a:= TCallMethod(callList[i]);

  Result:= a.Callback;

end;

  从上面的代码可以看到,TEventObject的目的是包装TList对象,屏蔽了TList类的大多数方法,只对外暴露了2个过程、1个属性与1个索引,分别用于添加、删除处理事件过程、获得过程个数及通过序号检索记录的过程变量。

使用

  使用时,在需要使用事件的类中将事件的类型由过程指针改为TEventObject,即可使该事件拥有多处理的能力。如下面代码所示:

  TMyClass=class

  private

    someFired: TEventObject;

  public

    constructor Create;

    procedure DoSth;

    property OnSomeFired: TEventObject read someFired write someFired; // 多处理事件

  end;

  以下是实现代码:

constructor TMyClass.Create;

begin

  Self.someFired:= TEventObject.Create;

end;

procedure TMyClass.DoSth;

var

  i: Integer;

  method: TNotifyEvent;

begin

  if someFired.Count > 0 then

    for i:= 0  to someFired.Count - 1 do

    begin

      method:= TNotifyEvent(someFired[i]); // 在该类中事件的真正类型是TNotifyEvent,因此在触发事件时先要转成这种类型的过程指针后再进行调用

      method(Self);

    end;

end;

  定义了一个包含多处理事件的类后,再看看这种类如果在其客户类中被调用。如以下代码:

{类TClientClass中}

var

  myObj: TMyClass;

  //…

  myObj:= TMyClass.Create;

  myObj.OnSomeFired.Add(SomeFired1);

  myObj.OnSomeFired.Add(SomeFired2);

  当客户类代码中调用TMyClass的DoSth方法时,事件将被触发,而2个处理过程SomeFired1与SomeFired2则会依次调用,实现多处理的目的。

再发展一步

  上面的TEventObject类可以实现事件多处理的目的,但它对加入其中的过程指针类型没有检查,这是一个隐患。因此可以针对每一种事件要求的过程指针类型从TEventObject继承一个类,实现类型检查。如要求事件的类型是TNotifyEvent,就可以继承一个TNotifyEventObject类,如下面代码:

  TNotifyEventObject = class(TEventObject)

  public

    procedure Add(p: TNotifyEvent); overload;

    procedure Remove(p: TNotifyEvent); overload;

  end;

  以下是实现部分:

procedure TNotifyEventObject.Add(p: TNotifyEvent);

begin

  inherited Add(TMethod(p));

end;

procedure TNotifyEventObject.Remove(p: TNotifyEvent);

begin

  inherited Remove(TMethod(p));

end;

  在这个类中重载了添加与移除方法,可以有效地对过程指针类型进行检查。

以上是我对Delphi 7中事件多处理问题的一个解决办法。​