Chapter 14
Language Improvements

Visual FoxPro’s programming language is rich and powerful. But there’s always room for improvement. Beyond the enhancements described in other chapters, VFP 9 includes a variety of changes in the language that can make your code faster to write, faster to run, and even allow you to do things you couldn’t in earlier versions.

Some of the enhancements in the programming language in VFP 9 are discussed in other chapters, such as improvements in the SQL sub-language (Chapter 8, “SQL Changes”) and enhancements in the SQL pass-through functions (Chapter 11, “Working with Remote Data”). However, a number of other changes make life easier for VFP developers. One big enhancement is support for Windows message events. Other smaller changes include improvements in some string functions.

Fewer limits

One of the design goals for VFP 9 was to eliminate or at least raise the limits of earlier versions. For example, arrays can now have more than 64K elements. Given VFP’s powerful cursor engine, why would you ever need an array of more than 65,535 elements? There may be several valid reasons, but Rick Schummer ran into one himself: ADIR(). A client asked him to process a directory of images in some way. It turns out the directory had more than 14,000 images in it (why you’d do that is a valid question, but it wasn’t Rick’s choice). ADIR() creates an array with five columns, and 14,000 x 5 is more than 65,535, so his program bombed on the ADIR() statement. Fortunately, VFP 9 was in beta by this point, so he ran the program in that version without problems. You are limited by the amount of memory available or 2 GB, whichever is lower. Also, arrays containing objects are still limited to 64K elements because that’s the maximum number of objects that can be created.

Procedure size limits have been removed. Previously, the compiled size of a procedure was limited to 64K. The limit now is just available memory. Again, in a powerful OOP
system such as VFP, you have to wonder why someone would design a system that pushed these limits.

VFP 9 also lifts the former limit of 128 levels on the call stack. The new STACKSIZE setting in CONFIG.FPW allows you to set the maximum nesting level from 32 to 64,000. Once again, other than highly recursive code, we can’t see why this was an issue for VFP developers, but perhaps it was an easy thing to do so the VFP team just went ahead with it.

Improved string handling

VFP has always had fast yet powerful string handling functions. VFP 9 improves some of these functions even further.

TRIM(), RTRIM(), LTRIM(), and ALLTRIM()

You can now specify what characters the TRIM functions—TRIM(), RTRIM(), LTRIM(), and ALLTRIM()—remove from a string. These functions accept new parameters: a numeric value indicating case-sensitivity (0 or omitted for case-sensitive, 1 for case-insensitive) and one or more characters to remove. For example, in the past, you might write code like the following to strip all white space (spaces, tabs, carriage returns, and line feeds) from the start and end of a string while preserving any occurrences in the middle:

do while inlist(left(lcString, 1), chr(13), chr(10), chr(9), ' ')

  lcString = substr(lcString, 2)

enddo

do while inlist(right(lcString, 1), chr(13), chr(10), chr(9), ' ')

  lcString = left(lcString, len(lcString) - 1)

enddo

In VFP 9, this simply becomes:

lcString = alltrim(lcString, 0, chr(13), chr(10), chr(9), ' ')

ALINES()

There are a couple of changes in ALINES(). First, the third parameter, which used to indicate whether the lines should be trimmed (.T.) or not (.F.), can now accept a numeric value. See Table 1 for a list of the values, which are additive. The second change is if ALINES() is used on a Varbinary or Blob data, the resulting array contains Varbinary elements.

Table 1. The values for the third parameter to ALINES().

Value

Description

1

For character values, remove leading and trailing spaces from lines. For Varbinary and Blob values, remove trailing zeroes. This is the same as passing .T. (which you can still do for backward compatibility) in earlier versions.

2

Include the last element in the array even if it’s empty.

4

Omit empty elements in the array.

8

Use case-insensitive parsing.

16

Include the parsing characters in the array.

 

Here’s an example (taken from TestALines.PRG) that shows the differences between using 2 and 4 for the third parameter. There are four lines in the array with no parameter specified, five when 2 is used, and three when 4 is used.

text to lcText noshow

Hi

What's New

 

readers

 

endtext

lnLines0 = alines(laLines, lcText)

lnLines2 = alines(laLines, lcText, 2)

lnLines4 = alines(laLines, lcText, 4)

messagebox(transform(lnLines0) + ' lines with no parameter' + chr(13) + ;

  transform(lnLines2) + ' lines with "include last line" (2)' + chr(13) + ;

  transform(lnLines4) + ' lines with "no empty elements" (4)')

 

In the next example, also taken from TestALines.PRG, the elements only have the closing brackets when 16 is specified:

lcText = '<html><body>This is some text</body></html>'

lcMessage = ''

for lnI = 1 to alines(laLines, lcText, '>')

  lcMessage = lcMessage + laLines[lnI] + chr(13)

next lnI

messagebox('Without "include parsing characters" (16):' + chr(13) + chr(13) + ;

  lcMessage)

lcMessage = ''

for lnI = 1 to alines(laLines, lcText, 16, '>')

  lcMessage = lcMessage + laLines[lnI] + chr(13)

next lnI

messagebox('With "include parsing characters" (16):' + chr(13) + chr(13) + ;

  lcMessage)

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include TestALines.PRG.

TEXT

The TEXT command has a new clause, FLAGS nValue, that customizes the behavior of the command. Specifying 1 for nValue suppresses output to the file specified in _TEXT. (_TEXT provides a way of performing text merge to a file opened with low-level file functions. See the sample code for an example.) Specifying 2 preserves blank lines preceding the text. These values are additive. Using 2 is especially helpful because without it, you have to manually add a blank line between outputs to avoid having the end of one output appear on the same line at the start of another. A common use for this is generating log files or HTML.

The following code (TestText.PRG) shows the effect of these values. The first message shows “This is test 1This is test 2” on one line and “This is test 3” on another line (it was preceded by a carriage return and line feed because 2 was included in the FLAGS value) and the second message shows that “This is test 3” was not sent to the output file because 1 was included in the value.

_text = fcreate('somefile.txt')

text to lcText noshow

This is test 1

endtext

 

text to lcText additive noshow

This is test 2

endtext

 

text to lcText additive noshow flags 3

This is test 3

endtext

 

fclose(_text)

messagebox('The contents of the string are:' + chr(13) + chr(13) + lcText)

messagebox('The contents of the file are:' + chr(13) + chr(13) + ;

  filetostr('somefile.txt'))

The PRETEXT clause of the TEXT command supports a new (additive) value: add 8 to eliminate linefeeds before each line.

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include TestText.PRG.

STREXTRACT()

VFP 9 supports a new value in the last parameter (the flags parameter) for STREXTRACT(): add 4 to include the delimiters in the return value. This is particularly handy when parsing HTML or XML, because previously you had to manually add the delimiters back to the expression. In the following example (TestStrExtract.PRG), the return value of the first STREXTRACT() doesn’t include “<img ” or “>” but the second one does.

text to lcText noshow

<html>

<body>

Here is a picture: <img src="SomeFile.GIF" width="150" height="200">

</body>

</html>

endtext

 

messagebox('Without "include delimiters" (4) flag:' + chr(13) + chr(13) + ;

  strextract(lcText, '<img ', '>', 1) + chr(13) + chr(13) + ;

  'With "include delimiters" (4) flag:' + chr(13) + chr(13) + ;

  strextract(lcText, '<img ', '>', 1, 4))

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include TestStrExtract.PRG.

Object-related functions

Developers who write design-time tools will like this new feature: passing 0 for the third parameter to NEWOBJECT() creates the specified object without firing any initialization code (that is, code in events such as Init, Load, Activate, and BeforeOpenTables). When the object is destroyed, no destructor code (such as in Destroy or Unload) is fired. This is typically needed when you want to examine the structure of a class using AMEMBERS() (which means you have to instantiate it), but don’t want its normal behavior to occur. For example, suppose the following class exists in TestClass.PRG:

define class Noisy as Custom

  function Init

    messagebox('In init')

  endfunc

  function Destroy

    messagebox('In destroy')

  endfunc

enddefine

The following code won’t display the message boxes from the Noisy class, only the one in this code:

loObject = newobject('Noisy', 'testclass.prg', 0)

messagebox('Noisy has ' + transform(amembers(laMembers, loObject, 1)) + ;

  ' members')

release loObject

AGETCLASS() and PEMSTATUS(, 0) (which tells you if the specified member was changed) now operate in the VFP run-time. Most developers don’t need to use AGETCLASS() at run time, but some tool developers may. However, the change to PEMSTATUS(, 0) is important because one of the things it provides is the ability to tell whether a method has code in it or not.

Here’s an example. Suppose you use the Chain of Responsibility error handling mechanism described in the error handling white paper available on the Technical Papers page of http://www.stonefield.com. This mechanism needs to determine if its container has any code in its Error method or not, because if it doesn’t, passing the error to that method breaks the chain and improperly handles errors as a result. (An example of such a container is a base class Page in a PageFrame.) The following code uses PEMSTATUS(, 0) to determine whether it can safely pass the error to its container; if not, it continues to look up the containership hierarchy until it finds a container it can use.

loParent = This.Parent

do while vartype(loParent) = 'O'

  do case

    case pemstatus(loParent, 'Error', 0)

      exit

    case type('loParent.Parent') = 'O'

      loParent = loParent.Parent

    otherwise

      loParent = .NULL.

  endcase

enddo while vartype('loParent') = 'O'

if vartype(loParent) = 'O'

  loParent.Error(tnError, tcMethod, tnLine)

else  

* handle the error ourselves since this is the end of the chain

endif vartype(loParent) = 'O'

In earlier versions of VFP, this code only works at run time if debug information is included in the EXE. In VFP 9, this works even if the debug info setting is off.

One other PEMSTATUS() change: PEMSTATUS(, 5) now returns .F. for hidden
native properties. Visually subclassable objects, such as DataEnvironment, Relation, and CursorAdapter, inherit from an internal RECT class, so they have properties defined in
that class, such as Visible, Top, Left, and so forth, that aren’t applicable. VFP hides these properties in the Properties window and IntelliSense, and AMEMBERS() doesn’t see
them, but in earlier versions of VFP, PEMSTATUS(, 5) returns .T. The VFP 9 version now returns .F.

Microsoft added the Collection base class in VFP 8, and it’s been a wonderful addition except for one peculiarity: if you add objects to a collection, the FOR EACH command returns COM objects rather than VFP objects. This causes problems with functions such as AMEMBERS() and COMPOBJ(), as TestForEach.PRG illustrates:

loCollection = createobject('Collection')

loCollection.Add(createobject('Custom'))

for each loObject in loCollection

  messagebox('The object has '+ transform(amembers(laMembers, loObject, 1)) + ;

    ' members as a COM object')

next loObject

for each loObject in loCollection foxobject

  messagebox('The object has '+ transform(amembers(laMembers, loObject, 1)) + ;

    ' members as a VFP object')

next loObject

The first message shows zero members for the object. That’s obviously not right, but has to do with the fact that loObject isn’t a VFP Custom object, but is recast as a COM object. The second message displays the correct value; because of the new FOXOBJECT keyword in the FOR EACH command, loObject is a real VFP object.

CLEAR CLASSLIB is enhanced in VFP 9 to implicitly do a CLEAR CLASS for each class within the specified class library.

You can no longer set the value of a class’ property to an instantiated object when the property is defined. For example, the assignment to MyProperty in following code now gives a “statement is not valid in a class definition” error when you try to instantiate the class:

define class MyClass as Custom

  MyProperty = createobject('Custom')

enddefine

Perform the assignment to the property in the Init method of the class instead.

Text Box: "

TestForEach.PRG is included in the Developer Download files for this chapter, available at www.hentzenwerke.com.

Windows message events

Windows communicates events to applications by passing them messages. Although VFP exposes some of these messages through events in VFP objects, such as MouseDown and Click, many messages are not available to VFP developers. One common request is the ability to detect an application switch. For example, we’re aware of a VFP application that hooks into GoldMine, a popular contact management system, displaying additional information about the current contact. If the user switches to GoldMine, moves to a different contact, and then switches back to the VFP application, it would be nice to refresh the display so it shows information about the new contact. Unfortunately, there was no direct way to do this in earlier versions of VFP; the application uses a timer to constantly check which contact is currently displayed in GoldMine.

VFP 9 extends the BINDEVENT() function added in VFP 8 to support Windows messages. The syntax for this use is:

bindevent(hWnd, nMessage, oEventHandler, cDelegate)

where hWnd is the Windows handle for the window that receives events, nMessage is the Windows message number, and oEventHandler and cDelegate are the object and method that fire when the message is received by the window. Unlike VFP events, only one handler can bind to a particular hWnd and nMessage combination. Specifying a second event handler object or delegate method causes the first binding to be replaced with the second. VFP doesn’t check for valid hWnd or nMessage values; if either is invalid, nothing happens because the specified window can’t receive the specified message.

For hWnd, you can specify _Screen.hWnd or _VFP.hWnd to trap messages sent to the application or a form’s hWnd for those messages sent to the form. VFP controls don’t have a Windows handle, but ActiveX controls do, so you also bind to them.

There are hundreds of Windows messages. Examples of such messages are: WM_POWERBROADCAST (0x0218), sent when a power event occurs such as low battery or switching to standby mode; WM_THEMECHANGED (0x031A), which indicates the Windows XP theme has changed; and WM_ACTIVATE (0x0006), raised when switching to or from an application. (Windows messages are usually referred to by a name starting with WM_.) Documentation for almost all Windows messages is available at http://msdn.microsoft.com/library/en-us/winui/winui/windowsuserinterface/windowui.asp. The values for the WM_ constants are in the WinUser.H file that’s part of the Platform SDK, which you can download from http://www.microsoft.com/msdownload/platformsdk/sdkupdate/.

The event handler method must accept four parameters: hWnd, the handle for the window that received the message, nMessage, the Windows message number, and two Integer parameters, the contents of which vary depending on the Windows message (the documentation for each message describes the values of these parameters). The method must return an Integer, which contains a result value. One of the return values is BROADCAST_QUERY_DENY (0x424D5144, which represents the string “BMQD”) that prevents the event from occurring.

If you want the message to be processed in the normal manner, which is something most event handlers should do, you have to call the VFP Windows message handler in the event handler method; this is sort of like using DODEFAULT() in VFP method code. The event handler method most likely returns the return value of the VFP Windows message handler. Here’s an example of an event handler that does this (it does nothing else):

lparameters hWnd, ;

  Msg, ;

  wParam, ;

  lParam

local lnOldProc, ;

  lnResult

#define GWL_WNDPROC -4

declare integer GetWindowLong in Win32API ;

  integer hWnd, integer nIndex

declare integer CallWindowProc in Win32API ;

  integer lpPrevWndFunc, integer hWnd, integer Msg, integer wParam, ;

  integer lParam

lnOldProc = GetWindowLong(_screen.hWnd, GWL_WNDPROC)

lnResult  = CallWindowProc(lnOldProc, hWnd, Msg, wParam, lParam)

return lnResult

Of course, the event handler doesn’t need to declare the Windows API functions or call GetWindowLong each time; you could put that code in the Init method of the class, storing the return value of GetWindowLong in a custom property, and then using that property in the call to CallWindowProc in the event handler. The next sample shows this.

To determine which messages are bound, use AEVENTS(ArrayName, 1). It fills the specified array with one row per binding and four columns, containing the values of the parameters passed to BINDEVENT().

You can unbind events using UNBINDEVENT(hWnd [, nMessage ]). Omitting the second parameter unbinds all messages for the specified window. Pass only 0 to unbind all messages for all windows. Events are also automatically unbound the next time the message occurs after the event handling object is destroyed.

The VFP team added three SYS() functions related to Windows events in VFP 9. SYS(2325, wHandle) returns the wHandle (an internal VFP wrapper for hWnd) for the client window of the window whose wHandle is passed as a parameter. (A client window is a window inside a window; for example, _Screen is a client window of _VFP.) SYS(2326, nWnd) returns the wHandle for the window specified with hWnd. SYS(2327, wHandle) returns the hWnd for the window specified with wHandle. The documentation for these functions indicates they’re for BINDEVENT() scenarios using the VFP API Library Construction Kit. However, you can also use them to get the hWnd for the client window of a VFP IDE window. The FindIDEClientWindow method of the IDEWindowsEvents class in TestWinEventsForIDE.PRG shows an example using these functions.

TestWinEventsForIDE.PRG demonstrates event binding to VFP IDE windows. Set lcCaption to the caption of the IDE window you want to bind events to, run the program, and activate and deactivate the window, move it, resize it, and so forth. You should see Windows events echoed to the screen. When you finish, type RESUME and press Enter in the Command window to clean up. To test this with the client window of an IDE window, uncomment the indicated code. You can also bind to other events by adding BINDEVENT() statements to this code; use the constants in WinEvents.H for the values for the desired events.


Text Box: ¥

TestWinEventsForIDE.PRG only works with non-dockable IDE windows, so before you run this program, right-click in the title bar of the window you want to test and ensure Dockable is turned off.

#include WinEvents.H

 

lcCaption      = 'Command'

loEventHandler = createobject('IDEWindowsEvents')

lnhWnd         = loEventHandler.FindIDEWindow(lcCaption)

* Uncomment this code to receive events for the window's client window instead

*lnhWnd         = loEventHandler.FindIDEClientWindow(lcCaption)

if lnhWnd > 0

  bindevent(lnhWnd, WM_SETFOCUS,      loEventHandler, 'EventHandler')

  bindevent(lnhWnd, WM_KILLFOCUS,     loEventHandler, 'EventHandler')

  bindevent(lnhWnd, WM_MOVE,          loEventHandler, 'EventHandler')

  bindevent(lnhWnd, WM_SIZE,          loEventHandler, 'EventHandler')

  bindevent(lnhWnd, WM_MOUSEACTIVATE, loEventHandler, 'EventHandler')

  bindevent(lnhWnd, WM_KEYDOWN,       loEventHandler, 'EventHandler')

  bindevent(lnhWnd, WM_KEYUP,         loEventHandler, 'EventHandler')

  bindevent(lnhWnd, WM_CHAR,          loEventHandler, 'EventHandler')

  bindevent(lnhWnd, WM_DEADCHAR,      loEventHandler, 'EventHandler')

  bindevent(lnhWnd, WM_KEYLAST,       loEventHandler, 'EventHandler')

  clear

  suspend

  unbindevents(0)

  clear

else

  messagebox('The ' + lcCaption + ' window was not found.')

endif lnhWnd > 0

 

define class IDEWindowsEvents as Custom

  cCaption = ''

  nOldProc = 0

 

  function Init

    declare integer GetWindowLong in Win32API ;

      integer hWnd, integer nIndex

    declare integer CallWindowProc in Win32API ;

      integer lpPrevWndFunc, integer hWnd, integer Msg, integer wParam, ;

      integer lParam

    declare integer FindWindowEx in Win32API;

      integer, integer, string, string

    declare integer GetWindowText in Win32API ;

      integer, string @, integer

    This.nOldProc = GetWindowLong(_screen.hWnd, GWL_WNDPROC)

  endfunc

 

  function FindIDEWindow(tcCaption)

    local lnhWnd, ;

      lnhChild, ;

      lcCaption

    This.cCaption = tcCaption

    lnhWnd        = _screen.hWnd

    lnhChild      = 0

    do while .T.

      lnhChild = FindWindowEx(lnhWnd, lnhChild, 0, 0)

      if lnhChild = 0

        exit

      endif lnhChild = 0

      lcCaption = space(80)

      GetWindowText(lnhChild, @lcCaption, len(lcCaption))

      lcCaption = upper(left(lcCaption, at(chr(0), lcCaption) - 1))

      if lcCaption = upper(tcCaption)

        exit

      endif lcCaption = upper(tcCaption)

    enddo while .T.

    return lnhChild

  endfunc

 

  function FindIDEClientWindow(tcCaption)

    local lnhWnd, ;

      lnwHandle, ;

      lnwChild

    lnhWnd = This.FindIDEWindow(tcCaption)

    if lnhWnd > 0

      lnwHandle = sys(2326, lnhWnd)

      lnwChild  = sys(2325, lnwHandle)

      lnhWnd    = sys(2327, lnwChild)

    endif lnhWnd > 0

    return lnhWnd

  endfunc

 

  function EventHandler(hWnd, Msg, wParam, lParam)

    ? 'The ' + This.cCaption + ' window received event #' + transform(Msg)

    return CallWindowProc(This.nOldProc, hWnd, Msg, wParam, lParam)

  endfunc

enddefine

When you run TestWinEventsForIDE.PRG, you’ll find not all events occur for all IDE or client windows. This is likely due to the way VFP implements windows, which is somewhat different from other Windows applications.

WindowsMessagesDemo.SCX is another example. It shows hooking into activate and deactivate events as well as certain Windows shell events, such as inserting or removing a CD or USB drive. The latter shows an interesting use of Windows events: the code registers _VFP to receive a subset of Windows shell events as a custom Windows event. The following code, taken from the Init method of this form, handles the necessary setup. Items in upper-case are constants defined in either WinEvents.H or ShellFileEvents.H. The call to SHChangeNotifyRegister tells Windows to register _VFP to receive disk events, media insertion and removal events, and drive addition and removal events using a custom message. This code then binds device change events and the custom message we just defined to the HandleEvents method of the form:

declare integer SHChangeNotifyRegister in shell32 ;

  integer hWnd, integer fSources, integer fEvents, integer wMsg, ;

  integer cEntries, string @SEntry

 

* Register us to receive certain shell events as a custom Windows event.

 

lcSEntry = replicate(chr(0), 8)

This.nShNotify = SHChangeNotifyRegister(_vfp.hWnd, SHCNE_DISKEVENTS, ;

  SHCNE_MEDIAINSERTED + SHCNE_MEDIAREMOVED + SHCNE_DRIVEADD + ;

  SHCNE_DRIVEREMOVED, WM_USER_SHNOTIFY, 1, @lcSEntry)

 

* Bind to the Windows events we're interested in.

 

bindevent(_vfp.hWnd, WM_DEVICECHANGE,  This, 'HandleEvents')

bindevent(_vfp.hWnd, WM_USER_SHNOTIFY, This, 'HandleEvents')

Text Box: ¥

The call to SHChangeNotifyRegister requires Windows XP. If you’re using an earlier operating system, comment out the assignment statement for This.nSHNotify in the Init method of WindowsMessagesDemo.SCX.

Support for Windows event binding is an incredible addition to VFP; it allows you to hook into just about anything that goes on in Windows. We expect to see many cool uses of this as the VFP community starts to learn about its capabilities.

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include TestWinEventsForIDE.PRG and WindowsMessagesDemo.SCX.

Internationalization issues

FoxPro has had strong support for internationalization issues since FoxPro 2.x and the addition of code pages and collate sequences. VFP 9 adds additional support.

The fourth parameter for GETFONT() indicates what language script to display by default. In VFP 8, passing 0 means you want the Script combo box in the Font dialog disabled, while 1 means select Western by default. The problem is that unless you know the default language script setting for the user, what value should you pass for this parameter? In VFP 9, 0 now means Western and 1 means the user default script. As before, omit the parameter to disable the combo box.

There are three different mechanisms to indicate the character set for text: the code page, which is used for files such as tables, the FontCharSet property of various controls such as text boxes, and the locale ID, which applies to certain functions such as STRCONV(). Unfortunately, in earlier versions of VFP, there wasn’t an easy way to convert between these different mechanisms. In VFP 9, STRCONV() accepts a locale ID, code page, or FontCharSet value for the third parameter (previously, only a locale ID was supported). A new fourth parameter indicates which type is used: 0 or omitted means locale ID, 1 means code page, and 2 means FontCharSet.

SET SYSMENU and DEFINE POPUP have new RTLJUSTIFY and LTRJUSTIFY clauses that tell VFP to justify the text in a right-to-left or left-to-right manner. These clauses are ignored unless Windows is configured to a Middle-Eastern locale. You can also set the justification for ToolTips using the new SYS(3009) function. SYS(3009, 1) specifies
right-to-left and SYS(3009, 0) specifies left-to-right. This function returns the current value
as a character.

There are three other internationalization changes. The new SYS(3101, [ nCodePage ]) function sets the code page used for character data translation in COM operations in the current datasession; it returns the current setting as a numeric value. SYS(3007, [ nFontCharSet ]) sets the language script used for ToolTips for controls; it returns the current value as a character. The FONT clause of the DEFINE MENU, DEFINE PAD, DEFINE POPUP, DEFINE BAR, DEFINE WINDOW, MODIFY WINDOW, and BROWSE commands can now accept a value for the language script to use; for example, BROWSE FONT Arial, 10, 161 specifies the Greek character set.

Other enhanced commands and functions

SET PATH

SET PATH has a new ADDITIVE clause that allows you to add to the existing VFP path. Formerly, you had to do something like:

lcCurrPath = set('PATH')

set path to &lcCurrPath, C:\MyNewDirectory

Now it’s as simple as:

set path to 'C:\MyNewDirectory' additive

Note that quotes are required around the path even if there aren’t any spaces in it when you use the ADDITIVE clause; if you omit them, the path becomes “C:\MyNewDirectory ADDITIVE.” The size of the path has also increased from 1024 to 4095 characters.

TYPE()

The TYPE() function has a new parameter: specify 1 to determine if the variable is an array or collection; TYPE() returns “A” if it’s an array, “C” if it’s a collection, or “U” if neither. For example (TestType.PRG):

X = 1

dimension Y[1]

clear

? type('X', 1)           && displays "U"

? type('Y')              && displays "L", the data type for Y[1]

? type('Y', 1)           && displays "A"

loObject = createobject('Collection')

? type('loObject')       && displays "O"

? type('loObject', 1)    && displays "C"

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include TestType.PRG.

INPUTBOX()

You can now specify what value to return if the user clicks Cancel or presses Esc in the dialog displayed by the INPUTBOX() function by specifying a value for the new sixth parameter. In previous versions of VFP, this function returns a blank string in this case. Here’s an example that demonstrates this; the message displays “Cancel value” when you click the Cancel button or press Esc.

messagebox(inputbox('Click on cancel', 'Test inputbox()', 'Default value', 0, ;

  'Timeout value', 'Cancel value'))

SYS(1104), SYS(3056), and SYS(2019)

Three SYS() functions have been enhanced:

·         SYS(1104), which purges cached memory, now supports an optional workarea or alias parameter. Specifying this tells VFP to purge the cache for the specified table or cursor.

·         SYS(3056) can now write the settings of the Tools | Options dialog to the Registry if you use 2 as the optional parameter. This does the same thing as clicking the Set As Default button in that dialog.

·         VFP 8 added the ability to specify an external configuration file by adding ALLOWEXTERNAL = ON to the configuration file built into an EXE. SYS(2019) returns the name of the internal configuration file if you specify 2 as the optional parameter. Specify 1 or omit the parameter to return the name of the external configuration file.

FFLUSH()

The FFLUSH() function, used to write out to disk changes to a file opened with low-level file functions, has a new second parameter. Pass .T. to force VFP to immediately write the file to disk. If you omit this parameter or pass .F., VFP writes the file when it gets a chance to.

SET DOHISTORY

Remember the SET DOHISTORY command? Okay, we don’t either. It’s a command that outputs lines of code as they execute. This was important in the days before Microsoft added the Trace window to FoxPro, but we haven’t used it since. (Even the help topic for SET DOHISTORY states it’s for backward compatibility only.) In VFP 9, the output now goes to the Debug Output window, if it’s open, rather than to the current output device (such as _Screen or a printer if SET PRINT ON is used).

MROW() and MCOL()

You can now pass 0 to MROW() and MCOL() to return the position of the mouse pointer based on the active form. This prevents a problem using these functions under some conditions: they normally reference the form whose name is returned by WOUTPUT(), but WOUTPUT() doesn’t return the name of the form when its AllowOutput property is .F.

EXECSCRIPT()

You can now pass parameters to code executed by EXECSCRIPT() by reference. For example, the following code, taken from TestExecScript.PRG, gives a “missing operand” error on the second EXECSCRIPT() statement in VFP 8 but runs properly in VFP 9:

text to lcCode noshow

lparameters tnValue

tnValue = tnValue + 1

endtext

 

x = 5

execscript(lcCode, x)

? x  && displays 5

execscript(lcCode, @x)

? x  && displays 6

Text Box: "

The Developer Download files for this chapter, available at www.hentzenwerke.com, include TestExecScript.PRG.

SCATTER

The SCATTER command no longer allows the MEMVAR and NAME clauses to be specified at the same time, since that use is ambiguous. Earlier versions permit this, with the MEMVAR clause overriding NAME.

BINTOC() and CTOBIN()

These functions, which convert between numeric values and their binary character equivalents, are much more useful in VFP 9 because they’ve been enhanced to support more types of conversion. If you write code that does low-level manipulation of binary files, such as DBFs, or calls Windows API functions, you can replace many lines of code with single calls to BINTOC() and CTOBIN().

In earlier versions of VFP, the first parameter for BINTOC() is an integer value (the numeric value to convert to a binary string) and the second (which is optional; the default is 4) is the values 1, 2, or 4, indicating the length of the resulting string. VFP 9 supports a wider range of values for the first parameter and the second parameter can now accept character “flags” values.

Table 2 displays the possible values for the second parameter and the relationship they have with the first parameter. You can specify 1, 2, 4, or 8 as numeric or character values and “B,” “F,” “R,” or “S” in upper or lower case. “R” and “S” can be combined with one of the other values (for example, “4RS”) but the other values are mutually exclusive.

Table 2. The values for second parameter for BINTOC().

Value

Description

1

Creates a one-byte string. The first parameter must be an Integer between -128 and 127.

2

Creates a two-byte string. The first parameter must be an Integer between -32,768 and 32,767.

4

Creates a four-byte string. The first parameter must be an Integer between - 2,147,483,648 and 2,147,483,647. This is the default if the second parameter is omitted.

8

Creates an eight-byte string. The first parameter must be a Numeric, Float, Double, or Currency value.

B

Creates an eight-byte string from the Double value in the first parameter.

F

Creates a four-byte string from the Numeric or Float value in the first parameter.

R

Reverses the binary string so the least significant byte is in the first character and the most significant in the last character. This is the format normally used for binary values on Intel processors.

S

Prevents the sign bit from being toggled.

 

In earlier versions of VFP, CTOBIN() accepted just one parameter: the binary string to convert to a numeric value. In VFP 9, the optional second parameter specifies how to convert the string. See Table 3 for the possible values. As with BINTOC(), you can specify 1, 2, 4, or 8 as numeric or character values and “N,” “Y,” “R,” or “S” in upper or lower case. “R” and “S” can be combined with one of the other values but the other values are mutually exclusive.

Table 3. The values for second parameter for CTOBIN().

Value

Description

1

The first parameter is a one-byte Integer data type. This value doesn’t have to be specified but can be for clarity.

2

The first parameter is a two-byte Integer data type. This value doesn’t have to be specified but can be for clarity.

4

The first parameter is a four-byte Integer data type. This value doesn’t have to be specified but can be for clarity.

8

The first parameter is an eight-byte expression.

B

The first parameter is an eight-byte Double data type. This is the default if the first parameter is eight bytes and the second parameter isn’t specified.

N

The first parameter is a four- or eight-byte Numeric or Float data type.

Y

The first parameter is an eight-byte Currency data type. CTOBIN() returns a Currency value.

R

The first parameter has the least significant byte in the first character and the most significant in the last character.

S

The sign bit will not be treated as a sign bit.

 

Due to their limited conversion abilities, the most common use for these functions in earlier versions of VFP is to create smaller index keys. For example, Integers are eight-byte values but an index on BINTOC(MyIntegerField) takes only four bytes per key. The VFP 9 versions, however, are a lot more useful, especially since they can convert to and from the format for binary values used on Intel processors. This format is often used in binary files and Windows API functions.

An example of how BINTOC() and CTOBIN() can be used with Windows API functions is the _ComDlg class in _System.VCX in the FFC subdirectory of the VFP home directory. This class provides a simple OOP interface to the Common Dialogs API functions, giving you more control over the appearance and behavior of the dialogs than the VFP GETFILE() and PUTFILE() functions. The DialogHandler method of _ComDlg has to deal with Windows structures, which can be represented in VFP as strings containing binary values. DialogHandler calls the IntegerToString method of this class to convert a numeric value into its binary equivalent, and then stores this value into the appropriate place in the string “structure.” Here’s the code for IntegerToString:

LPARAMETERS nInteger, nBytes

LOCAL cRetVal

IF pCount() < 2

  nBytes = 4

ENDIF

cRetVal = ""

FOR nCurByte = 1 to nBytes

  cRetVal = cRetVal + ;

    CHR(BITAND(BITRSHIFT(nInteger, 8 * (nCurByte -1) ), 255))

ENDFOR

RETURN cRetVal

In VFP 9, calls to this method can be replaced with either of the following statements, depending on the desired length of the result:

bintoc(nInteger, "2RS")

bintoc(nInteger, "4RS")

DialogHandler also calls the StringToInteger method in this class to convert binary values in the string “structure” back to numeric values; it does this to obtain the return values from the API functions. Here’s the code for StringToInteger:

LPARAMETERS cPDWORD, nBytes

LOCAL nCurByte, nRetVal

IF PCOUNT() < 2

  nBytes = LEN(cPDWord)

ENDIF

nRetVal = 0

FOR nCurByte = 1 to nBytes

  nRetVal = nRetVal + ASC(SUBSTR(cPDWord, nCurByte, 1))*(256^(nCurByte-1))

ENDFOR

RETURN nRetVal

Calls to this method can be replaced with:

ctobin(cPDWord, 'RS')


Here are some other examples:

x = bintoc(10.789)

? ctobin(x)             && Returns 10

x = bintoc($23.45, 8)

? ctobin(x, 'Y')     && Returns 23.45

? ctobin(x, 'N')     && Returns 1.3142213770657E-286

? ctobin(x, 'B')     && Returns 0.0000000000000E+0

Other new commands and functions

ICASE()

ICASE(), which is an abbreviation for “Immediate CASE,” is one of our favorite new functions. It’s similar to IIF(), except it acts like a DO CASE structure rather than an IF structure. This function saves having to use nested IIF() calls when you need an expression that selects between more than two values. Field expressions in the Report Designer are a common use for this. For example, rather than:

iif(SHIPVIA = 1, 'Fedex', iif(SHIPVIA = 2, 'UPS', ;

  iif(SHIPVIA = 3, 'DHL', 'Mail')))

you can use:

icase(SHIPVIA = 1, 'Fedex', SHIPVIA = 2, 'UPS', SHIPVIA = 3, 'DHL', 'Mail')

Parameters go in pairs. The first of a pair is the expression to evaluate and the second is the value to return if the expression is .T. If the last parameter isn’t one of a pair (that is, there’s an odd number of parameters), it’s considered to be the “otherwise” value, such as “Mail” in the preceding example. If you don’t supply an “otherwise” value and none of the expressions evaluates to .T., ICASE() returns .NULL.

CAST()

CAST(), another of our favorite new functions, allows you to convert data from one type into another. Data type conversion must be within reason, of course; Date fields cannot be converted to Numeric and vice versa. See the VFP help topic for a conversion chart showing which types can be converted to other types.

The syntax for CAST() is:

CAST( uExpression AS DataType [ ( nFieldWidth [, nPrecision ] ) ]

  [ NULL | NOT NULL ] )

uExpression is the expression to convert and DataType is the data type to convert it to. You can specify DataType as a letter (for example, “T” for DateTime”) or a name (as discussed in Chapter 12, “Other Data Changes,” VFP 9 supports long names for data types, such as “Numeric”). As in the CREATE TABLE command, some data types require a field width and possibly precision. You can also indicate whether null values are allowed or not.

You can use CAST() anywhere you would use an expression, even in SQL SELECT statements. Here’s a simple example that converts a DateTime field in a table into a Date value in a cursor:

select ORDERID, cast(ORDERDATE as Date) as ORDERDATE from ORDERS

CAST() can be used to force a SQL SELECT statement to create a field of the correct data type and size from an expression. For example, this SQL SELECT statement:

select UNITPRICE * QUANTITY as TOTAL, ;

  iif(UNITPRICE * QUANTITY >= 1000, 50, 0) as SHIPPING ;

  from ORDERDETAILS

works properly if the first record has a total amount of $1,000 or more, but not if the amount is less. That’s because VFP uses the first record to determine the data type and size of the column. If the amount is less than $1,000, the expression returns 0, so the SHIPPING column will be N(1, 0). As a result, it isn’t big enough for 50, and displays asterisks instead. A trick that helps with this is to specify the smallest number using a placeholder with the proper number of digits, such as 00. However, it may not work in every situation, such as when you call a user-defined function. It also can’t help if you want a different data type than VFP thinks you need, such as Currency.

If you change the SQL SELECT statement to:

select UNITPRICE * QUANTITY as TOTAL, ;

  cast(iif(UNITPRICE * QUANTITY >= 1000, 50, 0) as Numeric(5, 2)) as SHIPPING ;

  from ORDERDETAILS

or even:

select UNITPRICE * QUANTITY as TOTAL, ;

  cast(iif(UNITPRICE * QUANTITY >= 1000, 50, 0) as Currency) as SHIPPING ;

  from ORDERDETAILS

the values in the first record are unimportant; SHIPPING will always be the desired data type and size.

See Chapter 8, “SQL Changes,” for more examples using CAST().

Text Box: "

TestCast.PRG, which shows how CAST() works, is included in
the Developer Download files for this chapter, available at www.hentzenwerke.com.

SYS(2910)

What would a new version of VFP be without new SYS() functions? In addition to those discussed earlier in this chapter, SYS(2910) allows you to determine or set the number of items that appear in IDE (Interactive Development Environment) list and combo boxes, such as IntelliSense, and in the new AutoComplete feature for text boxes (discussed in Chapter 13, “Forms and Controls”). This corresponds to the List display count setting in the View page of the Tools | Options dialog. The range is 5 to 200; the default is 15. SYS(2910) by itself returns the current value as a character string.

Tablet PC support

There are two new features that provide better support for Tablet PCs. The ISPEN() function returns .T. if the last mouse event was a pen tap. A logical place to use this is in the Click method of a control if you want to perform different behavior for mouse clicks and pen taps. A new property of _Screen, DisplayOrientation, indicates the display orientation. The values for this property are shown in Table 4. Interestingly, this property is read-write; changing its value causes the display orientation to change as if you changed it from the Control Panel.

Table 4. The values for _Screen.DisplayOrientation.

Value

Description

0

Upright landscape

1

Upright portrait

2

Inverted landscape

3

Inverted portrait

 

Clearing error information

The new CLEAR ERROR command resets the internal error structures to the same state as if no error happened. This means AERROR() does not alter the specified array, the ERROR() function returns zero, MESSAGE() and MESSAGE(1) return an empty string, and SYS(2018) returns an empty string.

This command allows developers to control the reporting of errors more efficiently. Prior to Visual FoxPro 9, AERROR() would fill the passed array with the details of last error whether an error occurred within the code you were checking or not. Now you can CLEAR ERROR before executing code with a potential problem, then check the results with AERROR() to see if an error was triggered.

Microsoft warns in the Help file against using CLEAR ERROR inside structured error handling (TRY…CATCH…FINALLY) because the exception object might not be valid after CLEAR ERROR.

Text Box: "

The Developer Downloads for this chapter, available from www.hentzenwerke.com, include a program called ClearErrorExample.PRG, which demonstrates how CLEAR ERROR
works. Run this program in an older version of VFP and then in VFP 9
to see the difference with respect to the behavior of AERROR().

Summary

The language enhancements in VFP 9 make it easier than ever before to deliver the kinds of powerful applications our users expect. Some of them make your code faster to write, faster to run, and more maintainable because you can write less code. Others allow you to do things you couldn’t before, such as hooking into Windows events.

 

 

Updates and corrections for this chapter can be found on Hentzenwerke’s website, www.hentzenwerke.com. Click “Catalog” and navigate to the page for this book.