Delphi - apelând dinamic la diferite funcții

Am o vizualizare arborescentă (VirtualTree) care are noduri. Când un utilizator face clic pe un nod, trebuie să execut o anumită funcție, trecând numele textului nodului. Această funcție este unul dintre atributele nodului. De exemplu, presupuneți două noduri.

Nod 1, Nume = MyHouse, Function = BuildHouse
Nod 2, Nume = MyCar, function = RunCar

Când fac clic pe Nodul 1, trebuie să sun la funcția BuildHouse ("MyHouse"); Când fac clic pe Nodul 2, trebuie să apel RunCar ("MyCar");

Argumentele sunt întotdeauna șiruri de caractere. Trebuie remarcat faptul că acestea sunt funcții adevărate, care nu sunt membri ai unei clase.

Există prea multe noduri pentru a avea un tip de structură de cod CASE sau IF/THEN. Am nevoie de o modalitate de a apela dinamic funcțiile diferite, adică fără a fi codificat comportamentul. Cum pot face acest lucru? Cum pot apela o funcție când trebuie să caut numele funcției la execuție, nu să compilesc timpul?

Mulțumiri, GS

9
Ne pare rău pentru offtopic, dar am văzut că virtualtree este foarte popular în cazul în care pot obține această componentă?
adăugat autor opc0de, sursa
Subclasele și metodele virtuale reprezintă cea mai bună abordare, dacă este posibil. În caz contrar, indicatorii funcției Pascal/Delphi sunt bine. Larry Lustig oferă un exemplu excelent de mai jos.
adăugat autor paulsm4, sursa
Urăsc să îmi necromneze postul meu ... dar o altă alternativă (în funcție de scenariu) este pur și simplu să-ți declare metoda pointer AS un pointer de metode. EXEMPLU: tip TNodeFunction = procedura (AInput: String) a obiectului; . Mai multe detalii aici: docwiki.embarcadero.com/RADStudio/XE3/en/…
adăugat autor paulsm4, sursa
@ opc0de la codul Google: code.google.com/p/virtual-treeview
adăugat autor gabr, sursa

3 răspunsuri

Larry a scris un exemplu frumos cu privire la modul de utilizare a indicatorilor de funcții, însă există încă problema stocării acestora în așa fel încât VirtualTree să le poată accesa. Există cel puțin două abordări pe care le puteți folosi aici.

1. Stocați indicatorii funcției cu datele

În cazul în care numele și funcția aparțin împreună în întreaga aplicație, de obicei, doriți să le puneți într-o singură structură.

type
  TStringProc = procedure (const s: string);

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

var
  FNodeData: array of TNodeData;

Dacă aveți două funcții de șir ...

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

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

... le puteți pune în această structură cu următorul cod.

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

VirtualTree ar trebui doar să stocheze un index în acest matrice ca date suplimentare care aparțin fiecărui nod.

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

OnGetText citește acest întreg din datele nodurilor, privește în FNodeData și afișează numele.

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

La clic (am folosit OnFocusChanged pentru acest exemplu), ați prelua din nou indexul din datele nodurilor și apelați funcția corespunzătoare.

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. Stochează indicatorii de funcții direct în VirtualTree

Dacă funcțiile de șir sunt utilizate numai atunci când afișați arborele, este logic să gestionați independent structura de date (nume de noduri) independent și să stocați indicatorii de funcții direct în datele nodurilor. Pentru a face acest lucru, trebuie să extindeți NodeDataSize la 8 (4 bytes pentru pointer în structura de nume, 4 octeți pentru pointerul funcției).

Dat fiind faptul că VirtualTree nu oferă nicio modalitate frumoasă de procesare a datelor de utilizator, îmi place să folosesc următorii ajutători pentru a accesa "slots" cu dimensiuni pointer în datele de utilizator. (Imaginați-vă că datele de utilizator sunt o matrice cu primul index 0 - acele funcții accesează această pseudo-matrice.)

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;

Constructor de copaci (FNodeNames stochează numele nodurilor individuale):

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

Funcția helper AddNode stochează numele nodului în FNodeNames, creează un nou nod, stabilește indexul de nod în primul "slot" de date utilizator și procedura de șir în al doilea "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;

Afișarea textului este identică cu cea precedentă (cu excepția faptului că acum folosesc funcția de ajutor pentru a accesa datele utilizatorului).

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

OnFocusChanged aduce indexul de nume din primul "slot" al datelor utilizatorului, indicatorul de funcții din al doilea "slot" și solicită funcția corespunzătoare.

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. Abordarea orientată spre obiect

Există, de asemenea, o opțiune de a face acest lucru într-un mod orientat-obiect. (Știu că am spus "cel puțin două abordări" la început, pentru că această a treia abordare nu corespunde pe deplin definiției dvs. (funcțiile de șir sunt funcții pure, nu metode).)

Configurați ierarhia clasei cu o singură clasă pentru fiecare funcție de șir posibil.

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;

Nodurile (instanțe ale clasei) pot fi stocate direct în VirtualTree.

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

Pentru a obține textul nodului, transferați pur și simplu datele de utilizator înapoi la TNode și accesați proprietatea Nume ...

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

... și pentru a apela funcția corespunzătoare, procedați la fel, dar apelați metoda virtuală Process.

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

Problema cu această abordare este că trebuie să distrugeți manual toate aceste obiecte înainte ca VirtualTree să fie distrusă. Cel mai bun loc pentru a face acest lucru este în evenimentul OnFreeNode.

procedure vtTreeFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
begin
  TNode(VTGetNodeData(vtTree, node, 0)).Free;
end;
19
adăugat
+1 - Foarte bine!
adăugat autor paulsm4, sursa
+1, răspuns excelent
adăugat autor TLama, sursa

Delphi permite să creeze variabile care indică funcții și apoi să apeleze funcția prin variabila. Deci, puteți să vă creați funcțiile și să atribuiți o funcție unui atribut corect al nodului (sau puteți atribui funcții, de exemplu, proprietății data </​​code> la îndemână a mai multor clase de elemente de colecție).

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
adăugat

Nu folosesc niciodată VirtualTree, dar vă pot spune două modalități.

Primul mod:

dacă utilizați Delphi 2009 sau versiunea superioară încercați să utilizați rtti pentru metoda de apelare dinamic

acesta este un exemplu pentru 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;

În al doilea rând, dacă cunoașteți tipul parametrilor și numărul de funcții, puteți pune un indicator al funcției dvs. în fiecare nod!

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

2
adăugat
Procedați ca TProc = procedură (var p1: string; p2: integer); ca utilizator1009073 specific că nu sunt metode ale unei clase.
adăugat autor Gerry Coll, sursa