User-configurable text objects
by Peter Rorlick [dBVIPS], co-founder of  Montreal Business Software. He has been developing xbase solutions since 1981.
IMAGINE letting the user change the content and appearance of the text objects in your applications.  And imagine the user doing this without having access to the source code, and without any involvement from the developer.  Can it be done simply and elegantly?  Sure it can!

The basic idea here is to attach a special right-click mouse event to our text objects.  When the user right-clicks on a text object, we present a dialog, in which he/she can change various properties of the text object: font, color, position, and so on - even the text itself.  The user's preferences get saved to a table.  Henceforth, whenever the form opens, the user's preferences are read from the table and are applied to the text object.

The idea is not complicated.  All that remains is to implement it so that it performs smoothly and does not add to a developer's burden.  The answer, of course, is to use a custom class.  In fact more than one, as we shall see.

We'll begin by creating the table to store the users' preferences.

Structure for table ConfigTx.DBF

Field  Field Name   Type       Length  Dec   Index
    1  ObjName      CHARACTER     50           Y
    2  pText        CHARACTER    100           N
    3  pTop         NUMERIC        8    2      N
    4  pLeft        NUMERIC        8    2      N
    5  pWidth       NUMERIC        8    2      N
    6  pHeight      NUMERIC        8    2      N
    7  pColorNorm   CHARACTER     22           N
    8  pWrap        LOGICAL        1           N
    9  pFunction    CHARACTER     15           N
   10  pPicture     CHARACTER     15           N
   11  pBorder      LOGICAL        1           N
   12  pFontName    CHARACTER     25           N
   13  pFontSize    NUMERIC        2           N
   14  pFontBold    LOGICAL        1           N
   15  pFontItal    LOGICAL        1           N

Each record in this table will constitute the preferences for a single text object on a single form.

The purpose of the ObjName field is to identify which text object on which form each record applies to.  For example, “MYTESTFORM.MYTEXT1” would mean that the record applies to the MyText1 object in the MyTestForm class.

The rest of the fields (2 through 15) obviously correspond to the properties of the text object that we'll let the user mess around with.

This table and its MDX do not have to be deployed with your applications.  If it's absent, the table will be created automatically when the user right-clicks on any configurable text object.

The custom text class

It's time to define our custom Text class.

class zText(parentObj) of Text(parentObj) custom

   with( this )
     OnRightMouseDown = {; do configTx.wfm with true, this.form, this}
     text = 'ZText'
     wrap = false
     height = 0.8
     width = 20

   Function Init
      if file('ConfigTx.dbf')
        // Look up Form.ClassName+'.', and set properties if found.
        if type('_app.qConfigTx')=='U'
          // Create and activate a query on ConfigTx.dbf (once only):
          _app.qConfigTx = new Query()
          _app.qConfigTx.SQL = 'select * from ConfigTx'
          _app.qConfigTx.Active = true
          _app.qConfigTx.Rowset.IndexName = 'ObjName'
        // To get the full object name for objects that reside on
        // containers/notebooks, we parse the ownership tree
        // (Thanks to George Burt for this technique):
        local mObject
        private mTemp1
        Mobject  =
        Mtemp1 = this.parent
        do while type("Mtemp1.parent") # 'U'
          Mobject ="."+Mobject
          Mtemp1 = Mtemp1.parent
        if _app.qConfigTx.Rowset.FindKey(Form.ClassName+'.'+mObject)
          // We've found a set of user preferences for this text object.
          // Let's apply those preferences:
          this.Text        = Trim(_app.qConfigTx.Rowset.Fields['pText'].Value)
          this.Top         = _app.qConfigTx.Rowset.Fields['pTop'].Value
          this.Left        = _app.qConfigTx.Rowset.Fields['pLeft'].Value
          this.Width       = _app.qConfigTx.Rowset.Fields['pWidth'].Value
          this.Height      = _app.qConfigTx.Rowset.Fields['pHeight'].Value
          this.ColorNormal = Trim(_app.qConfigTx.Rowset.Fields['pColorNorm'].Value)
          this.Wrap        = _app.qConfigTx.Rowset.Fields['pWrap'].Value
          this.Function    = Trim(_app.qConfigTx.Rowset.Fields['pFunction'].Value)
          this.Picture     = Trim(_app.qConfigTx.Rowset.Fields['pPicture'].Value)
          this.FontName    = Trim(_app.qConfigTx.Rowset.Fields['pFontName'].Value)
          this.FontSize    = _app.qConfigTx.Rowset.Fields['pFontSize'].Value
          this.FontBold    = _app.qConfigTx.Rowset.Fields['pFontBold'].Value
          this.FontItalic  = _app.qConfigTx.Rowset.Fields['pFontItal'].Value
          this.Border      = _app.qConfigTx.Rowset.Fields['pBorder'].Value
          if _app.qConfigTx.Rowset.Fields['pBorder'].Value
            this.BorderStyle = 7   // Client style
      endif    // if file('ConfigTx.dbf')


What's the Init method?

It's like an onOpen event, but it's preferable for two reasons:

1. If we used an onOpen event, it would fire after the form becomes visible, so the user would first see the text object displayed without his preferences, then his preferences would be applied and the look of the object would change right before his eyes.  Uggg-ly!  Our Init “event” will “fire” before the form opens - which will be faster as well as cleaner-looking.  How will the Init fire?  We'll deal with that in a moment.

2. Developers might want to use the text object's onOpen event for other purposes - and in fact they might already have done so.  By staying clear of the onOpen event, we make it safer and easier for developers to apply this technology retroactively to applications that have already been developed.

Getting the Init method to fire automatically

We need to ensure that the text object's Init method will fire each time the form opens.  To accomplish that, we'll build a custom form class that employs a very useful trick: extending the behavior of the Open() and ReadModal() methods.  The added behavior is essentially to examine each object on the form, and fire its Init method if it has one.  Here's the complete source code for the custom form class:

// Pete's zForm (VdB7 base form) class
// Thanks to Romain and Bowen for their ideas.
//  Things happen in the following order, each time you call or form.readModal():
//   1. Form.init() method executes.
//   2. Init() methods of objects on the form execute, one by one.
//      (But only the first time the form is opened.)
//   3. Form.AfterControlInits() method executes.
//   4. The form becomes visible.
//   5. Form.OnOpen() method executes.
//   6. OnOPen() methods of objects on the form execute, one by one.
//  Bear in mind that the CONSTRUCTOR section of your form fires ONLY when you do
//    <something> = new WhateverForm()

class zForm of Form custom

  with (this)
     top = 3
     height = 15
     width = 50
     Left = 10
     text = 'zForm'
     AutoCenter = true
  this.FirstTime = true

  Proc Open
  return super::open()

  Proc ReadModal
    if class::BeforeOpen()
      return super::ReadModal()

  Func BeforeOpen
    if form.FirstTime
      CLASS::RunControlInits(Form)   // Only runs the first time the form is opened.
    form.FirstTime := false
  return true   // Allow the form to open

  Proc Init
    // Over-ride this procedure in forms that are inherited from this class,
    // to do any stuff that needs to happen when the form opens.
    // Use an OnOpen event handler only for the stuff that must be done AFTER
    // a form opens - i.e., stuff that requires a form window handle.

  Proc AfterControlInits
    // Over-ride this procedure in forms that are inherited from this class,
    // to do any stuff that needs to happen when the form opens but AFTER
    // the Init methods of its controls have been run.

  Proc RunControlInits(InitialObject)
    // Run the Init methods of any controls that have such methods.
    // (This is often faster than running OnOpen events, where the
    // form is already open.)
    // For example, your form's Init method could have something like this:
    //    Form.EntryField1.Init = CLASS::DoWhatever  // function pointer
    //    Form.EntryField2.Init = {; SomeClass::SomeMethod() ; CLASS::DoWhatever() }
    // In some cases, this can give you better performance than using the OnOpen
    // events of the controls. The reason is that when the Init methods are fired
    // the form is not yet open, so there are no GUI controls to refresh.
    private o
    o = InitialObject.First
      if Type('o.Init') $ 'FP/CB'   // If this control object has an Init method...
      // The next line tests whether o is a container or notebook (but not a
      // form, because forms have a UseTablePopUp property) that contains
      // other objects...
      If Type('o.First')=='O' and type('o.UseTablePopUp')=='U'
        CLASS::RunControlInits(o)   // recurse through the objects of the container
      o = o.before
    until ==


The preferences dialog

The dialog form that gets invoked when the user right-clicks on a text object is called ConfigTx.wfm.  I won't bother showing or explaining its source code in this article (it's fairly mundane), but here's what it looks like (the form entitled “Adjust properties of text object”):

The ConfigTx.wfm dialog also features a few very nice goodies (and these classes are included in

Color picker - an Entryfield class for specifying Foreground/Background colors, that includes an embedded wrench button to select the color combination from a nifty color-palette dialog.

Font picker - a Container class that includes controls for fontName, fontSize, fontBold, and fontItalic, plus an embedded wrench button to let the user specify all those things via a standard font selection dialog.

Zoom editor - an Editor class that includes a button to invoke a dialog that lets the user view or edit the text on a much larger surface (or even full-screen), with the ability to Save or Cancel changes.

FlashButton - a Pushbutton class that “lights up” as the mouse moves over it.

How to use these classes

To make all the text objects user-configurable in any of your own applications, all you have to do is:
    1.  Include the files in (minus Demo.wfm) in the project.
    2.  set procedure to additive at the start of your Main.prg or your Setup.prg.
    3.  Make sure that all your forms inherit from zForm.
    4.  Make sure that all your text objects inherit from zText.

Note: If you always use custom classes in your applications (as you should!), Steps 3 and 4 should require only two tiny changes to your library of custom classes:
    Change this:   Class MyText(oForm) of Text(oForm)
    to this:             Class MyText(oForm) of zText(oForm)
    Change this:  Class MyBaseForm of Form custom
    to this:            Class MyBaseForm of zForm custom

This is a good illustration of why it's so much better to always use custom classes throughout your applications - even if your custom class definitions are behaviorally identical to the native classes.  For example:

  class MyEntryfield(o) of Entryfield(o) custom

Why bother with this?  The answer is that sometime in the future, you'll be able to universally change the behavior of all the entryfields throughout your application(s), by making a small change to the custom class.  You'll just need to edit one CC file, instead of having to edit 300 WFMs.  For example, suppose your users wanted all the entryfields to have a yellow background when they get focus.  This is so easy - if you've been using custom classes all along:

  class MyEntryfield(o) of Entryfield(o) custom
    ColorHighlight = 'N/RG+'  // yellow background when in focus.

Just add one line, and rebuild and redeploy the EXE.  That's one of the benefits of OOP.

The classes discussed in this article are just small examples of the leverage you can get from good object-oriented programming.

Click here to download the complete source code and the Demo.wfm.  Unzip into a new folder, change to that folder in dBASE, and run Demo.wfm to try it out.