Этот вопрос затрагивает несколько концепций и проблем. Прежде всего, вы смешали несколько типов записей и некоторые свойства, и я хотел бы сначала разобраться с этим. Затем я дам вам краткую информацию о том, как читать поля «Left» и «Top» записи, когда эта запись является частью поля в классе ... Затем я дам вам советы о том, как сделать эта работа в общем. Я, вероятно, собираюсь объяснить немного больше, чем требуется, но здесь полночь, и я не могу спать!
Пример:
TPoint = record
Top: Integer;
Left: Integer;
end;
TMyClass = class
protected
function GetMyPoint: TPoint;
procedure SetMyPoint(Value:TPoint);
public
AnPoint: TPoint;
property MyPoint: TPoint read GetMyPoint write SetMyPoint;
end;
function TMyClass.GetMyPoint:Tpoint;
begin
Result := AnPoint;
end;
procedure TMyClass.SetMyPoint(Value:TPoint);
begin
AnPoint := Value;
end;
Вот сделка. Если вы напишите этот код, во время выполнения он будет делать то, что, кажется, будет делать:
var X:TMyClass;
x.AnPoint.Left := 7;
Но этот код не будет работать так же:
var X:TMyClass;
x.MyPoint.Left := 7;
Потому что этот код эквивалентен:
var X:TMyClass;
var tmp:TPoint;
tmp := X.GetMyPoint;
tmp.Left := 7;
Способ исправить это - сделать что-то вроде этого:
var X:TMyClass;
var P:TPoint;
P := X.MyPoint;
P.Left := 7;
X.MyPoint := P;
Продолжая, вы хотите сделать то же самое с RTTI. Вы можете получить RTTI как для поля «AnPoint: TPoint», так и для поля «MyPoint: TPoint». Поскольку при использовании RTTI вы, по сути, используете функцию для получения значения, вам нужно использовать технику «Создание локального копирования, изменения, обратной записи» с обоими (код того же типа, что и для примера X.MyPoint).
При выполнении с RTTI мы всегда будем начинать с «корня» (экземпляра TExampleClass или TMyClass) и использовать только серию методов Rtti GetValue и SetValue, чтобы получить значение глубокого поля или набора значение того же глубокого поля.
Предположим, у нас есть следующее:
AnPointFieldRtti: TRttiField; // This is RTTI for the AnPoint field in the TMyClass class
LeftFieldRtti: TRttiField; // This is RTTI for the Left field of the TPoint record
Мы хотим подражать этому:
var X:TMyClass;
begin
X.AnPoint.Left := 7;
end;
Мы будем разбивать это на шаги, мы стремимся к этому:
var X:TMyClass;
V:TPoint;
begin
V := X.AnPoint;
V.Left := 7;
X.AnPoint := V;
end;
Поскольку мы хотим сделать это с RTTI и хотим, чтобы оно работало с чем-либо, мы не будем использовать тип «TPoint». Итак, как и следовало ожидать, мы сначала делаем это:
var X:TMyClass;
V:TValue; // This will hide a TPoint value, but we'll pretend we don't know
begin
V := AnPointFieldRtti.GetValue(X);
end;
Для следующего шага мы будем использовать GetReferenceToRawData, чтобы получить указатель на запись TPoint, скрытую в V: TValue (вы знаете, ту, о которой мы притворяемся, мы ничего не знаем - кроме факта, что это ЗАПИСЬ). Как только мы получим указатель на эту запись, мы можем вызвать метод SetValue, чтобы переместить «7» внутрь записи.
LeftFieldRtti.SetValue(V.GetReferenceToRawData, 7);
Вот и все. Теперь нам просто нужно переместить TValue обратно в X: TMyClass:
AnPointFieldRtti.SetValue(X, V)
С головы до хвоста это будет выглядеть так:
var X:TMyClass;
V:TPoint;
begin
V := AnPointFieldRtti.GetValue(X);
LeftFieldRtti.SetValue(V.GetReferenceToRawData, 7);
AnPointFieldRtti.SetValue(X, V);
end;
Это, очевидно, может быть расширено для обработки конструкций любой глубины. Просто помните, что вам нужно сделать это шаг за шагом: первый GetValue использует «корневой» экземпляр, затем следующий GetValue использует экземпляр, извлеченный из предыдущего результата GetValue. Для записей мы можем использовать TValue.GetReferenceToRawData, для объектов мы можем использовать TValue.AsObject!
Следующий сложный момент заключается в том, что вы можете реализовать свою двунаправленную древовидную структуру. Для этого я бы порекомендовал сохранить путь от «root» к вашему полю в виде массива TRttiMember (приведение будет использовано для поиска фактического типа runtype, поэтому мы можем вызывать GetValue и SetValue). Узел будет выглядеть примерно так:
TMemberNode = class
private
FMember : array of TRttiMember; // path from root
RootInstance:Pointer;
public
function GetValue:TValue;
procedure SetValue(Value:TValue);
end;
Реализация GetValue очень проста:
function TMemberNode.GetValue:TValue;
var i:Integer;
begin
Result := FMember[0].GetValue(RootInstance);
for i:=1 to High(FMember) do
if FMember[i-1].FieldType.IsRecord then
Result := FMember[i].GetValue(Result.GetReferenceToRawData)
else
Result := FMember[i].GetValue(Result.AsObject);
end;
Реализация SetValue будет немного более сложной. Из-за этих (надоедливых?) Записей нам нужно будет сделать все , что делает процедура GetValue (потому что нам нужен указатель Instance для самого последнего элемента FMember), тогда мы сможем вызвать SetValue, но нам может потребоваться вызвать SetValue для его родителя, а затем для родителя его родителя и т. д. Это, очевидно, означает, что нам нужно СОХРАНИТЬ все неповрежденные промежуточные значения TValue, на тот случай, если они нам понадобятся. Итак, поехали:
procedure TMemberNode.SetValue(Value:TValue);
var Values:array of TValue;
i:Integer;
begin
if Length(FMember) = 1 then
FMember[0].SetValue(RootInstance, Value) // this is the trivial case
else
begin
// We've got an strucutred case! Let the fun begin.
SetLength(Values, Length(FMember)-1); // We don't need space for the last FMember
// Initialization. The first is being read from the RootInstance
Values[0] := FMember[0].GetValue(RootInstance);
// Starting from the second path element, but stoping short of the last
// path element, we read the next value
for i:=1 to Length(FMember)-2 do // we'll stop before the last FMember element
if FMember[i-1].FieldType.IsRecord then
Values[i] := FMember[i].GetValue(Values[i-1].GetReferenceToRawData)
else
Values[i] := FMember[i].GetValue(Values[i-1].AsObject);
// We now know the instance to use for the last element in the path
// so we can start calling SetValue.
if FMember[High(FMember)-1].FieldType.IsRecord then
FMember[High(FMember)].SetValue(Values[High(FMember)-1].GetReferenceToRawData, Value)
else
FMember[High(FMember)].SetValue(Values[High(FMember)-1].AsObject, Value);
// Any records along the way? Since we're dealing with classes or records, if
// something is not a record then it's a instance. If we reach a "instance" then
// we can stop processing.
i := High(FMember)-1;
while (i >= 0) and FMember[i].FieldType.IsRecord do
begin
if i = 0 then
FMember[0].SetValue(RootInstance, Values[0])
else
if FMember[i-1].FieldType.IsRecord then
FMember[i].SetValue(FMember[i-1].GetReferenceToRawData, Values[i])
else
FMember[i].SetValue(FMember[i-1].AsObject, Values[i]);
// Up one level (closer to the root):
Dec(i)
end;
end;
end;
... И так и должно быть. Теперь несколько предупреждений:
- НЕ ожидайте, что это скомпилируется! Я написал каждый кусочек кода в этом посте в веб-браузере. По техническим причинам у меня был доступ к исходному файлу Rtti.pas для поиска имен методов и полей, но у меня нет доступа к компилятору.
- Я был бы ОЧЕНЬ осторожен с этим кодом, особенно если включены СВОЙСТВА. Свойство может быть реализовано без вспомогательного поля, процедура установки может не выполнить то, что вы ожидаете. Вы можете столкнуться с циклическими ссылками!