#delphi #function #dynamic #call
#delphi #функция #динамический #вызов
Вопрос:
У меня есть treeview (виртуальное дерево), в котором есть узлы. Когда пользователь нажимает на узел, мне нужно запустить определенную функцию, передавая текстовое имя узла. Эта функция является одним из атрибутов узла. Например, предположим, что существует два узла.
Узел 1, Имя = myHouse, Функция =BuildHouse
Узел 2, Имя = myCar, функция = RunCar
Когда я нажимаю на узел 1, мне нужно вызвать функцию BuildHouse (‘myHouse’);
Когда я нажимаю на узел 2, мне нужно вызвать RunCar (‘myCar’);
Аргументы всегда являются строками. Следует отметить, что это настоящие функции, А НЕ члены класса.
Узлов слишком много, чтобы иметь структуру кода типа CASE или IF / THEN. Мне нужен способ вызывать различные функции динамически, т. е. без жесткого кодирования поведения. Как мне это сделать? Как мне вызвать функцию, когда мне нужно выполнить поиск по имени функции во время выполнения, а не во время компиляции?
Спасибо, GS
Комментарии:
1. Подклассы и виртуальные методы — лучший подход, если это практично. В остальном указатели на функции Pascal / Delphi в порядке. Ларри Люстиг приводит отличный пример ниже.
2. Извините за оффтопик, но я видел, что virtualtree очень популярен, где я могу взять этот компонент?
3. @opc0de в Google Code: code.google.com/p/virtual-treeview
4. Я ненавижу уничтожать свой собственный пост… но другой альтернативой (в зависимости от сценария) является простое объявление вашего указателя на метод КАК указатель на метод. ПРИМЕР:
type TNodeFunction = procedure (AInput: String) of object;
. Более подробная информация здесь: docwiki.embarcadero.com/RADStudio/XE3/en /…
Ответ №1:
Ларри написал хороший пример того, как использовать указатели на функции, но все еще существует проблема их хранения таким образом, чтобы 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);
Вспомогательная функция AddNode сохраняет имя узла в FNodeNames, создает новый узел, устанавливает индекс узла в первый «слот» пользовательских данных и строковую процедуру во второй «слот».
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 извлекает индекс имени из первого «слота» пользовательских данных, указатель на функцию из второго «слота» и вызывает соответствующую функцию.
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;
Ответ №2:
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;
Ответ №3:
Я никогда не использую 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;
Комментарии:
1. Сделайте это
TProc = procedure (var p1: string; p2: integer);
, поскольку user1009073 конкретно указывает, что они не являются методами класса.