How do I create a generic TValue for enumerated RTTI field?
Asked Answered
S

3

6

In the question here a method for creating a compatible TValue to use with SetValue is shown. I'm trying to make a generic version of this, to use RTTI to store a class into an INI file. This is my cut down code:

procedure TMyClass.LoadRTTI(xObject: TObject);
var
  LContext: TRttiContext;
  LClass: TRttiInstanceType;
  xField : TRttiField;
  szNewValue : String;
  xValue : TValue;
begin
  LContext := TRttiContext.Create;
  LClass := LContext.GetType(xObject.ClassType) as TRttiInstanceType;

  for xField in LClass.GetDeclaredFields do
  begin
    szNewValue := IniFile.ReadString(szSection, xField.Name, '');
    if szNewValue <> '' then // emumerated will be '0' (zero) as that is what GetValue.AsString returns
    begin
      case xField.FieldType.TypeKind of
      tkEnumeration: xValue := StrToIntDef(szNewValue, xField.GetValue(xObject).AsOrdinal);
      end;
      xField.SetValue(xObject, xValue); // FAILS HERE with 'Invalid calss typecast
    end;
  end;
end;

In the answer referenced, the solution was to get the value using the TValue.From() method, but that appears to require a variable of the appropriate type. I don't have such a type as my code doesn't know what it is.

I am seeking an example of a generic way to obtain a value in a string from the RTTI, and put it back again afterward. I've not found a good tutorial that covers this yet.

Syncrisis answered 24/8, 2011 at 15:9 Comment(0)
O
8

You must get an instance to the TValue to set before assing a value, and then convert the string to the enumerated value using the GetEnumValue function

Try this code :

procedure TMyClass.LoadRTTI(xObject: TObject);
var
  LContext: TRttiContext;
  LClass: TRttiInstanceType;
  xField : TRttiField;
  szNewValue : String;
  xValue : TValue;
begin
  LContext := TRttiContext.Create;
  LClass := LContext.GetType(xObject.ClassType) as TRttiInstanceType;

  for xField in LClass.GetDeclaredFields do
  begin
    szNewValue := IniFile.ReadString(szSection, xField.Name, '');
    if szNewValue <> '' then // emumerated will be '0' (zero) as that is what GetValue.AsString returns
    begin
      case xField.FieldType.TypeKind of
      tkEnumeration: 
                   begin
                     //get the instance to the TValue to set
                     xValue:=xField.GetValue(xObject);
                     //convert the data to a valid TValue
                     xValue:=TValue.FromOrdinal(xValue.TypeInfo,GetEnumValue(xValue.TypeInfo,szNewValue));
                   end;

      end;
      //assign the new value from the TValue
      xField.SetValue(xObject, xValue); 
    end;
  end;
end;
Overweigh answered 24/8, 2011 at 16:1 Comment(1)
Okay, that worked well once I'd stopped being clever in my saving code. To get the value to save to the INI, just use xField.GetValue(xObject).ToString;Syncrisis
B
6

Here is some example code showing how do this:

var
  V : TValue;
  OrdValue : Integer;
  C : TRttiContext;
  F : TRttiField;
  lTypeInfo : PTypeInfo;
begin

  // Pick a Enumerated Field
  F := C.GetType(TForm).GetField('FFormStyle');

  // Get the TypeInfo for that field
  lTypeInfo := F.FieldType.Handle;

  // Setting TValue from an Enumeration Directly.
  V := TValue.From(FormStyle);
  ShowMessage(V.ToString);
  // Setting TValue from the ordinal value of a Enumeration
  OrdValue := ord(FormStyle);
  V := TValue.FromOrdinal(lTypeInfo,OrdValue);
  ShowMessage(V.ToString);
  // Setting TValue from the String Value of an enumeration.
  OrdValue := GetEnumValue(lTypeInfo,'fsStayOnTop');
  V := TValue.FromOrdinal(lTypeInfo,OrdValue);
  ShowMessage(V.ToString);
end;
Brazier answered 24/8, 2011 at 16:7 Comment(1)
Thank you for that - it is a good set of options and alternatives that will make the question a better record of how to do this in other situations.Syncrisis
R
0

I had the same problem, but I solved it another way. A faster way:

type
  CustType = (ctNone, ctEverything, ctNothing);

  TObjctCust = class(TObject)
    InfoType: CustType;
  end;

procedure TForm34.Button1Click(Sender: TObject);
var
  CurContext: TRttiContext;
  Test: TObjctCust;
  CurClassType: TRttiType;
  CurFields: TArray<TRttiField>;
  I: Integer;
  Field: TRttiField;
  TypeValue: Integer;
  LFieldPointer: Pointer;
  TypedSmallInt: SmallInt;
begin
  Test := TObjctCust.Create;

  CurContext := TRttiContext.Create;
  CurClassType := CurContext.GetType(Test.ClassType);
  CurFields := CurClassType.GetFields;

  //Here you can set any integer value you'd like to set in the type field. For example the result of query (AsInteger, AsOrdinal)
  TypeValue := 1;
  for I := 0 to Length(CurFields) -1 do
  begin
    Field := CurFields[I];
    if Field.FieldType.TypeKind = tkEnumeration then
    begin
      //Here is the solution, I change the value direct in the field position
      LFieldPointer := Pointer(PByte(Test) + Field.Offset);
      TypedSmallInt := TypeValue;
      Move(TypedSmallInt, LFieldPointer^, Field.FieldType.TypeSize);
    end;
  end;

  ShowMessage(IntToStr(Ord(Test.InfoType)));
end;
Remex answered 20/5, 2015 at 14:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.