Get the offset of a field in a delphi record at runtime
Asked Answered
C

4

7

Given a record type:

TItem = record
   UPC : string[20];
   Price : Currency;
   Cost : Currency;
   ...
end; 

And the name of a field as a string, how can I get the offset of that field within the record? I need to do this at runtime - the name of the field to access is decided at runtime.

Example:

var
   pc : Integer;
   fieldName : string;
   value : Currency;
begin
   pc := Integer(@item);                    // item is defined and filled elsewhere
   fieldName := Text1.Text;                 // user might type 'Cost' or 'Price' etc
   Inc(pc, GetItemFieldOffset(fieldName));  // how do I implement GetItemFieldOffset?
   value := PCurrency(pc)^;
   ..

I'm using Delphi 7.

Coccidioidomycosis answered 2/7, 2010 at 8:44 Comment(2)
@joe yeah, I know how it smells.. I'm not actually tying a Textbox to this; that was just the simplest example I could think of to illustrate the problem. It's actually part of a sort of rules engine, which executes commands which can refer to database fields by name. The problem is that the fields in question have already been loaded into a record when this code executes.Coccidioidomycosis
I went with a manually maintained if-else in the end, cutting out the pointer thing entirely and just mapping name : string to value : currency.Coccidioidomycosis
H
8

You can't. Delphi 7 does not emit RTTI for records. There are other options (as seen the previous answers) but those require manual mapping of "Field Name" -> "Offset".

Henbit answered 2/7, 2010 at 9:52 Comment(0)
C
5

As alex said, Delphi 7 doesn't emit RTTI for records, so you can't retrieve the required info at runtime. However, in later versions (Delphi 2010+) it does, and the following code:

TItem = record
   UPC : string[20];
   Price : Currency;
   Cost : Currency;
//...
end;
    var
       rttiContext: TRttiContext;
       rttiType: TRttiType;
       fields: TArray<TRttiField>;
       item: TItem;
    begin
        rttiType := rttiContext.GetType(TypeInfo(TItem));
        caption := rttiType.Name + ' {';
        fields := rttiType.GetFields;
        for i := low(fields) to high(fields) do
        begin
          caption := caption +'{name='+fields[i].Name+',';
          caption := caption +'offset='+IntToStr(fields[i].Offset)+'}';
        end;
        caption := caption + '}';

will produce 'TItem {{name=UPC,offset=0}{name=Price,offset=24}{name=Cost,offset=32}}'

You can also set the field value in a particular instance (although you should really also verify the type) using:

if fields[i].Name = 'Price' then
  fields[i].SetValue(@item, 10);
Cyclopentane answered 2/7, 2010 at 11:52 Comment(1)
Right. Put another way, RTTI does for you automatically and generically for all types what you can do for yourself specifically and most efficiently for only those types that need it.Geniagenial
P
4

Following would work for your simplified scenario but I doubt it will be possible to make a generic function for this kind of thing.

The best I can think if is to add some kind of registration object but it still would require you to register all the records you need an offset from.

function GetItemFieldOffset(const Value: string): Integer;
var
  item: TItem;
begin
  if Value = 'UPC' then Result := 0
  else if Value = 'Price' then Result := Integer(@item.Price) - Integer(@item)
  else if Value = 'Cost' then Result :=  Integer(@item.Cost) - Integer(@item)
  else raise Exception.CreateFmt('Unhandled condition (%0:s)', [Value]);
end;
Patience answered 2/7, 2010 at 9:23 Comment(0)
W
1

Is this what you are looking for

 type
   TItem = record
     UPC : string[20];
     Price : Currency;
     Cost : Currency;
     ...
   end; 

 var
   myRecord    : TItem ;
   myRecordPtr : ^TItem ;

 begin
   myRecord.price:= 100;
   myRecord.UPC := '111';
   myRecordPtr := @myRecord;
   if edit1.text = 'UPC' then   
     ShowMessage(myRecordptr.UPC);  // Displays '111'
   else if edit1.text = 'price' then   
     ShowMessage(myRecordptr.Price);  // Displays '100'
 end;
Wimble answered 2/7, 2010 at 8:52 Comment(9)
Nope, I have a string like 'UPC'. I want to use reflection or the delphi equivalent to get the value of the field that the string names.Coccidioidomycosis
The string could be 'UPC', or it could be 'Price' or anything else - I won't know until runtime.Coccidioidomycosis
I hope that i understood wrong. But ShowMessage(myRecordptr.Price) will display 100. So what you need is just replace UPC with price.Wimble
I can't do that at runtime though. I have a situation where the user will type in a textbox "Price" or "UPC" or whatever, and then I must display the value of that field (simplified scenario).Coccidioidomycosis
But in this line Inc(pc, GetItemFieldOffset('Cost')); you are specifying cost.Did you mean that you dont know whether to specify cost or price here until run timeWimble
will the if condition solve your problem. Check the edited postWimble
Yes that will work - but for various reasons I don't want to use an If-ElseIf block. There are many fields in the real record, and I don't want to have to update my code if it changes.Coccidioidomycosis
Re: not updating - I'm afraid you can't avoid that in D7.Retro
You can avoid the if-else cascade if you use some kind of registry (e.g. procedure AddField(const AName string; AOffset: Integer); which could write to a stringlist).Retro

© 2022 - 2024 — McMap. All rights reserved.