- Delphi Cookbook
- Daniele Spinetti Daniele Teti
- 558字
- 2025-04-04 16:22:47
How to do it...
The code in this recipe allows you to write code like the following snippets. In this following snippet, the Color property for all control in the form will be set to clRed. I don't know which kinds of controls there are on the form, but if they have a property named Color, that property will be set to clRed:
Duck.Apply(Self, 'Color', clRed);
In this snippet, the Caption property of the controls is in the array; if it exists, it will be set to 'Hello There':
Duck.Apply( TArray<TObject>.Create(Button1, Button2, Edit1), 'Caption', 'Hello There');
The following code disables all the TDataSource on the form, preventing data editing:
Duck.Apply(Self, 'Enabled', False, function(Item: TObject): boolean begin Result := Item is TDataSource; end);
The following code sets the font name to Courier New for some controls:
Duck.Apply(TArray<TObject>.Create(Edit1, Edit2, Button2), 'Font.Name', 'Courier New');
This code works for every kind of control. If you change the TButton to TSpeedButton, it continues to work. If you replace a TListBox with a TComboBox, the code still works. The concept is simple—if you have a property X, then set that property independently of the actual object type.
Let's see the code that actually does the job.
The main Duck class is a mere method container (this is the reason its name is Duck, and not TDuck; it is not a real type) declared, as shown, in the following code:
type Duck = class sealed
class procedure Apply(ArrayOf: TArray<TObject>; PropName: string;
PropValue: TValue;
AcceptFunction: TFunc<TObject, boolean> = nil); overload;
class procedure Apply(AContainer: TComponent; PropName: string;
PropValue: TValue;
AcceptFunction: TFunc<TObject, boolean> = nil); overload;
end;
Methods are very similar, and the second one adds a helper to work with TComponent; the real job is done by the first one:
class procedure Duck.Apply(ArrayOf: TArray<TObject>; PropName: string;
PropValue: TValue; AcceptFunction: TFunc<TObject, boolean>);
var
CTX: TRttiContext;
Item, PropObj: TObject;
RttiType: TRttiType;
Prop: TRttiProperty;
PropertyPath: TArray<string>;
i: Integer;
begin
CTX := TRttiContext.Create;
try
for Item in ArrayOf do
begin
if (not Assigned(AcceptFunction)) or (AcceptFunction(Item)) then
begin
RttiType := CTX.GetType(Item.ClassType);
if Assigned(RttiType) then
begin
PropertyPath := PropName.Split(['.']);
Prop := RttiType.GetProperty(PropertyPath[0]);
if not Assigned(Prop) then
Continue;
PropObj := Item;
if Prop.GetValue(PropObj).isObject then
begin
PropObj := Prop.GetValue(Item).AsObject;
for i := 1 to Length(PropertyPath) - 1 do
begin
RttiType := CTX.GetType(PropObj.ClassType);
Prop := RttiType.GetProperty(PropertyPath[i]);
if not Assigned(Prop) then
break;
if Prop.GetValue(PropObj).isObject then
PropObj := Prop.GetValue(PropObj).AsObject
else
break;
end;
end;
if Assigned(Prop) and (Prop.IsWritable) then
Prop.SetValue(PropObj, PropValue);
end;
end;
end;
finally
CTX.Free;
end;
end;
This is not very simple, I know, but you can see all the pieces we've already talked about. Obviously, we use RTTI to get the names and set the values of the properties.
The main loop cycles over the array parameter, and asks AcceptFunction whether the object must be inspected or not. AcceptFunction is optional, so the value can be nil. In this case, all the objects are inspected. To allow a syntax such as Font.Name, there is a small parser that splits the strings and walks through each piece to check whether there is a property with that name. If the last piece (or the only one) is found, then check whether that property is writable; if it is writable, set the property to the passed value. In this way, you can write code that walks through a complex object graph with a simple syntax:
Duck.Apply(TArray<TObject>.Create( DataSource1, DataSource2, Button2), 'DataSet.Active', true);