Delphi - dynamisch verschillende functies aanroepen

Ik heb een treeview (VirtualTree) met knooppunten. Wanneer een gebruiker op een knooppunt klikt, moet ik een specifieke functie uitvoeren, waarbij de tekstnaam van het knooppunt wordt doorgegeven. Deze functie is een van de kenmerken van het knooppunt. Neem bijvoorbeeld twee knooppunten.

Node 1, Name = MyHouse, Function = BuildHouse
Node 2, Name = MyCar, function = RunCar

Wanneer ik op knooppunt 1 klik, moet ik de functie BuildHouse ('MyHouse') aanroepen;
Wanneer ik op knoop 2 klik, moet ik RunCar ('MyCar') aanroepen;

Argumenten zijn altijd snaren. Opgemerkt moet worden dat dit echte functies zijn, NIET leden van een klasse.

Er zijn te veel knooppunten om een ​​CASE of IF/THEN type codestructuur te hebben. Ik heb een manier nodig om de verschillende functies dynamisch aan te roepen, d.w.z. zonder het gedrag hard te coderen. Hoe doe ik dit? Hoe roep ik een functie aan wanneer ik tijdens runtime de naam van de functie moet opzoeken, niet compileertijd?

Bedankt, GS

9
Excuses voor het off-topic maar ik heb gezien dat virtualtree erg populair is, waar kan ik dit onderdeel krijgen?
toegevoegd de auteur opc0de, de bron
Subclasses en virtuele methoden zijn de beste aanpak, indien praktisch. Anders zijn Pascal/Delphi Functieaanwijzingen prima. Larry Lustig geeft hieronder een uitstekend voorbeeld.
toegevoegd de auteur paulsm4, de bron
Ik haat het om mijn eigen bericht te necromance ... maar een ander alternatief (afhankelijk van het scenario) is gewoon om aan te geven dat uw methode pointer AS een methode pointer. VOORBEELD: type TNodeFunction = procedure (AInput: String) van object; . Meer informatie hier: docwiki.embarcadero.com/RADStudio/XE3/en/…
toegevoegd de auteur paulsm4, de bron
toegevoegd de auteur gabr, de bron

3 antwoord

Larry heeft een mooi voorbeeld geschreven over het gebruik van functie-aanwijzers, maar er is nog steeds het probleem om ze zodanig op te slaan dat VirtualTree er toegang toe heeft. Er zijn tenminste twee benaderingen die je hier zou kunnen gebruiken.

1. Sla functie-wijzers op met de gegevens

Als de naam en functie bij elkaar horen in uw hele toepassing, wilt u ze meestal in één structuur samenvoegen.

type
  TStringProc = procedure (const s: string);

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

var
  FNodeData: array of TNodeData;

Als je twee string-functies hebt ...

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

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

... je kunt ze in deze structuur plaatsen met de volgende code.

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

VirtualTree zou dan alleen een index in deze array moeten opslaan als extra gegevens behorende bij elk knooppunt.

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

OnGetText leest dit gehele getal uit de knooppuntgegevens, kijkt naar de FNodeData en geeft de naam weer.

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

Bij klikken (ik gebruikte OnFocusChanged voor dit voorbeeld) zou u de index opnieuw ophalen uit de knooppuntgegevens en de juiste functie oproepen.

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. Sla functie-pointers direct op in VirtualTree

Als uw tekenreeksfuncties alleen worden gebruikt wanneer u de structuur weergeeft, is het zinvol om de gegevensstructuur (knooppuntnamen) onafhankelijk te beheren en functiepunten direct in de knooppuntgegevens op te slaan. Om dat te doen, moet u NodeDataSize uitbreiden naar 8 (4 bytes voor de aanwijzer naar naamstructuur, 4 bytes voor de functie-aanwijzer).

Omdat de VirtualTree geen prettige manier biedt om gebruikersgegevens te verwerken, gebruik ik de volgende helpers graag voor toegang tot individuele "slots" van aanwijzer in de gebruikersgegevens. (Stel je voor dat gebruikersgegevens een array zijn met de eerste index 0 - die functies hebben toegang tot deze pseudo-array.)

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;

Boombouwer (FNodeNames slaat namen van individuele knooppunten op):

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

Helperfunctie AddNode slaat de knooppuntnaam op in FNodeNames, maakt een nieuw knooppunt, plaatst de knooppuntindex in de eerste gebruikersgegevens "slot" en reeksprocedure in de tweede "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;

Tekstweergave is identiek aan het vorige voorbeeld (behalve dat ik nu de hulpfunctie gebruik om toegang te krijgen tot gebruikersgegevens).

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

OnFocusChanged haalt naamindex op uit de eerste gebruikersgegevens "slot", functie-aanwijzer van de tweede "slot" en roept de juiste functie aan.

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. Objectgerichte benadering

Er is ook een mogelijkheid om het object-georiënteerd te doen. (Ik weet dat ik in het begin "ten minste twee benaderingen" heb gezegd. Dat komt omdat deze derde benadering niet volledig voldoet aan uw definitie (tekenreeksfuncties als pure functies, geen methoden).)

Stel klassenhiërarchie in met één klasse voor elke mogelijke tekenreeksfunctie.

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;

Knopen (exemplaren van de klasse) kunnen direct in de VirtualTree worden opgeslagen.

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

Als u de knooppunttekst wilt ophalen, plaatst u de gebruikersgegevens gewoon terug naar TNode en opent u de eigenschap Name ...

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

... en om de juiste functie aan te roepen, doe hetzelfde maar roep de virtuele procesmethode aan.

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

Het probleem met deze aanpak is dat je al die objecten handmatig moet vernietigen voordat de VirtualTree wordt vernietigd. De beste plaats om dit te doen is in het OnFreeNode-evenement.

procedure vtTreeFreeNode(Sender: TBaseVirtualTree; Node: PVirtualNode);
begin
  TNode(VTGetNodeData(vtTree, node, 0)).Free;
end;
19
toegevoegd
+1 - ZEER goed!
toegevoegd de auteur paulsm4, de bron
+1, uitstekend antwoord
toegevoegd de auteur TLama, de bron

Delphi maakt het mogelijk om variabelen te creëren die naar functies wijzen en vervolgens de functie via de variabele oproepen. U kunt dus uw functies maken en een functie toewijzen aan een correct getypt attribuut van het knooppunt (of u kunt functies toewijzen aan bijvoorbeeld de handige data </​​code> -eigenschap van veel klassen met verzamelingsitems).

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
toegevoegd

Ik gebruik VirtualTree nooit, maar ik kan je er 2 manieren voor vertellen.

Eerste manier:

als u Delphi 2009 of de bovenste versie gebruikt, probeer dan rtti te gebruiken om de methode dynamisch te gebruiken

dit is een voorbeeld voor 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;

De tweede manier is als u het parametertype en het aantal functies kent, kunt u in elke knoop een aanwijzer van uw functie plaatsen!

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

2
toegevoegd
Zorg dat die TProc = procedure (var p1: string; p2: geheel getal); als user1009073 specifiek zegt dat het geen methoden van een klasse zijn.
toegevoegd de auteur Gerry Coll, de bron