Mining for Gold in the FFC
Doug Hennig
Sometimes you have to sift through
a lot of rocks before you find true nuggets of gold. Not so with the FoxPro
Foundation Classes that ship with VFP 6; there's tons of gold in them thar
hills.
One of the design goals for VFP 6 was to make it easier for programmers
new to VFP to get up and running with the tool. The Application Wizard is
one of the results of this goal, and the FoxPro Foundation Classes -- or
FFC -- is another. The FFC, located in the FFC subdirectory of the VFP home
directory, is a collection of class libraries that provide a wide range
of functions. Don't think that just because previous versions of FoxPro
have included some, shall we say, less useful (to be polite) example files
that these fall into that category. While some of these do appear to be
more demoware than really useful, there are still lots of great classes
in here. It's well worth the effort to spend some time looking at these
classes, picking through the rocks to find the nuggets.
The best way to check out the FFC is using a new VFP 6 tool called the Component
Gallery, accessible from the VFP Tools menu. I won't discuss the features
of the Component Gallery in this article; it's simple enough to use that
you can navigate your way around just by playing with it. The FFC classes
are displayed in the Foundation Classes folder of the Visual FoxPro Catalog
(when I refer to where classes can be found later in this article, I won't
specify this folder or this catalog, just the subfolder under Foundation
Classes). Classes are grouped by type (for example, Buttons, Dialogs, and
Utilities) and display a description in the status panel when you select
them. Even better, right-clicking on a class displays a context menu giving
you access to the Help topic and sample files (either to run or to view)
for that class. This makes it very easy to look through the FFC and see
which classes might interest you.
This article will look at some of the FFC classes and see how we might use
them or even subclass them to make them even more useful. Before we get
started, though, I'll discuss _BASE.VCX.
Base classes
VFP 6 includes a set of subclasses of VFP
base classes in _BASE.VCX. Although these classes aren't located in the
Foundation Classes folder in the Component Gallery (they're in the My Base
Classes folder), this VCX is located in the FFC subdirectory, and all FFC
classes are subclassed from _BASE classes. If we use FFC classes in our
applications, _BASE.VCX comes along for the ride. So, a question arises:
Although this column has been developing a robust set of base classes, and
many of you have your own set, should we consider using _BASE classes for
our base classes instead? The reason for even considering this is, why have
two VCXs in our projects that provide essentially the same thing?
After looking at the _BASE classes, the conclusion I've come to is no; I'll
continue using my own SFCTRLS.VCX classes. _BASE classes don't have the
visual changes I've made to my base classes -- for example, the AutoSize
property for classes such as _CheckBox and _Label is set at the default
.F.; I almost always want it set to .T., so that's what I've done in SFCheckBox,
SFLabel, and other classes with this property. Furthermore, _BASE classes
also don't have the behavior I want -- for example, their Error methods
pass the error on to an ON ERROR handler rather than using the Chain of
Responsibility design pattern (up the class hierarchy and then the containership
hierarchy) that my classes do, as I discussed in my January 1998 column
(see "Error Handling Revisited"). Finally, these classes have
a lot of custom properties and methods that support the Application Wizard.
Since I don't plan to use the Application Wizard to create applications,
these properties and methods would just complicate things. So, we'll just
have to live with the fact that _BASE.VCX will be included in our projects,
whether we want it or not, if we use FFC classes.
_SysToolbars
It's highly unlikely you'll want the VFP
development environment toolbars (such as the Standard and Database toolbars)
to be visible when an application is running. Unfortunately, there isn't
a single command that hides them all, so most developers create an array
of toolbar names, spin through the array and see whether the current toolbar
is visible, and, if it is, hide it. Of course, you have to do just the opposite
when the application closes down, at least in a development environment.
Because this is a common task, the FFC includes a class called _SysToolbars;
this class is located in _APP.VCX and appears as "System Toolbars"
in the Application subfolder of the Component Gallery. This class is very
simple; it hides and restores the system toolbars either automatically or
manually. To use it automatically, either pass .T. to its Init method when
you instantiate it programmatically or set its lAutomatic property to .T.
in the Property Sheet when you drop it on a form. In this case, any visible
system toolbars are hidden when the object is instantiated and redisplayed
when it's destroyed. To use it manually, call its HideSystemToolbars and
ShowSystemToolbars methods. You'll likely want to instantiate it as soon
as possible at application startup using code similar to this:
oSysToolbars = newobject('_SysToolbars', '_app.vcx', ;
'', .T.)
|
As long as oSysToolbars stays in scope, the toolbars stay hidden. You can
either manually release this object or let it go out of scope when the application
terminates.
_Resizable
This class, which is defined in _CONTROLS.VCX
and appears as "Resize Object" in the User Controls subfolder
of the Component Gallery, moves and resizes all the controls in a form when
the form is resized. It takes care of all the mundane details of drilling
down through containers (such as PageFrames), adjusting the Top, Left, Height,
and Width properties of controls. I've done this kind of thing manually
before in the Resize method of the form, writing reams of code to handle
all the applicable controls. Believe me, anything that can automate this
tedious chore is very welcome.
To see how _Resizable works, run the ResizeDemo form (by the way, this form
also contains an _SysToolbars object with lAutomatic set to .T. so you can
see how that class works). Leave "Resize" unchecked and resize
the form. See how dumb it looks as more background appears when you make
the form larger or controls get hidden as you make the form smaller? Not
the sign of a professional application. Now check "Resize" (but
leave "SFResizable" unchecked for now); when you resize the form,
the edit box is resized, and the other controls are moved automatically.
However, there are a couple of problems with this class. First, it resizes
or moves all the controls. Typically, when you resize a form, you want to
resize certain controls (such as edit boxes) and move the ones below and
to the right of them to account for the new size; the rest you want to leave
alone. Another problem is that it moves everything proportional to the original
size of the form; the effect is that a resized form looks like a blown up
or shrunk version of the original, with controls further apart or closer
together. The result is sort of like what happens to writing on a balloon
as it's blown up. In my experience, what you really want is a form that
looks just like it was but with some of the controls larger or smaller and
the rest just moved relative to the resized controls. After all, the whole
reason for the user to resize a form is to be able to see more of those
controls they'd normally have to scroll (grids, list boxes, edit boxes,
TreeView and ListView controls, and so forth), not to get a bigger form
with more background.
The good news is that we don't need to throw _Resizable out and create a
new class with the behavior we want. _Resizable has all of the logic we
need; it just doesn't do things quite right. So, let's subclass it and make
the subclass act the way we expect.
The subclass I created is called SFResizable, and it's contained in SFFFC.VCX
in the accompanying download
file. I changed both the Height and Width properties
to 17 so the control doesn't take up as much space when it's dropped on
a form. Since we need to treat some controls differently than others (some
will be moved, while others might be resized), we need a way to indicate
how each control should be treated. I originally considered adding new properties
to my base classes (for example, lResize, which, if .T., would indicate
that this control should be resized) but rejected that because then SFResizable
would only work with a certain set of base classes, and that would make
it far less reusable. Instead, I decided to make SFResizable self-contained,
so I created the following new properties:
-
cRepositionLeftList: A comma-delimited list of
the names of controls that should be moved left-right only as the form
is resized.
-
cRepositionList: A comma-delimited list of the
names of controls that should be moved left-right and up-down as the form
is resized.
-
cRepositionTopList: A comma-delimited list of
the names of controls that should be moved up-down only as the form is
resized.
-
cResizeList: A comma-delimited list of the names
of controls that should be resized as the form is resized.
I also overrode the AddToArray and SetSize methods. AddToArray adds size
and position information about a control to an array property of the class;
it's called from the LoopThroughControls method, which processes all of
the controls in the form. The problem with the original AddToArray is that
it stores the size and position information of the control as values proportional
to the size and position of the form rather than the actual values for the
control. So, I simply used the Visual Basic method of subclassing (I copied
the code from _Resizable.AddToArray and pasted it into SFResizable's method)
and then modified the code to store the original values. SetSize is also
called from LoopThroughControls; it adjusts the size and position of a control
by the difference between the original (stored in the InitialFormHeight
and InitialFormWidth properties) and current sizes of the form. Since I
don't want every control resized and moved, I changed the code to use the
following logic:
-
If the control's name is in the cRepositionList
or cRepositionTopList properties, its Top value is adjusted.
-
If the control's name is in the cRepositionList
or cRepositionLeftList properties, its Left value is adjusted.
-
If the control's name is in the cResizeList property,
its Width and, for certain types of controls (Label, Editbox, Listbox,
Grid, PageFrame, Line, OLEControl, OLEBoundControl, Shape, and Container),
Height values are adjusted.
To use SFResizable, drop it on a form and call its AdjustControls method
in the Resize method of the form. Enter the names of those controls that
should be resized as the form is resized into the cResizeList property of
the SFResizable object; grids, edit boxes, and other controls that can scroll
are obvious candidates. Enter the names of those controls that should be
moved up-down and left-right as the form is resized into the cRepositionList
property. For example, controls below and to the right of an edit box might
qualify for this adjustment. For those that should only be moved up and
down, enter their names into the cRepositionTop property. Examples include
controls below a grid, because these controls need to move up or down as
the grid's Height is changed. Finally, enter the names of controls that
should be moved left and right, such as those to the right of an edit box,
as the form is resized into the cRepositionLeft property.
Run the ResizeDemo form again, but this time check both "Resize"
and "SFResizable". When you resize the form, the text box beside
"Label 1" doesn't move; the edit box and shape surrounding it
don't move but are resized; the "Resize" and "SFResizable"
check boxes and the check boxes beside the edit box move left and right
but not up and down; the text box beside "Label 2" moves up and
down but not left and right; and the "Reset" button moves both
up-down and left-right. In other words, the form behaves as we'd expect
when it's resized.
A perfect addition to SFResizable would be a builder to make it easy to
specify which controls should be moved or resized, with perhaps a list of
the controls in the form and check boxes indicating how the selected control
should be treated.
Registry
As you know, the Registry is the "in"
place to store configuration, preference, and other application settings;
INI files are officially passe (although I know lots of developers who,
like me, would rather lead a user through editing an INI file over the phone
than dare have them use a tool like REGEDIT). Settings should normally be
stored in keys with a path similar to HKEY_CURRENT_USER\Software\My Company
Name\My Application\Version 1.1\Options.
The FFC Registry class (defined in REGISTRY.VCX and appearing as "Registry
Access" in the Utilities subfolder of the Component Gallery) is a wrapper
class for the Windows API calls that deal with the Registry. Rather than
having to know that you must open a key using the RegOpenKey function before
you can use RegQueryValueEx to read the key's value, all you have to know
with the Registry class is that you call its GetRegKey method. Here's an
example that gets the name of the VFP resource file:
#include REGISTRY.H && located in HOME() + 'FFC'
loRegistry = newobject('Registry', 'Registry.vcx')
lcResource = ''
loRegistry.GetRegKey('ResourceTo', @lcResource, ;
'Software\Microsoft\VisualFoxPro\6.0\Options', ;
HKEY_CURRENT_USER)
|
(Yeah, I know it's easier to use SET(`RESOURCE', 1), but this is just an
example.)
REGISTRY.VCX also contains subclasses of Registry that make it easier to
read and write VFP settings (FoxReg), ODBC settings (ODBCReg), applications
by file extension (FileReg), and even (horrors!) INI files (OldINIReg).
Registry-specific constants are defined in REGISTRY.H so you can specify
HKEY_CURRENT_USER (as I did in the preceding code) rather than its value
(-2147483647).
Here are the useful methods in REGISTRY.VCX. Unless otherwise specified,
all return a code indicating success (0, or the constant ERROR_SUCCESS)
or the WinAPI error code; see REGISTRY.H for error codes.
-
GetRegKey: Puts the value of the specified key
into a variable.
-
SetRegKey: Sets the value of the specified key
to the specified value (the entire key path is created if it doesn't exist).
-
DeleteKey: Deletes the specified key (and all
subkeys) from the Registry.
-
DeleteKeyValue: Removes the value from the specified
key.
-
EnumOptions: Populates an array with all the
settings under a specific key and their current values.
-
IsKey: Returns .T. if the specified key exists.
Registry can be instantiated at application startup (perhaps by an Application
object) to get the settings the application needs: location of the data
files on a LAN, names of the most recently used forms (so they can appear
at the bottom of the File menu like Microsoft applications do), and so forth.
It can also be dropped on a form to save and restore form-specific settings
such as the Left, Top, Height, and Width values (so it has the same size
and position as when this user closed it), grid column widths and positions
(so users can rearrange grids and have them appear that way the next time),
and so on.
Being a picky guy, I have two problems with the Registry class. First, because
the return value of GetRegKey is a success or error code, you have to pass
the variable you want the value placed into by reference. I'd prefer it
to return the value of the key; after all, if an error occurs, the WinAPI
error code is probably as useful to me as the "details" section
of a GPF dialog, and even if I do want it, it could easily be stored in
a property of Registry I can query. Second, it's likely that both the key
path ("Software\Microsoft\VisualFoxPro\6.0\Options" in the previous
example) and the user key (usually HKEY_CURRENT_USER) are going to be the
same for every method call of a specific instance of a Registry object.
Being the lazy sort, I'd rather put these values into properties and not
pass them every time I call a method.
As with _Resizable, I've subclassed Registry into SFRegistry (also in SFFFC.VCX)
to have the behavior I want. Although I planned to, it turned out that I
didn't have to add properties for the default key path (cAppKeyPath) and
user key (nUserKey) -- they already existed, even though they're not used
by Registry (they're used by the subclasses in REGISTRY.VCX). Since Registry.Init
sets nUserKey to HKEY_CURRENT_USER, there normally isn't even a need to
change this property. I changed the GetRegKey, SetRegKey, DeleteKey, DeleteKeyValue,
EnumOptions, and IsKey methods to accept different parameters than the matching
Registry class methods (I rearranged the parameters so optional ones are
at the end) and return a more appropriate value (the key value in the case
of GetRegKey, the number of options in the array in the case of EnumOptions,
and .T. if the method succeeded in the case of SetRegKey, DeleteKey, and
DeleteKeyValue). These methods also store the WinAPI result code into a
new nResult property, which can be used to determine what went wrong if
a method fails. Here's an example; this is the code from EnumOptions:
lparameters taRegOptions, ;
tlEnumKeys, ;
tcKeyPath, ;
tnUserKey
local lcKeyPath, ;
lnUserKey, ;
lnReturn
* If the key path and user key weren't passed, use the
* defaults.
lcKeyPath = This.GetKeyPath(tcKeyPath)
lnUserKey = This.GetUserKey(tnUserKey)
* Use the parent class method to enumerate the key,
* store the result code, and return the number of
* options it found.
lnSuccess = dodefault(@taRegOptions, lcKeyPath, ;
lnUserKey, tlEnumKeys)
This.nResult = lnSuccess
lnReturn = iif(lnSuccess = ERROR_SUCCESS, ;
alen(taRegOptions, 1), 0)
return lnReturn
|
GetKeyPath and GetUserKey are new methods called by all the overridden methods
to either use the key path and user key values passed, or the defaults (stored
in the cAppPathKey and nUserKey properties) if they weren't passed.
Here's an example of the use of SFRegistry; this code would be used in the
Init method of a form to restore the size and position it had the last time
the user had it open. This code assumes that an SFRegistry object named
oRegistry was dropped on the form and its cAppPathKey property was set in
the Property Sheet to the key path for this application.
lcKey = This.oRegistry.cAppPathKey + '\' + This.Name
lcTop = This.oRegistry.GetRegKey('Top', lcKey)
This.Top = iif(isnull(lcTop), This.Top, val(lcTop))
* similar code for Left, Width, and Height
|
(Since the Registry class only supports reading and writing strings, VAL()
must be used on the return value. Also, if this is the first time this form
is run, a key might not exist for it in the Registry, in which case .NULL.
is returned, so this code handles that case.) A method of the form (such
as Release) would use This.oRegistry.SetRegKey to save the current form
size and position in the Registry.
For another example, run REGISTRYDEMO.PRG. It creates some new keys, displays
their values, and shows how EnumOptions works. (It also deletes the keys
it creates so it doesn't pollute your Registry permanently.)
_ObjectState
If you've been doing your homework on design
patterns, you're probably aware of a pattern known as Memento. A Memento
is intended to save the state of something, presumably so it can be restored
after it's been changed. The FFC includes a class called _ObjectState that's
sort of like a Memento: It saves the value of one or more properties of
another object and can later restore them to their former values.
_ObjectState is defined in _APP.VCX and appears as "Object State"
in the Application subfolder in the Component Gallery. It has an oObject
property that contains an object reference to the object whose state it
maintains; this property can be set by passing the object reference to the
Init method of the _ObjectState object or by setting it manually. It has
a Set method that sets the value of the specified property of the managed
object to the specified value, optionally first saving the original value
in an array property of itself. It also has a Restore method that restores
the value of one or all saved properties. The Restore method is also called
from the Destroy method, so when the _ObjectState object goes out of scope,
everything is restored automatically. Since the array property has a single
row for each saved property, you can't use this class to provide multiple
levels of undo; however, you could subclass _ObjectState and add this behavior
if desired.
Where might we use such a class? One situation involves things a user can
change but might want to change back. For example, you might allow a user
to rearrange and resize the columns in a grid, but provide a "Reset
to Default" function that puts them back. Another place would be when
you temporarily change the properties of an object (perhaps so it behaves
differently), do something with the object, and then change them back again.
Rather than having a set of local variables that save the property values
and then having to manually restore them before the routine ends, you could
do something like this:
local loObjectState
loObjectState = newobject('_ObjectState', '_app.vcx', ;
'', This)
loObjectState.Set('<first property', <new value>)
loObjectState.Set('<second property', <new value>)
* do something here
|
When this code ends, loObjectState goes out of scope, so the saved property
values are automatically restored.
As usual, I have one slight quibble with the class implementation: Saving
the current value of the property is optional. There really isn't a need
to use _ObjectState if you're not going to save the value (after all, you
can just store the new value to the property yourself in that case), so
I think the default behavior should be to save and have it optionally not
save. So, I created a subclass of _ObjectState called SFObjectState (in
SFFFC.VCX) that simply overrides the Set method, as follows:
lparameters tcProperty, ;
tuValue, ;
tlNoSave
return dodefault(tcProperty, tuValue, not tlNoSave)
|
In other words, rather than passing .T. to save, you have to pass .T. to
not save.
To see an example of this class in action, run the ResizeDemo form, check
both "Resize" and "SFResizable", then resize the form.
Now click on the Reset button and watch everything snap back to the original
size and position. This was done by saving the Height and Width properties
of the form in its Init method:
with This.oObjectState
.oObject = This
.Set('Height', This.Height)
.Set('Width', This.Width)
endwith
|
and restoring them in the Click method of the button:
Thisform.oObjectState.Restore()
|
Of course, we didn't have to save the original size and position of every
object on the form because changing the form Height and Width causes the
Resize method to fire, which moves everything for us. Also, although I didn't
specify individual properties to Restore (so it restored all saved values),
I could have restored them individually if I wanted that control.
Conclusion
The FFC contains a lot of useful classes.
Some of them are great as is, while others can be subclassed to add minor
improvements. Either way, I suggest you spend some time looking at these
classes and thinking about how you might use them. I'm sure you'll find
some gems in the pile.
Download sample code for this article here.
Doug Hennig is a partner with
Stonefield Systems Group Inc. in Regina, Saskatchewan, Canada. He is the
author of Stonefield's add-on tools for FoxPro developers, including Stonefield
Database Toolkit and Stonefield Query. He is also the author of The Visual
FoxPro Data Dictionary in Pinnacle Publishing's The Pros Talk Visual FoxPro
series. Doug has spoken at the 1997 and 1998 Microsoft FoxPro Developers
Conferences (DevCon), as well as user groups and regional conferences all
over North America. He is a Microsoft Most Valuable Professional (MVP).
75156.2326@compuserve.com, dhennig@stonefield.com.