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` `endWith` `Function Init` `if file('ConfigTx.dbf')` `// Look up Form.ClassName+'.'+this.name, 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'` `endif` `*` `// 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 = this.name` `Mtemp1 = this.parent` `do while type("Mtemp1.parent") # 'U'` `Mobject = Mtemp1.name+"."+Mobject` `Mtemp1 = Mtemp1.parent` `enddo` `*` `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` `endif` `endif // if file('ConfigTx.dbf')` `*` `return` `endclass`

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 form.open() 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` `endwith` `this.FirstTime = true` `Proc Open` `class::BeforeOpen()` `return super::open()` `Proc ReadModal` `if class::BeforeOpen()` `return super::ReadModal()` `endif` `return` `Func BeforeOpen` `form.Init()` `if form.FirstTime` `CLASS::RunControlInits(Form) // Only runs the first time the form is opened.` `endif` `form.AfterControlInits()` `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.` `return` `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.` `return` `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` `do` `if Type('o.Init') $ 'FP/CB' // If this control object has an Init method...` `o.Init()` `endif` `// 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` `EndIf` `o = o.before` `until o.name == InitialObject.First.name` `return` `endclass`

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 `bu07rorl.zip`):

> 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 `bu07rorl.zip` (minus `Demo.wfm`) in the project.
2. `set procedure to zControl.cc 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)`

and:

| 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` `endclass`

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.` `endclass`

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 `bu07rorl.zip` into a new folder, change to that folder in dBASE, and run `Demo.wfm` to try it out.