Properties are the most visible parts of components. The application developer can see and manipulate them at design time and get immediate feedback as the components react in the Form designer. Well-designed properties make your components easier for others to use and easier for you to maintain.
To make the best use of properties in your components, you should understand the following:
From the application developer's standpoint, properties look like variables. Developers can set or read the values of properties as if they were fields. (About the only thing you can do with a variable that you cannot do with a property is pass it as a var parameter.)
Properties provide more power than simple fields because
A simple example is the Top property of all controls. Assigning a new value to Top does not just change a stored value; it repositions and repaints the control. And the effects of setting a property need not be limited to an individual component; for example, setting the Down property of a speed button to True sets Down property of all other speed buttons in its group to False.
A property can be of any type. Different types are displayed differently in the Object Inspector, which validates property assignments as they are made at design time.
All components inherit properties from their ancestor classes. When you derive a new component from an existing one, your new component inherits all the properties of its immediate ancestor. If you derive from one of the abstract classes, many of the inherited properties are either protected or public, but not published.
To make a protected or public property available at design time in the Object Inspector, you must redeclare the property as published. Redeclaring means adding a declaration for the inherited property to the declaration of the descendant class.
If you derive a component from TWinControl, for example, it inherits the protected Ctl3D property. By redeclaring Ctl3D in your new component, you can change the level of protection to either public or published.
The following code shows a redeclaration of Ctl3D as published, making it available at design time.
type TSampleComponent = class(TWinControl) published property Ctl3D; end;
When you redeclare a property, you specify only the property name, not the type and other information described below in "Defining properties". You can also declare new default values and specify whether to store the property.
Redeclarations can make a property less restricted, but not more restricted. Thus you can make a protected property public, but you cannot hide a public property by redeclaring it as protected.
This section shows how to declare new properties and explains some of the conventions followed in the standard components. Topics include
A property is declared in the declaration of its component class. To declare a property, you specify three things:
Properties declared in a published section of the component's class declaration are editable in the Object Inspector at design time. The value of a published property is saved with the component in the form file. Properties declared in a public section are available at runtime and can be read or set in program code.
Here is a typical declaration for a property called Count.
type
TYourComponent = class(TComponent)
private
FCount: Integer; { used for internal storage }
procedure SetCount (Value: Integer); { write method }
public
property Count: Integer read FCount write SetCount;
end;
There are no restrictions on how you store the data for a property. In general, however, Delphi components follow these conventions:
The principle that underlies these conventions is that only the implementation methods for a property should access the data behind it. If a method or another property needs to change that data, it should do so through the property, not by direct access to the stored data. This ensures that the implementation of an inherited property can change without invalidating derived components.
The simplest way to make property data available is direct access. That is, the read and write parts of the property declaration specify that assigning or reading the property value goes directly to the internal-storage field without calling an access method. Direct access is useful when you want to make a property available in the Object Inspector but changes to its value trigger no immediate processing.
It is common to have direct access for the read part of a property declaration but use an access method for the write part. This allows the status of the component to be updated when the property value changes.
The following component-type declaration shows a property that uses direct access for both the read and the write parts.
type
TSampleComponent = class(TComponent)
private { internal storage is private}
FMyProperty: Boolean; { declare field to hold property value }
published { make property available at design time }
property MyProperty: Boolean read FMyProperty write FMyProperty;
end;
You can specify an access method instead of a field in the read and write parts of a property declaration. Access methods should be protected, and are usually declared as virtual; this allows descendant components to override the property's implementation.
Avoid making access methods public. Keeping them protected ensures that application developers do not inadvertently modify a property by calling one of these methods.
Here is a class that declares three properties using the index specifier, which allows all three properties to have the same read and write access methods:
type
TSampleCalendar = class(TCustomGrid)
public
property Day: Integer index 3 read GetDateElement write SetDateElement;
property Month: Integer index 2 read GetDateElement write SetDateElement;
property Year: Integer index 1 read GetDateElement write SetDateElement;
private
function GetDateElement(Index: Integer): Integer; { note the Index parameter }
procedure SetDateElement(Index: Integer; Value: Integer);
...
Because each element of the date (day, month, and year) is an int, and because setting each requires encoding the date when set, the code avoids duplication by sharing the read and write methods for all three properties. You need only one method to read a date element, and another to write the date element.
Here is the read method that obtains the date element:
function TSampleCalendar.GetDateElement(Index: Integer): Integer;
var
AYear, AMonth, ADay: Word;
begin
DecodeDate(FDate, AYear, AMonth, ADay); { break encoded date into elements }
case Index of
1: Result := AYear;
2: Result := AMonth;
3: Result := ADay;
else Result := -1;
end;
end;
This is the write method that sets the appropriate date element:
procedure TSampleCalendar.SetDateElement(Index: Integer; Value: Integer);
var
AYear, AMonth, ADay: Word;
begin
if Value > 0 then { all elements must be positive }
begin
DecodeDate(FDate, AYear, AMonth, ADay); { get current date elements }
case Index of { set new element depending on Index }
1: AYear := Value;
2: AMonth := Value;
3: ADay := Value;
else Exit;
end;
FDate := EncodeDate(AYear, AMonth, ADay); { encode the modified date }
Refresh; { update the visible calendar }
end;
end;
The read method for a property is a function that takes no parameters (except as noted below) and returns a value of the same type as the property. By convention, the function's name is Get followed by the name of the property. For example, the read method for a property called Count would be GetCount. The read method manipulates the internal storage data as needed to produce the value of the property in the appropriate type.
The only exceptions to the no-parameters rule are for array properties and properties that use index specifiers (see "Creating array properties"), both of which pass their index values as parameters. (Use index specifiers to create a single read method that is shared by several properties. For more information about index specifiers, see the Object Pascal Language Guide.)
If you do not declare a read method, the property is write-only. Write-only properties are seldom used.
The write method for a property is a procedure that takes a single parameter (except as noted below) of the same type as the property. The parameter can be passed by reference or by value, and can have any name you choose. By convention, the write method's name is Set followed by the name of the property. For example, the write method for a property called Count would be SetCount. The value passed in the parameter becomes the new value of the property; the write method must perform any manipulation needed to put the appropriate data in the property's internal storage.
The only exceptions to the single-parameter rule are for array properties and properties that use index specifiers, both of which pass their index values as a second parameter. (Use index specifiers to create a single write method that is shared by several properties. For more information about index specifiers, see the Object Pascal Language Guide.)
If you do not declare a write method, the property is read-only. For a published property to be usable at design time, it must be defined as read/write.
Write methods commonly test whether a new value differs from the current value before changing the property. For example, here is a simple write method for an integer property called Count that stores its current value in a field called FCount.
procedure TMyComponent.SetCount(Value: Integer); begin if Value <> FCount then begin FCount := Value; Update; end; end;
When you declare a property, you can specify a default value for it. Delphi uses the default value to determine whether to store the property in a form file. If you do not specify a default value for a property, Delphi always stores the property.
To specify a default value for a property, append the default directive to the property's declaration (or redeclaration), followed by the default value. For example,
property Cool Boolean read GetCool write SetCool default True;
Note: Declaring a default value does not set the property to that value. The component's constructor method should initialize property values when appropriate. However, since objects always initialize their fields to 0, it is not strictly necessary for the constructor to set integer properties to 0, string properties to null, or Boolean properties to False.
When redeclaring a property, you can specify that the property has no default value, even if the inherited property specified one.
To designate a property as having no default value, append the nodefault directive to the property's declaration. For example,
property FavoriteFlavor string nodefault;
When you declare a property for the first time, there is no need to include nodefault. The absence of a declared default value means that there is no default.
Here is the declaration of a component that includes a single Boolean property called IsTrue with a default value of True. Below the declaration (in the implementation section of the unit) is the constructor that initializes the property.
type
TSampleComponent = class(TComponent)
private
FIsTrue: Boolean;
public
constructor Create(AOwner: TComponent); override;
published
property IsTrue: Boolean read FIsTrue write FIsTrue default True;
end;
...
constructor TSampleComponent.Create(AOwner: TComponent);
begin
inherited Create(AOwner); { call the inherited constructor }
FIsTrue := True; { set the default value }
end;
Some properties lend themselves to being indexed like arrays. For example, the Lines property of TMemo is an indexed list of the strings that make up the text of the memo; you can treat it as an array of strings. Lines provides natural access to a particular element (a string) in a larger set of data (the memo text).
Array properties are declared like other properties, except that
The read and write methods for an array property take additional parameters that correspond to the indexes. The parameters must be in the same order and of the same type as the indexes specified in the declaration.
There are a few important differences between array properties and arrays. Unlike the index of an array, the index of an array property does not have to be an integer type. You can index a property on a string, for example. In addition, you can reference only individual elements of an array property, not the entire range of the property.
The following example shows the declaration of a property that returns a string based on an integer index.
type TDemoComponent = class(TComponent) private function GetNumberName(Index: Integer): string; public property NumberName[Index: Integer]: string read GetNumberName; end; ... function TDemoComponent.GetNumberName(Index: Integer): string; begin Result := 'Unknown'; case Index of -MaxInt..-1: Result := 'Negative'; 0: Result := 'Zero'; 1..100: Result := 'Small'; 101..MaxInt: Result := 'Large'; end; end;
Delphi stores forms and their components in form (.DFM) files. A form file is a binary representation of the properties of a form and its components. When Delphi developers add the components you write to their forms, your components must have the ability to write their properties to the form file when saved. Similarly, when loaded into Delphi or executed as part of an application, the components must restore themselves from the form file.
Most of the time you will not need to do anything to make your components work with form files because the ability to store a representation and load from it are part of the inherited behavior of components. Sometimes, however, you might want to alter the way a component stores itself or the way it initializes when loaded; so you should understand the underlying mechanism.
These are the aspects of property storage you need to understand:
The description of a form consists of a list of the form's properties, along with similar descriptions of each component on the form. Each component, including the form itself, is responsible for storing and loading its own description.
By default, when storing itself, a component writes the values of all its public and published properties that differ from their default values, in the order of their declaration. When loading itself, a component first constructs itself, setting all properties to their default values, then reads the stored, non-default property values.
This default mechanism serves the needs of most components, and requires no action at all on the part of the component writer. There are several ways you can customize the storing and loading process to suit the needs of your particular components, however.
Delphi components save their property values only if those values differ from the defaults. If you do not specify otherwise, Delphi assumes a property has no default value, meaning the component always stores the property, whatever its value.
To specify a default value for a property, add the default directive and the new default value to the end of the property declaration.
You can also specify a default value when redeclaring a property. In fact, one reason to redeclare a property is to designate a different default value.
Note: Specifying the default value does not automatically assign that value to the property on creation of the object. You must make sure that the component's constructor assigns the necessary value. A property whose value is not set by a component's constructor assumes a zero value--that is, whatever value the property assumes when its storage memory is set to 0. Thus numeric values default to 0, Boolean values to False, pointers to nil, and so on. If there is any doubt, assign a value in the constructor method.
The following code shows a component declaration that specifies a default value for the Align property and the implementation of the component's constructor that sets the default value. In this case, the new component is a special case of the standard panel component that will be used for status bars in a window, so its default alignment should be to the bottom of its owner.
type
TStatusBar = class(TPanel)
public
constructor Create(AOwner: TComponent); override; { override to set new default }
published
property Align default alBottom; { redeclare with new default value }
end;
...
constructor TStatusBar.Create(AOwner: TComponent);
begin
inherited Create(AOwner); { perform inherited initialization }
Align := alBottom; { assign new default value for Align }
end;
You can control whether Delphi stores each of your components' properties. By default, all properties in the published part of the class declaration are stored. You can choose not to store a given property at all, or you can designate a function that determines at runtime whether to store the property.
To control whether Delphi stores a property, add the stored directive to the property declaration, followed by True, False, or the name of a Boolean method.
The following code shows a component that declares three new properties. One is always stored, one is never stored, and the third is stored depending on the value of a Boolean method:
type
TSampleComponent = class(TComponent)
protected
function StoreIt: Boolean;
public { normally not stored }
property Important: Integer stored True; { always stored }
published { normally stored always }
property Unimportant: Integer stored False; { never stored }
property Sometimes: Integer stored StoreIt; { storage depends on function value }
end;
After a component reads all its property values from its stored description, it calls a virtual method named Loaded, which performs any required initializations. The call to Loaded occurs before the form and its controls are shown, so you do not need to worry about initialization causing flicker on the screen.
To initialize a component after it loads its property values, override the Loaded method.
Note: The first thing to do in any Loaded method is call the inherited Loaded method. This ensures that any inherited properties are correctly initialized before you initialize your own component.
The following code comes from the TDatabase component. After loading, the database tries to reestablish any connections that were open at the time it was stored, and specifies how to handle any exceptions that occur while connecting.
procedure TDatabase.Loaded;
begin
inherited Loaded; { call the inherited method first}
try
if FStreamedConnected then Open { reestablish connections }
else CheckSessionName(False);
except
if csDesigning in ComponentState then { at design time... }
Application.HandleException(Self) { let Delphi handle the exception }
else raise; { otherwise, reraise }
end;
end;
By default, only published properties are loaded and saved with a component. However, it is possible to load and save unpublished properties. This allows you to have persistent properties that do not appear in the Object Inspector. It also allows components to store and load property values that Delphi does not know how to read or write because the value of the property is too complex. For example, the TStrings object can't rely on Delphi's automatic behavior to store and load the strings it represents and must use the following mechanism.
You can save unpublished properties by adding code that tells Delphi how to load and save your property's value.
To write your own code to load and save properties, use the following steps:
To store and load unpublished properties, you must first create a method to store your property value and another to load your property value. You have two choices:
For example, consider a property that represents a component that is created at runtime. Delphi knows how to write this value, but does not do so automatically because the component is not created in the form designer. Because the streaming system can already load and save components, you can use the first approach. The following methods load and store the dynamically created component that is the value of a property named MyCompProperty:
procedure TSampleComponent.LoadCompProperty(Reader: TReader); begin if Reader.ReadBoolean then MyCompProperty := Reader.ReadComponent(nil); end; procedure TSampleComponent.StoreCompProperty(Writer: TWriter); begin Writer.WriteBoolean(MyCompProperty <> nil); if MyCompProperty <> nil then Writer.WriteComponent(MyCompProperty); end;
Once you have created methods to store and load your property value, you can override the component's DefineProperties method. Delphi calls this method when it loads or stores the component. In the DefineProperties method, you must call the DefineProperty method or the DefineBinaryProperty method of the current filer, passing it the method to use for loading or saving your property value. If your load and store methods are of type TWriterProc and type TReaderProc, then you call the filer's DefineProperty method. If you created methods of type TStreamProc, call DefineBinaryProperty instead.
No matter which method you use to define the property, you pass it the methods that store and load your property value as well as a boolean value indicating whether the property value needs to be written. If the value can be inherited or has a default value, you do not need to write it.
For example, given the LoadCompProperty method of type TReaderProc and the StoreCompProperty method of type TWriterProc, you would override DefineProperties as follows:
procedure TSampleComponent.DefineProperties(Filer: TFiler);
function DoWrite: Boolean;
begin
if Filer.Ancestor <> nil then { check Ancestor for an inherited value }
begin
if TSampleComponent(Filer.Ancestor).MyCompProperty = nil then
Result := MyCompProperty <> nil
else if MyCompProperty = nil or
TSampleComponent(Filer.Ancestor).MyCompProperty.Name <> MyCompProperty.Name then
Result := True
else Result := False;
end
else { no inherited value -- check for default (nil) value }
Result := MyCompProperty <> nil;
end;
begin
inherited; { allow base classes to define properties }
Filer.DefineProperty('MyCompProperty', LoadCompProperty, StoreCompProperty, DoWrite);
end;
pubsweb@inprise.com
Copyright © 1999, Inprise Corporation. All rights reserved.