Unable to declare string property defaults - delphi

I'm writing a component which consists of many properties which are to appear in the Delphi IDE Object Inspector (published properties)...
type
TMyComponent = class(TComponent)
private
FMyProperty: String;
published
property MyProperty: String read FMyProperty write SetMyProperty default 'Something';
end;
However, it's not allowing me to apply a default value to a string property...
[DCC Error] MyUnit.pas(278): E2146 Default values must be of ordinal, pointer or small set type
All other property defaults work fine (Integer, Enum, etc.).
My goal is to A) not save string properties to the DFM if they're the default value, and B) show the value in the Object Inspector as Bold if it's not the default, and regular if it is. There are over 130 properties which show for this component, and about 50 of them are string properties, some with rather large default values.
Why am I not allowed to declare a string property with a default value? Is this a shortcoming with Delphi, or is there a technical reason why strings can't be defaulted?
EDIT
If you really want to know what I'm doing, I'm encapsulating Inno Setup and wrapping the functionality into a component with an extensive property/collection editor. This topic pertains to just the Setup section alone, which consists of actually over 100 properties. Only about 20 of these properties are expected to actually be used for simple implementation, and therefore I don't want all the rest of those string properties to bloat the size of the DFM (if they're set to their defaults). Based on how the component is set up, it will produce an Inno Setup script file.

Only numeric properties can have a default value specified in the property declaration. However, you can use the stored specifier instead, eg:
type
TMyComponent = class(TComponent)
private
FMyProperty: String;
function MyPropertyIsStored: Boolean;
procedure SetMyProperty(const Value: String);
public
constructor Create(AOwner: TComponent); override;
published
property MyProperty: String read FMyProperty write SetMyProperty stored MyPropertyIsStored;
end;
constructor TMyComponent.Create(AOwner: TComponent);
begin
Inherited;
FMyProperty := 'my default value';
end;
function TMyComponent.MyPropertyIsStored: Boolean;
begin
Result := FMyProperty <> 'my default value';
end;
procedure TMyComponent.SetMyProperty(const Value: String);
begin
if FMyProperty <> Value then
begin
FMyProperty := Value;
// update component as needed...
end;
end;

It's not clear what you're trying to do. Assigning default to a property has two uses:
To decide whether the property value is streamed to the DFM or not (used for ordinal or Boolean properties, usually, such as Visible - since the default would be True, there's no reason to write it to the DFM unless it's False. (See note below)
For array properties, to indicate that the array is the default property of the class (such as Delphi's TList.Items, where having Items the default allows you to use List[x] instead of List.Items[x] in your code.
If your intent is to provide a default value to the string so it shows in the Object Inspector, simply set the value in the component constructor. If the user has assigned a different value, the value set in the constructor will be overwritten when the DFM content is streamed in.
As far as why string default values are not allowed, this is clearly stated in the documentation (see "Storage Specifiers" (emphasis mine):
The default and nodefault directives are supported only for ordinal types and for set types, provided the upper and lower bounds of the set's base type have ordinal values between 0 and 31; if such a property is declared without default or nodefault, it is treated as if nodefault were specified. For reals, pointers, and strings, there is an implicit default value of 0, nil, and '' (the empty string), respectively.
Note: The default is used in conjunction with the stored specifier that Remy's answer describes. From the same docs linked and quoted above (same section):
When saving a component's state, the storage specifiers of the component's published properties are checked. If a property's current value is different from its default value (or if there is no default value) and the stored specifier is True, then the property's value is saved. Otherwise, the property's value is not saved.

Related

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

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.

Can a Variant property have default value?

I've written a component who has a Variant property for which I would like to set a default value.
TMyComponent = class(TComponent)
private
FVariantValue : Variant;
published
property VariantValue : Variant read FVariantValue write FVariantValue default False;
end;
On compilation, I get the following error on the VariantValue property line:
E2026 Constant expression expected
Doing the same thing with a Boolean property doesn't cause any kind of error.
I read a little bit of documentation but I didn't find nothing about Variant properties default values.
Be careful here. The default directive does not do anything to set the value of the property itself. It only affects whether or not the value is explicitly saved in the .dfm file. If you specify a default value for a property you still have to ensure that the constructor initializes the backing field to that value.
Properties : Storage Specifiers
When saving a component's state, the storage specifiers of the component's published properties are checked. If a property's current value is different from its default value (or if there is no default value) and the stored specifier is True, then the property's value is saved. Otherwise, the property's value is not saved.
Note: Property values are not automatically initialized to the default value. That is, the default directive controls only when property values are saved to the form file, but not the initial value of the property on a newly created instance.
This is just a hint to the component streaming system that it doesn't need to store this value explicitly in the .dfm - your part of the contract is to ensure that you actually initialize the backing field to that value. The appropriate place to do this type of initialization is in the component's constructor:
constructor TMyComponent.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FVariantValue := False;
end;
That said, False is a boolean, not a variant, so it cannot be used as a constant expression of Variant type. Since a variant is a complex type it cannot be expressed as a single constant and, therefore, cannot have a default property.
Per Remy, if you want to ensure that the variant is not saved in the .dfm file when the backing variant is False, you can use the stored directive with a parameterless method which returns False when the variant evaluates to a boolean False. For example :
property VariantValue : Variant read FVariantValue write FVariantValue stored IsVariantValueStored;
where
function TMyComponent.IsVariantValueStored : Boolean;
begin
Result := not VarIsType(FVariantValue, varBoolean);
if not Result then
Result := FVariantValue;
end;
Variant properties cannot have default values.
The best thing to do is set
FVariantValue := false;
in the constructor or procedure AfterConstruction; override;

How Delphi object inspector grey some properties?

Recently I found that Delphi object inspector displays some properties in grey. Here is an example:
I wonder what does it mean? How such properties are defined? I did not find any differences in definition of let's say DSHostname and ProxyHost. But as you can see DSHostname is displayed normally and ProxyHost in grey.
Here is a relevant declaration of properties in question:
/// <summary>The host to proxy requests through, or empty string to not use a proxy.</summary>
property ProxyHost: string read FProxyHost write FProxyHost;
/// <summary>The port on the proxy host to proxy requests through. Ignored if DSProxyHost isn't set.
/// </summary>
[Default(8888)]
property ProxyPort: Integer read FProxyPort write FProxyPort default 8888;
/// <summary>The user name for authentication with the specified proxy.</summary>
property ProxyUsername: string read FProxyUsername write FProxyUsername;
/// <summary>The password for authentication with the specified proxy.</summary>
property ProxyPassword: string read FProxyPassword write FProxyPassword;
Finally I got a proof that Remy Lebeau was right in his guess. I made a descendant of TDSClientCallbackChannelManager which has published property TestProxyHost. This property does nothing but mirroring ProxyHost in Get and Set. Here is the full code for the component:
unit uTestCallbackChannelManager;
interface
uses
System.SysUtils, System.Classes, Datasnap.DSCommon;
type
TTestCallbackChannelManager = class(TDSClientCallbackChannelManager)
private
function GetTestProxyHost: string;
procedure SetTestProxyHost(const Value: string);
published
property TestProxyHost: string read GetTestProxyHost write SetTestProxyHost;
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents('Samples', [TTestCallbackChannelManager]);
end;
{ TTestCallbackChannelManager }
function TTestCallbackChannelManager.GetTestProxyHost: string;
begin
Result := ProxyHost;
end;
procedure TTestCallbackChannelManager.SetTestProxyHost(const Value: string);
begin
ProxyHost := Value;
end;
end.
After I installed TTestCallbackChannelManager into component palette I dropped in on a form in a test project.
In Object Inspector the ProxyHost property is displayed in grey and TestProxyHost as normal. Now if I change TestProxyHost then ProxyHost is changed as well. Here is a screenshot:
This means:
RTTI information of ProxyHost property was not altered in any way and it is indeed a read/write property in both design- and run-time.
The only way to achieve such behavior is on the Property Editor level. The Property Editor registered for this particular property name in this component type "tells" Object Inspector "Hey, I can not set this property for you" (but an other code can do it directly).
This also explains why if I uncheck "Show read only properties" flag in Object Inspector options ProxyHost (and 3 related properties) are still shown in the Object Inspector. It is because of Object Inspector reads the properties from dfm as read/write and then creates Property Editors for them and once Property Editor says it can't write the property they are shaded in grey (but still shown as Property Editor is already created).
The only problem is what logic behind the Property Editor? When the properties become available and how to use them? It looks like the properties are introduced very recently in xe10 or a bit earlier. And Embarcadero provides no documentation about these properties (at least for now I could not find any). But this is a subject of separate question. I suspect that support for these properties has not been tested (or may be not implemented) yet and so they are intended for use in future releases.

Delphi: browsing components inside a property editor

When a property is a simple component of any class, the IDE's property editor is able to drop down a list of all compatible components in all the project's forms.
I want to do some equivalent task, but with some filtering based on acceptable component classes for the property; these classes common ancestor is only TComponent and they have custom interfaces.
Currently I have a working property editor that uses a paValueList attribute and some filtering in the GetValues procedure, based on checking the supported interfaces, but it is limited to the current form :-(.
How to browse all the forms like the IDE does?
I want to do some equivalent task, but with some filtering based on acceptable component classes for the property; these classes common ancestor is only TComponent and they have custom interfaces.
If you are filtering for only 1 interface, you should change the property in question to accept that interface type instead of a TComponent, and then the default property editor for interface properties (TInterfaceProperty) will filter the components automatically for you:
property MyProperty: IMyInterface read ... write ...;
Currently I have a working property editor that uses a paValueList attribute and some filtering in the GetValues procedure, based on checking the supported interfaces, but it is limited to the current form :-(.
How to browse all the forms like the IDE does?
To manually filter the components in a custom property editor, you need to do the same thing that the default component property editor (TComponentProperty) does to obtain the compatible components, and then you can filter them further as needed.
Internally, TComponentProperty.GetValues() simply calls Designer.GetComponentNames(), passing it the PTypeData of the property type that is being edited:
procedure TComponentProperty.GetValues(Proc: TGetStrProc);
begin
Designer.GetComponentNames(GetTypeData(GetPropType), Proc);
end;
So, if your property accepts a TComponent (since that is the only common ancestor of your intended components):
property MyProperty: TComponent read ... write ...;
Then GetPropType() in this case would return TypeInfo(TComponent).
GetComponentNames() (whose implementation is in the IDE and not available in the VCL source code) enumerates the components of the Root (Form, DataModule, or Frame) that owns the component being edited, as well as all linked Root objects that are accessible in other units specified in the edited Root's uses clause. This is documented behavior:
DesignIntf.IDesigner60.GetComponentNames
Executes a callback for every component that can be assigned a property of a specified type.
Use GetComponentNames to call the procedure specified by the Proc parameter for every component that can be assigned a property that matches the TypeData parameter. For each component, Proc is called with its S parameter set to the name of the component. This parameter can be used to obtain a reference to the component by calling the GetComponent method.
Note: GetComponentNames calls Proc for components in units that are in the uses clause of the current root object's unit (Delphi) or included by that unit (C++), as well as the entity that is the value of Root.
So, in your GetValues() implementation, call Designer.GetComponentNames() specifying the PTypeData for TComponent and let the IDE enumerate all available units and provide you with a list of each component's Name. Then you can loop through that list calling Designer.GetComponent() to get the actual TComponent objects and query them for your desired interface(s):
procedure TMyComponentProperty.GetValues(Proc: TGetStrProc);
var
Names: TStringList;
I: Integer;
begin
Names := TStringList.Create;
try
Designer.GetComponentNames(GetTypeData(TypInfo(TComponent)), Names.Append);
for I := 0 to Names.Count-1 do
begin
if Supports(Designer.GetComponent(Names[I]), IMyInterface) then
Proc(Names[I]);
end;
finally
Names.Free;
end;
end;
In fact, this is very similar to what the default TInterfaceProperty.GetValues() implementation does:
procedure TInterfaceProperty.ReceiveComponentNames(const S: string);
var
Temp: TComponent;
Intf: IInterface;
begin
Temp := Designer.GetComponent(S);
if Assigned(FGetValuesStrProc) and
Assigned(Temp) and
Supports(TObject(Temp), GetTypeData(GetPropType)^.Guid, Intf) then
FGetValuesStrProc(S);
end;
procedure TInterfaceProperty.GetValues(Proc: TGetStrProc);
begin
FGetValuesStrProc := Proc;
try
Designer.GetComponentNames(GetTypeData(TypeInfo(TComponent)), ReceiveComponentNames);
finally
FGetValuesStrProc := nil;
end;
end;
The only difference is that TInterfaceProperty does not waste memory collecting the names into a temp TStringList. It filters them in real-time as they are being enumerated.
Remy's solution works perfectly for my needs.
Nevertheless I've "simplified" a bit the filtering procedure:
procedure TMyComponentProperty.ReceiveComponentNames(const S: string);
var
Temp: TComponent;
Intf: IInterface;
begin
if Assigned(FGetValuesStrProc) then
begin
Temp := Designer.GetComponent(S);
if Assigned(Temp) then
if Temp.GetInterface(IMyInterface, IntF) then
FGetValuesStrProc(S);
// May add other interfaces checks here
end;
end;

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