Delphi 实现事件侦听与触发

我最近在Cnblogs上看到有一些朋友在讨论关于事件侦听,他的想法是,给Delphi即有的事件,例如click, mouse加上多播机制,在我认为,这种做法是不可取的,delphi在设计之初,就是把所谓的事件当做是一个回调函数来处理,这就使得每个事件只能有一个响应者,在这之上给组件加上事件多播机制只会让代码更加难懂,更加难用,增加代码量。而且用这种方法增加的事件多播机制只能用于界面上, 不能很好的用于处理数理, 实现数据与界面的分离, 降低耦合度, 在此, 我把我的事件多播实现机制与原理分享给大家. 至于好与坏, 请在实践中进行检验.

 

武稀松的实现方法相似, 本方法在实现上用到了以下几个知识点:

  1. RTTI的 TMethod 结构体, 该结构体可用于比较两个事件处理方法是否相同, 用于移除事件侦听.
  2. 用于了指针, 用于传递事件数据, 需要了解如何获取内存, 释放内存
  3. 泛型. 用于保存事件数据.

下面来具体分析 代码.

unit EventListener;

interface
uses System.Generics.Collections;

type
  TNwEventProc = procedure(const EventType:string;EventData:Pointer) of object;
  INiEventListener = interface
    procedure PerformEvent(EventType: String; EventData: Pointer);
    procedure removeEventListener(EventType: String; EventProc: TNwEventProc);
    procedure addEventListener(EventType: String; EventPro: TNwEventProc);
  end;

  TEventData = record
    EventType:String;
    EventProc:TNwEventProc;
  end;
  TNTEventList = TList<TEventData>;
  TNwEventListener = class(TInterfacedObject, INiEventListener)
  protected
    FEventList:TNTEventList;
  public
    procedure PerformEvent(EventType: String; EventData: Pointer);
    procedure removeEventListener(EventType: String; EventProc: TNwEventProc);
    procedure addEventListener(EventType: String; EventPro: TNwEventProc);

    destructor Destroy; override;
    constructor Create();
  end;

TNwEventProc 是事件处方法, 有两个参数, 一个是事件类型EventType, 一个是事件数据 EventData.  事件类型是字符串, 用于自己定义事件. 事件数据是一个指针, 指向事件的数据所在内存. 这个内存需要你自己手动去管理.

INiEventListener  是事件处理器的接口, 它包括三个就去, 分别是执行事件PerformEvent, 添加事件 AddEventListener 和删除事件RemoveEventListener. 当我们需要在某一事件发生的时候处理某数据时, 就可以调用添加事件AddEventListener来增加某个事件的侦听, 当这个事件发生时, 相应的事件就会被处理. 如果你不想再这个事件发生时处理数据, 就需要调用删除事件RemoveEventListener来移除事件的侦听. 在使用时, 你可以使用一个全局的事件处理器来管理事件; 如果你只想对某一类增加事件处理功能, 这时, 你的类就需要继承自TNwEventListener, 该类已经为你实现了INiEventListener接口, 在需要让某一个事件触发时,你只需要调用PerformEvent就可以. 这个时候的事件数据就需要事先进行约定.

下面附上全部的事件处理器代码.

unit EventListener;

interface
uses System.Generics.Collections;

type
  TNwEventProc = procedure(const EventType:string;EventData:Pointer) of object;
  INiEventListener = interface
    procedure PerformEvent(EventType: String; EventData: Pointer);
    procedure removeEventListener(EventType: String; EventProc: TNwEventProc);
    procedure addEventListener(EventType: String; EventPro: TNwEventProc);
  end;

  TEventData = record
    EventType:String;
    EventProc:TNwEventProc;
  end;
  TNTEventList = TList<TEventData>;
  TNwEventListener = class(TInterfacedObject, INiEventListener)
  protected
    FEventList:TNTEventList;
  public
    procedure PerformEvent(EventType: String; EventData: Pointer);
    procedure removeEventListener(EventType: String; EventProc: TNwEventProc);
    procedure addEventListener(EventType: String; EventPro: TNwEventProc);

    destructor Destroy; override;
    constructor Create();
  end;

var
  IEventListener:INiEventListener;

implementation
uses System.SysUtils, Rtti;
{ TNwEventListener }

procedure TNwEventListener.addEventListener(EventType: String;
  EventPro: TNwEventProc);
var
  EventData:TEventData;
begin
  EventData.EventType := EventType;
  EventData.EventProc := EventPro;
  FEventList.Add(EventData);
end;

constructor TNwEventListener.Create;
begin
  inherited;
  FEventList := TNTEventList.Create();
end;

destructor TNwEventListener.Destroy;
begin
  FEventList.Clear;
  FreeAndNil(FEventList);
  inherited;
end;

procedure TNwEventListener.PerformEvent(EventType: String; EventData: Pointer);
var
  I:Integer;
begin
  for I := FEventList.Count - 1 downto 0 do
  begin
    if FEventList.Items[I].EventType = EventType then
      FEventList.Items[I].EventProc(EventType, EventData);
  end;
end;

procedure TNwEventListener.removeEventListener(EventType: String;
  EventProc: TNwEventProc);
var
  I: Integer;
  EventData:TEventData;
  ItemProc:TNwEventProc;

  function SameMethod(S, T:TNwEventProc):Boolean;
  var
    MethodS, MethodT:TMethod;
  begin
    MethodS := TMethod(S);
    MethodT := TMethod(T);
    Result := ( MethodS.Code = MethodT.Code ) and ( MethodS.Data = MethodT.Data );
  end;

begin
  for I := FEventList.Count - 1 downto 0 do
  begin
    EventData := FEventList.Items[I];
    ItemProc := EventData.EventProc;
    if (EventData.EventType = EventType) and SameMethod(ItemProc, EventProc) then
    begin
      FEventList.Delete(I);
    end;
  end;

end;

end.

你可能会关心如何使用的问题, 下面,附上使用代码

//使用全局对象的方法

type
  TUseEventListener = class
  protected
    procedure OnEvent( EventType:String; EventData:Pointer );
  public
    procedure InitEvents;
  end;

var
  UseEvent:TUseEventListener;
begin
    { TUseEventListener }
  procedure TUseEventListener.InitEvents;
  begin
    IEventListener.addEventListener( 'OnMessage', OnEvent );
  end;
  
  procedure TUseEventListener.OnEvent( EventType:String; EventData: Pointer );
  begin
    ShowMessage( String( PChar( EventData ) ) );
  end;

  IEventListener := TNwEventListener.Create;
  UseEvent := TUseEventListener.create;
  UseEvent.InitListener;
  
  IEventListener.PerformEvent( 'OnMessage', PChar( 'The Message should be shown.' ));
end.

 

使用类对象的方法:

const
  evtDataChanged = 'evtDataChanged';

Type
  PData = ^TData;
  TData = record
    Name: String;
    City: String;
    CellPhone: String;
    Age: Integer;
  end;

  //数据处理类, 用于提供数据
  TNwDataClass = class( TNwEventListener )
  public
    procedure AddData( Name, City, CellPhone:String; Age: Integer );
  end;

  //界面显示类
  TNwInterface = class( TForm )
    procedure FormCreate( Sender: TObject );
  protected
    procedure OnEvent( EventType:String; EventData:Pointer );
  public
    procedure AddDataToList( Data: TData );
  end;

  // TNwDataClass 应该有一个全局的实例, 用于提供数据. 在下面的代码中, 就以
  // instanceDataClass 为这个实例
implementation
 
 { TNwDataClass  }
 procedure TNwDataClass.AddData( Name, City, CellPhone:String; Age: Integer );
 var
   Data: PData;
 begin
   //数据处理代码.

   GetMem( Data, SizeOf( TData ) );
   try
     Data^.Name := Name;
     Data^.City := City;
     Data^.CellPhone := CellPhone;
     Data^.Age := Age;
     Self.PerformEvent( evtDataChanged, Data );
   finally
     FreeMem( Data );
   end;
 end;

  { TNwInterface }
  procedure TNwInterface.FormCreate( Sender: TObject );
  begin
    instanceDataClass.addEventListener( evevtDataChanged, OnEvent );
  end;
  
  procedure TNwInterface.OnEvent( EventType:String; EventData: Pointer );
  begin
    AddDataToList( PData( EventData )^ );
  end;

  procedure TNwInterface.AddDataToList( Data: TData );
  begin
    //用于处理显示数据的代码.
  end;

 

总体来看, 这种做法方便简单实用. 欢迎在使用过程中有什么问题与我交流!

基于THREE JS的 JSON 模型格式 理解

THREE.js 是一个开源的基于WebGL的渲染引擎,提供了很多方便有用的类与方法,为我们使用WebGL提供了大大的便利,简化了我们的学习成本,让你只要专注于项目本身,而不会花太多时间留在WebGL的技术实现细节。当你在处理3D网络应用或项目时,你就会遇到一个问题,选择一个适合的模型格式就会摆在你的案头。obj, 3ds, dae还是其他什么呢?对于那些以二进制进行数据保存的格式而已,网络加载是一个问题,js处理二进制也是一个问题, 所以,如果有一个可以完美支持JS特性的格式,那真是令人高兴的事情,JSON Model Format 就是这样一种模式格式。

JSON Model Format 的基本格式如下:

{
	"metadata": { "formatVersion" : 3 },	

	"materials": [ {
		"DbgColor" : 15658734, // => 0xeeeeee
		"DbgIndex" : 0,
		"DbgName" : "dummy",
		"colorDiffuse" : [ 1, 0, 0 ],
	} ],

	"vertices": [ 0,0,0, 0,0,1, 1,0,1, 1,0,0, ... ],
	"normals":  [ 0,1,0, ... ],
	"colors":   [ 1,0,0, 0,1,0, 0,0,1, 1,1,0, ... ],
	"uvs":      [ [ 0,0, 0,1, 1,0, 1,1 ], ... ],

	"faces": 	[43, 0, 4, 5, 1, 0, 0, 1, 2, 3, 0, 1, 2, 3]
}

其中Metadata 中包含formatVersion 用于指示当前文件所用的格式版本号,这将决定于解析器如何解析下面的文件内容。

接下来另一个重要的域是Materials, 指定该模型所用到的素材,一个模型可能用到多个素材,所以,Materials是一个数组,每一项都是material对象。每个Material对象都包括DbgIndex 指示该Material的索引,DbgName显示Matrial的名字。

接下来的一个域是vertices, 也就是顶点了,顶点以三个数为一组,每三个数组成一个顶点坐标,x, y, z. Normals用于保存法线坐标,跟顶点一样,每三个数组成一个法线向量。 Colors是顶点颜色, THREE.js 允许给每个顶点指定颜色。 uvs是贴图坐标,faces是模型的面了,我们都知道模型是由三角面、四角面或多边形面组成的, faces就是用来保存这个面的信息。

一个面可以包括多个信息,比如说,他是三角面还是四边形面或是其他?每个顶点是不是有颜色?是不是只有一个面法线还是有面顶点法线?要区分并保存这些信息,faces是如何做的吗?其他他的格式是这样的,基本上可以看做是流式。以一个标志为开头,后面跟一系列的数据说明。例如:

[ 43, 0, 4, 5, 1, 0, 0, 1, 2, 3, 0, 1, 2, 3 ]

第一个数, 43 就是这个标志. 把这个标志表示成二进制,它的每个二进制位都代表着不同的意思。43 表示成二进制如下:

00 10 10 11

43 表示成一个字节,8个二进制位,每个位都有特定含意,如果这个位为1(置位),表示这个位在后续的数据流中有表示,如果为0,则说明后续的数据流中没有该位所代表的特定含意的数据表示。那么这8个位都表示什么意思呢?

二进制位

含意

0

面的类型。如果为0,表示三角面,为1表示四边形面

1

指示该面是否有素材,0表示没有,1表示有

2

指示该面是否有贴图坐标,0表示没有,1表示有

3

指示该面是否有顶点贴图坐标,0表示没有,1表示有

4

指示该面是否有面法线,0表示没有,1表示有

5

指示该面是否有顶点法线,0表示没有,1表示有

6

指示是否为面指定了颜色,0表示没有,1表示有

7

指示是否有顶点颜色,0表示没有,1表示有

 

后面的数据是按照从小大到的顺序排列的。 例如如果标志为43的话,表示 四边形面,有素材,有顶点贴图坐标,有顶点法线。所以,43后面跟着的四位数:0, 4, 5, 1 就是对应顶点的索引。 那么接下来的 0 就表示当前面的素材索引为 0, 然后接下来的 0, 1, 2, 3, 表示顶点贴图坐标的索引,最后的0, 1, 2, 3 则是顶点法线的索引。.

以上说的是大部分模型都会有的数据。有些特殊的模型还会有别的域,比如说

 "morphTargets" : [{ "name": "animation_000000", "vertices": [ ... ] }, ... { "name": "animation_000000", "vertices": [ ... ]}],
 "bones" : [],
 "skinIndices" : [],
 "skinWeights" : [],
 "animation" : {}

morphTargets存在表示模型存在变形动画,后面跟着的都是每帧的顶点数据。 bones用于保存骨骼信息,animation用于保存动画信息。

以上是基本的对JSON Model Format 的说明与解析,希望能对你有所帮助。

本文为原创,如果你想转载,请注明“梦溪笔记”出品!

3D-Book