Delphi - 动态调用不同的函数

时间:2011-11-12 03:07:26

标签: delphi function dynamic call

我有一个有节点的树视图(VirtualTree)。当用户点击某个节点时,我需要运行一个特定的函数,传递该节点的文本名称。此函数是节点的属性之一。例如,假设有两个节点。

节点1,名称= MyHouse,功能= BuildHouse
节点2,Name = MyCar,function = RunCar

当我点击节点1时,我需要调用BuildHouse('MyHouse')函数;
当我点击Node 2时,我需要调用RunCar('MyCar');

参数总是字符串。应该注意的是,这些都是真正的功能,而不是一个类的成员。

有太多的节点要有CASE或IF / THEN类型的代码结构。我需要一种动态调用各种函数的方法,即不需要对代码进行硬编码。 我该怎么做呢?当我必须在运行时查找函数名称而不是编译时间时,如何调用函数?

谢谢, GS

3 个答案:

答案 0 :(得分:19)

Larry已经就如何使用函数指针编写了一个很好的例子,但仍然存在以VirtualTree可以访问它们的方式存储它们的问题。你可以在这里使用至少两种方法。

1。使用数据存储函数指针

如果名称和功能在整个应用程序中属于一起,您通常希望将它们组合成一个结构。

type
  TStringProc = procedure (const s: string);

  TNodeData = record
    Name: string;
    Proc: TStringProc;
  end;

var
  FNodeData: array of TNodeData;

如果你有两个字符串函数......

procedure RunCar(const s: string);
begin
  ShowMessage('RunCar: ' + s);
end;

procedure BuildHouse(const s: string);
begin
  ShowMessage('BuildHouse: ' + s);
end;

...您可以使用以下代码将它们放入此结构中。

procedure InitNodeData;
begin
  SetLength(FNodeData, 2);
  FNodeData[0].Name := 'Car';   FNodeData[0].Proc := @RunCar;
  FNodeData[1].Name := 'House'; FNodeData[1].Proc := @BuildHouse;
end;

然后,VirtualTree只需要将索引存储到此数组中,作为属于每个节点的附加数据。

InitNodeData;
vtTree.NodeDataSize := 4;
vtTree.AddChild(nil, pointer(0));
vtTree.AddChild(nil, pointer(1));

OnGetText从节点数据中读取此整数,查看FNodeData并显示名称。

procedure vtTreeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column:
  TColumnIndex; TextType: TVSTTextType; var CellText: string);
begin
  CellText := FNodeData[integer(vtTree.GetNodeData(Node)^)].Name;
end;

单击(我在本例中使用了OnFocusChanged),您将再次从节点数据中获取索引并调用相应的函数。

procedure vtTreeFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; 
  Column: TColumnIndex);
var
  nodeIndex: integer;
begin
  if assigned(Node) then begin
    nodeIndex := integer(vtTree.GetNodeData(Node)^);
    FNodeData[nodeIndex].Proc(FNodeData[nodeIndex].Name);
  end;
end;

2。将函数指针直接存储到VirtualTree

如果仅在显示树时使用字符串函数,则独立管理数据结构(节点名称)并将函数指针直接存储到节点数据中是有意义的。为此,您必须将NodeDataSize扩展为8(指针进入名称结构为4个字节,函数指针为4个字节)。

由于VirtualTree没有提供任何处理用户数据的好方法,我喜欢使用以下帮助程序来访问用户数据中单个指针大小的“插槽”。 (想象一下,用户数据是第一个索引为0的数组 - 这些函数访问这个伪数组。)

function VTGetNodeData(vt: TBaseVirtualTree; node: PVirtualNode; ptrOffset: integer): pointer;
begin
  Result := nil;
  if not assigned(node) then
    node := vt.FocusedNode;
  if assigned(node) then
    Result := pointer(pointer(int64(vt.GetNodeData(node)) + ptrOffset * SizeOf(pointer))^);
end;

function VTGetNodeDataInt(vt: TBaseVirtualTree; node: PVirtualNode; ptrOffset: integer): integer;
begin
  Result := integer(VTGetNodeData(vt, node, ptrOffset));
end;

procedure VTSetNodeData(vt: TBaseVirtualTree; value: pointer; node: PVirtualNode;
  ptrOffset: integer);
begin
  if not assigned(node) then
    node := vt.FocusedNode;
  pointer(pointer(int64(vt.GetNodeData(node)) + ptrOffset * SizeOf(pointer))^) := value;
end;

procedure VTSetNodeDataInt(vt: TBaseVirtualTree; value: integer; node: PVirtualNode;
  ptrOffset: integer);
begin
  VTSetNodeData(vt, pointer(value), node, ptrOffset);
end;

树构建器(FNodeNames存储各个节点的名称):

Assert(SizeOf(TStringProc) = 4);
FNodeNames := TStringList.Create;
vtTree.NodeDataSize := 8;
AddNode('Car', @RunCar);
AddNode('House', @BuildHouse);

Helper函数AddNode将节点名存储到FNodeNames中,创建一个新节点,将节点索引设置为第一个用户数据“slot”,将字符串过程设置为第二个“slot”。

procedure AddNode(const name: string; proc: TStringProc);
var
  node: PVirtualNode;
begin
  FNodeNames.Add(name);
  node := vtTree.AddChild(nil);
  VTSetNodeDataInt(vtTree, FNodeNames.Count - 1, node, 0);
  VTSetNodeData(vtTree, pointer(@proc), node, 1);
end;

文本显示与前一种情况相同(除了我现在使用辅助函数访问用户数据)。

procedure vtTreeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column:
  TColumnIndex; TextType: TVSTTextType; var CellText: string);
begin
  CellText := FNodeNames[VTGetNodeDataInt(vtTree, node, 0)];
end;

OnFocusChanged从第一个用户数据“slot”获取名称索引,从第二个“slot”获取函数指针并调用相应的函数。

procedure vtTreeFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode;
  Column: TColumnIndex);
var
  nameIndex: integer;
  proc: TStringProc;
begin
  if assigned(Node) then begin
    nameIndex := VTGetNodeDataInt(vtTree, node, 0);
    proc := TStringProc(VTGetNodeData(vtTree, node, 1));
    proc(FNodeNames[nameIndex]);
  end;
end;

3。面向对象的方法

还可以选择以面向对象的方式进行。 (我知道我在开头说“至少有两种方法”。那是因为第三种方法并不完全符合你的定义(字符串函数是纯函数,而不是方法)。)

为每个可能的字符串函数设置一个类层次结构。

type
  TNode = class
  strict private
    FName: string;
  public
    constructor Create(const name: string);
    procedure Process; virtual; abstract;
    property Name: string read FName;
  end;

  TVehicle = class(TNode)
  public
    procedure Process; override;
  end;

  TBuilding = class(TNode)
  public
    procedure Process; override;
  end;

{ TNode }

constructor TNode.Create(const name: string);
begin
  inherited Create;
  FName := name;
end;

{ TVehicle }

procedure TVehicle.Process;
begin
  ShowMessage('Run: ' + Name);
end;

{ TBuilding }

procedure TBuilding.Process;
begin
  ShowMessage('Build: ' + Name);
end;

节点(类的实例)可以直接存储在VirtualTree中。

Assert(SizeOf(TNode) = 4);
vtTree.NodeDataSize := 4;
vtTree.AddChild(nil, TVehicle.Create('Car'));
vtTree.AddChild(nil, TBuilding.Create('House'));

要获取节点文本,只需将用户数据转换回TNode并访问Name属性...

procedure vtTreeGetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column:
  TColumnIndex; TextType: TVSTTextType; var CellText: string);
begin
  CellText := TNode(VTGetNodeData(vtTree, node, 0)).Name;
end;

...并调用相应的函数,执行相同操作但调用Process虚方法。

procedure vtTreeFocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode;
  Column: TColumnIndex);
begin
  TNode(VTGetNodeData(vtTree, node, 0)).Process;
end;

此方法的问题在于,必须在VirtualTree销毁之前手动销毁所有这些对象。最好的地方是在OnFreeNode事件中。

procedure vtTreeFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
begin
  TNode(VTGetNodeData(vtTree, node, 0)).Free;
end;

答案 1 :(得分:14)

Delphi允许创建指向函数的变量,然后通过变量调用函数。因此,您可以创建函数并将函数分配给节点的正确类型属性(或者您可以将函数分配给,例如,许多集合项类的方便data属性)。

interface

type
  TNodeFunction = function(AInput: String): String;

implementation

function Func1(AInput: String): String;
begin
   result := AInput;
end;

function Func2(AInput: String): String;
begin
   result := 'Fooled You';
end;

function Func3(AInput: String): String;
begin
   result := UpperCase(AInput);
end;

procedure Demonstration;
var
  SomeFunc, SomeOtherFunc: TNodeFunction;
begin

     SomeOtherFunc = Func3;

     SomeFunc := Func1;
     SomeFunc('Hello');   // returns 'Hello'
     SomeFunc := Func2;
     SomeFunc('Hello');   // returns 'Fooled You'

     SomeOtherFunc('lower case'); // returns 'LOWER CASE'

end;

答案 2 :(得分:2)

我从不使用VirtualTree,但我可以告诉你2种方法。

第一种方式:

如果您使用的是Delphi 2009或更高版本,请尝试使用rtti动态调用方法

这是rtti

的一个例子
uses rtti;

function TVLCVideo.Invoke(method: string; p: array of TValue): TValue;
var
  ctx     : TRttiContext;
  lType   : TRttiType;
  lMethod : TRttiMethod;

begin
  ctx := TRttiContext.Create;
  lType:=ctx.GetType(Self.ClassInfo); // where is the your functions list ? if TFunctions replace the Self with TFunctions class
  Result := nil;
  try
    if Assigned(lType) then
      begin
       lMethod:=lType.GetMethod(method);

       if Assigned(lMethod) then
        Result := lMethod.Invoke(Self, p);  // and here is same replace with your functions class
      end;
  finally
    lMethod.Free;
    lType.Free;
    ctx.Free;
  end;
end;

第二种方法是如果你知道函数的参数类型和数量,你可以在每个节点中放置一个函数指针!

但您必须定义一个过程或函数类型,如as Tproc = procedure (var p1: string; p2: integer) of object;