Give Your Clients Control of the GUI

This article demonstrates how users can alter their GUI at runtime.

Delphi Developer February 1999
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.



Listing 4. The SetTextCase method.



Procedure TPSIDBEdit.SetTextCase(Const bCheckState:

                                 Boolean) ;

Var

   bPost : Boolean ;



   Function CanChange : Boolean ;

   Begin

      Try

         If Not bCheckState Then Begin

            Result := True ;

            Exit ;

         End;

         If (DataSource <> Nil) Then Begin

            If Not (DataSource.DataSet.State

                  In [dsEdit, dsInsert]) Then Begin

               If DataSource.DataSet.Active Then Begin

                  DataSource.DataSet.Edit ;

                  bPost := True ;

               End;

            End;

         End;

         Result := True ;

      Except

         Result := False ;

      End;

   End;



Begin

   // If the Text changes and the DataSet

   // is not in EditState, then an Exception

   // is generated. Make sure DataSet is

   // in EditState before changing Text.

   fIsChanging := True ;

   Try

      bPost := False ;

      If CanChange Then Begin

         Case CharCase Of

            ecNormal : {Do Nothing} ;

            ecUpperCase : Text := UpperCase(Text) ;

            ecLowerCase : Text := LowerCase(Text) ;

            ecProperCase: Text := ToProper(Text);

         End;



         If bPost Then

            DataSource.DataSet.Post ;

      End Else

         MessageDlg('Another user may be using this '+

                    'record.'+#13+#13+

                    'TextCase changes may not be '+

                    'visible for this record.' ,

                     mtWarning,[mbOK],0);

   Finally

      fIsChanging := False ;

   End;

End;





Changing the CharCase property executes the SetCharCase method (see Listing 5), which, in turn, executes the SetTextCase method.



Listing 5. The SetCharCase method.



Procedure TPSIDBEdit.SetCharCase(Value:TPSICharCase);

Begin

   If fCharCase <> Value Then Begin

      fCharCase := Value ;

      SetTextCase(True) ;

   End;

End;



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 . . ."


 

Share this article!

Follow us!

Find more helpful articles: