From: [email protected] (Andrew Dent)
Subject: tips for Fox programmers
Date: Wed, 17 Mar 93 21:05:07 WST

The following is a long list of tips for FoxBase+/Mac programmers
(although many are generally applicable to Foxbase).

ABOUT THESE NOTES
All copyright to these notes is hereby relinquished and I transfer them to the
public domain. However, please leave this message at the top of the notes. If
you have any corrections or additional hints, forward to me and I will
incorporate them in the notes for future release.

Some of these notes may be a bit cryptic - they were written as more of a
reminder to me when coming back to FB from other environments. Feel free
to call for explanations, or mail me improved descriptions!

16th March 1993

Andy Dent
A.D. Software, 94 Bermuda Dve, Ballajura Western Australia 6066
phone/fax 61-9-249-2719
CompuServe 100033,3241
Internet   [email protected]

PS I have a couple of nifty shareware XCMDS for double-clicking Browse &
external scrolling list dialogs, with their own arrays!



--------------------==========--------------------==========--------------------
DUPLICATE RECORD, OR CARRY FORWARD
Normally you can't use the SET CARRY option unless you want to be stuck in APPEND
mode with all its disadvantages (including the Append menu!).

The following code snippet allows you to use SET CARRY ON to duplicate the
current record, but editing it with a READ. It relies on having an empty format
file for the Append, which exits so fast (due to the ctrl-end in the keyboard
buffer) that there is no flicker.

set carry on
set format to empty
keyboard(chr(23))
append
set format to test   && the real format file
read

NOTE
The standard technique to duplicate a record is COPY TO a temp file and then
APPEND FROM.

--------------------==========--------------------==========--------------------

ZERO-FILLING & LEFT-JUSTIFYING
To get a string representation of a number, the STR function puts in leading spaces.
However, for constants, you can use TRANSFORM with an empty string, as shown below.

Note that these examples also zero fill a field of five characters wide
eg: NUM=123 would return "00123"

right(replicate("0",5)+transform(123, ""),5)   && doesn't work with variables
right(replicate("0",5)+ltrim(str(val(NUM)+1,5)),5)

if you were using these functions frequently at the same width, make up a variable:
fiveZeros = replicate("0",5)
right(fiveZeros+ltrim(str(val(NUM)+1,5)),5)

--------------------==========--------------------==========--------------------

PUBLIC VARIABLES MUST ALSO BE INITIALISED
It isn't enough to just declare variables as Public, you also need to assign
values before using them in a Get.

Strings must have at least one character assigned - a null string is not enough!

If a Public variable is shown as Hidden to an interrupt procedure (eg: ON MENU)
then it is probably being referenced indirectly by a procedure called from a
higher level - check (DISPLAY MEMORY) for a private variable shown as
"@publicVarName" instead of its contents being shown.

--------------------==========--------------------==========--------------------

PASSING PARAMETERS
1) DO ... procedures (NOT UDF's!!!)
If we pass an expression, the PARAMETER variable is PRIVATE.  However, if we
pass a variable, the value of the PARAMETER variable is copied back to the
caller, even if they are different names!  So, in Pascal terms, passing an
expression gives you Call-by-value and passing a variable gives you
Call-by-reference. A DISPLAY MEMORY will show the local var as referring to the
public var, and the public var will be Hidden from any routines that might want
to change it!

WARNING - this technique only works in procedures called by DO and doesn't apply
to UDF's - they are always call-by-value!


2) Arrays & other variables in UDFs
If we pass the quoted name of the parameter, we can use the macro substitution
operator to get the name of the actual variable - either PUBLIC or PRIVATE to
someone higher in our calling hierarchy.
ie:
PROCEDURE A
aLocal(1) = 1
DO B WITH "aLocal"
..

PROCEDURE B
PARAMETER numVar
&numVar(1) = 2
RETURN

Note that this trick works with arrays as the macro substitution is
performed on the variable name before applying the subscript.  Conversely,
this is a big gotcha - if you want to substitute something in an array you
have to copy it to a temp var & substitute that (usually doing something with
filenames and a command such as REPORT FORM).

--------------------==========--------------------==========--------------------

BUILDING COMMANDS ON THE FLY
You can build complete command lines into variables and execute with the macro
substitution operator, eg:
fred="dir"
&fred

--------------------==========--------------------==========--------------------

CHANGING WINDOWS.
Any menu click cancels the current READ.  If you do a MENU ON -3,1 this
enables the trapping of window clicks as if they were in a menu.  They will
still bring the window forward but you at least get the chance to detect and
react to the change.

If we want to make the background window active, we need to do something about
saving our current context.  We can't afford to nest procedure calls for
window-flipping - have to save context and exit procedure. Otherwise, user will
build stack of procedure calls in order of calling windows, unloading the stack
only when they terminate given windows. Your top loop should have some form of
CASE statement to react to returns to that loop, and execute the appropriate
procedure. All procedures will then simply RETURN when their window is
deactivated, saving their context somehow so they can carry on when the user
clicks on their window again (a really nice touch would be to save the field
name and have a routine that jumps down to that field by stuffing down-arrows or
tabs into the keyboard buffer).

--------------------==========--------------------==========--------------------

USING ARRAYS TO TALK TO FILES
If we use the neat trick of copying everything into an array, we have the
problem of how to initialize the array.  A transparent method is to make an
empty COPY STRUCTURE of the target file and do a SCATTER TO with that file, to
both clear the fields of the array and define them properly.  Note that you
can define the array with a PUBLIC &arrayName(FCOUNT()) assuming you are
copying all the fields of the array (remember that SCATTER skips MEMO's!!!).

--------------------==========--------------------==========--------------------

FINDING DUPLICATE RECORDS (Untested idea)
You can use an index to find records that are duplicates on the key expression,
using the delete facility to hide the others, and maybe a separate field in the
database isDup to mark them for later processing:
 recall all
 set unique on
 reindex
 delete all
 set order to 0
EITHER
 set deleted on
 && you now see only the duplicates!!!!!!
 set deleted off
OR
 locate for !deleted()
 ...
 continue
OR
 replace all isDup with .F.
 replace isDup with .T. for !deleted()

* to cleanup
 recall all
 set unique off

--------------------==========--------------------==========--------------------

INDEXING DATES IN REVERSE ORDER
The normal expression to index on dates is:
       dtoc( eventdate ,1)    && produces a date like 19930120
you can easily build a reverse-date-order index with the following expression, which
simply subtracts the date from a constant. Note that the value will not require
padding for most dates, only if you should attempt to use years of less than 4
digits (ie: earlier than 1000AD).

       transform(99999999-val(dtoc( eventdate ,1)),"99999999")

--------------------==========--------------------==========--------------------
VERY FAST RELATED CONTEXTS WITHOUT RELATIONS!
Let's say you have a Client file containing the field "intid"
and a Code file with the same field, N Codes per 1 Client.
If the Code file is indexed on an expression STARTING with "intid"
then the following filter in Code will give blazingly fast results:

        set filter to intid=Client->intid

Try it with Browse windows open in both Client and Code - click on a different
Client record and then reactivate the Code browser.


--------------------==========--------------------==========--------------------

CREATING AN ARBITRARY SET OF RECORDS
If you want to get a set of records for processing, the only way is with a filter
(or a filtered index, if you can carry the startup overheads). However, there are
times when your set of records can't be described by a filter expression (until
we're allowed to use UDF's in filters).

The basic technique is to setup a "set filter" file and relate your original
to it. Assuming the "set filter" file is in area 8, original then gets a filter like:

        set filter to found(8)

Note that the establishment of the "set filter" file can be optimised if it is
created with a COPY TO   FOR where the FOR condition will substantially reduce the
records copied. You then go through the "set filter" file and eliminate records
based on whatever arbritrary rules you like. A neat way to eliminate records is to
use a "deleted" field and a filtered index (see below).

--------------------==========--------------------==========--------------------

RELATED INDEXES
If you have indexes using a related field in their expressions
(eg:  INDEX ON UPPER(People->surname)) then you need to REINDEX every time a
key field is changed in the related file (ie:  field mod, rec add or rec del).
This is because FoxBASE can update its local indexes but doesnUt know if
anything is related TO the file.

--------------------==========--------------------==========--------------------

FILTERED INDEXES
An INDEX ON statement can have a FOR clause.  This is a quick way to build
throw-away indexes that point to a current set of records - much more
efficient than repeated operations using a LOCATE or setting a
SET FILTER TO expression.

eg: INDEX ON FOR DELETED() lets you use the Delete marking in Browse to select a
set of records.
WARNING - using the Deleted() function is NOT reliable between sessions and
doesn't work multi-user. If you want to ignore deleted records then use your own
Logical field (eg: deleted) and create an index like:
INDEX ON UPPER(catcode+itemcode) FOR !deleted TO "D:CodeDescr.idx"

You can then scan through these deleted records by doing
SET ORDER TO 0   && turn off index so you can see all recs
LOCATE FOR deleted
..
CONTINUE

OR

If you will have a LOT of changes to the file, and want to get deleted records
fast for reindexing, have a second index "on recno() for deleted" and just
 set order to 2
 go top
 if !found()
   append blank
 endif

--------------------==========--------------------==========--------------------

RELATIONS NOT UPDATED (From CompuServe)
When you REPLACE the primary key of a relation, so you should now be related to
a different child record, FoxBase+/Mac doesn't update the relational link
automatically (see below). You can force an update with GOTO RECNO().

Cross-platform warning - FB2.1 for DOS does update the link but reportedly
FoxPRO is same as Mac.

 parent child
 key    key
 1      1
 2      2
 use child index key
 select 2
 use parent
 set relation to key into child
 go top
 ?key,child.key
 1 1
 ?lock()
 .t.
 replace key with 2
 unlock &&!!!
 ?key,child.key
 2 1 &&!!!

--------------------==========--------------------==========--------------------

MANY-MANY FILES AND REPORTING
If you have a many-many situation, you need an intermediate file such as:

Office.dbf       jobs.dbf         people.dbf
fund_code<<------fund_code         name
                accnt_num-------->accnt_num
                job_title

You can use an index on a RELATED field to index the intermediate file in the
order of one of the many files. This is often necessary when reporting.

eg:
 select jobs
 index on people->name to jobsName

Records for which there is not a related record will show at the front of the list.
To change this, you need to use the IIF and FOUND functions:

 index on iif(found(3),People->Name,"ZZZ") to jobsName
* assuming People is open in work area 3

Note that you can also use IIF in a similar manner in LIST commands or reports
 set heading off
 ?"Job Title   Name"
 list Job_title, iif(found(3),People->Name,"N/A")

--------------------==========--------------------==========--------------------

PUTTING WINDOWS TO TOP
If we respond to window events, we have to leave windows on top of us if they
are DA's.  This isn't a problem under Multifinder but, under Finder, we need
to insert code to trap these windows.  In the code that responds to the MENU
of -3, add the lines:
IF VAL( SYS(1035) ) = -1 THEN
 READ
ENDIF
This READ will be interrupted by the next window event which will make a
reentrant call to our ON MENU procedure and then return, terminating the READ
and letting the original invocation of the ON MENU procedure carry on.

--------------------==========--------------------==========--------------------

USING BROWSE WINDOWS
You need the SAVE option for BROWSE to let you use your menus, and then have the
issue of detecting whether to return to the browse after a screen change. If you
set a flag to say ignore screen changes (because you went into a screen that
returns to the list) you can check with VAL(SYS(1025))<>-8 meaning BROWSE isn't
top and (if return flag not set) therefore assume it's been closed.

To programmatically close a BROWSE ...SAVE window you need to re-issue a
BROWSE (without the SAVE) to be able to KEYBOARD(chr(23)) to put a
ctrl-W into the typeahead buffer. A BROWSE...SAVE just ignores the typeahead.

If you still have a BROWSE active in the background and programmatically change
the current record, before initiating a READ, the record will change back to the
current BROWSE record.

--------------------==========--------------------==========--------------------

BEEPING THE USER
??chr(7) prints a bell character (on whatever screen is current - it doesn't
matter) which beeps in the current system beep. The double ? means no line advance
occurs so it doesn't affect the current screen display.

--------------------==========--------------------==========--------------------

ADDING ITEMS TO A LIST
A nicer way to display shortened names is as follows:
IIF( LEN( TRIM(var) )>30, LEFT(var,29)+"I",TRIM(var))

--------------------==========--------------------==========--------------------

DUAL LISTS IN A WINDOW
You can have more than one list (within the limits of the 64kb of MVARSIZ) but
have trouble with who clicked where. A VALID UDF can be used to set a global
variable indicating which of the lists was last clicked. The only flaw with this
technique is the visual - you can't turn off the array selector on the other
list(s) because the VALID can't affect current GETS.

(An untried thought - a manual SAY with the right settings could be used to invert
the selected area on the other list to mimic the appearance of it being no longer
selected).

--------------------==========--------------------==========--------------------

SPEEDING UP SCREENS (tip from "Dynamics of FoxBASE+/Mac Programming")
If screen format is generated with the painting tool, it will contain a lot of
unnecessary COLOR, FONT and STYLE clauses.  Removing these speeds up the screen
considerably.
(From my tests)
It seems safe to remove the COLOR clause from all except default button frames.
The FONT clause can be removed or the name or size removed when that matches the
default. The STYLE can be safely removed when 0, 1 or 65536 but looks funny when
taken off default button.

--------------------==========--------------------==========--------------------

SPEEDING UP SCREENS WITH "SET FORMAT TO" & SCREEN MEMORY USED
Screens are a LOT faster when used in a .FMT file, via SET FORMAT TO instead of
including the contents of the .FMT before your READ.

WARNING: when using SET FORMAT TO there are offscreen buffers declared for
imageing the screen prior to showing it (which is how it is so slick to
display). If you are running in colour then these buffers are around THREE
HUNDRED kb for a single Mac II-size screen. Be careful to dispose of these
screens by a SCREEN n DELETE  and avoid nesting too many screens (eg:
"drill-down" through 4 levels of 256 colour screens (8-bit) uses over 1Mb of RAM
just
for the screen buffers, if all are SET FORMAT TO type screens. Consider
giving the user the choice of speed vs memory and DOing the .FMT files to avoid
allocating the memory for the offscreen buffers.

Commands reputed to limit the amount of memory used for screen buffers:
 SET COLOR TO
   This sets the screen to the <default> B/W.

 SET INTENSITY OFF
   This turns off the enhanced screen attributes.


--------------------==========--------------------==========--------------------

SOMETHING TO TEST - SPEEDING UP PICTURE BUTTONS.
Try having one background picture which contains all the buttons and using
"invisible" picture buttons with the \F option.
We already know that icons are faster than picture buttons - this
technique may be even faster and can be used when the picture is too big for an
icon representation. It certainly cuts down the number of resources being
invoked by a screen.

Note that the flip-side of using icon buttons instead of picture buttons is that
you may want picture buttons smaller than the standard (232x32) icon mask.

--------------------==========--------------------==========--------------------

OPTIMISING MEMO FILES
If you have frequently modified/deleted memo and/or picture fields then the
variable length
memo file will become internally fragmented over time.

To clean up the file, simply do a COPY ... TO command to create another database
and then rename the .dbf & .dbt files back to the original names.

--------------------==========--------------------==========--------------------

WORKAREAS
(suggestion) If files are permanently assigned to a workarea, a variable is
declared that's the workarea number, so it can be used as a parameter to
commands such as REINDEX.  The variable should be the same name as the file, eg:

SELECT A
USE Projects
Projects = SELECT()

--------------------==========--------------------==========--------------------

SAVING & RESTORING THE CURRENT SELECTION
You can save and restore the currently selected workarea by getting a STRING
version of the workarea number. It has to be a string so it can be translated by
the "&" substitution operator,eg:

curSel = str(select())
..... do lots of other stuff
select &curSel

--------------------==========--------------------==========--------------------

GOTCHA WITH "REPLACE"
REPLACE can only be used reliably on the current file, even though it is
legal syntax to do a replace on fields in another file, it won't always save!

--------------------==========--------------------==========--------------------

NEED FOR QUOTED FILENAMES OR PATHS
Historically, commands involving files or directories were mean't to be typed in
as direct commands and so a shortcut was used in not requiring the literal
filenames to be quoted (unlike literal strings in other contexts).

On the Mac, you MUST have quotes around filenames with spaces and some other
characters. There's nothing stopping you having quotes around a name even when
it has no spaces.

Thus, the easiest is something like
 aPath = chr(34) + aPathVariable + chr(34)
 set default to &aPath

--------------------==========--------------------==========--------------------

QUOTED STRINGS
The PUTFILE function returns a quoted string.  The quotes go into your variable
so you have to strip them. The alternatives are
filePath=PUTFILE(...)
filePath=&filePath
 OR
filePath = substr( filePath, 2, len(filePath)-2)

You can't include quotes in a quoted string by doubling-up (as in most
languages) so you have to use chr(34) as shown earlier. If you are using chr(34)
a lot it is a good idea to assign it to a global variable (eg: qt).

To include quotes in a message, use the curly quotes (option-[ and
shift-option-[) eg: msg = "Sorry, I don't know RHarryS"

--------------------==========--------------------==========--------------------

CUSTOMISING REPORTS FOR DIFFERENT PRINTERS
The (acknowledged) problem is that page setup orientation reverts to Portrait
for different printers Q we need a way to save the Page Setup without having to
have a version of each report .FRX saved for each possible printer.

There IS a technique that enables you to have one set of report files, and
customise them at runtime to have the correct setup for the current driver.

Basically, you need a resource copying XCMD, like Rinaldi's CopyRes (there are a
few around).

You need to delete the PREC resource in the report file and then copy from
your (previously saved) file containing the resources for the printer type.

UNRELIABLE TECHNIQUE
If you have a FoxUser file, the Page Setup command updates the PREC resource in
there. Note: this updates the original FoxUser file, opened from the same folder
as your FoxBase or FoxRun application. It doesn't apply to any later SET
RESOURCE TO file, regardless of name.

You can copy this PREC resource, using the XCMD, to your report .FRX file.

--------------------==========--------------------==========--------------------

VECTORING TO DIFFERENT ROUTINES (implementation of OOP)
A context variable can be used for a CASE which selects amongst DO statements.
(eg: a Delete procedure which has a CASE to handle each different file). This is
a POOR solution as it groups by function rather than grouping functions together
into the one object. It is also very vulnerable to accidentally editing other
code and thus makes it impossible to "tickoff" an area as tested.

A faster and neater way is to use the context variable to build a procedure name
which is used by something like EG:
G_file = "WORK"
DO NEW_&file           &&  is translated to DO NEW_WORK

Another approach is to have a single routine (eg: Client) which takes a
parameter which is a string describing the message (eg: "Show Associated Cases")
and which is checked inside a big CASE statement. This is a bit slower but lets
you write much cleaner and more readable code.

If you have a number of files with similar callbacks (ie: using the same model for
editing records, saving etc.) then you can combine the above techniques:
 G_file = "WORK"
 DO &G_file WITH "Edit"

Appropriate selection of your callbacks lets you write generic PostEdit and PostBrowse
routines
eg: (following a Cancel button press)
 if (GF_Save .or. GF_Revert)
   do &G_currObj with Iif(GF_Save, "Save", "Revert")
 endif


--------------------==========--------------------==========--------------------

XCMD PARAMETERS
Parameters into and the return value of XCMD's are limited to 255 characters and
can't include arrays. However, the GetGlobal and SetGlobal callbacks allow you to
transfer more text (possibly limited by Hypercard's original 32kb - I haven't
tested that size).

Also, you can use GetFieldByName to get the result of array expressions, so you can
transfer Fox arrays into/out of your XCMD cell by cell.

Get FieldByName also evaluates functions such as RECNO().

--------------------==========--------------------==========--------------------

XCMD QUICKDRAW GLOBALS
Inside an XCMD (at least, one created with Think C) your Quickdraw globals don't
point to the right place as they are A4-based.

You can copy the real globals to yours with code as shown below (applec is defined
if using MPW instead of Think C).

// COPY REAL QUICKDRAW GLOBALS TO THOSE WE SEE IN OUR A4-BASED GLOBALS
       {
               char *realGlobalsAt = *((char **) CurrentA5);
#ifdef applec
               BlockMove(realGlobalsAt-126, &qd, sizeof(qd));
#else
               BlockMove(realGlobalsAt-126, &randSeed, (long) 126);
#endif

/* NOTE: if you don't cast constant sizes to BlockMove you only update the low
        word of D0 and so you get whatever was in the high word taken as part
        of your length - probably resulting in MASSIVE overwriting of memory!
*/
       }

--------------------==========--------------------==========--------------------

NEW FEATURES IN FOXBASE/MAC VERSION 2.0 (by deduction, they have never listed them)

XCMDS

HIERARCHICAL MENUS

ReportWriter

Allow items in Apple, File & Edit menus (using negative menu numbers)

Detect window changes with menu -3,1

New programming options on BROWSE (not in manual - look at Help)

SCREEN ... FIXED to create modal screens

Scatter & Gather

CHANGE & APPEND default screens changed.

BROWSE menu got a separate APPEND BLANK instead of using File - New.

Faster?