From: dent@DIALix.oz.au (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 dent@dialix.oz.au 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 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?