Delphi - 動的に異なる関数を呼び出す

私はノードを持つtreeview(VirtualTree)を持っています。ユーザーがノードをクリックすると、ノードのテキスト名を渡して特定の関数を実行する必要があります。この関数は、ノードの属性の1つです。たとえば、2つのノードを想定します。

ノード1、名前= MyHouse、関数= BuildHouse
ノード2、名前= MyCar、関数= RunCar

ノード1をクリックすると、BuildHouse( 'MyHouse');関数を呼び出す必要があります。 ノード2をクリックすると、RunCar( 'MyCar')を呼び出す必要があります。

引数は常に文字列です。これらは真の関数であり、クラスのメンバーではありません。

CASE型またはIF/THEN型のコード構造を持つノードが多すぎます。さまざまな関数を動的に呼び出す方法、つまりビヘイビアをハードコーディングする必要はありません。 これはどうすればいいですか?コンパイル時ではなく、実行時に関数の名前を参照する必要があるときに関数を呼び出すにはどうすればよいですか?

ありがとう、 GS

9
追加された
ビュー: 1
申し訳ありませんが、私はこの仮想ツリーが非常に人気があることを見てきましたが、どこでこのコンポーネントを入手できますか?
追加された 著者 opc0de,
実用的であれば、サブクラスやバーチャルメソッドが最良のアプローチです。それ以外の場合は、Pascal/Delphi関数ポインタは問題ありません。 Larry Lustigは以下の優れた例を挙げています。
追加された 著者 paulsm4,
自分の投稿にネモマンスするのは嫌いですが、シナリオに応じて別の方法として、単にメソッドポインタをメソッドポインタ AS として宣言するだけです。例:オブジェクトの type TNodeFunction = procedure(AInput:String); 詳細はこちら: docwiki.embarcadero.com/RADStudio/XE3/en/…
追加された 著者 paulsm4,
@ opc0de Googleコード: code.google.com/p/virtual-treeview
追加された 著者 gabr,

3 答え

Larryは関数ポインタの使い方について素晴らしい例を書いていますが、VirtualTreeが関数ポインタにアクセスできるようにそれらを格納するという問題はまだあります。ここでは少なくとも2つのアプローチがあります。

1.関数ポインタをデータとともに格納する

名前と関数がアプリケーション全体で一緒になっている場合は、通常はそれらを1つの構造体にまとめたいと思うでしょう。

type
  TStringProc = procedure (const s: string);

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

var
  FNodeData: array of TNodeData;

2つの文字列関数がある場合は...

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);

ヘルパー関数AddNodeはノード名をFNodeNamesに保存し、新しいノードを作成し、ノードインデックスを最初のユーザーデータ "slot"に設定し、文字列プロシージャを2番目の "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"から名前インデックスをフェッチし、2番目の "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.オブジェクト指向アプローチ

オブジェクト指向の方法でそれを行うオプションもあります。 (私は最初に "少なくとも2つのアプローチ"と言っていました。これは、この3番目のアプローチがあなたの定義に完全に準拠していないからです(メソッドではなく純関数としての文字列関数)。

クラス階層を、可能な文字列関数ごとに1つのクラスで設定します。

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;
19
追加された
+1 - 非常に良い!
追加された 著者 paulsm4,
+1、優れた答え
追加された 著者 TLama,

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;
13
追加された

私は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;

第2の方法は、関数の型とカウントを知っていれば、すべてのノードに関数のポインタを置くことができます!

But you have to define a procedure or function type like as Tproc = procedure (var p1: string; p2: integer) of object;

2
追加された
user1009073として、それらがクラスのメソッドではないと具体的に言う TProc = procedure(var p1:string; p2:integer);
追加された 著者 Gerry Coll,