FoxProFoxPro Developer's Conference '94 |
Session
122
Session 123
Session 223
Using
GENSCRNX 2.0
Steven M. Black
Steven Black Consulting / UP!
GENSCRNX
GENSCRNX is a program that enhances FoxPros standard screen generator program. It was developed by Ken Levy at the Jet Propulsion Laboratory in Pasadena, California, and released into the public domain. GENSCRNX is available and supported on CompuServe in the FoxForum.
Please refer to the Windows help file named GENX.HLP, included on the conference diskette, for the latest on this topic.
1.1 Introduction to GENSCRNX
GENSCRNX is a screen generator shell that plays on the inputs and outputs of GenScrn.PRG (or whatever generator program you use). The screen meta data (a .DBF file with an .SCX extension) is interpreted by GenScrn.PRG and transformed into a source file and given an .SPR extension. GENSCRNX intercepts GenScrn.PRG inputs and outputs, and provides enhanced control over the generation process. GENSCRNX has its own commands and directives, which you can supplement with "GENSCRNX drivers" that you can use and create. .
The GENSCRNX process, just like the GENMENUX process, looks something like this:
Why GENSCRNX?
FoxPro is bundled with GenScrn.PRG, the source code that handles converting FoxPro screen builder meta data to the source code that produces the screen. It is thus possible to modify GenScrn.PRG to create screens with special features.
But if you modify GenScrn.PRG, you create a devilish maintenance problem for yourself. You must now migrate modifications into future versions of GenScrn.PRG as Microsoft releases them, and you probably should keep the original, unchanged version around for use on projects where you don't want the modified behaviors. Clearly, it would be better if you could avoid modifying GenScrn.PRG itself, and if you could have a mechanism to add new generator directives at will to FoxPro's native capabilities. GENSCRNX provides all this capability.
Big Advantages
Several advantages accrue from using GENSCRNX. Here are four major ones: first, GENSCRNX can enhance screen creation without modifying the native FoxPro GenScrn program. Second, GENSCRNX works with all versions of FoxPro 2.x, so version 2.0 applications can use many built-in GENSCRNX capabilities. Third, GENSCRNX contains a rich and expanding set of commands and directives to add features to your screens. Fourth, GENSCRNX permits you to store standard screen objects in a library table and incorporate these objects into your screen designs or merge their characteristics into selected objects.
How Its Used
The accompanying files on disk contain everything you need to get up and running with GENSCRNX. In short, you must initialize the m._GENSCRN memory variable to "<<PATH>>GENSCRNX.PRG" either by assigning it in your CONFIG.FP files, or by setting the m._GENSCRN FoxPro system memory variable.
The GENSCRNX system will automatically make the call to the GenScrn.PRG located in the FoxPro startup directory. To specify a different screen generation program for GENSCRNX to call, you must set a variable named _GENSCRNX in CONFIG.FP or by making a variable assignment as if m._GENSCRNX were a system memory variable, for instance: m._GENSCRNX = "<<PATH>><<MyGenerator>>".
Side Effects
This is not a noticeable side-effect, but GENSCRNX automatically creates two null invisible buttons at row,col 0,0 as the first and last GET in the Screen Layout. Each screen of a screen set will have two null invisible buttons with the name corresponding to the screen set. Since the invisible button's WHEN is set to .F., the objects are null objects and have no effect on the generated screen. The purpose of this is to allow generic reference to the first or last GET object in any screen of a screen set. For example, in a screen set with one screen, the first GET would be m.scnobj1 and the last GET would be m.scnend1.
Example: _CUROBJ=OBJNUM('m.scnobj1')
Example: the first GET of the first screen of a screen set would have an invisible button at 0,0 called m.scnobj1 while the second screen of a screen set would have an invisible button called m.scnobj2.
*:SCNOBJ specified in the Setup snippet enables the invisible button m.scnobjn above to be generated. Although it is generated by default, *:SCNOBJ can be used to override a SCNOBJ=OFF setting in the CONFIG.FP files. *:NOSCNOBJ specified in the Setup snippet disables the invisible button m.scnobjn above to be generated. *:NOSCNOBJ can be used to override a SCNOBJ=ON setting in the CONFIG.FP files. *:NOSCNOBJ is automatic when either the #NOREAD directive exists in the Setup snippet or no GET objects exist for the screen.
1.2 GENSCRNX Command Reference
To supplement this chapter, I recommend you print GENSCRNX.TXT (or GENSCRNX.WRI if you have a laser printer) and VERSION.TXT, which will total about 15 pages.
You can control the behavior of GENSCRNX in five ways: with configuration commands, memory variables, setup snippet commands, comment snippet commands, and with commands in the other screen snippets. Each method is examined below.
GENSCRNX CONFIG.FP commands
Many GENSCRNX features can be invoked globally by placing statements in your CONFIG files. Table 1 shows a summary of CONFIG.FP commands. All CONFIG.FP directives are case insensitive, and it doesnt matter if you wrap arguments in quotes or not.
Unlike the FoxPro CONFIG.FP commands, GENSCRNX CONFIG.FP statements are evaluated at generate-time. You can change GENSCRNX commands in CONFIG.FP without having to exit and re-start FoxPro. A helpful rule of thumb for CONFIG.FP command syntax: all file specification commands begin with the underscore character.
Table 1.1 GENSCRNX CONFIG.FP command reference
GENSCRNX Generate-time Memory Variables
Unlike GenScrn, GENSCRNX recognizes generate-time memory variables. These can be used to override features set in CONFIG.FP and to invoke others. Table 2 lists the memory variables you can set to control GENSCRNX or override CONFIG.FP GENSCRNX settings. Of course, you may use any generate-time memory variables to control your drivers.
Table 1.2 GENSCRNX generate-time memory variables Reference
GENSCRNX Setup Snippet Commands
Setup snippet commands are used to set GENSCRNX behavior for the current screen set. These are listed in Table 3.
Table 1.3 GENSCRNX setup snippet command reference
GENSCRNX Comment Snippet Commands
Comment snippet commands are used to control GENSCRNX processes on individual objects. These are listed in Table 4.
Be aware that, for speed optimization reasons, comment and procedure snippets are not automatically processed by GENSCRNX. To guarantee your comment and procedure snippets are processed by GENSCRNX, put the characters "*:" on any line in the screens Setup snippet (if it doesnt already contain them). If GENSCRNX doesnt find anything to do in the Setup snippet, it assumes the entire screen is without GENSCRNX features, and moves on immediately to executing GenScrn.PRG.
Table 1.4 GENSCRNX procedure snippet command reference
GENSCRNX Procedure Snippet Commands and Directives
Procedure snippet commands and directives work in any snippet. Table 5 lists these. Be aware that if GENSCRNX doesnt find the characters "*:" or "{{" in the screens setup snippet, the other procedures are not searched for these commands.
Table 1.5 GENSCRNX procedure snippet command reference
1.3 Understanding GENSCRNX Libraries
Among GENSCRNXs many capabilities, perhaps none is more useful than library objects. Here we will look at how to create, use, and maintain GENSCRNX library objects.
What is a GENSCRNX Library?
Recall that FoxPros screen files, those with .SCX and .SCT extensions, are really just standard FoxPro tables with different extensions. The .SCX file is a .DBF, and the .SCT is a standard FoxPro memo, just like any other .FPT file. All information about a screen its screen objects is conveniently stored this way by FoxPro's screen builder.
GENSCRNX exploits FoxPros open architecture, and allows you to create a library of reusable screen objects. This library file is called FOXSCX.DBF and is, by default, created and kept in the FoxPro startup directory. You can control the name and location of the screen object library with the _FOXSCX=<path\filename> CONFIG.FP directive. I recommend that you map the FOXSCX table to the same file for all platforms.
So a GENSCRNX library is a FoxPro table containing reusable screen objects. A library object is a record in the GENSCRNX library table. The library is built and maintained through the interface of a dummy FoxPro screen (a library screen) into which you can paste your re-usable screen objects.
A Note on Terminology
A Library Object is a reusable object kept in FOXSCX.DBF. A Library Screen is a screen with a *:DEFLIB statement in its setup snippet, which triggers the creation and subsequent maintenance of the Library Objects contained therein. A Working Screen is any screen in your application; it contains Working Objects.
Creating GENSCRNX Libraries
Creating GENSCRNX libraries is easy! Just follow these steps.
1. Make sure your FoxPro m._GENSCRN variable is set to GENSCRNX.
2. Create a new screen, call it, say, MY_LIB.SCX. Note that you can have any number of library screens.
3. Put a GENSCRNX *:DEFLIB statement in the setup snippet, like this:
*:DEFLIB My_LibName
The *:DEFLIB identifies the screen as a GENSCRNX library, and triggers special processing when the screen is generated with the GENSCRNX shell. "My_LibName" is any name you choose. I typically chose a name that describes the type of objects contained in this screen, like "Controls", or "Lookups", or maybe "Dates_With_Valids". GENSCRNX respects the first 10 characters of a library name.
4. Copy and Paste any reusable screen objects into the library screen. For each object copied to the library screen, place a *:DEFOBJ statement in the comment snippet, like this:
*:DEFOBJ <Object Name>.
This object name is the alias that you will use in your working objects to refer to this library object. The GENSCRNX sample files contain many useful objects you can use to start your own libraries. Up to 24 characters are allowed for an object name.
5. From the PROGRAM menu, chose GENERATE. Unlike working screens, generating a library screen (a screen containing a *:DEFLIB statement) doesnt create an .SPR file. Instead, GENSCRNX creates or refreshes the screens library objects in FOXSCX.DBF. Note that, no matter which library name you use in the *:DEFLIB statement, all library objects will go into the FOXSCX table. You can therefore create many different libraries, but all library objects will be stored in the same FOXSCX library table.
Library Referencing
I suppose the word "inheritance" could be used as a synonym for library referencing. Inheritance, however, has connotations in computer science that make it an inappropriate term to use here. I'll use referencing instead, which means the copy and pasting of library object properties into your working screens.
To reference library objects, place a either a *:BASLIB or an *:INCLIB statement in the setup of your working screen, like this:
*:BASLIB My_LibName
This causes GENSCRNX to search the My_LibName library for object library references. We'll discuss the difference between *:BASLIB and *:INCLIB in a moment. You may have mutiple *:BASLIB and *:INCLIB statements if your screen refers to objects from several different libraries.
Referencing Mechanics
Its helpful to remember the following points about how GENSCRNX swaps library information into your working screen:
· The GENSCRNX *:BASLIB and *:INCLIB setup directives set the sequencing of libraries for searching in the FOXSCX table. Only the libraries specified are searched.
· As a general rule, if a field in your working screen object is empty, and the library field is not, referencing (inheritance) will occur for that field.
· For snippet expressions, the library expression is ANDed and if the snippet is a procedure, the library procedure is appended. This is reversed if the library object contains a directive called *:BASBEFORE. A working object will receive library-object colors if the colors of the working screen are set to default (or automatic).
· A working object will receive the library object font information if the working objects font is set to MS Sans Serif, 8, N.
· A working object will reference the library object Picture clause if its picture clause is either empty or set to @K (which is default in Windows). If the library object contains *:SAVEPICT, the referencing will overwrite the working objects picture clause. If the library object contains *:SAVESIZE, the referencing will overwrite the working objects height and width.
· Fields that are numeric or logical are referenced if the working objects value is empty (0 or .F.) and the library objects field is not empty. For example, if RANGEHI was 0 in the surface object and 3 in the library object, the 3 would be inherited. If the DISABLED check box was not set in the surface object but was checked in the library object, the DISABLED setting would be inherited. Once a field (numeric or logical) contains a non-empty value, it will no longer inherit from any referencing operations (such as the library object referencing another library object) or when an object uses multiple referencing (multiple *:BASOBJ directives in one Comment snippet).
Explicit Referencing
You can explicitly append all the library object snippets to your working object by placing a *:BASOBJ statement in the comment snippet of your working object. For example,
*:BASOBJ <ObjName>
will automatically assign the snippets and attributes from a previously created library object named <ObjName>.
You may aggregate the snippets from many library objects into your working objects with multiple *:BASOBJ statements, like this:
*:BASOBJ
<Object1>
*:BASOBJ <Object2>
*:BASOBJ <Object3>
The attributes of referenced library objects are inherited in the order they are listed.
It isnt necessary for the library object and the working object be of the same object type, since the snippet fields are found in all object records. So the VALID of a library Push Button could well be applied to a Radio Button, or any other type of screen control.
The attributes of the screen itself are stored in whats called the screen header record. This is the first record in the .SCX file for each platform. The screen header record (ObjType=1) contains, among other things, the Setup, Cleanup, and the Read When, Show, Valid, Activate, and Deactivate snippets.
As far as GENSCRNX is concerned, the screen header record is a screen object like any other. There is, however, no way to reach the screen header comment snippet in the Screen Builder. Thats why GENSCRNX screen header directives are stored in the Setup snippet.
Implicit Referencing
If your working object variable has the same name as a library object, and the library is triggered with *:BASLIB, then inheritance is triggered automatically, even without a *:BASOBJ statement. This can lead to unexpected results; so be careful when naming a library objects variable.
For example, if you have a library object named m.My_Memvar, and a working object also to m.My_Memvar, then a link to the library happens automatically. Provided, of course, that the working screen has the appropriate *:BASLIB statement.
Using *:INCLIB instead of *:BASLIB doesnt trigger implicit inheritance.
Partial Referencing
You can pull-in distinct fields from library objects into your working objects by using the GENSCRNX ":: " operator. The construct used for partial inheritance is {{<Library Name>::<Snippet Name>}}
For example, pull the contents of the VALID clause for the My_Library_Object into the current snippet with the following statement:
{{My_Library_Object::VALID}}
These may also be combined. For example, say you have a bunch of Date library objects. Assume that one called "Date_This_Year" validates dates in this fiscal year. Another named "Date_Is_Workday" validates dates in the work week. A third called "Date_Is_Holiday" validates holidays. You could construct a compound VALID statement to validate working days for this fiscal year in the following way:
RETURN
{{Date_This_Year::VALID}} AND ;
{{Date_is_Workday::VALID}} AND ;
NOT {{Date_Is_Holiday::VALID}}
In this way you can create complex validation criteria based on simple normalized components.
The default behavior in GENSCRNX is to act on the comment snippet. So the statements below are equivalent:
{{My_Library_Object::}}
{{My_Library_Object::COMMENT}}
Methodical Referencing
By using a refinement of the partial referencing mechanism, you can pull-in portions of library object snippets. This means you can store different "methods" associated with an action in the same snippet and refer just to the methods you need in the working object. To separate methods in the library object, use the *:METHOD directive.
The construct used for methodical referencing is
{{<Library Name>::<Snippet Name>::<Method Name>}}
For example, suppose a library GET field which has a variety of WHEN conditions checking for say security clearance and pop-up features. You could structure the library WHEN as follows:
*--
WHEN snippet of a library object
*-- named "Security_Check"
:*METHOD Security_MED
*-- Disalow if below level 3
IF m.gnSecurity < 3
RETURN .F.
ENDIF
*:ENDMTHD
:*METHOD
Security_HIGH
*-- Disalow if below level 4
IF m.gnSecurity < 4
RETURN .F.
ENDIF
*:ENDMTHD
*--
WHEN snippet of a library object
*-- named "Date_Stuff"
:*METHOD Calendar_POPUP
*-- Initialize a calendar popup with RIGHTMOUSE
*-- Note that expression in braces ( {{name}} ) will be
*-- evaluated by GENSCRNX as the memvar of the working
*-- object at generate time.
ON KEY LABEL RIGHTMOUSE DO calendar ;
WITH IIF( EMPTY( {{name}}, DATE(), {{name}})
*:ENDMTHD
You can structure the WHEN of a working object as follows:
*--
WHEN of working date object
{{My_Library::Security_Check::Security_MED}}
{{My_Library::Date_Stuff::Calendar_Popup}}
Would produce the following generated code, assuming the working object memory variable was "ldStartDate"
*--
Disalow if below level 3
IF m.gnSecurity < 3
RETURN .F.
ENDIF
*-- Initialize a calendar popup with RIGHTMOUSE
*-- Note that expression in braces ( ldStartDate ) will
be
*-- evaluated by GENSCRNX as the memvar of the working
*-- object at generate time.
*-- generate time.
ON KEY LABEL RIGHTMOUSE DO calendar ;
WITH IIF( EMPTY( ldStartDate, DATE(), ldStartDate)
Destructive Referencing
By using the GENSCRNX *:INSOBJ command, you can replace the whole working object with a library object. In this case, the working object becomes a placeholder for the insertion of a library object.
Note that you can do more than insert a library object. With the *:INSTXT command, you can insert FoxPro source code in the exact place in the .SPR where the working object would be referenced.
Recursive Referencing
Library objects can contain references to other library objects. GENSCRNX is recursive, and will keep processing until all library objects are added.
Maintaining Library Objects
The FOXSCX table, being a superset of the FoxPro .SCX structure, contains a PLATFORM field. Thus, its important to generate your library screen under each platform to make sure each library object has a record for each platform you support in FoxPro.
Whenever you regenerate a library screen, GENSCRNX will refresh any existing objects in the FOXSCX table, and adds any new ones it cannot find.
Remember that the actual library isnt the library screen; its the FOXSCX table, which is created by generating the library screen. Therefore, you must re-generate the library screen for all platforms whenever you make a change to a library screen.
There you have it! I hope this compels you to try and use the power of GENSCRNX library objects. Once you get the hang of this, you will find that using the GENSCRNX library functions will both reduce the amount of source code required to produce an application, and make it much easier to maintain.
The latest available version of GENSCRNX is included on the source diskette. As always, you should periodically check the FoxForum libraries for the latest and greatest version.
Timing Considerations
GENSCRNX inserts library objects in the second and third processing "hooks" (_SCXDRV2 and _SCXDRV3). These two hooks are recursive, which is how GENSCRNX can insert library objects that reference other library objects. Therefore, if you have GENSCRNX processes that need to act on all objects, be sure to use a later "hook". I usually use _SCXDRV5.
Conversely, for the same reasons, if you have a GENSCRNX driver that adds objects, its best to ensure that this driver is called at (or prior to) hook number 3.
1.4 Understanding GENSCRNX Drivers
The GENSCRNX process can be split into three distinct stages.
Screen Pre-Processing with GENSCRNX
Since the .SCX file is a standard FoxPro table, you can invoke standard FoxPro programs to manipulate them. In a nutshell, that's the whole idea behind GENSCRNX drivers. I suspect most developers are more comfortable writing procedural code to manipulate tables than they are at using the screen builder.
TIP #1: When thinking of GENSCRNX drivers, imagine your screens as FoxPro tables, and not in terms of the screen builder's interface.
Let's create a very simple driver. This short program will allow is to watch GENSCRNX step through the screen file.
*
Program...........: STEP.PRG
* Author............: Steven Black
* Created...........: 03/22/94
*) Description.......: A GENSCRNX driver that
*) temporarily suspends GENSCRNX's execution
*) with a BROWSE
* Calling Samples...: *:SCXDRV3 STEP
* (in the screen's setup snippet)
*--
Stop GENSCRNX's execution at every record
*-- in the screen set with a BROWSE
ACTIVATE SCREEN
BROWSE TIMEOUT 2
RETURN
To execute this driver open any screen and place the following line in any line of the setup snippet starting in column 1.
*:SCXDRV3 STEP
I'll discuss the meaning of the "3" in *:SCXDRV3 in a minute.
Now choose "Generate" under the Program menu pad, then watch what happens. The result of this (admittedly useless) driver is a sequence of repeating BROWSEs that stay on screen for 2 seconds and then pass control back to GENSCRNX. You'll see one BROWSE for the screen header record (Objtype = 1) and then one browse flash for each item in the screen file. That's because GENSCRNX calls pre-processing drivers in a SCAN loop with the temporary screen file in the current workarea.
FACT #1: GENSCRNX calls all pre-processing drivers from within a SCAN loop.
Did you notice that only records for the a single platform appeared in the BROWSE?
FACT #2: In the absence of the *:PLATONLY directive GENSCRNX will process the records for all platforms, but always SCANs all platforms separately.
Now if we want to make our BROWSE appear only for, say, Windows push button records, we could invoke the following driver:
*
Program...........: STEP_PB.PRG
* Author............: Steven Black
* Created...........: 03/22/94
*) Description.......: A GENSCRNX driver that
*) temporarily suspends GENSCRNX's execution
*) with a BROWSE on GET objects
* Calling Samples...: *:SCXDRV3 STEP_PB
* (in the screen's setup snippet)
*--
Stop GENSCRNX's execution at every Push Button
*-- in the screen set with a BROWSE. Push Buttons
*-- have objtype = 12.
IF
platform = "WINDOWS" and objtype = 12
ACTIVATE SCREEN
BROWSE TIMEOUT 2
ENDIF
RETURN
Get the picture? Now the BROWSE only appears when we are on Windows push button records.
Let's agree on this: we always want to minimize the time it takes GENSCRNX to do its job; GENSCRNX and drivers are overhead. Instead of having GENSCRNX call the driver for every record, let's rewrite the STEP_PB driver so GENSCRNX calls the driver only once, and let the driver handle the SCAN; when the SCAN is done, we'll be at EOF() which will also terminate GENSCRNX's driver-invoking SCAN. So the STEP_PB driver is better written as:
*
Program...........: STEP_PB.PRG
* Author............: Steven Black
* Created...........: 03/22/94
*) Description.......: A GENSCRNX driver that temporarily
*) suspends GENSCRNX's execution with
*) a BROWSE on GET objects
* Calling Samples...: *:SCXDRV3 STEP_PB
* (in the screen's setup snippet)
*--
Bail out immediately if we aren't dealing
*-- with a set of Windows records
IF Platform <> "Windows"
GO BOTTOM
RETURN
ENDIF
*--
Stop GENSCRNX's execution at every Push Button
*-- in the screen set with a BROWSE.
*-- Push Buttons are objtype = 12.
SCAN FOR objtype = 12
ACTIVATE SCREEN
BROWSE TIMEOUT 2
ENDSCAN
RETURN
Enough silly drivers! let's get some work done. But first, a quick fact about invoking GENSCRNX drivers:
FACT #3: GENSCRNX drivers can be invoked in two ways: as CONFIG.FP statements affecting all generated screens, or as screen-specific setup statements. The syntax for driver invocation in CONFIG.FP is
_SCXDRVx=MyDriver
In setup snippets, the syntax is
*:SCXDRVx MyDriver
You can attach any number of drivers to the same hook.
Here's a useful driver, invoked globally by CONFIG.FP, that does two things: it automatically makes all your window backgrounds gray, and re-fonts all you GETs and EDIT regions to Courier New 10 Bold:
*
Program...........: StdWin
* Author............: Steven Black
* Created...........: 03/22/94
*) Description.......: A GENSCRNX driver to
*) 1) color application windows gray.
*) 2) makes GET fonts Courier New 10 Bold
* Calling Samples...: _SCXDRV3= GrayWin
* (in CONFIG.FP)
*--
Bail out immediately if we aren't dealing
*-- with Windows or MAC records
IF ! INLIST( Platform, "WINDOWS",
"MAC")
GO BOTTOM
RETURN
ENDIF
SCAN
FOR objtype = 1 OR objtype = 15
DO CASE
*-- screen attributes are stored in records
*-- having objtype = 1
CASE objtype = 1
REPLACE fillred WITH 192, ;
fillblue WITH 192, ;
fillgreen WITH 192
*--
SAYs are Objtype = 15, objcode = 0
*-- GETs are objtype = 15, objcode = 1
*-- Edit regions are objtype = 15, objcode = 2
*-- Regular weight is fontstyle = 0
*-- Bold is fonstyle = 1
*-- Italic is fontstyle = 2
*-- Bold + Italic is fontstyle = 3
CASE objType = 15 AND objcode > 0
REPLACE fontface WITH "Courier New", ;
fontsize WITH 10, ;
fontstyle WITH 1
ENDCASE
ENDSCAN
RETURN
This driver has some merit. Changing Window fill colors takes a lot of effort; doing it manually I count six steps per screen. Setting object font attributes takes up to six steps per object. Mousing these global changes in a modest application could take hours. Life is too short.
Tips for Building GENSCRNX Drivers
So far we've looked at simple drivers that do simple things. You may be asking yourself, how to remember all the OBJTYPE and OBJCODE values? Also, invoking global drivers from CONFIG.FP is very handy, but what if a few of our screens don't need the treatment? Or how to write global all-purpose drivers and still maintain control at the object-level to deal with exceptional cases?
The best solution to these problems is to design your drivers flexibly to give you a maximum control to be able to bail out of the process at the environmental, screen, or object level. I'll demonstrate this by developing a GENSCRNX driver called GX_TOPIC.PRG.
EXAMPLE: Create a driver called GX_Topic.prg that automatically adds SET TOPIC TO commands to the WHEN field of every screen control in your application to initialize context-sensitive help.
The first thing I recommend is A Good File Header. Here is the standard header I use for drivers, with some details filled-in for the driver we will develop:
*!*********************************************
*!
*! PROCEDURE: GX_TOPIC
*!
*!*********************************************
PROCEDURE GX_TOPIC
* Program...........: GX_TOPIC.PRG
* Author............: Steven M. Black
* Created...........: 03/22/94
*) Description.......: A GENSCRNX driver to place
*) SET TOPIC TO statements in the
*) WHEN of all screen controls
*] Dependencies......:
* Calling Samples...:
*--
SCX ObjTypes
#DEFINE cnHeader 1
#DEFINE cnText 5
#DEFINE cnLine 6
#DEFINE cnBox 7
#DEFINE cnList 11
#DEFINE cnPushButton 12
#DEFINE cnRadioButton 13
#DEFINE cnCheckBox 14
#DEFINE cnSayGet 15
#DEFINE cnPopUp 16
#DEFINE cnPicture 17
#DEFINE cnSpinner 22
#DEFINE cnInvButton 23
*--
SCX Snippet types
#DEFINE cnExpression 0
#DEFINE cnProcedure 1
*--
Some native GENSCRNX functions return
*-- CHR(0) for "Null".
#DEFINE ccNull CHR(0)
*-- Continues following the text below....
That's a good start! No need to remember numeric SCX object types; just use Hungarian notation names for referencing. This makes both writing and reading the code much easier.
Take note of the last three #DEFINE statements: Most snippets can be procedures or expressions. For example, the WHEN field in the .SCX has a sister field called WHENTYPE. The WHENTYPE field can be either 0 or 1, depending on whether the WHEN snippet is an expression or a procedure. I use these two #DEFINE statements to allow me to forget the meaning of snippet-type field values.
Some very useful GENSCRNX functions, like WORDSEARCH(), return CHR(0) in the event a directive or string isn't found. I've define this as well and we will make use of this constant a little later on.
Environmental, Platform, and Screen Bailouts: The next thing you should do is quickly determine whether the driver has any business with this screen or this platform. For example, you may want the ability to set a generate-time memory variable to override driver invocations emanating from CONFIG.FP statements, or the screen's own setup statement.
Efficient bailouts can dramatically increase the performance of your drivers because you'll exit quickly when you need to. In addition to the example below, consult the 3D and Winmove drivers, and GENSCRNX.PRG itself, for more examples of bailouts.
Since GENSCRNX drivers can be invoked by CONFIG.FP statements, we need screen-level bailouts. Since drivers can also be invoked from screen setup statements, we need a way to override these with global settings. This inherent GENSCRNX flexibility makes bailout control critical so to make things happen when we want, affecting only the objects we want. Here is how I've chosen to structure the bailouts for the SET TOPIC TO driver:
*--
.... continued
*-- =====================================
*-- B A I L O U T S
*--
*-- Environmental bailout:
*-- m._GX_TOPIC="OFF" (memory variable)
*-- _GX_TOPIC="OFF" (in config.FP)
*--
*-- Platform bailout:
*-- None. We won't use a platform bailout
*-- bailout since all platforms support
*-- the SET TOPIC TO command.
*--
*-- Screen bailout:
*-- *:NOGX_TOPIC in the setup.
*-- =====================================
IF Objtype = cnHeader .AND. ;
(! drvenable( PROGRAM()).OR. ;
wordsearch("*:NOGX_TOPIC",.T.) <> ccNull
GOTO BOTTOM
RETURN .F.
ENDIF
*-- Continues following the text below....
TIP #2: Since GENSCRNX calls our custom drivers, all its internal procedures and memory variables are in our invocation stack and available to our drivers. A list of some of these useful things, including the DRVENABLE() and WORDSEARCH() functions used above, appears at the end of this article. Use them.
As you can see we have created our own driver setup snippet directive (*:NOGX_TOPIC) so we can tell the driver to skip individual screens. The same directive will be used in object comment fields to tell the driver to skip individual objects.
The DRVENABLE() function is a handy (and undocumented) GENSCRNX procedure. It checks the memory pool for the existence and value of the memory variable m._GX_SETUP (the underscore is added by the function). If the memory variable does not exist it checks the active CONFIG.FP file for a _GX_SETUP= statement. If m._GX_SETUP = "OFF" or, failing that, if _GX_SETUP=OFF in CONFIG.FP, our driver won't be invoked.
The WORDSEARCH() function is an extremely useful driver tool. It searches snippets for the occurrence of strings, returning CHR(0) if the string isn't found, and when found, returns the string immediately following the search string. WORDSEARCH() acts on the COMMENT field by default. When the second parameter is .T., it searches the SETUPCODE field. The second parameter can also be the name of the field in which to search.
Here is the rest of the source for the GX_TOPIC driver, which we encapsulate in a SCAN statement to process all screen records in a single pass.
*--
...continued
SCAN
*-- =====================================
*-- Object-level bailout
*-- =====================================
IF wordsearch("*:NOTOPIC") <> ccNull
LOOP
ENDIF
*--
SET the Topic in the WHEN snippet of screen
*-- controls
IF (objtype = cnSayGet AND objcode <> 0) OR ;
BETWEEN( objtype, cnList, cnCheckBox) OR ;
objtype = cnPopUp OR ;
objtype = cnSpinner
jcAddString ="SET TOPIC TO " + clean( TRIM( name))
DO CASE
*--
Nothing in the WHEN field
CASE EMPTY( when)
REPLACE when WITH jcAddString ;
whentype WITH cnProcedure
*--
If the WHEN is an expression, make it a
*-- Procedure and act accordingly
CASE whentype = cnExpression
REPLACE when WITH jcAddString + cr_lf + ;
"RETURN " + TRIM(when) ,;
whentype WITH cnProcedure
OTHERWISE
IF WORDSEARCH("SET TOPIC", "when") =
ccNull
REPLACE when WITH jcAddString + ;
cr_lf + ;
when
ENDIF
ENDCASE
ENDIF
ENDSCAN
RETURN .T.
*!*********************************************
*!
*! Procedure: clean
*!
*!*********************************************
PROCEDURE clean
* Author............: Steven M. Black
* Created...........: 08/23/93
*) Description.......: PROCEDURE clean
*) Supporting function for the GX_Topic driver.
*) Sanitizes the topic string. Converts strings
*) like "Client.Address" to
"Address".
PARAMETER
tcTopicString
PRIVATE jcRetVal, jnDotPos
jcRetVal = tcTopicString
jnDotPosition = AT( ".", tcTopicString)
IF
jnDotPosition > 0
jcRetVal = SUBS( jcRetVal, jnDotPosition + 1)
ENDIF
RETURN jcRetVal
Here are notes on that last segment of code:
· We have put an object-level bailouts inside the SCAN loop. If for whatever reason we don't want context sensitive help for a field, a *:NOGX_TOPIC comment directive will suffice to skip this record.
· Using the CLEAN() function is optional. I've included it here so we don't carry the leading "m." or "<table name>." in our topic name. You may choose to do otherwise.
· If there is nothing in the WHEN snippet, we simply add a SET TOPIC TO <object name> command and make the WHENTYPE a procedure.
· If the original WHENTYPE is an expression, we change it to a procedure, add the SET TOPIC TO <object name> to the snippet, and RETURN whatever the original expression was.
· If the original WHENTYPE is a procedure, and it doesn't already contain a SET TOPIC command, simply add the SET TOPIC TO <object name> to the top of the procedure.
Obviously a complete treatise of all the ways the SET TOPIC TO command can be deployed is beyond the scope of this example and beyond the focus of this article. I leave it to you to modify this driver to suit your needs.
Inside the GENSCRNX Pre-Generation Process
Here's an interesting look at what happens behind the scenes in the GENSCRNX's pre-generation phase. One of the most useful drivers when seeking a better understanding of GENSCRNX is the "suspend" driver. Quite simply, issue a SUSPEND command on any hook. The suspend driver looks like this:
*!*********************************************
*!
*! PROCEDURE: SUSPND
*!
*!*********************************************
PROCEDURE GX_TOPIC
* Program...........: SUSPND.PRG
* Author............: Steven M. Black
* Created...........: 03/22/94
*) Description.......: A GENSCRNX driver to
*) stop execution
*] Dependencies......:
* Calling Samples...: *:SCXDRVx SUSPND
* *:SPRDRVx SUSPND
SUSPEND
GO BOTTOM
RETURN .T.
This driver will stop on the hook you specify and present you with the command window. Type RESUME therein to get things going again.
Type SET in the command window when suspended in the pre-generation phase to invoke the VIEW window:
You'll see that GENSCRNX has six open workareas. Actually, the FOXSCX table is opened after SCXDRV1, so on the first hook you'll see only five open areas.
The current workarea is SCXDATA, which is the temporary table containing all the screen records for the platform being generated. This is the table that GENSCRNX SCANs when invoking drivers.
The CONFIGFP table contains a single record with the current CONFIG.FP file, as returned by SYS(2019), in a memo field. This explains why you can add GENSCRNX directives to the CONFIG.FP file and have them active without quitting and re-entering FoxPro. This is a handy fact to know. That also explains why you can play neat tricks with CONFIG.FP with GENSCRNX's brace commands such as inserting files or invoking UDF's in GENSCRNX's CONFIG.FP commands.
The PJXBASE table contains all the records of the current project. This is a handy feature that leads to countless possibilities. For example, you write GENSCRNX drivers that SELECT PJXBASE and acts on non-screen project items! If you do this, be careful to remember your pointers and return everything as it was at the end of your driver.
For example, you could have a dummy screen called AAAAAA.SCX which will be first in the project manager list, and thus first to be generated. In the setup snippet of AAAAAA.SCX you could call a GENSCRNX SCXDRV driver that pre-process the project and prepares everything for the rest of the subsequent build. You'll need to edit AAAAAA.SCX to trigger its generation if you don't BUILD ALL, of course, but given the power you now possess over the project at build-time that's an insignificantly small irritant.
Note that if you are generating .SPR files outside the project manager, GENSCRNX will create a pseudo-PJXBASE table with information relating only to the screen set currently being generated.
The PJXDATA table contains only the project records associated with the screen set being generated. Again, if you are generating .SPR files outside the project manager, GENSCRNX will create a pseudo-PJXDATA table
The SCXBASE table contains all the records for all platforms for the screen set currently being generated.
Finally, the FOXSCX table is the GENSCRNX library file from which GENSCRNX can swap screen and object information into the screen being generated.
GENSCRNX Screen Post-Processing
Post-processing drivers (SPRDRVx) are, in general, less useful and harder to write than pre-process (SCXDRVx) drivers. SPR drivers must act on a memo field containing the source code.
Issuing our SUSPND driver on the six SPR hooks, and then issuing the SET command, yields the following information:
· On hooks 1, 5 and 6 the current workarea is the table (SPRDATA.SCX) containing the temporary source memo field named "SPR". These hooks are called once.
· On hooks 2, 3 and 4 the current workarea is a temporary screen table (SCXDATA.SCX) and these hooks get invoked once for each record in the screen set.
That's an important difference. If you invoke your SPR drivers on hooks 2, 3, or 4 you need to have the driver SELECT SPRDATA to directly access the memo containing the source.
All of FoxPro's string functions are available to you to manipulate the source code in the SPR memo field of the SPRDATA table.
More Tips for Creating GENSCRNX Drivers
· Understand the .SCX table structure. Since _SCXDRVx drivers operate directly on .SCX files, it follows that you need to know about the .SCX structure and usage. For reference consult chapter 4 of the FoxPro "Update" manual. My favorite source for version 2.0 (and for version 2.5, with caution) is the Appendix of Template Programming With FoxPro 2 by John M. Miller, which is Volume 1 of the FoxPro 2.0 The Pros Talk Fox series. Beware that the .SCX structure has changed between FoxPro version 2.0 and 2.5.
· Experiment with simple, stand-alone screens. Developing GENSCRNX drivers is much easier if you first work with simple screens. This shortens the development cycle, and makes for smaller .SCX files for referencing and smaller .SPR files for debugging. With a small experimental .SCX, its easy to USE the file and BROWSE it and find immediately what you need to know about a screen-object record.
· Reverse-Engineer the sample screens. They are loaded with examples of GENSCRNX command usage.
· Occasionally browse FOXSCX.DBF to make sure it contains what you think it should. I found that, once I was finished with the GENSCRNX demonstration, I minimized confusion by specifying my own FOXSCX file so not to mix demo objects with my own.
1.5 Understanding GENSCRNX Hooks
GENSCRNX has eight pre-generation (SCXDRV) hooks and six post-generation (SPRDRV) hooks to which you may attach drivers. You may specify multiple drivers on each hook. Hooks are assigned globally in CONFIG.FP statements locally in individual screen setup snippets. CONFIG.FP drivers are executed before the drivers that are assigned in the setup snippet.
GENSCRNX hooks are invoked sequentially, and all drivers for a given hook are executed before GENSCRNX moves on to the next hook. If the same hook name is encountered more than once, only the first invocation is executed.
For example, if you have the following statements in CONFIG.FP
_SCXDRV3=<DriverA>
_SCXDRV3=<DriverB>
_SCXDRV5=<DriverC>
and the following in a screen's setup snippet
*:SCXDRV5
<DriverD>
*:SCXDRV3 <DriverE>
*:SCXDRV3 <DriverF>
The execution sequence on hook 3 is drivers A, B, E, F and for hook 5 the sequence is drivers C followed by D.
If the <file> parameter of a driver directive does not include a file extension, the following extensions are checked in this order: .EXE, .APP, .PRG, .FXP.
As a general rule, most SCX drivers should be attached to hooks 2, 3, or 5. If your screens use objects swapped in from the FOXSCX table and you want your drivers to affect all the new objects, be sure to use _SCXDRV3 or higher.
It's helpful to know that SCX hook number 6, SCXDRV6, is under the influence of a FILTER that presents only records added to the screen table by native GENSCRNX processes. In other words, you won't have much luck issuing drivers on the sixth hook.
Here is a brief summary narrative of what happens between each GENSCRNX hook:
1.6 Using GENSCRNX as TRANSPORTX
In the CONFIG.FP file, you can place the following:
_TRANSPRT="<path>GENSCRNX.PRG"
_TRNDRV1="<path><prg 1>"
_TRNDRV2="<path><prg 2>"
Whenever the FoxPro calls the transporter, the following occurs:
<prg 1> can be used as a custom control program to determine if the transporter needs to be called or any preprocessing needs to occur to the .SCX before TRANSPRT.PRG is called.
<prg 2> can be used to updated the .SCX after TRANSPRT.PRG is complete to override any unwanted defaults such as fonts, row/column, screen color, or .SCX header information.
1.7 Checklist for GENSCRNX
Have you checked the libraries on CompuServes FoxForum to ensure you have the latest version of GENSCRNX and GENMENUX?
Are your CONFIG.FP files set so _GENSCRN=<Path\>GENSCRNX.PRG?
Do your CONFIG.FP files have an MVCOUNT set to at least 512?
Are GENSCRNX commands and drivers sequenced correctly so the drivers apply to all the objects they should?
Do you use *:SET PLATONLY or set m._PLATONLY to improve generation?
Do screens containing GENSCRNX directives contain the characters *: or {{ in the SETUP snippet to signal to GENSCRX that it should seek out directives in the other screen objects?
GENSCRNX
(c)1994 Steven Black