Delphi事件的广播 good

明天就是五一节了,辛苦了好几个月,借此机会应该尽情放松一番。可是想到Blog好久没有写文章,似乎缺些什么似的。这几个月来在项目中又增长了许多经验,学到许多实际应用的知识。不如把一些比较有用的记录下来,供朋友们参考可好。

我想到Delphi的事件,那可真是个方便的东西,初学者在窗体上拉几个控件,并指定它们的事件,写几句代码,立刻就得到他们想要的效果。可是事件在方便的同时也有一个不足之处,就是只能指定一个接收事件的对象,这在某些应用中会受收限制,比如多视图对应一个业务逻辑时,当一个业务对象想通知视图更新它们的状态,如果用事件,那只能有一个视图得到通知。

有没有办法让对象触发事件时,多个对象同时能收到呢?其实仔细一想,还是有挺多的,根本的就是维护一张接收事件对象的列表,事件发生时,遍历列表并调用相应的方法。本文介绍两种方法,这两种方法都比较好用。

第一种方法是从ApplicationEvents控件的实现方式学来的。ApplicationEvents是为了方便地处理Application的所有事件,一个程序中放多个ApplicationEvents,它们都能同时传递Application的事件到事件接收类中,下面是一个例子,在一个窗体上放两个ApplicationEvents控件,并指定它们的OnMessage事件,并写如下代码:

procedure TForm1.ApplicationEvents1Message(var Msg: tagMSG;
  var Handled: Boolean);
begin
  Edit1.Text := IntToStr(i1);
  Inc(i1);
end;

procedure TForm1.ApplicationEvents2Message(var Msg: tagMSG;
  var Handled: Boolean);
begin
  Edit2.Text := IntToStr(i2);
  Inc(i2);
end;

运行程序,可以看到两个事件处理方法都发生了,i1和i2疯狂的增长。也就是说Application通过ApplicationEvents这个控件使得它的事件可以被多个对象同时接收,显然ApplicationEvents不是简单地传递Application的事件,一定是运用了某些技巧,看看它的源码如何。

打开AppEvnts这个单元,发现里面的代码并不多,在初始节中有这样的代码:

initialization
  ... ...

MultiCaster := TMultiCaster.Create(Application);
end.

MultiCaster是TMultiCaster类的一个全局对象,构造函数传进Appication对象,可以肯定,在里面MultiCaster将接收Application的所有事件,看看源码就知道了。

constructor TMultiCaster.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FAppEvents := TComponentList.Create(False);
  with Application do
  begin
    OnActionExecute := DoActionExecute;
    OnActionUpdate := DoActionUpdate;
    OnActivate := DoActivate;
    OnDeactivate := DoDeactivate;
    OnException := DoException;
    OnHelp := DoHelp;
    OnHint := DoHint;
    OnIdle := DoIdle;
    OnMessage := DoMessage;
    OnMinimize := DoMinimize;
    OnRestore := DoRestore;
    OnShowHint := DoShowHint;
    OnShortCut := DoShortcut;
    OnSettingChange := DoSettingChange;
    OnModalBegin := DoModalBegin;
    OnModalEnd := DoModalEnd;
  end;
end;

上面也可以看到有一个FAppEvents列表类,它应该就是保存所有的ApplicationEvents的列表,再看看ApplicationEvents的构造函数。

constructor TCustomApplicationEvents.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  if Assigned(MultiCaster) then
    MultiCaster.AddAppEvent(Self);
end;

每创建一个ApplicationEvents,它就将自己加进MultiCaster全局对象的列表中。

procedure TMultiCaster.AddAppEvent(AppEvent: TCustomApplicationEvents);
begin
  if FAppEvents.IndexOf(AppEvent) = -1 then
    FAppEvents.Add(AppEvent);
end;

事情已经很明白了,每当Application的一个事件触发时,MultiCaster必定会在事件处理处理方法中遍历所有的ApplicationEvents并触发它们的事件。比如Application的OnMessage事件触发时,MultiCaster的DoMessage得到调用,在它里面会调用所有ApplicationEvents的DoMessage方法。

procedure TMultiCaster.DoMessage(var Msg: TMsg; var Handled: Boolean);
var
  I: Integer;
begin
  BeginDispatch;
  try
    for I := Count - 1 downto 0 do
    begin
      AppEvents[I].DoMessage(Msg, Handled);
      if FCancelDispatching then Break;
    end;
  finally
    EndDispatch;
  end;
end;

而ApplicationEvents的DoMessage方法里触发一个OnMessage事件。

procedure TCustomApplicationEvents.DoMessage(var Msg: TMsg; var Handled: Boolean);
begin
  if Assigned(FOnMessage) then FOnMessage(Msg, Handled);
end;

原来Application是借由MultiCaster这个全局对象,将它的所有事件广播给ApplicationEvents,再由ApplicationEvents去触发自己的事件。整个过程就是这么简单。

依据这个原理,我们也可以设计自己的事件广播机制,首先我们的业务对象不一定像Application是全局对象,所以当任MultiCaster这样角色的对象也不一定是全局对象,”MultiCaster”必须在”Application”的生命周期中才有效,既然如此,应该让” MultiCaster”成为”Application”的私有成员,另外像” ApplicationEvents”也不必是独立的组件类,只需要是”MultiCaster”的一个方法即可,假设这个方法为AddObjEvents。如此一来,所有事件机制就都集成到”MultiCaster”一个类中了。

多说无益,用一个简单的例子来说明这种方法的应用最有效。为了尽可能地简单,我将一个画图程序简化为一个拖放矩形的程序:程序中有两个区,一个是画板区,画板区存在一个矩形,现要求可以用鼠标拖动这个矩形,也可以改变它的大小;另一个区是信息区,显示矩形的位置和大小,也可以通过填写信息区的矩形位置和大小信息来改变矩形。

从上面的要求可以看出,矩形就相当于业务对象,我们设计矩形类为TRectangle,两个区是业务对象的两种视图,为了让代码分离以便于以后的维护和扩展,两个区用两个Frame分离出来,这两个区都必须能够接收TRectangle的事件。我们用上面描述的方法去实现TRectangle类,且看下面的代码:

unit wdRect;

interface
uses
  Classes, Graphics, Contnrs;

type
  TRectangle = class;
  TOnRectChange = procedure(Rectangle: TRectangle) of object;
  //光标在矩形类中的位置标识
  TMouseInType = (mitNone, mitLeft, mitTop, mitRight, mitBottom, mitInner,
    mitLeftTop, mitLeftBottom, mitRightTop, mitRightBottom);

{ 矩形的事件触发类 }
  TRectEvents = class
  private
    FOnRectChange: TOnRectChange;
    FOnBeforeRectChange: TOnRectChange;
  public
    procedure DoRectChange(Rectangle: TRectangle);
    procedure BeforeRectChange(Rectangle: TRectangle);
    property OnRectChange: TOnRectChange read FOnRectChange write FOnRectChange;
    property OnBeforeRectChange: TOnRectChange read FOnBeforeRectChange
      write FOnBeforeRectChange;
  end;

{ 矩形的事件广播类 }
  TEventBroadcast = class
  private
    FEventList: TObjectList; //用于保存事件类
    procedure DoRectChange(Rectangle: TRectangle);
    procedure BeforeRectChange(Rectangle: TRectangle);
  public
    function AddRectEvent: TRectEvents;
    constructor Create;
    destructor Destroy; override;
  end;

TRectangle = class
  private
    FLeft: Integer;
    FTop: Integer;
    FWidth: Integer;
    FHeight: Integer;
    FEventBroadcast: TEventBroadcast;
    procedure SetHeight(const Value: Integer);
    procedure SetLeft(const Value: Integer);
    procedure SetTop(const Value: Integer);
    procedure SetWidth(const Value: Integer);
  public
    constructor Create;
    destructor Destroy; override;

//画自己
    procedure Draw(Canvas: TCanvas);
    //擦除自己
    procedure Erase(Canvas: TCanvas);
    //光标在什么位置
    function MouseInRect(X, Y: Integer): TMouseInType;
    //调整位置大小属性
    procedure AdjustRect;

property Left: Integer read FLeft write SetLeft;
    property Top: Integer read FTop write SetTop;
    property Width: Integer read FWidth write SetWidth;
    property Height: Integer read FHeight write SetHeight;
    property EventBroadcast: TEventBroadcast read FEventBroadcast;
  end;

var
  Rectangle: TRectangle;

implementation

{ TRectangle }

procedure TRectangle.AdjustRect;
begin
  {由于对矩形拖放之后,位置大小属性可以有些不同,所以需要一些调整}
  if FLeft >= FLeft + FWidth then
    FLeft := FLeft + FWidth;
  if FTop >= FTop + FHeight then
    FTop := FTop + FHeight;
  FWidth := Abs(FWidth);
  FHeight := Abs(FHeight);
end;

constructor TRectangle.Create;
begin
  FEventBroadcast := TEventBroadcast.Create;
end;

destructor TRectangle.Destroy;
begin
  FEventBroadcast.Free;
  inherited;
end;

procedure TRectangle.Draw(Canvas: TCanvas);
begin
  Canvas.Rectangle(FLeft, FTop, FLeft + FWidth, FTop + FHeight);
end;

procedure TRectangle.Erase(Canvas: TCanvas);
begin
  Canvas.Rectangle(FLeft, FTop, FLeft + FWidth, FTop + FHeight);
end;

function TRectangle.MouseInRect(X, Y: Integer): TMouseInType;
begin
  //计算鼠标是否在矩形的特定区域中
  if (X >= FLeft - 2) and (X <= FLeft + 2) and
    (Y > FTop + 2) and (Y < FTop + FHeight - 3) then
    Result := mitLeft
  else if (X >= FLeft + FWidth - 3) and (X <= FLeft + FWidth)
    and (Y > FTop + 2) and (Y < FTop + FHeight - 3) then
    Result := mitRight
  else if (Y >= FTop - 2) and (Y <= FTop + 2) and
    (X > FLeft + 2) and (X < FLeft + FWidth - 3) then
    Result := mitTop
  else if (Y >= FTop + FHeight - 3) and (Y <= FTop + FHeight)
    and (X > FLeft + 2) and (X < FLeft + FWidth - 3) then
    Result := mitBottom
  else if (X >= FLeft - 2) and (X <= FLeft + 2) and
    (Y >= FTop - 2) and (Y <= FTop + 2) then
    Result := mitLeftTop
  else if (X >= FLeft - 2) and (X <= FLeft + 2) and
    (Y >= FTop + FHeight - 3) and (Y <= FTop + FHeight) then
    Result := mitLeftBottom
  else if (X >= FLeft + FWidth - 3) and (X <= FLeft + FWidth) and
    (Y >= FTop - 2) and (Y <= FTop + 2) then
    Result := mitRightTop
  else if (X >= FLeft + FWidth - 3) and (X <= FLeft + FWidth) and
    (Y >= FTop + FHeight - 3) and (Y <= FTop + FHeight) then
    Result := mitRightBottom
  else if (X > FLeft + 2) and (X < FLeft + FWidth - 3) and
    (Y > FTop + 2) and (Y < FTop + FHeight - 3) then
    Result := mitInner
  else Result := mitNone;
end;

procedure TRectangle.SetHeight(const Value: Integer);
begin
  if FHeight <> Value then
  begin
    FEventBroadcast.BeforeRectChange(Self);
    FHeight := Value;
    FEventBroadcast.DoRectChange(Self);
  end;
end;

procedure TRectangle.SetLeft(const Value: Integer);
begin
  if FLeft <> Value then
  begin
    FEventBroadcast.BeforeRectChange(Self);
    FLeft := Value;
    FEventBroadcast.DoRectChange(Self);
  end;
end;

procedure TRectangle.SetTop(const Value: Integer);
begin
  if FTop <> Value then
  begin
    FEventBroadcast.BeforeRectChange(Self);
    FTop := Value;
    FEventBroadcast.DoRectChange(Self);
  end;
end;

procedure TRectangle.SetWidth(const Value: Integer);
begin
  if FWidth <> Value then
  begin
    FEventBroadcast.BeforeRectChange(Self);
    FWidth := Value;
    FEventBroadcast.DoRectChange(Self);
  end;
end;

{ TRectEvents }

procedure TRectEvents.BeforeRectChange(Rectangle: TRectangle);
begin
  if Assigned(FOnBeforeRectChange) then
    FOnBeforeRectChange(Rectangle);
end;

procedure TRectEvents.DoRectChange(Rectangle: TRectangle);
begin
  if Assigned(FOnRectChange) then
    FOnRectChange(Rectangle);
end;

{ TEventBroadcast }

function TEventBroadcast.AddRectEvent: TRectEvents;
var
  RectEvents: TRectEvents;
begin
  //增加一个事件类
  RectEvents := TRectEvents.Create;
  FEventList.Add(RectEvents);
  Result := RectEvents;
end;

procedure TEventBroadcast.BeforeRectChange(Rectangle: TRectangle);
var
  i: Integer;
begin
  //向外广播事件
  for i := 0 to FEventList.Count - 1 do
    TRectEvents(FEventList[i]).BeforeRectChange(Rectangle);
end;

constructor TEventBroadcast.Create;
begin
  FEventList := TObjectList.Create;
end;

destructor TEventBroadcast.Destroy;
begin
  FEventList.Free;
  inherited;
end;

procedure TEventBroadcast.DoRectChange(Rectangle: TRectangle);
var
  i: Integer;
begin
  //向外广播事件
  for i := 0 to FEventList.Count - 1 do
    TRectEvents(FEventList[i]).DoRectChange(Rectangle);
end;

end.

单元中的类结构并不复杂,TRectangle拥有TEventBroadcast,而TRectangle的事件皆由TEventBroadcast去处理,当矩形类的大小位置改变时,都会调用TEventBroadcast的两个方法BeforeRectChange和DoRectChange,这两个方法又会遍历所有的TRectEvents类并触发它们的事件。只要调用TEventBroadcast的AddRectEvent即可创建一个TRectEvents对象并加到列表中,所以外部如果要接收TRectangle的事件,则要调用AddRectEvent方法得到一个TRectEvents,再引用这个TRectEvents类的事件。

至于其他代码,大都是实现矩形的拖放功能,这里就略去不讲了。

另外三个单元分别是MainFrm:主窗体包含两个Frame;DrawFme:矩形所在的画布;InfoFme:矩形的信息显示。

MainFrm很简单,看下面的代码:

unit MainFrm;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, DrawFme, InfoFme, wdRect;

type
  TfrmMain = class(TForm)
    pnlInfo: TPanel;
    pnlDraw: TPanel;
    fmeDraw: TfmeDraw;
    fmeInfo: TfmeInfo;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  frmMain: TfrmMain;

implementation

{$R *.dfm}

procedure TfrmMain.FormCreate(Sender: TObject);
var
  RectEvents: TRectEvents;
begin
  Rectangle := TRectangle.Create;
  //引用矩形类的事件
  RectEvents := Rectangle.EventBroadcast.AddRectEvent;
  RectEvents.OnRectChange := fmeDraw.OnRectChange;
  RectEvents.OnBeforeRectChange := fmeDraw.OnBeforeRectChange;

RectEvents := Rectangle.EventBroadcast.AddRectEvent;
  RectEvents.OnRectChange := fmeInfo.OnRectChange;

//初始化画布的属性
  Rectangle.Width := 100;
  Rectangle.Height := 100;
end;

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  Rectangle.Free;
end;

end.

主窗体创建Rectangle类,并在FormCreate中引用它的事件。

InfoFme主要是显示Rectangle的信息,并可以通过输入矩形的位置和大小来改变它:

unit InfoFme;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, wdRect;

type
  TfmeInfo = class(TFrame)
    edtLeft: TEdit;
    edtTop: TEdit;
    edtWidth: TEdit;
    edtHeight: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    procedure edtLeftChange(Sender: TObject);
    procedure edtTopChange(Sender: TObject);
    procedure edtWidthChange(Sender: TObject);
    procedure edtHeightChange(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    procedure OnRectChange(Rectangle: TRectangle);
  end;

implementation

{$R *.dfm}

{ TfmeInfo }

procedure TfmeInfo.OnRectChange(Rectangle: TRectangle);
begin
  edtLeft.Text := IntToStr(Rectangle.Left);
  edtTop.Text := IntToStr(Rectangle.Top);
  edtWidth.Text := IntToStr(Rectangle.Width);
  edtHeight.Text := IntToStr(Rectangle.Height);
end;

procedure TfmeInfo.edtLeftChange(Sender: TObject);
begin
  Rectangle.Left := StrToIntDef(edtLeft.Text, 0);
end;

procedure TfmeInfo.edtTopChange(Sender: TObject);
begin
  Rectangle.Top := StrToIntDef(edtTop.Text, 0);
end;

procedure TfmeInfo.edtWidthChange(Sender: TObject);
begin
  Rectangle.Width := StrToIntDef(edtWidth.Text, 100);
end;

procedure TfmeInfo.edtHeightChange(Sender: TObject);
begin
  Rectangle.Height := StrToIntDef(edtHeight.Text, 100);
end;

end.

DrawFme处理了矩形的一些事件,并对鼠标的事件作一些处理,代码也并不复杂:

unit DrawFme;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ExtCtrls, wdRect;

type
  TfmeDraw = class(TFrame)
    imgDraw: TPaintBox;
    procedure imgDrawMouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure imgDrawMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure imgDrawMouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure imgDrawPaint(Sender: TObject);
  private
    { Private declarations }
    mitType: TMouseInType;
    FDown: Boolean;
    FOrgX, FOrgY: Integer;
  public
    procedure OnRectChange(Rectangle: TRectangle);
    procedure OnBeforeRectChange(Rectangle: TRectangle);
    { Public declarations }
  end;

implementation

{$R *.dfm}

{ TfmeDraw }

procedure TfmeDraw.OnRectChange(Rectangle: TRectangle);
begin
  Rectangle.Draw(imgDraw.Canvas);
end;

procedure TfmeDraw.imgDrawMouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
begin
  //确定光标的类型
  case Rectangle.MouseInRect(X, Y) of
    mitNone: Cursor := crDefault;
    mitInner: Cursor := crSizeAll;
    mitLeft, mitRight: Cursor := crSizeWE;
    mitTop, mitBottom: Cursor := crSizeNS;
    mitLeftTop, mitRightBottom: Cursor := crSizeNWSE;
    mitLeftBottom, mitRightTop: Cursor := crSizeNESW;
  end;
  //对矩形的拖放控制
  if FDown then
  begin
    if (mitType = mitInner) then
    begin
      Rectangle.Left := Rectangle.Left + (X - FOrgX);
      Rectangle.Top := Rectangle.Top + (Y - FOrgY);
    end
    else if (mitType = mitLeft) then
    begin
      Rectangle.Left := Rectangle.Left + (X - FOrgX);
      Rectangle.Width := Rectangle.Width - (X - FOrgX);
    end
    else if (mitType = mitTop) then
    begin
      Rectangle.Top := Rectangle.Top + (Y - FOrgY);
      Rectangle.Height := Rectangle.Height - (Y - FOrgY);
    end
    else if (mitType = mitRight) then
    begin
      Rectangle.Width := Rectangle.Width + (X - FOrgX);
    end
    else if (mitType = mitBottom) then
    begin
      Rectangle.Height := Rectangle.Height + (Y - FOrgY);
    end
    else if (mitType = mitLeftTop) then
    begin
      Rectangle.Left := Rectangle.Left + (X - FOrgX);
      Rectangle.Width := Rectangle.Width - (X - FOrgX);
      Rectangle.Top := Rectangle.Top + (Y - FOrgY);
      Rectangle.Height := Rectangle.Height - (Y - FOrgY);
    end
    else if (mitType = mitLeftBottom) then
    begin
      Rectangle.Left := Rectangle.Left + (X - FOrgX);
      Rectangle.Width := Rectangle.Width - (X - FOrgX);
      Rectangle.Height := Rectangle.Height + (Y - FOrgY);
    end
    else if (mitType = mitRightTop) then
    begin
      Rectangle.Top := Rectangle.Top + (Y - FOrgY);
      Rectangle.Height := Rectangle.Height - (Y - FOrgY);
      Rectangle.Width := Rectangle.Width + (X - FOrgX);
    end
    else if (mitType = mitRightBottom) then
    begin
      Rectangle.Height := Rectangle.Height + (Y - FOrgY);
      Rectangle.Width := Rectangle.Width + (X - FOrgX);
    end;
    FOrgX := X;
    FOrgY := Y;
  end;
end;

procedure TfmeDraw.imgDrawMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if Button = mbLeft then
    FDown := True;
  mitType := Rectangle.MouseInRect(X, Y);
  if mitType <> mitNone then
  begin
    FOrgX := X;
    FOrgY := Y;
  end;
end;

procedure TfmeDraw.imgDrawMouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if Button = mbLeft then
    FDown := False;
  Rectangle.AdjustRect;
end;

procedure TfmeDraw.OnBeforeRectChange(Rectangle: TRectangle);
begin
  Rectangle.Erase(imgDraw.Canvas);
end;

procedure TfmeDraw.imgDrawPaint(Sender: TObject);
begin
  imgDraw.Canvas.Pen.Mode := pmNot;
  imgDraw.Canvas.Brush.Style := bsClear;
  Rectangle.Draw(imgDraw.Canvas);
end;

end.

上面就是所有代码,相信仔细读一下就可理解,把上面的代码拷进你的工程中,运行看看效果,你可以拖动这个矩形,也可以拉动它的大小,还可以在信息框中同时看到矩形信息,你更可以在信息框中输入矩形的位置大小并在画布中立刻看到效果。如果需要完整代码的,请发邮件给我。

通过上面的例子,可以看出矩形的事件变成了一个类,并被另一个类管理着,事件的机制和矩形类的实现分离出来了。

下一篇我将介绍另一种方法,采用Observer模式来实现事件的广播,欲知详细如何,且听下回分晓。

上篇文章写了将事件分离成类的方法来实现事件的广播,这次将参考观察者模式来实现事件的广播。模式中主要有这两个角色:

发布者:发布者保存着一张观察者的列表,以便在必要的时候调用观察者的方法。

观察者:观察者是现实某些特定接口的类,对于发布者来说,它只关注这些接口,并不关注观察者具体是什么类。

为了让发布者更具通用性,我写了一个发布者的父类,它负责增删和管理观察者,一个类只要继续这个类,马上就有了发布者的特征,因此你也可以将这个单元作为你的发布者父类。看下面代码:

unit EventSubject;

interface
uses
  Classes;
type
  //事件发布者的基类
  TEventSubject = class
  protected
    FObservers: IInterfaceList;
  public
    //增加一个观察者
    procedure AddObserver(const Observer: IInterface);
    //移除一个观察者
    procedure RemoveObserver(const Observer: IInterface);
    constructor Create;
    destructor Destroy; override;
  end;
implementation

{ TEventSubject }

procedure TEventSubject.AddObserver(const Observer: IInterface);
begin
  if FObservers.IndexOf(Observer) < 0 then
    FObservers.Add(Observer);
end;

constructor TEventSubject.Create;
begin
  FObservers := TInterfaceList.Create;
end;

destructor TEventSubject.Destroy;
begin
  FObservers := nil;
  inherited;
end;

procedure TEventSubject.RemoveObserver(const Observer: IInterface);
begin
  FObservers.Remove(Observer);
end;

end.

接下来是否将Rectangle类直接继续自EventSubject,我进行了一些思考,最后还是决定分离成一个独立的类,这样类的职责更加分明一些。不过之前得声明一个接口,这个接口提供了矩形事件的服务:

//矩形事件的接口
  IRectEvent = Interface(IInterface)
    [‘{C9FAFE6C-3C51-4B3F-9E73-E8EA898D4061}‘]
    procedure OnRectChange(Rectangle: TRectangle);
    procedure BeforeRectChange(Rectangle: TRectangle);
  end;

而矩形事件发布者的实现相当的简单,只是遍历父类的FObservers列表,一一调用IRectEvent接口的方法:

//矩形事件发布者类
TRectEventSubject = class(TEventSubject)
public
  procedure DoRectChange(Rectangle: TRectangle);
  procedure BeforeRectChange(Rectangle: TRectangle);
end;

... ...

{ TRectEventSubject }

procedure TRectEventSubject.BeforeRectChange(Rectangle: TRectangle);
var
  i: Integer;
begin
  for i := 0 to FObservers.Count - 1 do
    if Supports(FObservers[i], IRectEvent) then
      (FObservers[i] as IRectEvent).BeforeRectChange(Rectangle);
end;

procedure TRectEventSubject.DoRectChange(Rectangle: TRectangle);
var
  i: Integer;
begin
  for i := 0 to FObservers.Count - 1 do
    if Supports(FObservers[i], IRectEvent) then
      (FObservers[i] as IRectEvent).OnRectChange(Rectangle);
end;

上面有一点要注意的是,由于FObservers保存的是一张IInterface的列表,所以必须调用Supports方法判断该接口是否为IRectEvent,才能进行转接和调用。

矩形事件发布者类完成之后,即可将原来的事件广播类和事件触发类去掉,因为这两个类的职责已经由矩形事件发布者类代替了。然后在Rectangle类中声明一个TRectEventSubject成员并引出一个属性。最后我把Rectangle全局对象从MainFrm中移到自己的单元中,在初始化节中创建和在结束节中释放,毕竟我们认为矩形类是一开始就有的吗,这样更不依赖于外部的界面:

initialization
  Rectangle := TRectangle.Create;
finalization
  Rectangle.Free;

end.

完成了wdRect单元的改造,接下来修改界面相关的单元。首先是主窗口,这里只剩下初始化矩形大小的代码了:

procedure TfrmMain.FormCreate(Sender: TObject);
begin
  //初始化画布的属性
  Rectangle.Width := 100;
  Rectangle.Height := 100;
end;

而画布单元和矩形信息单元呢,这两个相当于观察者的具体类,所以它们要实现IRectEvent接口,并有要声明和实现接口的两个方法,这里只以DrawFrame为例,看下面代码:

type
  TfmeDraw = class(TFrame, IRectEvent)
    ... ...
    //IRectEvent
    procedure OnRectChange(Rectangle: TRectangle);
    procedure BeforeRectChange(Rectangle: TRectangle);
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  end;

implementation

{$R *.dfm}

{ TfmeDraw }

... ...

procedure TfmeDraw.OnRectChange(Rectangle: TRectangle);
begin
  Rectangle.Draw(imgDraw.Canvas);
end;

procedure TfmeDraw.BeforeRectChange(Rectangle: TRectangle);
begin
  Rectangle.Erase(imgDraw.Canvas);
end;

constructor TfmeDraw.Create(AOwner: TComponent);
begin
  inherited;
  Rectangle.RectEventSubject.AddObserver(IInterface(Self));
end;

destructor TfmeDraw.Destroy;
begin
  Rectangle.RectEventSubject.RemoveObserver(IInterface(Self));
  inherited;
end;

end.

我省略了很多不相关的代码,首先它是在构造方法中将自己加进RectEventSubject中,使之成为一个观察者;在构造方法中又将自己从RectEventSubject从移除,不知有人会不会有疑问:现实接口的对象的生命周期会由接口管理,那么fmeDraw的释放会不会由IRectEvent管理呢,答案是不会,具体原因请看我的另一篇文章:接口小论。上面代码中另外两个方法即是实现IRectEvent的方法,作用和上篇的事件是一样的。

TfmeInfo也遵循了相同的规则实现IRectEvent接口,不过有一点是必须注意的,现实接口的类必须实现接口中所有的方法,FmeInfo不能象上篇一样只得到OnRectChange的事件,它还要实现BeforeRectChange方法,不过既然没有用,把BeforeRectChange当成一个空方法就行了,也并不伤大雅。

至此,程序改造完毕,可以看到,改动其实并不大,不过与第一种方法相比,用Observer模式性能要低一些,拉动画布中的矩形,可以明显看到矩形的闪动。

用哪一种方法更好其实看具体应用,我个人更喜欢第一种方法,主要是性能要高一些,另外灵活性也并不输Observer模式。可见模式都要看具体的应用,也不能生搬硬套吧。而模式当然也不是死的,自己再多些思考,也许能找出比这两种更好的方法,期待你的发现,如果你有更好的方法,可以在留言中告知,如果你要完整的Demo,可以发邮件给我,我很乐意与你交流。

http://blog.csdn.net/dropme/article/details/975736

时间: 2025-01-09 15:40:48

Delphi事件的广播 good的相关文章

QT信号槽与Delphi事件的对比

最近学QT,对信号槽机制感到有点新鲜: QObject::connect(slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int))); 自己总结其原理,就是一句话:把两个对象的函数相关(往往是设置同一个值),而不用考虑两个对象之间的关系,而且可以一对多发信号.缺点是,事先要规定哪些函数可以信号,哪些函数是槽.而且执行内容貌似有点单调. 这一机制号称先进,但Delphi里其实也有,而且应该更强大,因为信号源可以任意指定.而且设计时可以,动

delphi 事件和属性的绑定

TWindowState = (wsNormal, wsMinimized, wsMaximized); TScrollingWinControl = class(TWinControl) private FHorzScrollBar: TControlScrollBar; FVertScrollBar: TControlScrollBar; FAutoScroll: Boolean; FAutoRangeCount: Integer; FUpdatingScrollBars: Boolean;

Delphi消息的广播方式(先RegisterWindowMessage,后SendMessage HWND_BROADCAST,最后改写接收窗口的WndProc)

///////消息广播只能将消息传递到接收消息的主程序中,MDIChild窗体不能接收到广播消息:///////// unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Se

VB.net Wcf事件广播(订阅、发布)

这篇东西原写在csdn.net上,最近新开通了博客想把零散在各处的都转移到一处. 一.源起 学WCF有一段时间了,可是无论是微软的WebCast还是其他网上的教程,亦或我购买的几本书中,都没有怎么提到服务器端事件的订阅.(后来购买的一本WCF服务编程中提供了关于订阅和发布的例子) 网上也查了很多,但大部分都没有清楚地讲到这部分内容,有的虽然是讲这部分内容,但也是语焉不详.最重要的部分都没有讲到(也许是因为水平太差,看不懂).而且网上和书上所用的语言都是C#,这让人很恼火,虽然我能使用C#,但总是

BroadcastReceiver广播接收器基础

BroadcastReceiver 广播接收器 广播接收器,主要是用于app注册一些指定事件的广播接收器,系统在这些事件被触发的时候,会通知到注册的广播接收器,然后广播接收器在onReceive函数中对接收到的通知进行处理:广播接收器的两种注册方式:静态注册:<receiver android:name=".SMSBroadcastReceiver" > <intent-filter> <action android:name="android.

Delphi知识点与技术概述【第二章 核心类库】

第三章 核心类库 Delhpi可视化编程依赖于庞大的巨型类库.Delphi 标准类库包含了数百个类以及数以千计的方法. 内容提要: *RTL包.CLX与VCL CLX用作linux中,VCL用作Windows中 VCL是一个独立的大型库(组件,控件,非可视组件,数据集合,数据感应控件,等等). 库的核心非可视化组件与类属于RTL包. Vcl结构: CLX结构: BaseCLX VisualCLX DateCLX NetCLX 库的VCL专用部分: VCL还提供了Windows专用的: Delph

spring boot 源码赏析之事件监听

使用spring Boot已经快1年多了,期间一直想点开springboot源码查看,但由于种种原因一直未能如愿(主要是人类的惰性...),今天就拿springboot 的监听事件祭刀. springboot 中常用的事件监听主要有ApplicationStartedEvent,ApplicationEnviromentPreparedEvent,ApplicationPreparedEvent,ApplicationStoppedEvent等.用于监听springboot生命周期中的各种事件.

解析Javascript事件冒泡机制(转) 本文转自:http://blog.csdn.net/luanlouis/article/details/23927347

本文转自:http://blog.csdn.net/luanlouis/article/details/23927347 1. 事件 在浏览器客户端应用平台,基本生都是以事件驱动的,即某个事件发生,然后做出相应的动作. 浏览器的事件表示的是某些事情发生的信号.事件的阐述不是本文的重点,尚未了解的朋友,可以访问W3school教程 进行了解,这将有助于更好地理解以下的内容 . 2. 冒泡机制 什么是冒泡呢? 下面这个图片大家应该心领神会吧,气泡从水底开始往上升,由深到浅,升到最上面.在上升的过程中

Delphi回调函数及其使用

Delphi回调函数及其使用 1 回调函数的概述 回调函数是这样一种机制:调用者在初始化一个对象(这里的对象是泛指,包括OOP中的对象.全局函数等)时,将一些参数传递给对象,同时将一个调用者可以访问的函数地址传递给该对象.这个函数就是调用者和被调用者之间的一种通知约定,当约定的事件发生时,被调用者(一般会包含一个工作线程)就会按照回调函数地址调用该函数.  这种方式,调用者在一个线程,被调用者在另一个线程. 消息: 消息也可以看作是某种形式的回调,因为消息也是在初始化时由调用者向被调用者传递一个