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.How to use these classesFont 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.
To make all the text objects user-configurable
in any of your own applications, all you have to do is:
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:
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
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.
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.
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
endclass
ColorHighlight = 'N/RG+'
// yellow background when in focus.
endclass