How to get property of the 'record' type using TypInfo unit - delphi

I have this record type
TDoublePoint = record
X : Double;
Y : Double;
end;
then I have object with this property
uses ..TypInfo;
TCell = class(TPersistent)
private
FZoom : TDoublePoint
published
property Zoom : TDoublePoint read FZoom write FZoom;
end;
But when I want to get PropInfo of this property with this function:
function GetKind(AObject:TObject; Propertyname :shortstring):TTypeKind;
var p :ppropinfo;
begin
p:=GetPropInfo(AObject, Propertyname); // <p = nil
Result:= p^.proptype^.Kind;
end;
..
..
c := TCell.Create;
GetKind(c, 'Zoom'); // <- error
c.Free;
I get error, because variable p is nil in the function.
But Why?
There is tkRecord in the TTypeKind, so I expected no problems to read/write the property of record type, but it seems, it is not possible (?)
Google search did not tell much.

Delphi 7 does not generate RTTI for a record type by default, and so a published property that uses a record type will not have RTTI, either (you can use TypInfo.GetPropList() to confirm that).
At one point, this was a documented limitation:
Published properties are restricted to certain data types. Ordinal, string, class, interface, variant, and method-pointer types can be published.
However, there is a workaround. IF a record type contains any compiler-managed data types (long strings, interfaces, dynamic arrays, etc), then RTTI will be generated for that record type, as will any published property that uses that record type, and thus GetPropInfo() can find such properties (I have confirmed that this does work in Delphi 7).
Your TDoublePoint record does not contain any compiler-managed data types, so that is why GetPropInfo() is returning nil for your TCell.Zoom property.
That RTTI issue was fixed in a later version (not sure which one. I'm guessing maybe in Delphi 2010, when Extended RTTI was first introduced). For instance, the code you have shown works for me as-is in XE. GetPropInfo() can find the Zoom property as expected, without having to introduce any compiler-managed types into the TDoublePoint record type.

Related

Using ToString for Variant variables

The following code produces an EVariantInvalidOpError exception:
var
i : Variant;
begin
i := 10;
ShowMessage(i.ToString());
end;
All the following works good but I don't understand why the ToString function raises exception for Variant type variables:
var
i : Variant;
begin
i := 10;
ShowMessage(VarToStr(i));
end;
var
i : Integer;
begin
i := 10;
ShowMessage(i.ToString());
end;
Variants let you store values of various types in them, while the type may be unknown at compile-time. You can write an integer value into single variable of Variant type an later overwrite it with string value. Along with the value variant records stores also the type information in it. Among those values some of them are automatically allocated and/or reference counted. The compiler does a lot of stuff behind the scenes when writing or reading the value from Variant variable.
Variants of type varDispatch get even more special treat from the compiler. varDispatch indicates that the value is of type IDispatch (usually, but not necessarily related to Windows COM technology). Instance of IDispatch provides information about its methods and properties via GetTypeInfoCount and GetTypeInfo methods. You can use its GetIDsOfNames method to query the information by name.
Let's answer the question from your comment first:
Why does Delphi allow me to use the ToString function even if there is no helper implementing such function for the Variant type?
This is how Delphi implements concept called late binding. It allows you to call methods of an object which type is unknown at compile-time. The prerequisite for this to work is that the underlying variant type supports late binding. Delphi has built-in support for late binding of varDispatch and varUnknown variants as can be seen in procedure DispInvokeCore in unit System.Variants.
I don't understand why the ToString function raises exception for Variant type variables.
As discussed above, in run-time your program tries to invoke ToString method on variant value which in your case is of type varByte. Since it doesn't support late binding (as well as further ordinal variant types) you get the exception.
To convert variant value to string use VarToStr.
Here's a simple example of using late binding with Microsoft Speech API:
uses
Winapi.ActiveX,
System.Win.ComObj;
var
Voice: Variant;
begin
CoInitialize(nil);
try
Voice := CreateOleObject('SAPI.SpVoice');
Voice.Speak('Hello, World!');
finally
CoUninitialize;
end;
end.

Passing a Delphi record type to VB6 DLL

VB6 DLL
I have written a VB6 DLL which includes the following user-defined type (UDT), consisting only of primitive data types...
Public Type MyResults
Result1 As Double
Result2 As Double
Result3 As Double
Result4 As Double
Result5 As Double
End Type
...and the following method...
Public Sub Method1(ByRef Results As Variant)
Dim myRes As MyResults
myRes = Results
MsgBox "MyResults.Result1: " & myRes.Result1
...
End Sub
Delphi Test Harness
I have also written a test harness in Delphi and have created an equivalent Delphi record type to mimic the VB UDT, which is defined as follows...
TMyResults = packed record
Result1: Double;
Result2: Double;
Result3: Double;
Result4: Double;
Result5: Double;
end;
From Delphi, I would like to call Method1 in the VB6 DLL and pass a variable of this type as an argument, so that VB can interpret it as an equivalent "type". So far, I have tried the following Delphi call...
procedure TMyApp.CallMethod1FromVBDLL(var MyResults: TMyResults);
var
results: OleVariant;
dll: Variant;
begin
results := RecToVariant(MyResults, SizeOf(MyResults));
dll := CreateOleObject('ApplicationName.ClassName');
dll.Method1(results);
...
end;
...which uses the following function (source: http://www.delphigroups.info/2/2c/462130.html) to convert a record to a variant...
function TMyApp.RecToVariant(var AR; ARecSize: Integer): Variant;
var
p: Pointer;
begin
Result := VarArrayCreate([1, ARecSize], varByte);
p := VarArrayLock(Result);
try
Move(AR, p^, ARecSize);
finally
VarArrayUnLock(Result);
end;
end;
An EOleException is reported back to Delphi due to a Type mismatch on the myRes = Results line in Method1 of the DLL.
Am I preparing and passing the argument correctly in Delphi?
How should I be using the argument in VB6?
Any assistance/advice would be gratefully received.
The problem is that your Delphi code is creating an OleVariant that contains a byte array (Automation type VT_UI1 or VT_ARRAY), which is not what VB is expecting. It is expecting a UDT record (Automation type VT_RECORD) instead.
MSDN has an article explaining how to pass a UDT inside a Variant:
Passing UDTs
To pass single UDTs or safearrays of UDTs for late binding, the Automation client must have the information necessary to store the type information of the UDT into a VARIANT (and if late-binding, the UDT must be self-described). A VARIANT of type VT_RECORD wraps a RecordInfo object, which contains the necessary information about the UDT and a pointer the UDT itself. The RecordInfo object implements a new interface, IRecordInfo, for access to the information. It is important to know that the actual storage of the UDT is never owned by the VARIANT; the IRecordInfo pointer is the only thing that is owned by the VARIANT.
The data for a UDT includes an IRecordInfo pointer to the description of the UDT, pRecInfo, and a pointer to the data, pvRecord.
In other words, you need to create a class that implements the IRecordInfo interface and wraps your actual record data, then you can store an instance of that class inside the OleVariant using the VT_RECORD type. That will provide both COM and VB with the necessary metadata to marshal and access your record data.

Can't get simple property editor to be used

A have a set of custom components that we use to encapsulate some functionality and I'm trying to add a custom property editor and can't seem to work out how to get it to apply. Our registration unit has the following code in it
type
THexWordProperty = class(TIntegerProperty)
public
function GetValue: string; override;
end;
{ THexProperty }
function THexWordProperty.GetValue: string;
begin
Result := '$'+IntToHex(GetOrdValue, 4);
end;
followed by the following call
RegisterPropertyEditor(TypeInfo(TPeripheralMask),nil,'',THexWordProperty);
in the registration procedure.
Despite rebuilding the package, uninstalling, reinstalling and restarting Delphi I can't get any of my TPeripheralMask properties to display any different. As far as I can see this is the simplest possible property editor as I'm simply trying to get what is a simple ordinal property to display in a different (but still valid for input) form.
The actual property type is defined simply as type TPeripheralMask = Word;, is there something I should be doing to support additional RTTI for this type?
type
TPeripheralMask = Word;
This is a type alias. That means that TPeripheralMask and Word refer to the same type. You need to make a new type.
type
TPeripheralMask = type Word;
The relevant documentation says the following:
When you declare a type that is identical to an existing type, the
compiler treats the new type identifier as an alias for the old one.
Thus, given the declarations:
type TValue = Real;
var
X: Real;
Y: TValue;
X and Y are of the same type; at run time, there is no way to
distinguish TValue from Real. This is usually of little consequence,
but if your purpose in defining a new type is to utilize runtime type
information, for example, to associate a property editor with
properties of a particular type - the distinction between 'different
name' and 'different type' becomes important. In this case, use the
syntax:
type newTypeName = type KnownType

Use Rtti to set method field

I'm using Delphi XE to write a base class, which will allow descending classes to have dll methods mapped by applying an annotation. However I get a typecasting error, which is understandable.
In essence the base class should look like this:
TWrapperBase = class
public
FLibHandle: THandle;
procedure MapMethods;
end;
procedure TWrapperBase.MapMethods;
var
MyField: TRttiField;
MyAttribute: TCustomAttribute;
pMethod: pointer;
begin
FLibHandle := LoadLibrary(PWideChar(aMCLMCR_dll));
for MyField in TRttiContext.Create.GetType(ClassType).GetFields do
for MyAttribute in MyField.GetAttributes do
if MyAttribute.InheritsFrom(TMyMapperAttribute) then
begin
pMethod := GetProcAddress(FLibHandle, (MyAttribute as TMyMapperAttribute).TargetMethod);
if Assigned(pMethod) then
MyField.SetValue(Self, pMethod); // I get a Typecast error here
end;
And a descending class could look like this:
TDecendant = class(TWrapperBase)
private type
TSomeDLLMethod = procedure(aParam: TSomeType); cdecl;
private
[TMyMapperAttribute('MyDllMethodName')]
FSomeDLLMethod: TSomeDLLMethod;
public
property SomeDLLMethod: TSomeDLLMethod read FSomeDLLMethod;
end;
I could implement this differently, by hard coding the linking for each method in an overriden 'MapMethods'. This would however require each descendant to do so which I'd like to avoid.
I know that the TValue as used in this case will contain a pointer and not of the correct type (procedure(aParam: TSomeType); cdecl; in this case).
My question: Is there a way to pass the pointer from 'GetProcAdress' as the correct type, or to set the field directly (for example by using the field address 'PByte(Self)+MyField.Offset', which you can use to set the value of a record property)?
With the old Rtti, this could be done but only for published properties and without any type checking:
if IsPublishedProp(Self, 'SomeDLLMethod') then
SetMethodProp(Self, 'SomeDLLMethod', GetProcAddress(FLibHandle, 'MethodName');
There are two problems:
First your EInvalidCast is caused by TValue being very strict about type conversions. You are passing in a Pointer and want to set a field of type TSomeDLLMethod. You need to explicitly pass a TValue that has the correct type info.
if Assigned(pMethod) then
begin
TValue.Make(#pMethod, MyField.FieldType.Handle, value);
MyField.SetValue(Self, value);
end;
Now you will run into another EInvalidCast exception which is triggered because of a bug in XE inside the GetInlineSize method of the Rtti.pas which returns 0 for a tkProcedure kind of type. I don't know in what version this got fixed but it does not exist anymore in XE5.
For XE this can be fixed by using a unit I wrote some while ago (and which I just updated to fix this bug): RttiPatch.pas.
I also reported the original issue because Pointer is assignment compatible to a procedure type so TValue should also handle this: http://qc.embarcadero.com/wc/qcmain.aspx?d=124010
You could try something like:
Move(pMethod, PByte(Self) + Field.Offset, SizeOf(Pointer));
or
PPointer(PByte(Self) + Field.Offset)^ := pMethod;

Delphi - records with variant parts

I want to have a record (structure) with a 'polymorphic' comportment. It will have several fields used in all the cases, and I want to use other fields only when I need them. I know that I can accomplish this by variant parts declared in records. I don't know if it is possible that at design time I can access only the elements I need. To be more specific, look at the example bellow
program consapp;
{$APPTYPE CONSOLE}
uses
ExceptionLog,
SysUtils;
type
a = record
b : integer;
case isEnabled : boolean of
true : (c:Integer);
false : (d:String[50]);
end;
var test:a;
begin
test.b:=1;
test.isEnabled := False;
test.c := 3; //because isenabled is false, I want that the c element to be unavailable to the coder, and to access only the d element.
Writeln(test.c);
readln;
end.
Is this possible?
All variant fields in a variant record are accessible at all times, irrespective of the value of the tag.
In order to achieve the accessibility control you are looking for you would need to use properties and have runtime checks to control accessibility.
type
TMyRecord = record
strict private
FIsEnabled: Boolean;
FInt: Integer;
FStr: string;
// ... declare the property getters and settings here
public
property IsEnabled: Boolean read FIsEnabled write FIsEnabled;
property Int: Integer read GetInt write SetInt;
property Str: string read GetString write SetString;
end;
...
function TMyRecord.GetInt: Integer;
begin
if IsEnabled then
Result := FInt
else
raise EValueNotAvailable.Create('blah blah');
end;
Even if I heard that by original Niklaus Wirth's Pascal definition all should work as you expected, I saw no such behaviour in Delphi, starting from its ancestor, Turbo Pascal 2.0. Quick look at FreePascal showed that its behaviour is the same. As said in Delphi documentation:
You can read or write to any field of any variant at any time; but if you write to a field in one variant and then to a field in another variant, you may be overwriting your own data. The tag, if there is one, functions as an extra field (of type ordinalType) in the non-variant part of the record."
Regarding your intent, as far as I understood it, I would use two different classes, kind of
a = class
b : Integer
end;
aEnabled = class(a)
c: Integer
end;
aDisabled = class(a)
d: String //plus this way you can use long strings
end;
This way you can get some support from IDE's Code Editor even at designtime. More useful, though, is that it will be much more easier to modify and support later.
However, if you need quick switching of record variable values at runtime, #David Heffernan's variant , to use properties and have runtime checks, is more reasonable.
The example given is NOT a variant record, it includes all the fields all the time.
A true variant record has the variants sharing the same memory. You just use the "case discriminator: DiscType of ..... " syntax, no need for a separate field telling you what variant is active.

Resources