我有一个有节点的树视图(VirtualTree)。当用户点击某个节点时,我需要运行一个特定的函数,传递该节点的文本名称。此函数是节点的属性之一。例如,假设有两个节点。
节点1,名称= MyHouse,功能= BuildHouse
节点2,Name = MyCar,function = RunCar
当我点击节点1时,我需要调用BuildHouse('MyHouse')函数;
当我点击Node 2时,我需要调用RunCar('MyCar');
参数总是字符串。应该注意的是,这些都是真正的功能,而不是一个类的成员。
有太多的节点要有CASE或IF / THEN类型的代码结构。我需要一种动态调用各种函数的方法,即不需要对代码进行硬编码。 我该怎么做呢?当我必须在运行时查找函数名称而不是编译时间时,如何调用函数?
谢谢, GS
答案 0 :(得分:19)
Larry已经就如何使用函数指针编写了一个很好的例子,但仍然存在以VirtualTree可以访问它们的方式存储它们的问题。你可以在这里使用至少两种方法。
如果名称和功能在整个应用程序中属于一起,您通常希望将它们组合成一个结构。
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;
如果仅在显示树时使用字符串函数,则独立管理数据结构(节点名称)并将函数指针直接存储到节点数据中是有意义的。为此,您必须将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;
还可以选择以面向对象的方式进行。 (我知道我在开头说“至少有两种方法”。那是因为第三种方法并不完全符合你的定义(字符串函数是纯函数,而不是方法)。)
为每个可能的字符串函数设置一个类层次结构。
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;