I know that using a column's readonly property, i can avoid editing its field value. But this doesn't stop the inplace editor to show itself.
I need a way to make the column not only protected but "untouchable".
Is there a way, please ?
If I understand what you want correctly, you can do this quite simply, by creating a custom TDBGrid descendant and overriding
its CanEditShow method, as this determines whether the grid's InplaceEditor can be created:
type
TMyDBGrid = class(TDBGrid)
private
FROColumn: Integer;
protected
function CanEditShow : Boolean; override;
public
property ROColumn : Integer read FROColumn write FROColumn;
end;
function TMyDBGrid.CanEditShow: Boolean;
begin
Result := Inherited CanEditShow;
Result := Result and (Col <> ROColumn);
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
MyDBGrid := TMyDBGrid.Create(Self);
MyDBGrid.ROColumn := 1;
MyDBGrid.DataSource := DataSource1;
MyDBGrid.Parent := Self;
[...]
This minimalist example just defines one grid column by number as being one
where the InplaceEditor is not permitted; obviously you could use any mechanism
you like to identify the column(s) for which CanEditShow returns False.
Note that the code above doesn't account for the fact that the column numbering of the grid changes if you turn off the Indicator column (i.e. set Options.dgIndicator to False);
Obviously, you get more flexibility for customizing which columns are permitted an InplaceEditor by using an assignable event as in
type
TAllowGridEditEvent = procedure(Sender : TObject; var AllowEdit : Boolean) of object;
TMyDBGrid = class(TDBGrid)
private
FOnAllowEdit: TAllowGridEditEvent;
protected
function CanEditShow : Boolean; override;
procedure DoAllowEdit(var AllowEdit : Boolean);
public
property OnAllowEdit : TAllowGridEditEvent read FOnAllowEdit write FOnAllowEdit;
end;
function TMyDBGrid.CanEditShow: Boolean;
begin
Result := Inherited CanEditShow;
if Result then
DoAllowEdit(Result);
end;
procedure TMyDBGrid.DoAllowEdit(var AllowEdit: Boolean);
begin
if Assigned(FOnAllowEdit) then
FOnAllowEdit(Self, AllowEdit);
end;
procedure TForm1.AllowEdit(Sender: TObject; var AllowEdit: Boolean);
var
Grid : TMyDBGrid;
begin
Grid := Sender as TMyDBGrid;
AllowEdit := Grid.Col <> 1;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
MyDBGrid := TMyDBGrid.Create(Self);
MyDBGrid.ROColumn := 1;
MyDBGrid.DataSource := DataSource1;
MyDBGrid.Parent := Self;
MyDBGrid.OnAllowEdit := AllowEdit;
[...]
If you don't like creating the grid in code, you could put it in a custom package and install it
in the IDE or, if your Delphi version is recent enough, implement the
CanEditShow in a class helper.
Related
I'm developing a component for Query. It works like the "Properties" feature of DevExpress, but I need to place the order of the Unpublished Property I wrote to DFM with DefineProperties in the DFM file at the top of the TCollectionItem.
It works the same way in DevExpress. If you add a Field to the cxGrid and assign a value to the Properties property, you will see the value "PropertiesClassName" in the DFM file at the top.
When I open the DFM file and bring this Property to the top, the setter property of the "PropertiesClassName" Property works and I create that Class. It works seamlessly when reading data from the DFM stream. But no matter what I did I couldn't get the "PropertiesClassName" Property value to the top.
If you create a cxGrid on the form and add Field, and then take the "PropertiesClassName" property from DFM to the bottom of the DFM file, when you open the form again, you will see that it cannot find the relevant Class and an error occurs.
To change the DFM flow, I first assigned a value to the "PropertiesClassName" Property and then created the Class, but the problem was not solved. I did the opposite of this but the problem is still the same.
DFM Context
object QuerySearchEngine1: TQuerySearchEngine
SearchFields = <
item
FieldName = 'TestField'
Properties.Convert2String = True
PropertiesClassName = 'TSearchBooleanProperties'
end>
DFM Context should be like
object QuerySearchEngine1: TQuerySearchEngine
SearchFields = <
item
PropertiesClassName = 'TSearchBooleanProperties'
FieldName = 'TestField'
Properties.Convert2String = True
end>
Classes
TSearchField = class(TCollectionItem)
private
FFieldName: string;
FProperties: TSearchFieldProperties;
FPropertiesClassName: string;
private
procedure SetFieldName(const Value: string);
procedure SetProperties(const Value: TSearchFieldProperties);
private
procedure ReaderProc(Reader: TReader);
procedure WriterProc(Writer: TWriter);
procedure SetPropertiesClassName(const Value: string);
protected
constructor Create(Collection: TCollection); override;
procedure DefineProperties(Filer: TFiler); override;
public
property PropertiesClassName: string read FPropertiesClassName write SetPropertiesClassName;
published
property FieldName: string read FFieldName write SetFieldName;
property Properties: TSearchFieldProperties read FProperties write SetProperties;
end;
procedure TSearchField.DefineProperties(Filer: TFiler);
begin
inherited;
Filer.DefineProperty('PropertiesClassName', ReaderProc, WriterProc, FPropertiesClassName <> '');
end;
procedure TSearchField.SetPropertiesClassName(const Value: string);
begin
var Item: TSearchFieldPropertiesItem;
if TryValidateSearchFieldPropertiesClassName(Value, Item) then
begin
if not Assigned(FProperties) or not (FProperties.ClassType = Item.ClassType) then
begin
if Assigned(FProperties) then
begin
FProperties.Free;
FProperties := nil;
end;
FPropertiesClassName := Item.ClassType.ClassName;
FProperties := Item.ClassType.Create;
end;
end
else
begin
FPropertiesClassName := '';
if Assigned(FProperties) then
begin
FProperties.Free;
FProperties := nil;
end;
end;
end;
Property Editor
type
TSearchFieldPropertiesProperty = class(TClassProperty)
private
function GetInstance: TPersistent;
public
function GetAttributes: TPropertyAttributes; override;
procedure GetValues(Proc: TGetStrProc); override;
function GetValue: string; override;
procedure SetValue(const Value: string); override;
end;
function TSearchFieldPropertiesProperty.GetValue: string;
begin
for var I := 0 to Self.PropCount - 1 do
begin
var Inst := Self.GetComponent(I);
if Assigned(Inst) and Self.HasInstance(Inst) then
begin
if Inst is TSearchField then
begin
var PropInst := GetObjectProp(Inst, Self.GetPropInfo);
if Assigned(PropInst) then
begin
for var Item in SearchFieldPropertiesList do
begin
if PropInst.ClassType = Item.ClassType then
begin
Result := Item.Name;
Exit;
end;
end;
end;
end;
end;
end;
end;
procedure TSearchFieldPropertiesProperty.SetValue(const Value: string);
begin
var Item: TSearchFieldPropertiesItem;
if TryValidateSearchFieldPropertiesName(Value, Item) then
begin
var Inst := GetInstance;
if Assigned(Inst) then
begin
var Context := TRttiContext.Create;
var Rtype := Context.GetType(Inst.ClassType);
for var Prop in Rtype.GetProperties do
begin
if SameText(Prop.Name, 'PropertiesClassName') then
begin
Prop.SetValue(Inst, TValue.From<string>(Item.ClassType.ClassName));
Break;
end;
end;
end;
end;
end;
Pic for Design Time
The only problem is changing the order of the Property in that DFM flow.
Original answer at the bottom, here is a new suggestion:
We actually have something very similar in the JVCL where TJvHotTrackPersistent publishes a HotTrackOptions property.
This property is backed by an instance of TJvHotTrackOptions that gets derived in other classes that need specialized versions of it.
To tell the streaming subsystem to use the actual class found at streaming time, the constructor of that options class calls SetSubComponent(True); which places csSubComponent in the ComponentStyle property.
So what you should do is get rid of your DefineProperties, have TSearchFieldProperties inherit from TComponent and call SetSubComponent(True) in its constructor.
Then you create as many classes derived from TSearchFieldProperties as you need, each with its own set of published properties.
This means you should also get rid of the methods you showed in your submission.
In the end, you should have something along those lines:
type
TSearchFieldProperties = class(TComponent)
public
constructor Create(AOwner: TComponent); override;
end;
TIntegerSearchFieldProperties = class(TSearchFieldProperties)
private
FIntValue: Integer;
published
property IntValue: Integer read FIntValue write FIntValue;
end;
constructor TSearchFieldProperties.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
SetSubComponent(True);
end;
With this you do not fight against the streaming system but rather work with it in the way it is meant to be used.
But if you stop there, you'll notice there is no way for you to specify the actual class name to be used for the TSearchFieldProperties instance used for the TSearchField.Properties property.
The only way to get the class name to be streamed before the subcomponent is streamed is to actually declare the class name as a published property, declared before the subcomponent like this:
type
TSearchField = class(TCollectionItem)
published
// DO NOT change the order of those two properties, PropertiesClassName must come BEFORE Properties for DFM streaming to work properly
property PropertiesClassName: string read GetPropertiesClassName write SetPropertiesClassName;
property Properties: TSearchFieldProperties read FProperties write SetProperties;
end;
function TSearchField.GetPropertiesClassName: string;
begin
Result := Properties.ClassName;
end;
procedure TSearchField.SetPropertiesClassName(const AValue: string);
begin
FProperties.Free; // no need to test for nil, Free already does it
FProperties := TSearchFieldPropertiesClass(FindClass(AValue)).Create(self);
end;
It might work if you just declare the published property like without creating a csSubComponent hierarchy but you'll most likely stumble on other hurdles along the way.
Note: this answer is wrong because DefineProperties is called last in TWriter.WriteProperties and so there is no way to change the order properties defined like this are written.
What if you change your DefineProperties override from this:
procedure TSearchField.DefineProperties(Filer: TFiler);
begin
inherited;
Filer.DefineProperty('PropertiesClassName', ReaderProc, WriterProc, FPropertiesClassName <> '');
end;
to this:
procedure TSearchField.DefineProperties(Filer: TFiler);
begin
Filer.DefineProperty('PropertiesClassName', ReaderProc, WriterProc, FPropertiesClassName <> '');
inherited DefineProperties(Filer);
end;
Basically, call the inherited method AFTER you have defined your own property.
Note that I also specified which inherited method is called. I know it's not required, but it makes intent clearer and allows for Ctrl-Click navigation.
I want to change the value of T according to a particular selection but it's not changing. Please have a look. The variable T has been declared along with Form1:TForm1 before 'implementation'. Basically, T should get assigned a linear or non linear equation depending upon the the selection of the respected radio buttons. I put a TEdit in the form so as to get an idea whether it is working or not. The last part is just a way to check by taking an example of Integer values.
Also, if I am not able to give a clear idea then just suggest me how to store a value of the concerned value using the Radiobuttons of the RadioGroup.
procedure TForm1.RadioGroup1Click(Sender: TObject);
begin
if RadioGroup1.Items[RadioGroup1.ItemIndex] = 'Linear Tension' then
T:= 5;
if RadioGroup1.Items[RadioGroup1.ItemIndex] = 'Non-Linear tension' then
T:= 10;
end;
procedure TForm1.Edit1Change(Sender: TObject);
var
code: Integer;
value: Real;
begin
Val(Edit1.Text,value,code);
Edit1.Text := formatfloat('#.0', T);
end;
end.
It's really not a good idea to use a textual comparison for RadioGroup items. It's much better to simply use the ItemIndex directly:
procedure TForm1.RadioGroup1Click(Sender: TObject);
begin
case RadioGroup1.ItemIndex of
0: T := 5;
1: T := 10;
else
raise Exception.Create('No item selected - should not get here');
end;
ShowMessage(FloatToStr(T));
end;
Do not compare the captions because you will have magic values in your code.
Declare a ValueObject containing the Value and the Name
type
TTensionValue = record
private
FValue : Integer;
FName : string;
public
constructor Create( AValue : Integer; const AName : string );
class function EMPTY : TTensionValue;
property Value : Integer read FValue;
property Name : string;
end;
TTensionValues = TList<TTensionValue>;
class function TTensionValue.EMPTY : TTensionValue;
begin
Result.FValue := 0;
Result.FName := '';
end;
constructor TTensionValue.Create( AValue : Integer; const AName : string );
begin
// Validation of AValue and AName
if AName = '' then
raise Exception.Create( 'AName' );
if AValue < 0 then
raise Exception.Create( 'AValue' );
FValue := AValue;
FName := AName;
end;
Prepare a List with valid entries
type
TForm1 = class( TForm )
...
procedure RadioGroup1Click( Sender: TObject );
private
FTensions : TTensionValues;
procedure PopulateTensions( AStrings : TStrings );
public
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
end;
procedure TForm1.AfterConstruction;
begin
inherited;
FTensions := TTensionValues.Create;
FTensions.Add( TTensionValue.Create( 5, 'Linear Tension' ) );
FTensions.Add( TTensionValue.Create( 10, 'Non-Linear tension' ) );
end;
procedure TForm1.BeforeDestruction;
begin
FTenstions.Free;
inherited;
end;
Populate that list to the RadioGroup
procedure TForm1.PopulateTensions( AStrings : TStrings );
var
LValue : TTensionValue;
begin
AStrings.BeginUpdate;
try
AStrings.Clear;
for LValue in FTensions.Count - 1 do
AStrings.Add( LValue.Name );
finally
AStrings.EndUpdate;
end;
end;
procedure TForm1.FormShow( Sender.TObject );
begin
PopulateTensions( RadioGroup1.Items );
end;
Now you only ask the TensionList for the value
procedure TForm1.RadioGroup1Click( Sender: TObject );
begin
T := FTensions[RadioGroup1.ItemIndex].Value;
end;
The selected value now only rely on the chosen ItemIndex and not on the caption text.
From what I can tell, you're simply trying to change the value displayed on Edit1 when RadioGroup1 is clicked. To achieve this, all you'll need to do is move
Edit1.Text := formatfloat('#.0', T);
to the end of your RadioGroup1Click procedure.
I'm assuming Edit1Change is the onChange procedure of Edit1. If so, according to the documentation this procedure only gets called when the Text property already might have changed. So not only will this procedure not get called (how would delphi know you intend to use the value of T to change the text of Edit1?), when it does get called, it might result in a stack overflow, since changing the text value indirectly calls the onChange event. (though setting it to the same value it already had might not call it).
That being said, checking if a value is being changed properly, does not require a TEdit, a TLabel would be a better fit there. Though in your case, i would opt for simply placing a breakpoint and stepping through the code to see if the value get's changed correctly.
There are also some a lot of additional problems with your code, such as inconsistent formatting, magic values, bad naming conventions and lines of code that serve no purpose, I would suggest you read up on those before you get into bad habits.
I have a TFrame that I use for searching for entities in a Delphi 2010 VCL project, in the TFrame I have a button edit, that allows the user to open a specific form to browse for that entity. (All the browse forms inherit from a common base browse form)
Currently I achieve this by inheriting from the base frame, then implement the Browse event that fires off the specific form. The only difference each time is what form (type) is shown on the click event, is there a way I can achieve this with generics.
That way I can reuse the same base frame without having to rewrite the same code for each entity (there are over 100), and at form create of the host form pass the type constraint to open the appropriate form on browse.
I have tried adding a generic type to the frame:
type
Browser<T: TfrmBrowser, constructor> = class
class function BrowseForm(Owner: Tcomponent): T;
end;
class function Browser<T>.BrowseForm(Owner: Tcomponent): T;
var
_browseForm: T;
begin
_browseForm := T.Create; // 1st problem T.Create(Owner); throws a comile error
Result := _browseForm;
end;
and then in the picker frame I expose Start that can be called from the the host form's create event:
procedure TPickerFrame.Start<T>(const idProp, nameProp, anIniSection: string; aDto: IDto);
begin
_browseForm:= Browser<T>.BrowseForm(self);
_iniSectionName:= anIniSection;
_idField:= idProp;
_descriptionField:= nameProp;
_dto := aDto;
end;
the truth is, I don't really get generics in Delphi, and none of this is working.
Below are excerpts from the frame:
_browseForm: TfrmBrowser;
procedure TPickerFrame.Browse(var DS: TDataSet; var Txt: string; var mr: TModalResult);
begin
// How do I achieve this with Generics
// _browseForm := T.Create(nil); // <-- this line is what needs to know the form type at runtime
// Everything else from here is the same
_browseForm.ProductName := Application.Title;
_browseForm.PageSize := 20;
_browseForm.DatabaseType := bdbtADO;
_browseForm.ADOConnection := dmdbWhereHouse.BaseADOConnection;
_browseForm.INISectionName := _iniSectionName;
_browseForm.DoSelBrowse(DS, Txt, mr, _descriptionField, _text);
if mr = mrOk then
begin
DoSelect(DS);
end;
end;
Does anyone have any experience with a similar requirement? Any help would be appreciated.
Thanks
Below is an example of the rack master browser:
type
TfrmMbfRACK_MASTER = class(TMxfrmBrowseHoster)
procedure FormCreate(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
//...
private
FWHID: Integer;
procedure SetWHID(const Value: Integer);
{ Private declarations }
public
{ Public declarations }
procedure BuildADO(Sender: TObject; Q: TADOQuery); override;
end;
implementation
{$R *.DFM}
{ TfrmMbfRACK_MASTER }
procedure TfrmMbfRACK_MASTER.FormCreate(Sender: TObject);
begin
inherited;
fmeMxFrmBrowseHoster1.KeyField := 'RACK_ID';
// FWHID := -2; // 22/06/04
FWHID := 0; // 22/06/04
end;
procedure TfrmMbfRACK_MASTER.BuildADO(Sender: TObject; Q: TADOQuery);
begin
Q.Close;
Q.SQL.Clear;
Q.SQL.Add(
'SELECT R.RACK_DESC, R.RACK_BARCODE, W.ERP_WH, WC.CLASS_NAME, W.DESCRIPTION WAREHOUSE, R.RACK_PACKING_ORDER, ');
//...
end;
The base class
type
TMxfrmBrowseHoster = class(TfrmMxForm)
protected
// ...
procedure FormCreate(Sender: TObject);
procedure BuildADO(Sender: TObject; ADOQ: TADOQuery); virtual; abstract;
public
procedure TMxfrmBrowseHoster.FormCreate(Sender: TObject);
begin
TMxFormProductName := Application.Title;
fmeMxFrmBrowseHoster1.Initialise;
INISectionName := Name;
AbortAction := False;
fmeMxFrmBrowseHoster1.OnSelect := SelectNormaliser;
fmeMxFrmBrowseHoster1.OnNeedADO := BuildADO;
fmeMxFrmBrowseHoster1.INISectionName := self.Name;
fmeMxFrmBrowseHoster1.MultiSelect := dxBarLargeButton10.Down;
fmeMxFrmBrowseHoster1.AutoSaveGrid := True;
dxBarEdit1.OnChange := ActPageSizeChangedExecute;
FormStorage.RestoreFormPlacement;
ActConfirmDelete.Execute;
end;
I find your question a little on the vague side and I'm not 100% sure I understand exactly what you are asking. However, I know how to deal with your problem when calling the constructor. Perhaps that's all you need help with.
You need to use virtual constructor polymorphism and a bit of casting:
class function Browser<T>.BrowseForm(Owner: Tcomponent): T;
var
_browseForm: T;
begin
_browseForm := TfrmBrowser(T).Create(Owner);
Result := _browseForm;
end;
This relies on virtual constructor polymorphism. So you must make sure that each constructor for every class derived from TfrmBrowser is marked with the override directive.
I'm updating some properties in a component. In order to avoid missing property errors I'm using DefineProperties to read the old properties from the stream. Most properties work fine e.g. Integer, but I can't get properties based on TPersistent to work. The ReadProperty(TPersistent) procedure in TReader is protected, not public and requires a hack to access it. Even then, the ReadFontProperty procedure is never called and the missing property exception occurs.
How do I read the TFont property?
Here's some sample code of how I'm trying to do it.
...
type
TMyComponent = class(TComponent)
strict private
// Removed
//FIntegerProperty: Integer;
//FFontProperty: TFont;
// New
FNewIntegerProperty: Integer;
FNewFontProperty: TFont;
procedure ReadIntegerProperty(Reader: TReader);
procedure ReadFontProperty(Reader: TReader);
protected
procedure DefineProperties(Filer: TFiler); override;
published
// Removed properties
//property IntegerProperty: Integer read FIntegerProperty write FIntegerProperty;
//property FontProperty: TFont read FFontProperty write SetFontProperty;
// New properties
property NewIntegerProperty: Integer read FNewIntegerProperty write FNewIntegerProperty;
property NewFontProperty: TFont read FNewFontProperty write SetNewFontProperty;
end;
implementation
procedure TMyComponent.DefineProperties(Filer: TFiler);
begin
inherited;
// This works
Filer.DefineProperty('IntegerProperty', ReadIntegerProperty, nil, FALSE);
// This doesn't
Filer.DefineProperty('FontProperty', ReadFontProperty, nil, FALSE);
end;
procedure TMyComponent.ReadIntegerProperty(Reader: TReader);
begin
FNewIntegerProperty:= Reader.ReadInteger;
end;
type
THackReader = class(TReader);
procedure TMyComponent.ReadFontProperty(Reader: TReader);
begin
{ TODO : This doesn't work. How do we read fonts? }
THackReader(Reader).ReadProperty(FNewFontProperty);
end;
...
Update 1
Tried David's suggestion using the following code:
Filer.DefineProperty('Font.CharSet', ReadFontCharSet, nil, False);
...
procedure TMyComponent.ReadFontCharSet(Reader: TReader);
begin
Reader.ReadInteger;
end;
I get an Invalid Property Value error. I guess it's something to do with Charset being of type TFontCharset (= System.UITypes.TFontCharset = 0..255). How do I read this type of property?
In order to do this you need to work with each individual published property of TFont and you will need to use fully qualified names.
Filer.DefineProperty('FontProperty.Name', ReadFontName, nil, False);
Filer.DefineProperty('FontProperty.Height', ReadFontHeight, nil, False);
Filer.DefineProperty('FontProperty.Size', ReadFontSize, nil, False);
// and so on for all the other published properties of TFont
ReadFontName, ReadFontHeight etc. should read the old property values into the newly named component.
procedure TMyComponent.ReadFontName(Reader: TReader);
begin
FNewFontProperty.Name := Reader.ReadString;
end;
// etc. etc.
Update
You ask how to read the Charset property. This is complex because it can be written either as a textual identifier (see the FontCharsets constant in Graphics.pas), or as a plain integer value. Here is some rapidly hacked together code that will read your Charset.
procedure TMyComponent.ReadFontCharset(Reader: TReader);
function ReadIdent: string;
var
L: Byte;
LResult: AnsiString;
begin
Reader.Read(L, SizeOf(Byte));
SetString(LResult, PAnsiChar(nil), L);
Reader.Read(LResult[1], L);
Result := UTF8ToString(LResult);
end;
function ReadInt8: Shortint;
begin
Reader.Read(Result, SizeOf(Result));
end;
function ReadInt16: Smallint;
begin
Reader.Read(Result, SizeOf(Result));
end;
var
Ident: string;
CharsetOrdinal: Integer;
begin
Beep;
case Reader.ReadValue of
vaIdent:
begin
Ident := ReadIdent;
if not IdentToCharset(Ident, CharsetOrdinal) then begin
raise EReadError.Create('Could not read MyFont.Charset');
end;
FNewFontProperty.Charset := CharsetOrdinal;
end;
vaInt8:
FNewFontProperty.Charset := ReadInt8;
vaInt16:
FNewFontProperty.Charset := ReadInt16;
else
raise EReadError.Create('Could not read FontProperty.Charset');
end;
end;
I got quite a large application which is currently being styled up.
To save me changing all the buttons in the IDE/Object Inspector I am planning on just doing a few functions for the main objects like
procedure StyleButton(AButton : TButton)
begin
AButton.Color := clGreen;
AButton.Font.Style = [fsBold];
end;
etc etc and then add that to the forms onCreates as needed
StyleButton(Button1); whatever etc
There is no issue passing objects in params like this. It does just reference the first object right?
It works fine and I can't think of any issues, but because this is a large application which thousands of users I just want to be sure there will be no issues/memory leaks/resource consumpution issues.
Will also be doing similar things with TAdvStringGrid and TEdit/TMemo components.
Then allows just 1 place to change these settings.
Or someone have a better idea?
This is an excellent idea. The function will modify whichever object you pass to it.
You are not passing by reference. You are passing by value. The value you are passing is a reference. "Passing by reference" means you'd use the var or out keywords, which are not appropriate in this situation.
Your idea is just fine, as the other answerers have already said. Just want to propose a solution that goes even further than David's and something you may want to consider in order to avoid having to add many statements like:
StyleButton(Button1);
StyleButton(Button2);
to each and every form for each and every control you would like to style;
What I would propose is to add a single method call to for example each form's OnShow event:
procedure TForm1.FormShow(Sender: TObject);
begin
TStyler.StyleForm(Self);
end;
The TStyler could be implemented in a separate unit that looks like this:
interface
type
TStyler = class;
TStylerClass = class of TStyler;
TStyler = class(TObject)
public
class procedure StyleForm(const aForm: TCustomForm);
class procedure StyleControl(const aControl: TControl); virtual;
class function GetStyler(const aControl: TControl): TStylerClass;
end;
implementation
uses
Contnrs;
type
TButtonStyler = class(TStyler)
public
class procedure StyleControl(const aControl: TControl); override;
end;
TEditStyler = class(TStyler)
public
class procedure StyleControl(const aControl: TControl); override;
end;
TLabelStyler = class(TStyler)
public
class procedure StyleControl(const aControl: TControl); override;
end;
var
_Controls: TClassList;
_Stylers: TClassList;
{ TStyler }
class function TStyler.GetStyler(const aControl: TControl): TStylerClass;
var
idx: Integer;
begin
Result := TStyler;
idx := _Controls.IndexOf(aControl.ClassType);
if idx > -1 then
Result := TStylerClass(_Stylers[idx]);
end;
class procedure TStyler.StyleForm(const aForm: TCustomForm);
procedure _StyleControl(const aControl: TControl);
var
i: Integer;
StylerClass: TStylerClass;
begin
StylerClass := TStyler.GetStyler(aControl);
StylerClass.StyleControl(aControl);
if (aControl is TWinControl) then
for i := 0 to TWinControl(aControl).ControlCount - 1 do
_StyleControl(TWinControl(aControl).Controls[i]);
end;
var
i: Integer;
begin
_StyleControl(aForm);
end;
class procedure TStyler.StyleControl(const aControl: TControl);
begin
// Do nothing. This is a catch all for all controls that do not need specific styling.
end;
{ TButtonStyler }
class procedure TButtonStyler.StyleControl(const aControl: TControl);
begin
inherited;
if aControl is TButton then
begin
TButton(aControl).Font.Color := clRed;
TButton(aControl).Font.Style := [fsBold];
end;
end;
{ TEditStyler }
class procedure TEditStyler.StyleControl(const aControl: TControl);
begin
inherited;
if aControl is TEdit then
begin
TEdit(aControl).Color := clGreen;
end;
end;
{ TLabelStyler }
class procedure TLabelStyler.StyleControl(const aControl: TControl);
begin
inherited;
if aControl is TLabel then
begin
TLabel(aControl).Font.Color := clPurple;
TLabel(aControl).Font.Style := [fsItalic];
end;
end;
initialization
_Controls := TClassList.Create;
_Stylers := TClassList.Create;
_Controls.Add(TButton);
_Stylers.Add(TButtonStyler);
_Controls.Add(TEdit);
_Stylers.Add(TEditStyler);
_Controls.Add(TLabel);
_Stylers.Add(TLabelStyler);
finalization
FreeAndNiL(_Controls);
FreeAndNiL(_Stylers);
end.
This solution basically employs polymorphism and a registry that links control classes to styler classes. It also uses class procedures and functions to avoid having to instantiate anything.
Please note that the registry is implemented in this example as two lists that need to be kept in sync manually as the code assumes that finding a class at index X will find the styler at the same index in the other list. This can of course be improved upon very much, but is sufficient here to show the concept.
No, There is no issue (in your specific case) passing a object as parameter
procedure StyleButton(AButton : TButton)
when you do this you are passing a address memory (reference) and setting some properties of the referenced object, so there is not problem.
To add to what Rob and RRUZ have already said, you could consider an extra helper using open array parameters:
procedure StyleButtons(const Buttons: array of TButton);
var
i: Integer;
begin
for i := low(Buttons) to high(Buttons) do
StyleButton(Buttons[i]);
end;
You can then call this as:
StyleButtons([btnOK, btnCancel, btnRelease64bitDelphi]);
which is, in my view, more readable at the call-site than:
StyleButton(btnOK);
StyleButton(btnCancel);
StyleButton(btnRelease64bitDelphi);
Note that I passed the open array as a const parameter because that is more efficient when dealing with arrays. Because each element of the array is itself a reference to the button, you are able to modify the actual button. The const just means that you cannot change the reference.