Give Your Clients Control of the GUI

Copyright Pinnacle Publishing, Inc. All rights reserved.Give Your Clients Control of the GUI

Steve Zimmelman

How many times have you delivered what you thought was a completed application, only to hear your client say, "Gee, this is nice, but I'd really like the Name on the left, and the Status field should be red, not black, and . . ." Wouldn't it be nice if all your users had to do was right-click the mouse over the control, and up comes a popup menu allowing them to change the control's attributes? Or if they press Ctrl-ArrowKey or Shift-ArrowKey to change the position or size of the control?

Giving users control of the GUI is a two-step process. First you have to create controls with properties users can change at runtime, and then you must have a way to save and restore the changes. Usually, the control that gets the most use is the DBEdit. So this example will focus on creating a DBEdit that uses a customized PopupMenu that's activated on the right-click of the mouse. The menu will allow the user to change Color, Font, Tab Order, and Text Case. Additionally, the key combination of Ctrl-Arrow and Shift-Arrow will change the position and size of the control, respectively.

Before I begin, I want to introduce you to a new text-case property: ProperCase. Most proper-case conversions usually capitalize only the first letter, then any letter that follows a space. But this leaves a host of names and abbreviated titles looking a bit awkward, like McDonald, O'Hara, M.D., and so forth. So in my attempt to create a better mouse-trap, I included a proper-case conversion that has the intelligence to handle these special names. I placed the functions used for the proper-case conversion in a unit called StrFunc.pas and then included the unit StrFunc in the Uses statement of the component.

Creating the changeable DBEdit

The first thing I did was create a new component using TDBEdit as the parent class. I called the new class TPSIDBEdit because the name of the company I work for is PSI, and it was a simple way to distinguish the component. Next I created a new CharCase type that includes ProperCase. It will be used to override the DBEdit's TCharCase. I also changed the CharCase property to a new type, TPSICharCase. In addition to overriding the CharCase property, I introduced two new properties: AllowUserChange and PopupChangeMenu. The property AllowUserChange is a simple Boolean switch that engages or disengages the user's ability to change any of the component's properties at runtime, and PopupChangeMenu is a pointer to the PopupMenu that's in effect when AllowUserChange is set to True. A bulk of the component's changing is actually done by the PopupChangeMenu, but I'll discuss that a little later. Listing 1 presents the PSIDBEdit unit.

Listing 1. The PSIDBEdit unit.

unit PSIDBEdit;

interface

uses

Windows, SysUtils, Classes, Controls, Forms, Dialogs,

DBCtrls, Menus, db, StrFunc;

type

TPSICharCase = (ecNormal, ecUpperCase,

ecLowerCase, ecProperCase);

TPSIDBEdit = class(TDBEdit)

private

fCharCase : TPSICharCase ;

fIsChanging : Boolean ;

fAllowUserChange : Boolean ;

fStartMove : Boolean ;

fTop : Integer ;

fLeft : Integer ;

fChangeMenu : TpopupMenu ;

fPopupSave : TPopupMenu ;

Procedure SetAllowUserChange(Value:Boolean);

Procedure SetChangeMenu(Value:TpopupMenu) ;

Procedure SetPopUpMenu ;

Procedure SetCharCase(Value:TPSICharCase);

Procedure SetTextCase(Const bCheckState:Boolean) ;

protected

public

Procedure Loaded ; Override ;

Procedure Change ; Override ;

procedure KeyDown(var Key: Word;

Shift: TShiftState); override;

Procedure MouseDown(Button: TMouseButton; Shift:

TShiftState; X, Y: Integer);

Override ;

procedure MouseMove(Shift: TShiftState;

X, Y: Integer); Override;

procedure MouseUp(Button: TMouseButton; Shift:

TShiftState; X, Y: Integer);

Override;

published

Property AllowUserChange : Boolean

Read fAllowUserChange

Write SetAllowUserChange ;

Property CharCase : TPSICharCase

Read fCharCase

Write SetCharCase ;

Property PopupChangeMenu : TPopupMenu

Read fChangeMenu

Write SetChangeMenu ;

end;

I wanted the component to have two states: a default state, which would behave like its ancestor, and a design state, which would allow the user to change its properties at runtime. The Boolean property AllowUserChange distinguishes these two states. When AllowUserChange is True, the component's PopupMenu is changed to point to the value stored in the PopupChangeMenu property. If it's False, then the PopupMenu property reverts to its previous design time assignment. It also allows the component to be moved or resized. I thought it would be nice for the user to be able to see a difference between the two states, so I changed the cursor to crHandPoint (-21) in the SetAllowUserChange method.

The PopupMenu assignment is changed via the SetPopupMenu method when the AllowUserChange property is changed.

In order to swap the PopupMenu pointers, the default PopupMenu is stored in fPopupSave in the Loaded method (see Listing 2). Then the SetPopupMenu method is called to ensure that the proper popup menu is assigned based on the value of AllowUserChange.

Listing 2. The Loaded method.

Procedure TPSIDBEdit.Loaded;

Begin

Try

If (csDesigning in ComponentState) Then Exit ;

// Capture PopupMenu Assignment

fPopupSave := PopupMenu ;

SetPopupMenu ;

Finally

inherited Loaded;

End;

End;

With the advent of the new text case ProperCase and the new type TPSICharCase, I had to completely override and re-implement all of the other standard text-case conversions in the Change method of the component (see Listing 3).

Listing 3. The Change method.

Procedure TPSIDBEdit.Change ;

Var

iSelStart : Integer ;

Begin

Try

If (csDesigning in ComponentState)

Or fIsChanging Then Exit ;

// Capture Cursor Position

iSelStart := SelStart ;

SetTextCase(False) ;

// Restore Cursor Position

SelStart := iSelStart ;

Finally

Inherited ;

End;

End;

The actual changing of the Text was a little tricky. If you change the text programmatically when the object has focus, and the DataSet isn't in Edit or Insert mode, then the exception "Dataset not in Edit or Insert mode" is generated. So before changing the text, the component's DataSet.State must be interrogated and changed if necessary. We must also handle any potential multi-user conflicts that might occur. The parameter bCheckState is used to determine whether the DataSet.State needs to be placed in Edit or Insert mode before changing the text. If it does, then the method must also Post the changes. The Post method is called only if the memory variable bPost is set to True. Listing 4 shows where all this happens in the SetTextCase method.

In order for the control to have movement capabilities, I overrode the KeyDown method (see Listing 6). Borrowing the Delphi IDE keystrokes for component moving and sizing, I used the Ctrl-ArrowKeys for movement and the Shift-ArrowKeys for sizing. Pressing these keys causes the control to change its size or position by one pixel.

Listing 6. The KeyDown method.

Procedure TPSIDBEdit.KeyDown(var Key: Word;

Shift: TShiftState);

Begin

If (Key in [vk_up,vk_down,vk_left,vk_right])

And AllowUserChange Then Begin

If (Shift = [ssCtrl]) Then Begin

// Change position

Case Key Of

vk_Up : Top := Top - 1;

vk_Down : Top := Top + 1;

vk_Left : Left := Left - 1;

vk_Right: Left := Left + 1 ;

End;

End Else If (Shift = [ssShift]) Then Begin

// Change Size

Case Key Of

vk_Up : Height := Height - 1;

vk_Down : Height := Height + 1;

vk_Left : Width := Width - 1;

vk_Right: Width := Width + 1 ;

End;

End;

Key := 0 ;

End Else Begin

inherited KeyDown(Key,Shift);

End;

End;

I also wanted the user to be able to move the component with the mouse, so I overrode the methods MouseDown, MouseUp, and MouseMove.

So far, what you have is a DBEdit that allows a user to change its position and size, but little else. The Font, Color, Tab Order, 3D effect, and Border will be done with the addition of the component ChangeMenu.

The basis for this component is found in the unit TypInfo.pas, which allows you to have access to an object's RTTI (runtime type information). TypInfo.pas contains functions and procedures that can Get or Set an object's property values during the execution of the application. Some simple methods of Setting or Getting an object's property value at runtime can look something like Listing 7.

Listing 7. Getting and Setting an object's property at runtime.

Function GetProperty(Sender:TComponent

;sPropName:String):Variant

Var

PropInfo : PPropinfo;

Begin

// From the class information, get the property

PropInfo := GetPropInfo(Sender.ClassInfo,

sPropName);

// Does the property exist?

If (PropInfo <> Nil) Then Begin

Case propinfo^.PropType^.Kind Of

tkEnumeration,tkInteger:

Begin

Result := GetOrdProp(Sender,PropInfo)

End;

tkString,tkLString,tkWString:

Begin

Result := GetStrProp(Sender,PropInfo)

End;

End;

End Else

Result := Null;

End;

Procedure SetProperty(Sender:TComponent

;sPropName:String

;vValue:Variant)

Var

PropInfo : PPropinfo;

Begin

// From the class information, get the property

PropInfo := GetPropInfo(Sender.ClassInfo,

sPropName);

// Does the property exist?

If (PropInfo <> Nil) Then Begin

Case propinfo^.PropType^.Kind Of

tkEnumeration,tkInteger:

Begin

SetOrdProp(Sender,PropInfo,vValue);

End;

tkString,tkLString,tkWString:

Begin

SetStrProp(Sender,PropInfo,vValue);

End;

End;

End;

End;

Usage of these two methods could look like this:

SetProperty(DBEdit1,'Ctl3D',False);

DBEdit2.Ctrl3D := GetProperty(DBEdit1.'Ctl3D') ;

Granted, this example is probably over-simplified, but procedures like these can be quite handy if you don't know the component's name, or even its class.

The three main methods used in ChangeMenu -- GetProperty(), SetProperty(), and IsProperty() -- are found in the unit PropFunc.pas that's part of this library. IsProperty() is a Boolean function that checks for the existence of a specified property in an object and returns a value of True if the property exists. It can be used something like this:

If IsProperty(Form1.Components[i],'Ctl3D') Then

Components[i].Ctl3D := False ;

TChangeMenu is a subclass of TPopupMenu with two additional properties: FontDialog and ColorDialog, which are used to change the component's Font and Color and can point to any matching class types that are in the scope of the component. The methods MenuClick and OnMenuPopup do the bulk of the work and are assigned to the OnClick and OnPopup events of the menu items when the component is created. All this happens, of course, in the ChangeMenu unit (see Listing 8).

Listing 8. The ChangeMenu unit.

unit ChangeMenu;

interface

uses

Windows, Messages, SysUtils, Classes, Graphics,

Controls, Forms, Dialogs, Menus,comctrls,

StdCtrls,dbctrls, db,extctrls ;

type

TChangeMenu = class(TPopupMenu)

private

{ Private declarations }

pm_Font : TMenuItem ;

pm_bgColor : TMenuItem ;

pm_TabOrder : TMenuItem ;

pm_Ctrl3D : TMenuItem ;

pm_BorderStyle: TMenuItem ;

pm_Columns : TMenuItem ;

pm_Caption : TMenuItem ;

pm_Divider1 : TMenuItem ;

pm_UpperCase : TMenuItem ;

pm_LowerCase : TMenuItem ;

pm_MixedCase : TMenuItem ;

pm_ProperCase : TMenuItem ;

pm_Height : TMenuItem ;

pm_Width : TMenuItem ;

pm_Style : TMenuItem ;

fFontDialog :TFontDialog ;

fColorDialog :TColorDialog ;

Procedure SetColorDialog(Value:TColorDialog);

Procedure SetFontDialog(Value:TFontDialog);

Procedure MenuClick(Sender:TObject);

Procedure OnMenuPopup(Sender:TObject) ;

protected

{ Protected declarations }

public

{ Public declarations }

Procedure Loaded ; Override ;

Destructor destroy; override ;

Constructor Create(AOwner: TComponent); override;

published

{ Published declarations }

Property FontDialog :TFontDialog

Read fFontDialog

Write SetFontDialog ;

Property ColorDialog : TColorDialog

Read fColorDialog

Write SetColorDialog ;

end;

procedure Register;

implementation

uses PropFunc, col_edit ;

Because of the specialized use of this menu, the menu items are created only at runtime, not at design-time in the IDE. After the menu items are created, the method MenuClick() is assigned to the OnClick event of each menu item. The menu will look like Figure 1.

The Loaded method (see Listing 9) captures the pointer to the component's OnPopup method, then assigns the OnPopup event to the method OnMenuPoup(). This is done so the developer's OnPopup method will execute after the OnMenuPopup method has finished processing its own menu items.

Listing 9. The Loaded method.

Procedure TChangeMenu.Loaded;

Begin

Try

// Save pointer to Component's OnPopup Method

FOtherOnPopup := OnPopup ;

// Assign OnPopup Method

OnPopup := OnMenuPopup ;

Finally

Inherited ;

End;

End;

When the user right-clicks the mouse over the component, the OnMenuPopup method is executed (see Listing 10) and initializes the menu items based on the menu's PopupComponent property. PopupComponent is a pointer to the object that was responsible for activating the menu and is type TComponent. Notice the use of the method IsProperty to set the Visible property of the menu items.

Listing 10. The OnMenuPopup method.

Procedure TChangeMenu.OnMenuPopup(Sender:TObject) ;

// Initialize menu items based on the

// focused component's type and properties.

Var bSet : Boolean ;

Begin

// Don't show Font item if the Font

// Dialog property is Nil or the Font

// Property doesn't exist in the focused

// component.

pm_Font.Visible :=

(Not (FontDialog = Nil))

And IsProperty(PopupComponent,'Font');

// Don't show Color item if the ColorDialog

// Property is Nil or the Color property doesn't

// exist in the focused component.

pm_bgColor.Visible :=

Not (ColorDialog = Nil)

And IsProperty(PopupComponent,'Color');

// Initialize Radio and Checked Menu Items

If IsProperty(PopupComponent,

'BorderStyle') Then Begin

pm_BorderStyle.Checked :=

(GetProperty(PopupComponent,

'BorderStyle') = bsSingle) ;

pm_BorderStyle.Visible := True ;

End Else Begin

pm_BorderStyle.Visible := False ;

End;

If IsProperty(PopupComponent,'Ctl3D') Then Begin

pm_Ctrl3D.Checked :=

GetProperty(PopupComponent,'Ctl3D');

pm_Ctrl3D.Visible := True ;

End Else Begin

pm_Ctrl3D.Visible := False ;

End;

// If the TabOrder property exists in the component,

// show the Menu item 'Tab Order'.

pm_TabOrder.Visible:=

IsProperty(PopupComponent,'TabOrder') ;

// If the Columns property exists in the component

// and the component is TListView,

// show the menu item.

If (PopupComponent Is TListView) Then

pm_columns.Visible :=

( IsProperty(PopupComponent,'Columns') And

(TListView(PopupComponent).Columns.Count >0))

Else

pm_columns.Visible := False ;

// You should have the idea by now ...

pm_Caption.Visible :=

IsProperty(PopupComponent,'Caption');

// If the focused component has CharCase and

// AllowUser Change properties, then process more

// menu items.

If (IsProperty(PopupComponent,'CharCase')

And IsProperty(PopupComponent,

'AllowUserChange')) Then Begin

bSet := True ;

// Don't show CharCase items if the

// DataType isn't String.

If IsProperty(PopupComponent,

'DataSource') Then Begin

If (PopupComponent is TDBEdit) Then Begin

With TDBEdit(PopupComponent) Do Begin

If (DataField <> '') Then Begin

If Not (Field.DataType =

ftString) Then Begin

bSet := False ;

End;

End Else

bSet := False ;

End;

End;

End;

pm_UpperCase.Visible := bSet ;

pm_LowerCase.Visible := bSet ;

pm_MixedCase.Visible := bSet ;

pm_ProperCase.Visible := bSet ;

pm_Divider1.Visible := bSet ;

// Initialize CharCase Radio Items.

Case GetProperty(PopupComponent,'CharCase') Of

//ecNormal

0: pm_MixedCase.Checked := True ;

//ecUpperCase

1: pm_UpperCase.Checked := True ;

//ecLowerCase

2: pm_LowerCase.Checked := True ;

//ecProperCase

3: pm_ProperCase.Checked := True ;

End;

End Else Begin

pm_UpperCase.Visible := False ;

pm_LowerCase.Visible := False ;

pm_MixedCase.Visible := False ;

pm_ProperCase.Visible := False ;

pm_Divider1.Visible := False ;

End;

pm_Width.Visible := (PopupComponent is TBevel);

pm_Height.Visible := (PopupComponent is TBevel);

pm_Style.Visible := (PopupComponent is TBevel);

If pm_Style.Visible Then Begin

If GetProperty(PopupComponent,'Style')=0 Then

pm_Style.Caption := 'Raised Bevel'

Else

pm_Style.Caption := 'Lowered Bevel' ;

End;

// execute component's OnPopup Event.

If Assigned(FOtherOnPopup) Then

FOtherOnPopup(Sender);

End;

When an item is selected (see Listing 11) the method SetProperty() is used to change the object's property values.

Listing 11. The MenuClick method.

Procedure TChangeMenu.MenuClick(Sender:TObject);

// Process the menu selection for the focused

// component.

Var

sString : String ;

i,iInt : Integer ;

b : Boolean ;

Begin

If (TMenuItem(Sender)= pm_Ctrl3D) Then Begin

// Process 3D - If 3D is selected

// then BorderStyle must be changed

// to Single.

b := Not Boolean(GetProperty(PopupComponent,

'Ctl3D'));

SetProperty(PopupComponent,'Ctl3D',Ord(b));

If b And (GetProperty(PopupComponent,

'BorderStyle') = bsNone) Then

SetProperty(PopupComponent,

'BorderStyle',bsSingle);

End Else If (TMenuItem(Sender)=

pm_BorderStyle) Then Begin

// Process BorderStyle

If (GetProperty(PopupComponent,

'BorderStyle') = bsSingle) Then Begin

SetProperty(PopupComponent,

'BorderStyle',bsNone);

// If BorderStyle is set to None, then

// turn 3D Off.

SetProperty(PopupComponent,'Ctl3D',False);

End Else

SetProperty(PopupComponent,

'BorderStyle',bsSingle);

End Else If (TMenuItem(Sender)=

pm_TabOrder) Then Begin

// Process TabOrder

sString := IntToStr(GetProperty(PopupComponent,

'TabOrder'));

Try

sString := InputBox('Set Tab Order',

'Enter Tab Order',sString);

iInt := StrToInt(sString);

SetProperty(PopupComponent,'TabOrder',iInt) ;

Except

Raise Exception.Create('Tab Order Must '+

'Be An Integer');

End;

End Else If (TMenuItem(Sender)= pm_Font) Then Begin

// Process Font selection

If (FontDialog <> Nil) Then Begin

FontDialog.Font.Name :=

GetProperty(PopupComponent,'Font.Name') ;

FontDialog.Font.Size :=

GetProperty(PopupComponent,'Font.Size') ;

FontDialog.Font.Color :=

GetProperty(PopupComponent,'Font.Color') ;

FontDialog.Font.Style :=

TFontStyles(TFontStyle(

GetProperty(PopupComponent,'Font.Style')));

If FontDialog.Execute Then Begin

SetProperty(PopupComponent,'Font.Name'

,FontDialog.Font.Name);

SetProperty(PopupComponent,'Font.Size',

FontDialog.Font.Size);

SetProperty(PopupComponent,'Font.Color',

FontDialog.Font.Color);

SetProperty(PopupComponent,'Font.Style',

Ord(TFontStyle(FontDialog.Font.Style)));

// Force Font Height to refresh

// the component

i:= GetProperty(PopupComponent,

'Font.Height');

SetProperty(PopupComponent,

'Font.Height',i+(-5));

SetProperty(PopupComponent,

'Font.Height',i);

End;

End;

End Else If (TMenuItem(Sender) =

pm_BgColor) Then Begin

// Process Color

If ColorDialog <> Nil Then Begin

ColorDialog.Color :=

GetProperty(PopupComponent,'Color') ;

If ColorDialog.Execute Then Begin

SetProperty(PopupComponent,'Color',

ColorDialog.Color) ;

End;

End;

End Else If (TMenuItem(Sender) =

pm_Caption) Then Begin

// Process Caption

sString := GetProperty(PopupComponent,'Caption');

sString := InputBox('Change Caption',

'Enter Caption',sString);

SetProperty(PopupComponent,'Caption',sString) ;

End Else If (TMenuItem(Sender) =

pm_columns) Then Begin

// Process Columns for TListView. This is done

// with an external form.

Application.CreateForm(TfrmEditColumns,

frmEditColumns);

Try

frmEditColumns.ColObject :=

TListView(PopupComponent) ;

frmEditColumns.ShowModal ;

Finally

frmEditColumns.Free ;

End;

End Else If (TMenuItem(Sender)=pm_Width) Then Begin

// Process Width for TBevel

sString := IntToStr(GetProperty(PopupComponent,

'Width'));

Try

sString := InputBox('Set Width',

'Enter Width',sString);

iInt := StrToInt(sString);

SetProperty(PopupComponent,'Width',iInt) ;

Except

Raise Exception.Create('Width Must Be '+

'An Integer');

End;

End Else If (TMenuItem(Sender)=pm_Height) Then Begin

// Process Height for TBevel

sString := IntToStr(GetProperty(PopupComponent,

'Height'));

Try

sString := InputBox('Set Height',

'Enter Height',sString);

iInt := StrToInt(sString);

SetProperty(PopupComponent,'Height',iInt) ;

Except

Raise Exception.Create('Height Must Be '+

'An Integer');

End;

End Else If (TMenuItem(Sender)=pm_Style) Then Begin

// Process Bevel Stype for TBevel

// Raised or Lowered

If GetProperty(PopupComponent,'Style')=0 Then

SetProperty(PopupComponent,'Style',1)

Else

SetProperty(PopupComponent,'Style',0)

End Else Begin

TMenuItem(Sender).Checked :=

Not TMenuItem(Sender).Checked;

If pm_MixedCase.Checked Then Begin

// Normal

SetProperty(PopupComponent,'CharCase',0) ;

End Else If pm_UpperCase.Checked Then Begin

//CharCase := ecUpperCase ;

SetProperty(PopupComponent,'CharCase',1) ;

End Else If pm_LowerCase.Checked Then Begin

//CharCase := ecLowerCase ;

SetProperty(PopupComponent,'CharCase',2) ;

End Else If pm_ProperCase.Checked Then Begin

//CharCase := ecProperCase ;

SetProperty(PopupComponent,'CharCase',3) ;

End;

End;

End;

TChangeMenu can actually be used on almost any standard component that has any of the supported properties. It even supports TListView column headings and widths.

Half-way there

Creating a component that allows the user to change its size, position, font, color, and so forth is good. But it isn't very helpful without the ability so save and restore the changed properties. You don't want your users to have to customize the application again and again every time they run your application, do you? The next step is to create a component that has the ability to save and restore the classes and properties that the developer specifies.

TComponentStates

When designing the component to save the component property values, I wanted to make it flexible enough to save only the values that the developer wanted. The values needed to be stored in a place that would be easily accessible and transportable. I also wanted the values to be able to be accessed by more than one user or workstation. For these reasons, I chose to store the property values in an INI file instead of the Windows Registry.

TComponentStates works in much the same way TChangeMenu does by accessing the routines in PropFunc.pas to Save and Restore component properties at runtime. There are three exposed methods in this component: Save, Restore, and SetProperties. I think the first two are self-explanatory. The SetProperties method is used to set a specific property of all objects on the form that are of the same class type. For example, if I wanted to change the font of all the TDBEdit objects on Form1 to Arial 12, the code would look like this:

ComponentStates1.SetProperties(Form1,'TDBEdit',

'Font.Name','Arial');

ComponentStates1.SetProperties(Form1,'TDBEdit',

'Font.Size',12);

The routine to accomplish this is actually not too complicated. It uses the SetProperty method in PropFunc.pas, as discussed earlier. It spins through all the components on the form and checks the classname. If the classname is a match, it executes the SetProperty() method, changing the component's property (see Listing 12).

Listing 12. The SetProperties method.

Procedure TComponentStates.SetProperties(

Const Frm:TForm

;Const sClassName:String

;Const sPropertyName:String

;Const vValue:Variant);

//****************************************************

//Sets all components properties on the Form Frm where

//component.classname = sClassName with vValue.

//****************************************************

Var i : Integer ;

Begin

With Frm Do Begin

For i := 0 To (ComponentCount-1) Do Begin

If Components[i].ClassNameIs(sClassName) Then

Begin

SetProperty(Components[i],sPropertyName,

vValue);

End;

End;

End; //with Frm

End;

The most essential property in this component is ClassesToSave. It's a TStrings type that contains a list of classes and properties to save to the INI file. For example, to save all the essential information about each TPSIDBEdit that's on the form, the contents of ClassesToSave would look like this:

TPSIDBEdit.Left

TPSIDBEdit.Top

TPSIDBEdit.Height

TPSIDBEdit.Width

TPSIDBEdit.TabOrder

TPSIDBEdit.Font.Name

TPSIDBEdit.Font.Size

TPSIDBEdit.Font.Style

TPSIDBEdit.Font.Color

TPSIDBEdit.Color

TPSIDBEdit.Ctl3D

TPSIDBEdit.BorderStyle

TPSIDBEdit.CharCase

When the Save method is executed, the INI file would look something like this:

[Form1]

PSIDBEdit1.Left=313

PSIDBEdit1.Top=319

PSIDBEdit1.Height=21

PSIDBEdit1.Width=121

PSIDBEdit1.TabOrder=5

PSIDBEdit1.Font.Name=MS Sans Serif

PSIDBEdit1.Font.Size=8

PSIDBEdit1.Font.Style=0

PSIDBEdit1.Font.Color=-2147483640

PSIDBEdit1.Color=-2147483643

PSIDBEdit1.Ctl3D=1

PSIDBEdit1.BorderStyle=1

PSIDBEdit1.CharCase=0

The Save and Restore methods use the same procedure called Process (see Listing 13). The Boolean parameter bSave is the switch for saving or restoring the values.

Listing 13. The Process method.

Procedure TComponentStates.Process(bSave:Boolean);

Var

i,ii : Integer ;

sPropertyName : String ;

sSection : String ;

sClassName : String ;

sID : String ;

vValue : Variant ;

slPropList : TStrings ;

Begin

If Not Active Then Exit ;

slPropList := TStringList.Create ;

fIniFile := TIniFile.Create(FIniPath+

SaveToINIFileName);

Try

// Use the form's name for the section ID in

// the INI file.

sSection := fOwner.Name ;

For i := 0 To (fOwner.ComponentCount-1) Do Begin

slPropList.Clear ;

sClassName := fOwner.Components[i].ClassName ;

// Get Property List For Class

slPropList.Text :=

ProcessClass(fOwner.Components[i]) ;

If (slPropList.Text <> '') Then Begin

//*********************************************

//Process all defined properties for this Class

//*********************************************

For ii := 0 To (slPropList.Count-1) Do Begin

sPropertyName := slPropList.Strings[ii] ;

sID := fOwner.Components[i].Name+'.'+

sPropertyName ;

If bSave Then Begin

// Write Property Values to INI file

vValue := GetProperty(fOwner.Components[i],

sPropertyName);

If (vValue <> Null) Then Begin

Case VarType(vValue) Of

varInteger:

fIniFile.WriteInteger(sSection,sID,

vValue);

varString:

fIniFile.WriteString(sSection,

sID,vValue);

End; // Case VarType(vValue)

End;

End Else Begin

// ***********************************

// Read property from INI file and set

// component's property.

// ***********************************

// Get Current Property Value as a default

vValue := GetProperty(fOwner.Components[i],

sPropertyName);

If (vValue <> Null) Then Begin

// Read property value from INI file

Case VarType(vValue) Of

varInteger:

Begin

vValue :=

fIniFile.ReadInteger(sSection,

sID,

vValue);

End;

varString:

Begin

vValue :=

fIniFile.ReadString(sSection,

sID,

vValue);

End;

Else

vValue := Null ;

End; // Case VarType(vValue)

If (vValue <> Null) Then Begin

SetProperty(fOwner.Components[i],

sPropertyName,vValue);

End;

End;

End; // If bSave

End; // For ii

End; // If (slPropList.Text <> '')

End; // For i

Finally

slPropList.Free ;

fIniFile.Free ;

End;

End;

A single INI file can be used to save the component values for every form in the application. It uses the Form's name property for the INI file's section ID and places each component's values for the form under the ID.

Putting it all together

The final step is actually using these components together. I thought building a simple address book would be easiest.

Starting with the DataModule, I added a ColorDialog, FontDialog, ComponentStates, ChangeMenu, Table, and DataSource (see Figure 2). Then I filled in the FontDialog and ColorDialog properties of the ChangeMenu to point to the Color and Font dialogs that are on the DataModule. I then pointed the Table to ADDRESS.DBF, and the DataSource to the Table. I also changed the ComponentStates.Active property to True.

The next step is to lay out the form using the TPSIDBEdit and point them to their respective fields, and populate the ChangeMenu property to point to the TChangeMenu that's on the DataModule. I also added a couple of buttons that globally change the background and font of all the TPSIDBEdit's on the form, and a CheckBox that changes the component's AllowUserChange property to True or False. I used another subclassed label component, TLabel3D, that has some of the same changeable features found in TPSIDBEdit, in order to show the flexibility of these components. Label3D.pas is included in the accompanying Download file. Figure 3 shows the complete Address Book form.

Next I populated the ClassesToSave property (see Figure 4) of the ComponentStates component with all the classes and properties I wanted saved as part of this application.

In the OnShow event of the form, I placed the code that will restore the saved values from the INI file. The Restore() and Save() methods take one parameter and is the TForm that is to be processed. In this I case, I used the Self pointer to make it generic.

procedure TForm1.FormShow(Sender: TObject);

begin

DataModule1.ComponentStates1.Restore(Self);

end;

The Save() method can be implemented a couple of different ways. You can place the Save code in the OnClose event of the Form, or you can give the user a button or menu item to use to save the GUI changes. In this example, I used the OnClose event of the form. But in a live application, I'd probably use a Button or Menu item for the save.

procedure TForm1.FormClose(Sender: Tobject

;var Action: TCloseAction);

begin

DataModule1.ComponentStates1.Save(Self);

end;

To change the AllowUserChange property for all the components, I used the SetProperties method in the OnClick event of the CheckBox. I pass in the TForm using Self, the classname and the property I want changed, and the value to change it to.

procedure TForm1.cbAllowUserChangeClick(Sender:

TObject);

begin

// Globally set the property of AllowUserChange on

// all Components that are of type TPSIDBEdit

// and TLabel3D

With DataModule1.ComponentStates1 Do Begin

SetProperties(Self,'TPSIDBEdit','AllowUserChange',

cbAllowUserChange.Checked);

SetProperties(Self,'TLabel3D','AllowUserChange',

cbAllowUserChange.Checked);

End;

end;

To globally change the Font and Color of all the TPSIDBEdit objects on the form, I again used the SetProperties() method of the TComponentStates object:

procedure TForm1.btnGlobalBGClick(Sender: TObject);

begin

// Globally set the Color property of

// all Components that are of type TPSIDBEdit.

With DataModule1 Do Begin

If ColorDialog1.Execute Then Begin

ComponentStates1.SetProperties(

Self,'TPSIDBEdit','Color',

ColorDialog1.Color);

End;

End;

end;

procedure TForm1.btnGlobalFGClick(Sender: TObject);

begin

// Globally set the Font.Color property of

// all Components that are of type TPSIDBEdit.

With DataModule1 Do Begin

If ColorDialog1.Execute Then Begin

ComponentStates1.SetProperties(

Self,'TPSIDBEdit','Font.Color',

ColorDialog1.Color);

End;

End;

end;

While not every application you build will require the user to change the GUI, think how nice it would be if you didn't have to hard-code the changes, recompile, and hand your client a new application only to hear, "Oh, there's just one more thing . . ."