04.08.2013 Views

Enduring Variables - dFPUG-Portal

Enduring Variables - dFPUG-Portal

Enduring Variables - dFPUG-Portal

SHOW MORE
SHOW LESS

You also want an ePaper? Increase the reach of your titles

YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.

February 1996<br />

Create "<strong>Enduring</strong> <strong>Variables</strong>" in FoxPro<br />

Peter de Valença<br />

Here's a creative way to store variables that persist throughout an entire FoxPro or Visual FoxPro<br />

session, yet are immune to destruction by commands like CLEAR MEMORY. "<strong>Enduring</strong><br />

<strong>Variables</strong>" are great for desktop accessories, add-ons to the development environment, and as a<br />

standard interface between "plug and play" application components.<br />

The problem: FoxPro has a wide range of options t store and retrieve information, including<br />

types of variables (PRIVATE, PUBLIC, REGIONAL), memory files, databases, virtual tables<br />

and even ordinary text files. However, if you want to develop a variety of applications that share<br />

information in some regard, each method has a disadvantage. Some applications CLEAR<br />

MEMORY in cleanup code, thus ruling out the use of memory variables. Or, the disk location of<br />

databases, memory files, and ordinary text files may be unknown to some applications. Virtual<br />

tables (cursors) may have been closed and destroyed in the cleanup code of an application.<br />

The problem can be solved if each application is developed by the same software house.<br />

In-house protocols could instruct developers to maintain certain variables or databases. But such<br />

a protocol won't help if a customer combines applications and tools from different vendors (for<br />

example, in a menu system).<br />

Desk accessories and other FoxPro add-ons like development utilities present another problem.<br />

Add-ons often need to store values between invocations without destruction by a stray CLEAR<br />

MEMORY command.<br />

"<strong>Enduring</strong> variables" to the rescue<br />

What's needed are variables that will endure during an entire active FoxPro session, similar to<br />

FoxPro's system memory variables. I developed the function varX() to meet the need for such<br />

"enduring variables."<br />

"<strong>Enduring</strong> variables" aren't a substitute for conventional variables. They're simply an additional<br />

variable class. Think of them as a user-defined equivalent to the FoxPro system memory<br />

variables. Use them when you want the safety and persistence of a system memory variable, but<br />

the global accessibility of a PUBLIC variable. A varX() variable can be used wherever you'd use<br />

STATIC variables in languages that support them, or object properties in an OO language like<br />

Visual FoxPro. Visual FoxPro developers can also use varX() as an alternative to global<br />

variables that need to be accessed by many different objects (although unfortunately, varX() can't<br />

be used to store object references variables of type "O").<br />

I also offer varX() to the FoxPro community in hope that it will become an international standard<br />

(1)


for communication between applications and application components. Using varX(), we could<br />

develop a variety of applications that can share information and simultaneously appear in the<br />

same menu system. Each such application could be allowed to do destructive tasks such as<br />

closing databases, clearing memory, and redefining the "default" directory, without causing<br />

trouble for other modules.<br />

How varX() works<br />

The core of varX() is an out-of-use FoxPro system memory variable. In FoxPro for DOS, the<br />

variable _SPELLCHK is used, since no spell checker is implemented in the DOS version; in all<br />

other versions of FoxPro, _FOXGRAPH is used, since that variable is unused on all platforms<br />

except DOS. All enduring variables are stored in this system memory variable, and it is treated<br />

much as if it were a memo field. Thus, enduring variables are insensitive to commands that clear<br />

memory variables or close databases. The varX() function is the interface to this buffer. It's<br />

important that you don't change the values of these system memory variables directly when using<br />

varX().<br />

I suggest you place a copy of VARX.APP in the FoxPro directory. That way, you'll be able to<br />

tell an application the location of varX(). Simply add sys(2004) to the FoxPro path prior to the<br />

first call. The procedure ADDPATH.PRG (see the sidebar "Add to the FoxPro Path") offers a<br />

secure way to add sys(2004) to the path.<br />

Some developers may want the source code to varX() in order to be able to customize things or<br />

include the code in each project. However, with varX(), version control is very important.<br />

Compatibility with older versions and copies used by other parties should have the highest<br />

priority. That's why the FoxPro community should use VARX.APP and keep it excluded from<br />

projects, and why I haven't released the source code. It's my intention to release updates on a<br />

regular base on CompuServe's FoxForum.<br />

VarX() is provided as two applications: VARX.APP runs under FoxPro 2.6 (any platform) and<br />

VVARX.APP runs under Visual FoxPro. Visual FoxPro users should rename VVARX.APP to<br />

VARX.APP so that portability of varX() calls between FoxPro 2.6 and 3.0 is preserved.<br />

Reading built-in enduring variables<br />

The first and second parameters that varX() expects make up the name of an enduring variable.<br />

The function will return the value of this variable. The type returned depends on the data type<br />

originally stored.<br />

The varX() function comes with three built-in enduring variables, which can be read as follows:<br />

C = varX( 'SYMBOLS_VARX', '' )<br />

C = varX( 'INFO_VARX' )<br />

C = varX( 'SERIALID_VARX' )<br />

`'SYMBOLS_VARX' returns the symbols table that varX() uses. The first 25 columns of output


are reserved for future enhancements. Each symbol occupies an additional 26 columns. Of these<br />

26 columns, columns 1 and 2 are used for a separator, the next 20 columns for a unique symbol<br />

ID, and the last four to define the data type. ASCII codes 13+10 are used as separators. This<br />

enables you to extract symbol information with the FoxPro MLINE() function. Notice that the<br />

second parameter is empty. In this particular case it may be left out, as has been done on the<br />

other two calls.<br />

The `INFO_VARX' call returns some information about varX(). Here's how you can extract this<br />

information with MLINE():<br />

MLINE() call Description<br />

MLINE(varX('INFO_VARX'),1) Name (always "varX()")<br />

MLINE(varX('INFO_VARX'),2) varX() version number<br />

MLINE(varX('INFO_VARX'),3) Date of this version of varX()<br />

MLINE(varX('INFO_VARX'),4) Author's ID ("PDV" = Peter de Valença)<br />

MLINE(varX('INFO_VARX'),5) Copyright Notice<br />

MLINE(varX('INFO_VARX'),6) Current number of symbols<br />

The "SERIALID_VARX" call returns a character "Serial ID" based on your Serial Number.<br />

You'll need this information when declaring your own enduring variables; more on this in a<br />

moment.<br />

Creating enduring variables<br />

It's quite easy to add new enduring variables to the list. Simply call varX() with three or four<br />

parameters. Here's the calling syntax:<br />

= varX(cSymbolName,cUniqueID,cDataType[,uInitialValue])<br />

= varX(cSymbolName,cUniqueID,cDataType[,uInitialValue])<br />

cSymbolName is the name of the enduring variable you want to create.<br />

cUniqueID is your unique developer ID.<br />

cDataType is the data type of the value you want to create (described in text below).<br />

uInitialValue is the initial value of the enduring variable (optional).<br />

The unique ID should be a character constant that's used by you or your company only, so that<br />

your declarations won't ever interfere with the correct declarations of other parties. You can<br />

obtain a unique ID for a given copy of FoxPro by calling varX('SERIALID_VARX') in<br />

interactive mode. The maximum length for the symbol name plus the unique ID is 20 characters.<br />

Don't ignore or minimize the importance of the unique ID! As a matter of fact, when I developed<br />

this function, I saw the need to recognize an international community of FoxPro developers. We


should take care not to overwrite each other's enduring variables. The unique ID is based on the<br />

serial number of your copy of FoxPro. While it's possible to administrate Symbol Names<br />

in-house, it's impossible to do so for the international community.<br />

The description of the data type must be one of the following:<br />

"C" Character Expression<br />

"Niij" Numerical Value; ii=Length, j=Decimals (Example: "N082"is a number with two decimal places<br />

and a maximum of eight digits includingdecimal point.)<br />

"D" Date<br />

"L" Logical<br />

"E" Evaluated Character Expression; Maximum of 10 evaluations(more on this will follow)<br />

"Eii" Evaluated Character Expression; ii=Maximum numberof evaluations (more on this below)<br />

For example, the following command would create a symbol, "TVAL!#0}Sa59V, that's numeric<br />

and initialized to the value 12.345:<br />

varX("tval", "!#0}Sa59V", "N103" ,12.345)<br />

There's virtually no limit to the size of character expressions. In one test I performed, a<br />

100,000-byte string was stored as an enduring variable in varX()without creating problems..<br />

Performance in creating, reading, and changing enduring variables is acceptable. As a<br />

consequence, you can store the contents of memo fields as enduring variables (but note my<br />

warning near the end of this article about the ASCII code sequence 17 + 10; this could be a<br />

problem for some binary data in memo fields, but should pose no problem for textual data).<br />

Dates are stored as a Julian day number and returned in the format specified with SET DATE.<br />

varX() has another feature. You can create enduring variables that are evaluated when read; you<br />

can even invoke entire tools and applications! The data type of such an enduring variable is "E."<br />

Bytes between text merge delimiters in the expression will be evaluated. (You can change these<br />

delimiters with the FoxPro command SET TEXTMERGE DELIMITERS.) I give some examples<br />

in the next section.<br />

The data type parameter is ignored on succeeding calls referencing an existing enduring variable<br />

(except for the data type "E;" see the next section). This implies that it's not necessary to<br />

initialize a symbol before actual usage. Thus, a call like the following will always create the<br />

enduring variable and return the expected result:<br />

#declare varXid "!#0}Sa59V"<br />

if varX("tval", varXid, "N103") > 0<br />

Reading your own enduring variables


Reading your own enduring variables isn't different from reading built-in variables. Again, the<br />

result will be of the declared data type:<br />

result = varX("mystring", varXid, "c")<br />

You can omit the third parameter if you're certain that the enduring variable was declared in<br />

advance.<br />

Changing an enduring variable<br />

The value associated with a symbol can be changed with the fourth parameter. The result will<br />

reflect the new information. For example:<br />

= varX("mystring", varXid, "c", "my string")<br />

The function varX() is meant for advanced users. There's no read-only mode; even the built-in<br />

enduring variable "SYMBOLS_VARX" can be overridden. Of course, this would corrupt the<br />

buffer, so I don't recommend doing so.<br />

How to use the data type "E"<br />

If an enduring variable is of data type "E," bytes between text merge delimiters in the expression<br />

will be evaluated. The following (somewhat contrived) examples should help make this clear.<br />

While the data type parameter is normally ignored once an enduring variable is created, it's not<br />

entirely ignored when the data type is "E." You may choose to specify "Eii" where ii is a number<br />

in the range 0-99. For that particular call, the result will reflect the situation after a maximum of<br />

ii evaluations. For example, specifying "E0" would return the plain string instead of an evaluated<br />

string.<br />

The following code shows how to invoke a function -- in this case, the FoxPro function<br />

GETDIR(). You could also choose to call one of your own applications or tools.<br />

wait window varX("getDir", varXid, "E", ;<br />

"Directory ")<br />

In this next example, you can see how a logical result is handled. The IF expression will evaluate<br />

to "YES" if the application is running on the Windows platform. The result returned by varX() is<br />

always of type character when the "data type" is "E," so .T. is returned as "YES" and .F. as<br />

"NO." If you want a different value, use IIF() to transform "YES" to .T. or to some other string.<br />

if varX("yesno", varXid, "E", "") = "YES"


Numeric values are also returned as character strings with type "E" enduring variables, as shown<br />

in the following example, which uses VAL() to convert back to numeric:<br />

number = val(varX("number", varXid, "E", ""))<br />

The previous example also illustrates that you can pass any valid expression to varX(); in fact,<br />

varX() evaluates expressions using the FoxPro EVALUATE() function, so anything that<br />

EVALUATE() can process can be passed to varX().<br />

The following example can cause an infinite loop. In order to prevent that, the evaluation routine<br />

stops evaluating expressions after ten evaluations. This number can be adjusted for a specified<br />

enduring variable; for instance, in the previous examples, the data type parameter might have<br />

been "E01" or "E1".<br />

aaa = ""<br />

bbb = ""<br />

result = varX("aaabbb", varXid, "E", ;<br />

"Result is: ")<br />

Here's a final example to show how an incorrect call to varX() is handled:<br />

wait window varX("wrong", varXid, "E", ;<br />

"Not possible: ")<br />

The result of the above call will be: "Not possible: {{???: sys()}}." In other words, varX()<br />

echoes back three question marks, a colon, a space, and the passed expression -- all enclosed in<br />

double curly braces -- if it can't successfully evaluate the expression.<br />

Error messages<br />

Several error conditions are recognized by varX() and will result in the a simple error message<br />

being displayed with WAIT WINDOW NOWAIT. No error routine (either the default FoxPro<br />

routine or yours) is triggered and program execution will continue. These messages all have to<br />

do with reporting incorrect arguments passed to varX(). The exception is the message<br />

"Corrupted varX() buffer," which probably indicates that the system memory variable used as<br />

varX's buffer has been corrupted by some code outside of varX().<br />

One kind of error may go unnoticed in some cases. Data is stored in a variable length format by<br />

varX(). The ASCII codes 17 + 10 are used as separators. If this character sequence is part of a<br />

value, varX() will erroneously assume this is the spot where the next value is stored.<br />

Consequently, the buffer is corrupted from this position on.. You should take care not to store<br />

this sequence of ASCII codes, particularly when dealing with binary data from memo fields.


Conclusion<br />

I realize that varX() isn't a panacea for all problems, but it's a step ahead. I think of it as a useful<br />

tool in my toolkit -- a tool that simply extends my possibilities.<br />

Peter de Valença is a freelance software developer who works on projects for large companies and has<br />

specialized in FoxPro for the past three years. He has a Ph.D. in social psychology from the University of<br />

Amsterdam. Peter solicits your feedback about varX(). pvalenca@digiface.nl.<br />

Sidebar: Add to the FoxPro Path<br />

The following code will conditionally add a path to the FoxPro search path:<br />

PROCEDURE addPath<br />

parameter _px && make this lparameter in Visual FoxPro<br />

if atc(';' + _px + ';', ';' + set( 'path' ) + ';') = 0<br />

set path to (set( 'path' ) + ';' + _px)<br />

RETURN .T.<br />

endif<br />

RETURN .F.<br />

The character parameter _px is required and represents the path that must be added to the FoxPro<br />

path list. The path won't be added if it' s already in the FoxPro path.<br />

The returned logical value can be used to determine whether or not the path has been changed.<br />

As a usage example, to tell your application to also search the FoxPro directory, you might add<br />

the following call to addPath:<br />

do addPath with sys(2004)<br />

Rockin' with FoxPro<br />

Whil Hentzen<br />

What do you say when you're asked to join the All-Stars? Well, first of all you say "Yes." But<br />

I'm still a little cowed, following in the footsteps of Hart, Slater, and Grommes as editor of<br />

FoxTalk. As Bob has mentioned previously, this has been an orderly and planned transition -well,<br />

as orderly as could be between two guys who are both working 100-hour weeks. As you


know, demands on Bob grew beyond the time he had available, and something had to give.<br />

Fortunately, having written for FoxTalk for several years, I was in a good position to jump in.<br />

The changing of the guard is a good opportunity to review what's worked in the past, and to<br />

preview what we might see in the future. As you're aware, FoxTalk has been the high-end<br />

technical journal for FoxBASE and FoxPro for nearly a decade, and we're committed to retaining<br />

this ground. FoxPro has grown into such a huge product that it would be virtually impossible for<br />

any single publication to cover it completely -- and fortunately, there are a number of excellent<br />

resources serving the needs of FoxPro users and developers. Our niche is the serious,<br />

professional developer interested in developing robust, high-quality custom and vertical market<br />

applications with the various dialects of FoxPro, and who doesn't need introductory lessons in<br />

the product. We'll continue to target those individuals.<br />

A primary concern of many of you may be, "How will FoxTalk balance FoxPro 2.x and VFP?"<br />

and more generally, "What will I get out of FoxTalk in the next year or two?" To answer that, let<br />

me first explain what my work-a-day world looks like. There's sometimes a concern that<br />

someone on the author/speaker circuit has lost touch with the real world -- that so much time is<br />

spent writing that the writer doesn't deal with customers anymore -- and viewpoints become<br />

more theoretical than practical. While I've had my share of the "circuit," my business is writing<br />

custom applications. I run a shop with a half dozen people, and all we do is custom FoxPro work.<br />

About a third is still in FoxPro for DOS (believe it or not), half is in FoxPro for Windows 2.x,<br />

and the remainder is Visual FoxPro. As I write this, I'm wrapping up the delivery of a FoxPro for<br />

DOS application for about 50 sites around the country that must run on a 486/33 with 4M of<br />

RAM.<br />

My firm has the same needs as many of you: supporting DOS 2.x legacy apps, working with<br />

Windows 3.1 on machines that don't have a local hard disk, and trying to explain to a potential<br />

customer about the "tradeoffs" (isn't that a diplomatic way of putting it?) of running a VFP<br />

application on an 8M Windows for Workgroups 3.11 machine.<br />

I'm just as interested in cutting edge (or over the edge) work in VFP as the next developer, and<br />

we're going to be at the forefront of VFP coverage in FoxTalk, but I have one foot firmly planted<br />

in the real world as well.<br />

What does this mean to you? First and foremost, you're not going to be stranded if your bread<br />

and butter still comes from earlier versions of FoxPro. I don't think we'll be dealing much with<br />

FoxBASE or FoxPro 1.0 anymore; there is a limit, after all. But FoxPro 2.x applications are<br />

going to be with us for a while, and we'll continue to cover these platforms as well as<br />

state-of-the-art development techniques with Visual FoxPro.<br />

Second, remember that FoxTalk has been the high-end publication for FoxPro developers for<br />

nearly a decade. I remember when I started reading it back in the late 1980s. I considered myself<br />

fortunate if I understood half of the articles, and I know many of your professional careers have<br />

grown up paralleling FoxTalk as well. As I mentioned, we're committed to providing high-level,<br />

in-depth techniques and strategies for FoxPro developers. You won't find articles like "How to<br />

use DO WHILE" or "Writing your first application with VFP's wizards" in FoxTalk. Other


publications cover those subjects well.<br />

However, we are going to spend more time not only on complex technical issues, but also on<br />

strategic use of the tools we have. In other words, we won't be content with showing you how to<br />

create the latest cool class library or super-duper builder -- we'll go one step further and help you<br />

turn this knowledge into a competitive advantage in your business. The information you find in<br />

FoxTalk will not only help you write better programs, but also deliver them with higher quality.<br />

I'm passionately interested in making this happen. I'm also interested in hearing what you're<br />

looking for. Please contact me on CompuServe at 70651,2270 whenever the urge strikes. Our<br />

goal is to help you write and deliver better FoxPro applications. As I've been known to say<br />

before, "Software is always better when written at 105 decibels."<br />

Let's rock.<br />

Do You Need Client/Server?<br />

Robert Green<br />

How do you decide if and when to move your data to a client/server environment? It's deceptively<br />

simple: evaluate the extras client/server has to offer and benchmark typical operations using your<br />

database.<br />

I'm repeatedly asked the question, "How do you know if you need to go client/server?" I'll try to<br />

answer, or at least point you in the right direction. I don't want to leave the impression that a<br />

question of this magnitude can be dealt with in a single monthly column -- it can't. But at least<br />

we can toss a few ideas around.<br />

One can build a large number of powerful applications using Visual FoxPro and your developer<br />

toolkit of acronyms (APIs, DLLs, OCXs, OLE, ODBC, MAPI, and so forth). The hard question<br />

is this: when do you need to scale a database application up to the power of a SQL back end,<br />

such as SQL Server or Oracle?<br />

How about this for an answer, "When the back end can handle the data better than Visual FoxPro<br />

can." Let's look at scenarios where this might be the case. Since I'm most familiar with SQL<br />

Server, I'll use that for my examples.<br />

Visual FoxPro tables can store up to one billion rows and can be up to two gigabytes in size.<br />

This seems like a heck of a lot, and it is. But what if you need to store more data? What if you<br />

needed to store two billion rows or five gigabytes of data? You could break the data up into more<br />

than one table, but this imposes a cost in terms of application design and maintenance, as well as<br />

your ability to search for a row or group of rows.<br />

Consider this: SQL Server tables can store an unlimited number of rows and can be up to eight


terabytes.<br />

Let's say you need to store 1.5 gigabytes in a table. This is less than FoxPro's maximum, but is it<br />

taxing the engine? What will performance be like with that amount of data? Remember that<br />

FoxPro isn't a client/server database. When you query a table, all records examined get sent over<br />

the network to your workstation. If you find only one record, a huge amount of network traffic is<br />

still generated, both in getting the data and in sending it back to the server.<br />

If you were using a client/server architecture, you'd construct the FoxPro front end to request<br />

only the amount of data needed at any time. To update one row, you'd first send a Select<br />

statement to the back end to retrieve that one row. The back end then sends that single row to the<br />

workstation. When it's time to save changes, FoxPro sends the back end an Update statement,<br />

and the back end makes the change. There's a minimal amount of network traffic involved here.<br />

So even though FoxPro can store the amount of data involved in an application, that's not the<br />

sole consideration. If moving the data to a client/server environment will lead to better<br />

performance and reduced network traffic, consider doing it.<br />

Another possibility is that an SQL back end may handle large amounts of data better than<br />

FoxPro can. Suppose you need to run an aggregation on a large data set as part of a weekly or<br />

monthly process. You write a FoxPro Select statement and it takes 10 hours to run. What if the<br />

same Select statement took one hour to run on Oracle? I'm not suggesting this will be the case,<br />

but if your testing showed it to be true, that would indicate that you might want to move to<br />

client/server.<br />

Another fact is that SQL Server provides much better crash protection than Visual FoxPro. If the<br />

file server were to die, an open FoxPro table could be corrupted. You run the risk of losing large<br />

amounts of data. Anything modified since the last backup would be at risk. You can protect the<br />

file server with all kinds of fault tolerance, but a workstation crash could also cause this type of<br />

data loss.<br />

SQL Server can be set up so that you will never lose data. All data modifications are written into<br />

a transaction log, giving SQL Server the ability to rollback a transaction. This is also key in<br />

automatic recovery. Data modifications are written first to the log, then later to the database.<br />

When SQL Server starts, it checks to make sure all committed transactions in the log are<br />

reflected in the database. It also makes sure that any uncommitted transactions that made it into<br />

the database are rolled back. Additionally, the transaction log can be backed up separately. If it's<br />

on its own hard disk, you could lose the database but still be able to back up the log. No data<br />

would be lost. This type of data protection isn't possible in a FoxPro application.<br />

SQL Server is a multi-threaded application and is written to take advantage of Windows NT<br />

scalability. If you added a processor to the machine, the performance of SQL Server would<br />

improve. Add another processor and it would improve again. Visual FoxPro is not<br />

multi-threaded, so adding a processor won't change performance. SQL Server can also be run on<br />

the more powerful MIPS and Alpha processor machines. FoxPro can't be run on these machines.<br />

This means that the performance ceiling of Visual FoxPro is below that of SQL Server. If<br />

FoxPro maxes out your single processor Pentium machine, it can't be scaled up to more powerful


hardware.<br />

The number of users will have an impact on performance. Your FoxPro application may perform<br />

superbly with 10 users, but what happens when 20, or 50, or 100 users are banging away at the<br />

data? How much of a drop-off in application response time will occur as users are added? SQL<br />

back ends tend to suffer much less of a performance drop when the number of users increases.<br />

The time required to perform system maintenance tasks is also a consideration. How long does it<br />

take to backup the data, re-create indexes, import data, and so on? If there is enough data, will<br />

FoxPro perform these tasks in a reasonable amount of time? Will SQL Server? The old version<br />

of SQL Server, 4.21, was not all that fast when dealing with very large databases. The new<br />

version, 6.0, is dramatically faster. At the same time that it's reading data, it can also read ahead<br />

and place upcoming data into cache memory. In addition, SQL Server can perform a striped<br />

parallel backup. This means that instead of writing the backup to one file, it can write to multiple<br />

files, all at the same time.<br />

In addition, backup in SQL Server is dynamic. You don't have to kick users off the system. This<br />

is crucial if you are in a seven-days-a-week, 24-hours-a-day operation. Finally, remember that<br />

SQL Server can back up the transaction log separately. This allows you to back up the entire<br />

database, say, once or twice a week, then backup the incremental transaction logs only once or<br />

twice a day. The incremental backups take nowhere near as much time as the full database<br />

backup. Again, this is a big plus if there are people using the database while you're backing it up.<br />

How do you know whether SQL Server will out perform Visual FoxPro? You have to test it. Get<br />

the product, install it, create a nice big test database and run some benchmarks. Practice not only<br />

changing data but running big Select statements and data maintenance tasks. Simulate real-life<br />

situations and see for yourself.<br />

I've mentioned just a few of the things to look for in evaluating the move to client/server. We'll<br />

continue this discussion in the months ahead.<br />

Robert Green is a vice president with The Information Management Group, a Chicago-based Microsoft<br />

Solution Provider and Authorized Technical Education Center specializing in consulting and training in<br />

Microsoft database products. 312-280-1007, CompuServe 76104,2514 RGreen@imginc.com.<br />

Extend the Report Writer/Report Designer<br />

with UDFs<br />

John Stepp (2)<br />

Are you working on the Report from Hell and ready to abandon the 2.x Report Writer or 3.0<br />

Report Designer and resort to hand-coded report programs? Try adding some code to the report


layout instead.<br />

FoxPro's report writer can probably satisfy most of your basic reporting requirements. However,<br />

you've also probably encountered complex or special reporting requirements that tax the report<br />

writer. This article discusses using user defined functions (UDFs) to extend the functionality of<br />

the report writer.<br />

You may have tried to use the report writer to develop a complex report, only to realize that you<br />

can't quite get it to do what you want. Before FoxPro for Windows, I often relied on custom<br />

reports to satisfy my complex reporting requirements. Those reports consisted entirely of<br />

procedural code. Because I was working in a character-based environment and generally used<br />

one font, formatting the output was a simple process. Once I determined the location on the page<br />

for each piece of information, I simply used the appropriate @SAY command to print it.<br />

With the release of FoxPro for Windows, developers were given the ability to easily generate<br />

sophisticated-looking reports using various fonts, font sizes, styles, shading, bitmaps, and more.<br />

Users not only appreciated these new reports, but expected them. I could now use the report<br />

writer to generate professional reports for basic reporting requirements, but what about complex<br />

reports? Although it may be necessary at times, I consider formatting a custom FoxPro report in<br />

the Windows environment as nothing more than a last resort. If at all possible, I opt for the ease<br />

of use and quick formatting capabilities the report writer offers.<br />

When designing complex FoxPro reports in the Windows environment, you need to consider two<br />

seemingly contradictory requirements: access to the power and flexibility of procedural code,<br />

and the ease of use and precise formatting of the report writer.<br />

Does this mean you have to choose one over the other? Certainly not. UDFs provide an excellent<br />

bridge between the two approaches, serving as the "hook" between the report form and<br />

procedural code. The last time I checked the FoxPro manuals, they provided few insights or tips<br />

regarding UDFs within the report writer, so some of you may not be aware of their flexibility<br />

and power. This article demonstrates the use of UDFs within the report writer so you too can<br />

take advantage of the power and flexibility they offer.<br />

UDFs in reports: the fundamentals<br />

I'm sure many of you use UDFs on a regular basis for application development, since they<br />

encapsulate functionality into a black box that produces a single output. When properly written,<br />

calling routines needn't be concerned with how the UDF accomplishes its task -- they only need<br />

to be aware of and provide the required inputs and anticipate the subsequent output.<br />

For example, imagine a UDF named CustNm().When passed a unique customer id as input, the<br />

function returns the properly formatted customer name (or the empty string if the customer id is<br />

not found). Any routine can invoke CustNm() as long as it passes the required numeric input<br />

value. The syntax of the UDF call is as follows:<br />

m.TheCustNm = CustNm(CustId)


After CustNm() executes, memory variable TheCustNm contains the name corresponding to the<br />

customer id indicated by CustId, or the empty string if the customer id wasn't found.<br />

You can invoke a UDF just as easily within your reports. If you need to display or calculate a<br />

value that isn't readily available or isn't possible within the report writer, invoke a UDF to do it.<br />

The UDF can do almost anything you want, including execute SQL queries, perform complex<br />

calculations, or build output expressions. The possibilities are almost endless.<br />

You use a UDF such as CustNm() by making the UDF call a Report Expression. The customer<br />

name, instead of being stored in a memory variable, will be printed on the report at the location<br />

of the Report Expression that contains the UDF. This isn't a very practical way to get the<br />

customer name, but it demonstrates how simple it is to invoke a UDF within the report writer.<br />

A real-life case study<br />

To give you a better idea how you can use UDFs within your reports, I'll review the UDFs used<br />

within a sample report. Please keep in mind that the report is simply an example and doesn't<br />

necessarily represent an ideal solution.<br />

I'll first provide you with some background regarding the sample database. The database deals<br />

with environmental waste and the treatment systems available to treat it. The database consists of<br />

five tables, each of which I'll describe briefly. I've indicated the primary keys (denoted by PK),<br />

foreign keys (denoted by FK), and some of the other relevant columns for each table.<br />

Wstrm -- Individual units of waste requiring treatment (called waste streams).<br />

WstrmId (PK) -- Unique waste stream identifier.<br />

WasteType -- Type of waste (HLW, MLLW, or MTRU).<br />

Inv_m3 -- Current inventory of waste expressed in cubic meters.<br />

Fiveyr_m3 -- Projected five-year generation of waste expressed in cubicmeters.<br />

TrtOpt (FK) -- References the treatment option designated to treat the wastestream.<br />

Trt -- Treatment systems available to treat the waste.<br />

SystemNo -- Unique treatment system number.<br />

Option -- Treatment options available to treat waste streams. This table associates a waste<br />

stream with<br />

the treatment system that will treat the waste.<br />

TrtOpt (PK) -- Unique treatment option code.<br />

OnOff -- Indicates if waste targeted to this treatment option will be treatedon or off site.<br />

WasteType -- Primary type of waste this treatment option supports.<br />

SystemNo (FK) -- References the treatment system where treatment is to occur.<br />

OptCost -- Costs by fiscal year for each treatment option.


TrtOpt (PK) -- Unique treatment option code.<br />

Fy (PK) -- Fiscal year for which costs are being captured.<br />

OpEsc -- Treatment option operating costs, expressed in thousands of escalateddollars.<br />

CapEsc -- Treatment option capital costs, expressed in thousands of escalateddollars.<br />

CostWbs -- Milestones for each treatment option.<br />

TrtOpt (PK, FK) -- Treatment option code.<br />

WbsCode (PK) -- Code corresponding to milestone.<br />

StartFy -- Fiscal year in which milestone activity begins.<br />

EndFy -- Fiscal year in which milestone activity ends.<br />

The requirements for the sample report are as follows:<br />

Generate a matrix for each treatment system that displays the aggregate costs associated with the<br />

treatment system by fiscal year. Fiscal years will appear as columns and the costs as rows. The<br />

costs reported for each fiscal year will be broken down as follows: on-site capital, on-site<br />

operating, off-site operating, and total costs.<br />

First, you have to indicate when each system reaches any of three critical scheduling milestones,<br />

referred to as KD-0, Start Operations, and End Operations. An appropriate indicator will be<br />

displayed under the fiscal year column in which the milestone occurs. Also, you must calculate<br />

the waste volume to be treated each fiscal year within a treatment system's operating period. The<br />

operating life for a system is defined as the number of fiscal years between End Operations and<br />

Start Operations. The waste volume to be treated each year is simply the total waste volume<br />

divided by the operating life. You also have to maintain a cumulative waste volume across this<br />

period.<br />

Treatment schedules can extend 100 years and beyond, so be prepared to accommodate a<br />

dynamic number of columns in the report. The detail portion of the report will look something<br />

like this (treatment system information is also printed in the page header):<br />

1995 1996 1997 1998<br />

On-site Capital $1,400 $1,400 $1,500<br />

On-site Operating $4,800 $5,500 $5,600<br />

Off-site Operating $100 $200<br />

Total $6,200 $7,000 $7,300<br />

Milestones KD-0 StartOps EndOps<br />

Waste Treated 350 350 350<br />

Cumulative Waste Treated 350 700 1050<br />

For brevity, I'm going to include only code segments that are most relevant to this article. For the<br />

complete source code, please refer to the accompanying Download file.<br />

The beginning of the program defines the parameters required to invoke the report as well as a<br />

few important constants.


* FundProf.prg<br />

* Sample report to demonstrate the use of user<br />

* defined functions (UDFs) in support of complex<br />

* reports.Generates a treatment system schedule<br />

* and funding profile report.<br />

* Calling syntax:<br />

* DO FundProf WITH "STP Configuration", 'S', .T.<br />

* Parameters:<br />

* Subtitle - Text description of data source.<br />

* RptDest - Character value indicating report<br />

* destination. An 'S' sends output to the screen,<br />

* anything else sends output to the printer.<br />

* RollFys - Logical value indicating if cost data<br />

* for years beyond that specified by MAXRPTFY<br />

* are to be rolled up into one column. This<br />

* shortens the report by consolidating numerous<br />

* Fy data into one aggregate column.<br />

* Assumptions:<br />

* Presence of '95 cost data for at least one<br />

* treatment system.<br />

PARAMETER Subtitle, RptDest, RollFys<br />

PRIVATE ALL LIKE l*<br />

* Max number of years that can fit on one page.<br />

#DEFINE DATACOLS 14<br />

* Latest year for which detail costs will be<br />

* reported when the RollFys parameter is .T.<br />

#DEFINE MAXRPTFY "2019"<br />

* Column label for aggregated costs when<br />

* the RollFys parameter is .T.<br />

#DEFINE MAXRPTFY1 "2020+"<br />

There are 14 columns available for cost data in the report form, hence the definition of<br />

DATACOLS. This constant provides the basis for "shifting" the range of cost data on each pass<br />

of the report. MAXRPTFY is used in conjunction with the RollFys parameter. When RollFys is<br />

.T., instead of viewing many pages of data when costs extend well into the twenty-first century,<br />

all costs beyond 2019 are rolled up into one aggregate value for reporting, thereby limiting the<br />

report to a maximum of two pages per treatment system. MAXRPTFY1 is simply the column<br />

label to be displayed for this aggregate value.<br />

The following query retrieves the data set identified in the report requirements.


SELECT B.SystemNo, Fy, "Onsite Capital " ;<br />

AS CostDesc, SUM(CapEsc) AS EscCost ;<br />

FROM OptCost A, Option B ;<br />

WHERE ;<br />

A.TrtOpt = B.TrtOpt ;<br />

AND OnOff_Site = "ON" ;<br />

GROUP BY 1, 2 ;<br />

UNION ;<br />

SELECT B.SystemNo, Fy, "Onsite Operating " ;<br />

AS CostDesc, SUM(OpEsc) AS EscCost ;<br />

FROM OptCost A, Option B ;<br />

WHERE ;<br />

A.TrtOpt = B.TrtOpt ;<br />

AND OnOff_Site = "ON" ;<br />

GROUP BY 1, 2 ;<br />

UNION ;<br />

SELECT B.SystemNo, Fy, "Offsite Operating" ;<br />

AS CostDesc, SUM(OpEsc) AS EscCost ;<br />

FROM OptCost A, Option B ;<br />

WHERE ;<br />

A.TrtOpt = B.TrtOpt ;<br />

AND OnOff_Site = "OFF" ;<br />

GROUP BY 1, 2 ;<br />

UNION ;<br />

SELECT B.SystemNo, Fy, "Total " ;<br />

AS CostDesc, SUM(CapEsc+OpEsc) AS EscCost ;<br />

FROM OptCost A, Option B ;<br />

WHERE ;<br />

A.TrtOpt = B.TrtOpt ;<br />

AND OnOff_Site IN ("ON", "OFF") ;<br />

GROUP BY 1, 2 ;<br />

ORDER BY 1, 3, 2 ;<br />

INTO CURSOR DataSet<br />

This provides the data you need, but not in the format you need for the report. There are several<br />

options here, one of which is to use FoxPro's cross-tabulation procedure, GENXTAB.PRG, to<br />

convert the row-based fiscal year data into columns for the report. But using GENXTAB would<br />

require a few work-arounds and enhancements to the code itself. Another option is keeping the<br />

data in the row-based format, and allowing the UDFs to calculate totals on the fly for each cell in<br />

the report. This is the most generic approach, since an unlimited number of fiscal years would be<br />

supported.<br />

For the purposes of this example, I opted to develop code to invert the fiscal years in the data set<br />

so they would appear as columns. In effect, I wrote my own custom cross-tabulation code. The<br />

resulting table can then serve as the primary data source for the report. As briefly mentioned<br />

above, this approach places an upper limit on the number of fiscal years that can appear in the<br />

report. Since FoxPro supports a maximum of 255 columns per table, the cross-tabulation routine<br />

has to come in under this ceiling. For the purposes of the report requirements, 255 is more than<br />

adequate.


The following code segment makes up the main body of the program, from which all supporting<br />

functions are invoked. Following this code segment I'll point out some highlights:


* Global array to contain FYI that<br />

* appear in the data set.<br />

DIMENSION Fa(1)<br />

* Create reporting table to contain columnar<br />

* cost data.<br />

= CostTable ()<br />

* Record length of Fy array<br />

m.FyALen = ALEN(FyA)<br />

* Populate newly created reporting table with<br />

* four rows per treatment system (one row for each<br />

* dollar type reported).<br />

= PopCostTbl ()<br />

m.lMsg = "Generating treatment system report header " ;<br />

+ "information ..."<br />

DO MsgDisp.SPR WITH m.lMsg, "Cost Report", "Status"<br />

* Create and populate table to contain treatment<br />

* system header data.<br />

= PopulateHdr ()<br />

INDEX ON SystemNo TAG SystemNo<br />

SELECT Costs<br />

INDEX ON SystemNo TAG SystemNo<br />

SET RELA TO SystemNo INTO Header<br />

* Offset is used to support multiple pages per<br />

* treatment system in order to accommodate wide<br />

* ranges of Fys.Offset will be incremented by the<br />

* number of data columns for each subsequent page.<br />

m.Offset = 0<br />

* Calculate no. of pages required to accommodate<br />

* all Fys.<br />

m.lPages = CEILING(m.FyALen / DATACOLS)<br />

* Referenced in report to maintain cumulative<br />

* waste volumes across Fys for each system.<br />

m.CumWaste = 0<br />

RELEASE WINDOW Status<br />

* Calculate latest Fy that can fit on the<br />

* first page of the report.<br />

m.lPgFyMax = STR(VAL(FyA(1)) + DATACOLS - 2, 4)<br />

GO TOP<br />

* Loop through the required number of times to<br />

* accommodate all Fys of data. Pages are to<br />

* be printed even if a treatment system's costs<br />

* don't appear until a subsequent page.<br />

FOR m.li = 1 TO m.lPages


Function CostTable() creates the table structure that the report is based on. It starts by creating a<br />

cursor with two columns, "SystemNo" and "CostDesc", to identify each treatment system and<br />

cost category in the data set. It then copies the structure of this table to a temporary table using<br />

COPY STRUCTURE EXTENDED. The routine uses this temporary table to dynamically build<br />

the required table structure based on the fiscal years in the data set. Next, it populates array FyA<br />

with the list of fiscal years in the data set and loops through each one, defining a column<br />

corresponding to that fiscal year (named FyXXXX, where "XXXX" is a fiscal year). There are a<br />

few additional columns defined to support subtotals for various fiscal year ranges. Once all<br />

columns are defined, a new table, called Costs, is created from this structure. There is now a<br />

structure with a column for every fiscal year appearing in the data set.<br />

PopCostTbl() populates the Costs table with the data from the data set. Since the report covers<br />

four levels of cost data per treatment system, PopCostTbl() first appends four records per<br />

treatment system to the Costs table. It then loops through each row in the data set and transfers<br />

all cost data to the corresponding row and column in table Costs.<br />

PopulateHdr() creates and populates a treatment system header table with information associated<br />

with each treatment system. This information is printed as part of the page header for each<br />

treatment system.<br />

Now you're ready to begin printing the report, so let's divert and take a look at the report form<br />

(see Figure 1).<br />

All the information in the upper part of the treatment system page header is straightforward.<br />

There's a UDF reference for the field labeled "Primary Waste Type Treated." This UDF simply<br />

takes a waste type as a parameter and returns a string consisting of the corresponding textual<br />

description to be printed on the report. Using UDFs in the report writer is as simple as that!<br />

Except for the "CostDesc" field reference, there are nothing but UDF references in the detail<br />

section and in the areas immediately adjacent to the detail section! There isn't a single field,<br />

memory variable, or constant reference. Most complex reporting requirements won't require such<br />

a heavy dependence on UDFs, but it's nice to know it can be done. In this report, the UDFs<br />

deliver the ability to process and print data for multiple fiscal years of data that can span multiple<br />

pages.<br />

I'll now review the UDFs to give you a feel for how they work within the confines of the report<br />

writer. Don't be too concerned about understanding all of the logic within each UDF -- just try to<br />

recognize the flexibility the UDFs offer. Each UDF accepts one numeric parameter representing<br />

the report column from which it is called.<br />

ColLabel() generates the appropriate column label for each column in the report:


FUNCTION ColLabel<br />

* Returns the appropriate column label for the<br />

* current report column.<br />

PARAMETER ColIdx<br />

PRIVATE lRetVal, lColfy<br />

* By default, the empty string is returned.<br />

m.lRetVal = ""<br />

IF m.ColIdx + m.Offset = 1 OR InRange (m.ColIdx)<br />

IF m.ColIdx + m.Offset != 1<br />

m.lColFy = FyA(m.ColIdx + m.Offset - 1)<br />

ENDIF<br />

DO CASE<br />

CASE m.RollFys AND (m.ColIdx + m.Offset != 1) AND ;<br />

m.lColFy = MAXRPTFY AND ;<br />

Header.MaxFy >= MAXRPTFY<br />

* User requested that Fys beyond MAXRPTFY by<br />

* aggregated into one column and the previous<br />

* column represented the last Fy to be<br />

* individually reported, so return Fy<br />

* aggregation column label.<br />

m.lRetVal = MAXRPTFY1<br />

CASE (m.ColIdx + m.Offset != 1) AND ;<br />

(m.lColFy = Header.MaxFy OR ;<br />

(m.lColFy = MAXRPTFY1 AND ;<br />

Header.MaxFy >= MAXRPTFY))<br />

* The previous report column was the last<br />

* costed Fy for this system or it was the<br />

* aggregation column,so print the total cost<br />

* column label.<br />

m.lRetVal = "Total"<br />

CASE FyA(m.ColIdx + m.Offset) > Header.MaxFy AND ;<br />

!(FyA(m.ColIdx + m.Offset) = "95-00" AND ;<br />

Header.MaxFy > "2000")<br />

* If this column corresponds to an Fy beyond<br />

* what's available for this system, don't<br />

* print a column header. Check for the<br />

* special case of 1995-2000 subtotal column,<br />

* ensuring that it is only printed if cost<br />

* data goes beyond Fy2000.<br />

m.lRetVal = ""<br />

OTHERWISE<br />

* If none of the special cases apply, return<br />

* the appropriate Fy label.<br />

m.lRetVal = FyA(m.ColIdx+Offset)<br />

ENDCASE<br />

ENDIF<br />

RETURN m.lRetVal


Shade95_00() determines if the current column is to be shaded. To simplify the report, I've made<br />

an assumption that some cost data will be available between fiscal years 1995 and 2000, so this<br />

logic needs only to be applied to the seventh cost column. The UDF is invoked from the Print<br />

When clause of each of the shaded boxes defined in this column.<br />

FUNCTION Shade95_00<br />

* Determines if the 'Print When' clause of shaded<br />

* boxes are to be drawn.<br />

PRIVATE lRetVal<br />

IF m.Offset = 0 AND Header.MaxFy > "2000"<br />

* We are on the first page of the report for the<br />

* current treatment system, and it has costs<br />

* extending beyond Fy2000, so the shaded box<br />

* must be printed to highlight the calculated sum.<br />

m.lRetVal = .T.<br />

ELSE<br />

m.lRetVal = .F.<br />

ENDIF<br />

RETURN m.lRetVal<br />

You could easily place this logic in the Print When clause as an expression for the various<br />

shaded boxes. The drawback: you'd need to duplicate the logic in several places, so if it had to be<br />

changed you'd have to change it everywhere. Using a UDF also saves you the hassle of leafing<br />

through various dialog boxes for different objects as you search for all references. By using a<br />

UDF, you consolidate the logic and isolate potential changes to one function. You therefore take<br />

advantage of the modular nature of UDFs within the report writer, just as you do within your<br />

application code.<br />

FyCost() generates the appropriate cost data for each detail record and fiscal year in the report.


FUNCTION FyCost<br />

* Returns the appropriate cost data for the current<br />

* report column.<br />

PARAMETER ColIdx<br />

PRIVATE lRetVal<br />

m.lRetVal = 0<br />

IF m.ColIdx + m.Offset = 1<br />

m.lRetVal = EVAL("Fy" + FyA(m.ColIdx+Offset))<br />

ELSE<br />

IF InRange (m.ColIdx) AND ;<br />

!(FyA(m.ColIdx + m.Offset - 1) = MAXRPTFY1 AND ;<br />

Header.MaxFy < MAXRPTFY)<br />

DO CASE<br />

CASE m.RollFys AND ;<br />

FyA(m.ColIdx + m.Offset - 1) = MAXRPTFY AND ;<br />

Header.MaxFy >= MAXRPTFY<br />

* The previous column represented the last<br />

* Fy to be individually reported; print<br />

* Fy range column label.<br />

m.lRetVal = Fy2020P<br />

CASE FyA(m.ColIdx + m.Offset - 1) = ;<br />

Header.MaxFy OR ;<br />

FyA(m.ColIdx + m.Offset - 1) = MAXRPTFY1<br />

* The previous column represented the last<br />

* costed Fy for this system or it was the<br />

* aggregation column; print the total cost<br />

* column label.<br />

m.lRetVal = Total<br />

OTHERWISE<br />

* Current column represents data for a<br />

* fiscal year of data. Create the field<br />

* name using the current column index and<br />

* offset.<br />

m.lCurrFy = FyA(m.ColIdx+Offset)<br />

* Evaluate constructed field name to obtain<br />

* cost.<br />

m.lRetVal = EVAL("Fy" + m.lCurrFy)<br />

ENDCASE<br />

ENDIF<br />

ENDIF<br />

RETURN m.lRetVal<br />

KD0(), EndOps(), and StartOps() (hidden behind KD0) determine if their respective milestones


occur in the current fiscal year. If so, the appropriate indicator is returned for display in that<br />

column. These UDFs are very similar, so I'll show you the source for just one of them:<br />

FUNCTION StartOps<br />

PARAMETER ColIdx<br />

PRIVATE lRetVal<br />

IF InFyBounds (m.ColIdx) AND ;<br />

FyA(m.ColIdx+Offset) = Header.StartOps AND ;<br />

!EMPTY(Header.StartOps)<br />

m.lRetVal = "Start"<br />

ELSE<br />

m.lRetVal = ""<br />

ENDIF<br />

RETURN m.lRetVal<br />

Waste() determines if the current column represents data for a fiscal year that falls within the<br />

operating period of the current treatment system. If it does, the function returns the estimated<br />

amount of waste treated in that year (based on the assumption that waste treatment is spread<br />

evenly over a treatment system's operating life).


FUNCTION Waste<br />

* Determines if the current column represents data<br />

* within the operating period of a treatment<br />

* system. If so, the inferred capacity is returned<br />

* to indicate the estimated quantity of waste<br />

* treated for the year.<br />

PARAMETER ColIdx<br />

PRIVATE lRetVal<br />

m.lRetVal = 0<br />

IF InFyBounds (m.ColIdx)<br />

IF m.RollFys AND FyA(m.ColIdx+m.Offset) =MAXRPTFY1 AND;<br />

Header.EndOps > MAXRPTFY<br />

m.lRetVal = (VAL(Header.EndOps) - VAL(MAXRPTFY)) ;<br />

* Header.InfCap<br />

ELSE<br />

IF BETWEEN(FyA(m.ColIdx+Offset), Header.StartOps, ;<br />

Header.EndOps) AND ;<br />

!EMPTY(FyA(m.ColIdx+Offset))<br />

m.lRetVal = Header.InfCap<br />

ENDIF<br />

ENDIF<br />

ENDIF<br />

RETURN m.lRetVal<br />

CumWaste() maintains a cumulative sum of the waste volume treated each fiscal year within the<br />

operating period of the current treatment system.


FUNCTION CumWaste<br />

* Calculates and returns cumulative waste volume<br />

* for the current Fy column. When RptOnly is .T.<br />

* (for totaling columns), the current cumulative<br />

* volume is returned only - it is not updated.<br />

PARAMETER ColIdx, RptOnly<br />

IF !m.RptOnly<br />

IF InFyBounds (m.ColIdx)<br />

IF m.ColIdx = 1 AND m.Offset > 0<br />

* We've just started another page for this<br />

* treatment system, so restore the cumulative<br />

* waste totals from the last page so we can<br />

* continue with the cumulative sum.<br />

m.CumWaste = Header.CumWaste<br />

ENDIF<br />

DO CASE<br />

CASE m.RollFys AND ;<br />

FyA(m.ColIdx+m.Offset) = MAXRPTFY1 AND ;<br />

Header.EndOps > MAXRPTFY<br />

* We're on the cost aggregation column and<br />

* the operating life ends beyond the fiscal<br />

* year identified by MAXRPTFY, so we need<br />

* to return the total cumulative volume of<br />

* waste to be treated through all years,<br />

* which is simply the total waste volume<br />

* found in the Header table(there can be<br />

* discrepancies with the Mixed Waste<br />

* Treated total due to rounding).<br />

m.CumWaste = Header.TotWaste<br />

CASE BETWEEN(FyA(m.ColIdx+Offset),Header.StartOps,;<br />

Header.EndOps)<br />

m.CumWaste = m.CumWaste + Header.InfCap<br />

OTHERWISE<br />

m.CumWaste = 0<br />

ENDCASE<br />

IF m.ColIdx = DATACOLS<br />

* This is the last column for the page, so<br />

* s you must ave the current cumulative waste<br />

* total so the cumulative sum can resume on<br />

* the next page.<br />

REPLACE Header.CumWaste WITH m.CumWaste<br />

ENDIF<br />

ELSE<br />

m.CumWaste = 0<br />

ENDIF<br />

ENDIF<br />

RETURN C W t


That covers all of the relevant UDFs. Although some of the logic in these UDFs can be a bit<br />

awkward to follow, I hope this example helped you better understand and visualize how you can<br />

use UDFs within your reports.<br />

Refer to Figure 2 for two views of the final report output. You might want to try running the<br />

report yourself so you can take a closer look at it.<br />

Conclusion<br />

UDFs provide a mechanism for incorporating relatively complex code directly into your FoxPro<br />

report forms, thereby extending the report writer's native capabilities. UDFs also eliminate<br />

redundant code by providing an excellent way to consolidate report writer expressions that<br />

appear in multiple objects. The next time you're about to give up on the report writer, see<br />

whether UDFs can provide the extra power and flexibility you need.<br />

John Stepp is the systems manager for MAC Technical Services, an environmental management<br />

consulting firm. He uses FoxPro extensively to develop custom applications that support complex<br />

analysis. After hours, John is the principal of Micro Integration, a provider of custom applications<br />

development services specializing in data modeling and data design. CompuServe 75553,1112.<br />

Get a Better Calculator, Limit Forms, Fix<br />

VFPs Memory Problems, and More<br />

John V. Petersen<br />

Get a Calculator Accessory<br />

I write accounting software and would like to know how to call the calculator from a<br />

program, use it, then return the value to the program. First, can this be accomplished?<br />

Second, if this isn't possible, what alternatives exist?<br />

Desmond L. O'Kelly, CompuServe 75567,320<br />

You could call the calculator accessory in a program by issuing an ACTIVATE WINDOW<br />

Calculator command, then query the results of a calculation in the _CALCVALUE<br />

environmental variable. However, that method isn't readily useable in an application.<br />

Unfortunately, like most FoxPro system windows, the calculator window offers little or no<br />

control. One reason is that the calculator window is modeless, meaning that when it is activated,<br />

control immediately passes back to the next line of code. Even wrapping the call in a UDF won't<br />

help. The only alternative is to create your own calculator. Fortunately, the work has been done<br />

for you -- there are several contributions in the FOXFORUM on CompuServe. In addition, check


out the third-party products section in the FOXUSER forum for a commercially available<br />

alternative. [Visual FoxPro developers can refer to "A Better Calculator for Visual FoxPro" in<br />

our October 1995 issue. -- Ed.]<br />

Limiting Your Forms<br />

Is there an easy way to limit the number of times a form can be created in an application?<br />

Sometimes I may want to allow one or two instances only.<br />

The modeless development aspect of Visual FoxPro offers advantages not found in previous<br />

versions. Along with the good, however, comes new problems. Allowing your users unlimited<br />

instances of a form can lead to resource depletion. Unfortunately, however, limiting form<br />

instances in Visual FoxPro isn't without cost. What's needed in this case is a Form class that will<br />

do the checking for us to facilitate limiting the number of instances. The idea centers on looping<br />

through the forms collection of the _SCREEN object. (For more background on _SCREEN, see<br />

my article, "Explore the Visual FoxPro _SCREEN Object" in the October 1995 issue of<br />

FoxTalk.)<br />

You need two pieces of information in order to limit instances of a form: the name of the SCX,<br />

and how many instances of the form you want to permit.<br />

To determine the SCX on which the form is based, you can use SYS(1271), a function that is<br />

obscurely documented in the README.HLP file that comes with the Professional Edition of<br />

Visual FoxPro. By passing the form object reference to SYS(1271), the SCX filename, along<br />

with its full path, is returned. You can pass the maximum number of allowed instances to the Init<br />

method of the form class:<br />

First, create a form class with the following code in the Init():


LPARAMETERS tnNumberOfInstances<br />

LOCAL lnCount,lnTally,lcScx,llOK<br />

STORE 0 TO lnCount,lnTally<br />

**fetch the SCX file of this form<br />

lcScx = SYS(1271,THIS)<br />

llOK = .T.<br />

IF TYPE('tnNumberOfInstances') = 'N'<br />

tnNumberOfInstances = INT(tnNumberOfInstances)<br />

FOR lnCount = 1 TO _SCREEN.FormCount<br />

* since toolbars are included in the forms<br />

* collection, we will check the BaseClass property.<br />

IF UPPER(_SCREEN.Forms(lnCount).BaseClass) = 'FORM'<br />

IF SYS(1271,_SCREEN.Forms(lnCount)) = lcScx<br />

lnTally = lnTally + 1<br />

ENDIF<br />

ENDIF<br />

ENDFOR<br />

IF lnTally > tnNumberOfInstances<br />

=MessageBox('Only ';<br />

+ ALLTRIM(STR(tnNumberOfInstances)) ;<br />

+ ' instance(s) is(are) permitted.',16,;<br />

'Limited Instance Form')<br />

llOK = .F.<br />

ENDIF<br />

ELSE<br />

=MESSAGEBOX('Parameter must be numeric')<br />

llOK = .F.<br />

ENDIF<br />

RETURN llOK<br />

Next, create a form based on this class and add the following code to its Init() Event:<br />

IF !EVALUATE(THIS.Class+'::Init(2)')<br />

RETURN .F.<br />

ENDIF<br />

Substitute whatever maximum number of instances you want for the "2" in the code sample. As<br />

an alternative to adding the previous code to the form's Init, you could simply DO FORM<br />

WITH .<br />

There are many alternatives, including a custom form handler class. However, this will get the


job done nicely and is straightforward and simple.<br />

Work Around VFP's Memory Problems<br />

I seem to be running out of memory when using Visual FoxPro under Windows 95.<br />

Specifically, this seems to happen when I use the Database Designer. Is this a bug, and if so,<br />

what workarounds exist and when will it be fixed?<br />

Microsoft has acknowledged this problem and it will probably be addressed in a maintenance<br />

release. But since we can't be certain when the fix will appear, it's important to create some<br />

workarounds. My suggestion consists of both monitoring system resources and optimizing<br />

memory allocation.<br />

One of the most useful utilities to ship with Windows 95 is the resource monitor, which is<br />

contained in a file called RSRCMTR.EXE. Assuming you have installed this utility, you can find<br />

it in the root of the Windows 95 directory. When working with VFP, it's a good idea to have this<br />

utility present on your taskbar. It represents a meter of available resources and will clearly show<br />

when resources are getting low. All three classes of resources are monitored: System, User, and<br />

GDI.<br />

When resources get low, try closing any non-essential applications or Visual FoxPro windows; if<br />

this doesn't resolve the problem, close and restart Visual FoxPro.<br />

A View Problem<br />

On several occasions, I've had to make changes to field names in tables in a database, when<br />

those field names are also used in local views. Once these changes have been made, I<br />

encounter problems with the views using the affected tables. How can I avert these<br />

problems and have the views automatically reflect the structure changes of underlying<br />

tables?<br />

Unfortunately, it's not possible for your views to automatically reflect the structure changes<br />

made to associated tables. However, there's a remedy available to prevent you from having to<br />

re-create some or all of your view. The idea centers around "hacking" the DBC. Each view in a<br />

database has a corresponding record in the DBC file. Information regarding the view is stored in<br />

a memo field called Property. While much of the information in this memo is binary, the fields<br />

involved in the view are plain text. Being careful not to disturb the binary portions of the memo,<br />

change the text portions to reflect the new field names. Finally, close the DBC file and issue<br />

MODIFY VIEW to make sure all information regarding your view has been<br />

preserved.<br />

John V. Petersen, MBA is director of FoxPro and Visual FoxPro development for MaxTech, Inc., a<br />

consulting firm based in Northern Virginia specializing in database consulting, project development and<br />

mentoring, and training. John, who works out of Philadelphia, is active in the FoxPro community, has<br />

written for publications, and has spoken at user group meetings and at the 1995 Developer Days<br />

Conference. CompuServe 103360,1031.


Use the Windows API to Print to Multiple<br />

Print Queues<br />

Richard Aman (3)<br />

Windows provides a wealth of services for controlling network connections. Richard Aman shows<br />

how to use these services in either FoxPro 2.x or Visual FoxPro to easily send a report to any<br />

available output device. In the process, you'll learn about Visual FoxPro's new variant of<br />

DECLARE, which allows you to call external DLL routines as if they were native UDFs. You'll also<br />

learn how to use several useful Windows API routines.<br />

One of the more difficult application types I've implemented is an automatic scheduler. A<br />

scheduler performs procedures at various scheduled and unscheduled times throughout the day.<br />

The procedures must perform in unattended mode and make decisions without user input. Along<br />

with software-related decisions like branching and looping, the procedures must make hardware<br />

decisions. For example, the software must decide which printer to send output to, which network<br />

connection to use, when to reset the software, and which users are logged on when.<br />

When I implemented the scheduler in Windows, the main design decision was how to allow the<br />

system to use different printers in different locations for the various output reports -- key in an<br />

order-entry and work progress-based system like this one. I designed the system to make<br />

decisions based on orders entered, and by the various stages of manufacture that pieces have<br />

achieved.<br />

The Win.INI route<br />

To choose an output printer, I initially used sample code from the Microsoft Developer's<br />

Network (MSDN) CD. That sample code had multiple printers installed in Windows, then<br />

modified the WIN.INI file to change the default printer. The method worked fine, although it had<br />

limitations. First, it required that all printers to be used be installed in Windows -- a problem if<br />

you want to use the application on different machines. The second limitation was that Windows<br />

has only a limited number of printer ports. Though it's possible to have several printers assigned<br />

to the same port, keeping track of them can get messy. The third limitation is that it takes time to<br />

modify WIN.INI and notify running applications to update themselves with the changes.<br />

The method detailed on the MSDN CD used the Windows API functions GetProfileString() and<br />

PutProfileString() to access WIN.INI. This made me wonder what other functions for switching<br />

printers<br />

might be buried in the Windows API. I started looking through the Microsoft Developer's<br />

Network CD and the TechNet CD (both excellent resources) and the Windows API help file that<br />

comes with Visual FoxPro Professional Edition for other approaches to this problem.


A better way<br />

That's when I came across the WNetGetConnection(), WNetAddConnection(), and<br />

WNetCancelConnection() functions. These three Windows API functions combine to give you<br />

almost unlimited programmatic control over the network connections through an application.<br />

WNetGetConnection() returns the name of the network connection that a local device is mapped<br />

to, or NULL if the device isn't mapped on the network. WNetAddConnection() maps a local<br />

device to a network connection if it's not already connected. And WNetCancelConnection()<br />

disconnects a local device from a network connection. With these three functions, I implemented<br />

multiple printer output through LPT1 alone. I also found two other useful Windows API<br />

functions: WNetGetUser(), which returns the user network login ID, and ExitWindows(), which<br />

can be used to restart Windows from within FoxPro.<br />

My automatic scheduler application scans the orders table at a recurring interval for any new<br />

orders. When new orders are found, they're copied to separate temporary tables for printing.<br />

Based on the type, orders can print at one of three places in the plant. The print routine is passed<br />

through three parameters, which tell the routine the name of the temporary table, the report form<br />

to use, and where to send output. When the routine processes the print location decision, it calls<br />

WNetGetConnection() to check whether LPT1 is connected to a network print queue. If a current<br />

connection exists, it calls WNetCancelConnection() to disconnect LPT1 from the current<br />

network print queue. It then calls WNetAddConnection() to connect LPT1 to the proper network<br />

print queue. (To prevent a connection error, you'll need to cancel the existing connection before<br />

creating a new connection..)<br />

The basics<br />

Before I get to my sample code, I want to quickly cover some basic requirements for using this<br />

method of printer control and for using any of the Windows API functions (or functions in any<br />

Windows DLL).<br />

If you're using FoxPro 2.6 for Windows, first load in the library file FoxTools.fll, (supplied with<br />

FoxPro 2.6). This library file loads with the command SET LIBRARY TO FoxTools. Once the<br />

library is loaded, your program has access to a pair of functions called RegFn() and CallFn().<br />

RegFn() is used to register Windows API functions to FoxPro. CallFn() is used to call the<br />

functions previously registered with RegFn(). In Visual FoxPro 3.0, the DECLARE command<br />

replaces the need for FoxTools.fll and the RegFn() and CallFn() routines . This new DECLARE<br />

command is also used to register Windows API functions. Once the functions have been<br />

registered, they're called just like the internal FoxPro functions. However, for backward<br />

compatibility, FoxTools.fll, RegFn() and CallFn() can still be used in FoxPro 3.0. (Refer to the<br />

Visual FoxPro 3.0 help file for more information on the DECLARE command.)<br />

If you're working in FoxPro 2.x and are new to FoxTools/RegFn/CallFn, you can get details from<br />

"Use Microsoft Windows Services from FoxPro" by Robert W. Lord (FoxTalk, March 1994).<br />

Back issues can be ordered at 800-788-1900. In the meantime, here's a brief rundown of these<br />

commands and the new Visual FoxPro replacements for them:


Loading the FoxTools.fll library<br />

First, load the FoxTools.fll library for FoxPro 2.6:<br />

SET LIBRARY TO SYS(2004) + 'FoxTools.fll' ADDITIVE<br />

This command line uses the FoxPro function SYS(2004) to get FoxPro's home directory, which<br />

is where FoxTools.fll is installed during normal installation. Also, use the ADDITIVE clause to<br />

add the library to any existing loaded libraries; otherwise, FoxTools.fll will replace any existing<br />

loaded libraries.<br />

Using RegFn()<br />

The FoxTools function RegFn() registers a Windows API function with FoxPro. RegFn() takes<br />

three required parameters and one optional parameter. The first parameter is the name of the<br />

Windows API function you want to register. The second parameter is a string containing letter<br />

designations for the types of parameters the Windows API function requires ( `C' for character or<br />

string, `I' for integer, and so forth). The third parameter is a letter designation for the type of<br />

value the Windows API function will return to FoxPro. The fourth and optional parameter is the<br />

name of the Windows DLL that contains the function you want to register. If you don't include<br />

the DLL name, FoxPro automatically looks in the standard Windows libraries (USER.EXE,<br />

KRNL386.EXE, and GDI.EXE in Windows 3.x) to try to find the Windows API function. If the<br />

function isn't found, an error code is returned. If the function is successfully registered, a<br />

function handle is returned. That handle is then used by CallFn() to access the Windows API<br />

function, as I describe later.<br />

Using CallFn()<br />

The FoxTools function CallFn() is used to access a Windows API function from within FoxPro<br />

once it has been registered with RegFn(). The parameters passed to CallFn() are the Windows<br />

API function handle returned by RegFn(), and the parameters specified in the second parameter<br />

in RegFn() when the Windows API function was registered.<br />

Using DECLARE in Visual FoxPro<br />

DECLARE is an enhanced command in Visual FoxPro 3.0. In addition to defining arrays, the<br />

DECLARE command also removes the need for using FoxTools to access the Windows API<br />

functions. DECLARE allows the application to directly register Windows API functions with<br />

FoxPro. Once the functions are registered, they can be called like any other FoxPro internal<br />

function. The first parameter to the DECLARE command is the Windows API function return<br />

type. The second parameter is the name of the function you're registering. The third parameter is<br />

the DLL containing the Windows API function. The remainder of the parameters are the<br />

parameter types that FoxPro will pass to the Windows API function.<br />

The Windows API functions


WNetGetConnection()<br />

WNetGetConnection() is used to retrieve the network connection to which a local device is<br />

mapped. The first parameter is a variable containing the name of the local device you want to<br />

check. The second parameter is a variable initialized to spaces (I use 255) and will be supplied<br />

with the connection name by WNetGetConnection(). The last parameter is a variable containing<br />

the length of the second parameter (in this case 255). All three parameters need to be passed by<br />

reference. After initializing the variables, register the function with FoxPro. This lets FoxPro<br />

know that the function will be passed three parameters by reference, two strings and one integer,<br />

and the function will return an integer. Ensure the second parameter buffer is empty before<br />

calling this function, because an error won't clear the buffer and you might get incorrect results.<br />

Here's the syntax for setting up the WNetGetConnection call, first for FoxPro 2.x, then for<br />

Visual FoxPro:<br />

* FoxPro 2.x<br />

lnGetConn = RegFn('WNetGetConnection','@C@C@I','I')<br />

* Visual FoxPro<br />

DECLARE INTEGER WNetGetConnection IN win32api ;<br />

STRING @, STRING @, INTEGER @<br />

Call the function to return the current connection for the specified device:<br />

* FoxPro 2.x<br />

lnRetVal = CallFn(lnGetConn, @lcDeviceName, ;<br />

@lcConnName, @lnBuffLen)<br />

* Visual FoxPro<br />

lnRetVal = WNetGetConnection(@lcDeviceName, ;<br />

@lcConnName, @lnBuffLen)<br />

After the call to WNetGetConnection(), the buffer lcConnName will either contain the name of<br />

the network connection for the specified local device or will be empty if no connection currently<br />

exists for the local device. It will also be empty if an error occurred. Also, be sure to check the<br />

return value for any error codes. The Windows API return codes for WNetGetConnection()<br />

follow, as found in the Windows SDK:<br />

0 The function was successful.<br />

8 The system was out of memory.<br />

50 The function was not supported.<br />

59 An error occurred on the network.<br />

87 The local device name parameter was not a valid localdevice.<br />

234 The buffer was too small. (The connection name islonger than the allotted buffer length.)<br />

487 The pointer was invalid.


2250 The local device name parameter was not a redirectedlocal device.<br />

WNetAddConnection()<br />

WNetAddConnection() is used to map a local device to a network connection. The first<br />

parameter is the network connection to map the device to. The second parameter is the password<br />

to use and should be a null string to use the default password. Finally, the third parameter is the<br />

local device to map. After initializing the variables, register the function with FoxPro. This lets<br />

FoxPro know that the function will be passed three parameters as strings, and will return one<br />

integer:<br />

* FoxPro 2.6<br />

lnAddConn = RegFn('WNetAddConnection','CCC','I')<br />

* Visual FoxPro<br />

DECLARE INTEGER WNetAddConnection IN win32api ;<br />

STRING, STRING, STRING<br />

Call the function to map the local device to the network connection:<br />

* FoxPro 2.x<br />

lnRetVal = CallFn(lnAddConn,'\\SERVER1\HP4','','LPT1:')<br />

* Visual FoxPro<br />

lnRetVal = WNetAddConnection('\\SERVER1\HP4','','LPT1:')<br />

This function returns 0 if successful, or an error number for any error that occurs while<br />

attempting to create the network connection. I've listed the Windows API return codes for<br />

WNetAddConnection(), which follow, as found in the Windows SDK.<br />

0 The function was successful.<br />

5 A security violation occurred.<br />

8 The system was out of memory.<br />

50 The function was not supported.<br />

59 An error occurred on the network.<br />

67 The network resource name was invalid.<br />

85 The local device was already connected to a remoteresource.<br />

86 The password was invalid.<br />

487 The pointer was invalid.<br />

1200 The local device name was invalid.<br />

WNetCancelConnection()<br />

WNetCancelConnection() is used to remove a network connection mapping from a local device.


This is necessary because the WNetAddConnection() function will return an error if the device<br />

that you are trying to map is already mapped to a network connection. For this reason, I<br />

recommend checking the network mapping for the local device before attempting to create a new<br />

connection. Also, I recommend releasing any network mapping that exists first. The first<br />

parameter is the local device to cancel the network connection to. The second parameter tells<br />

Windows whether to close any open files, or simply to return an error. The second parameter<br />

should be 0 to close open files before disconnecting. After initializing the variables, register the<br />

function with FoxPro. This lets FoxPro know that the function will be passed two parameters,<br />

one string and one integer, and will return one integer.<br />

* FoxPro 2.6<br />

lnCancelConn = RegFn('WNetCancelConnection','CI','I')<br />

* Visual FoxPro<br />

DECLARE INTEGER WNetCancelConnection IN win32api ;<br />

STRING, INTEGER<br />

Call the function to cancel the connection for the specified local device:<br />

* FoxPro 2.6<br />

lnRetVal = CallFn(lnCancelConn,'LPT1:',0)<br />

* Visual FoxPro<br />

lnRetVal = WNetCancelConnection('LPT1:',0)<br />

This function returns 0 if successful, or it returns an error number for any error that occurs while<br />

attempting to cancel the network connection. Following are the Windows API return codes for<br />

WNetCancelConnection(), as found in the Windows SDK:<br />

0 The function was successful.<br />

8 The system was out of memory.<br />

50 The function was not supported.<br />

59 An error occurred on the network.<br />

87 The local device name parameter was not a valid localdevice or network name.<br />

487 The pointer was invalid.<br />

2250 The local device name parameter was not a redirectedlocal device or currently accessed network<br />

resource.<br />

2401 Files were open and the fForce parameter was 0. Theconnection was not canceled.<br />

WNetGetUser()<br />

WNetGetUser() is used to return the network login ID of the machine the application is running<br />

on. The first parameter is a variable containing the local name to return the network login ID for.<br />

It should be NULL for the current machine. The second parameter is a variable initialized to


spaces (I use 255 spaces) and will be filled in by WNetGetUser() with the network login ID. The<br />

third parameter is the length of the second parameter. All three parameters need to be passed in<br />

by reference in order for the function to operate correctly. After initializing the variables, register<br />

the function with FoxPro. This lets FoxPro know that the function will be passed two parameters<br />

by reference, one string and one integer, and the function will return one integer. Use NULL for<br />

the first parameter to get the current sign-on name because if the user is signed on more than<br />

once, the system makes a random choice of which login name to return:<br />

* FoxPro 2.x<br />

lnGetUser = RegFn('WNetGetUser','@C@C@I','I')<br />

* Visual FoxPro<br />

DECLARE INTEGER WNetGetUser IN win32api ;<br />

STRING @, STRING @, INTEGER @<br />

Call the function to get the network login ID of the machine:<br />

* FoxPro 2.x<br />

lnRetVal = CallFn(lnGetUser,@lcUserID,@lnBuffLen)<br />

lnRetVal = WNetGetUser(@lcUserID,@lnBuffLen)<br />

After the call to WNetGetUser(), the buffer lcUserID will contain either the network login ID or<br />

will be empty if the machine isn't logged in to the network. Also, check the return value for any<br />

error codes. Following are the Windows API return codes for WNetGetUser(), as found in the<br />

Windows SDK:<br />

8 The function could not allocate sufficient memoryto complete<br />

its operation.<br />

50 This function is not supported.<br />

59 A network error occurred.<br />

234 The buffer was too small to hold the complete username.<br />

487 The pointer is invalid.<br />

2202 The user is not logged in; there is no current username.<br />

ExitWindows() and ExitWindowsEx()<br />

ExitWindows() has two uses of interest, based on the first parameter passed in. The first<br />

parameter is a flag to tell Windows to either reboot or exit. The second parameter is reserved and<br />

should be 0. If you pass in a 67 to the first parameter, Windows will close down any running<br />

applications and exit to the DOS prompt. If you pass in a 66, Windows will close down any<br />

running applications and restart Windows. (the function is called ExitWindowsEx() in 32-bit<br />

Windows; the first parameter should be 0 to restart Windows.) I use a 66 in the automatic<br />

scheduler application . After initializing the variables, register the function with FoxPro. This<br />

lets FoxPro know that the function will be passed two parameters, both integers, and the function


will return one integer:<br />

* FoxPro 2.x<br />

lnExitWin = RegFn('ExitWindows','II','I')<br />

* Visual FoxPro<br />

DECLARE INTEGER ExitWindowsEx IN user32 ;<br />

INTEGER, INTEGER<br />

Call the function to restart Windows:<br />

* FoxPro 2.x<br />

lnRetVal = CallFn(lnExitWin,66,0)<br />

* Visual FoxPro<br />

lnRetVal = ExitWindowsEx(0,0)<br />

ExitWindows() and ExitWindowsEx() have only two return codes: 0 or FALSE if an error<br />

occurred, and 1 or TRUE if the function call was successful. In this use of ExitWindows() or<br />

ExitWindowsEx(), you probably need only check for a failure return code, since a successful call<br />

would have restarted Windows and FoxPro, meaning the return code value would have been<br />

released. But a check for success might be important in other uses.<br />

Why use ExitWindows() or ExitWindowsEx()? I use ExitWindows() because of a "memory<br />

leak" in FoxPro for Windows 2.6, which my scheduler runs under. When FoxPro generates a<br />

report, some of the memory used isn't released back to the pool of free memory. Consequently,<br />

after a certain number of reports, FoxPro can run out of memory. In my case, problems occur<br />

after approximately 75 work production tickets, each with a bitmap of the item and several<br />

different fonts. To prevent this, I set up FoxPro in the Windows StartUp group with a command<br />

line call to the automatic scheduler. Then, every 15 minutes I call a procedure called ResetWin<br />

(included in the accompanying Download file), which closes down FoxPro and restarts<br />

Windows. When Windows starts up, it runs FoxPro from the StartUp group, which then restarts<br />

the automatic scheduler, which picks up where it left off -- but with refreshed memory.<br />

Everything runs smoothly with this scheme in place.<br />

Presumably, this memory leak has been fixed in Visual FoxPro, but you may still want to use<br />

ExitWindowsEx() to automatically shut down or restart some application types, perhaps at<br />

particular times of the day when demand is minimal. It often helps to get a "clean slate"<br />

periodically for an automated server application on a dedicated workstation, particularly if the<br />

application has the potential of running unattended for days or weeks.<br />

Listing 1 illustrates my basic method of printer control. Prior to this procedure, the application<br />

will have selected the records to be printed, the report form to use, and the network print queue<br />

destination, all of which are passed in through parameters. I first initialize my variables and save<br />

the current library setting. I then make sure the Foxtools library is loaded for version 2.x. Next, I


egister the Windows API functions with FoxPro ( using the DECLARE command for version<br />

3.0 or the RegFn() for version 2.x ). Then I check for and release any existing connection for<br />

LPT1: using WNetGetConnection(). I then set LPT1: to the desired network print queue with<br />

WNetAddConnection(). After all is set up properly, I loop through the source table and generate<br />

a full-page form for each record (this allows other users to insert print jobs between my frequent<br />

multi-page output). Finally, I make a call to GetNetID() to see if I'm on the automatic scheduler.<br />

If so, I reset LPT1: to a default network print queue. I then restore the previous library setting<br />

and exit the routine.<br />

The call to the GetNetID UDF deserves a little more explanation. GetNetID() is included in the<br />

accompanying Download file. It calls the Windows WNetGetUser() function to return the<br />

network login ID of the machine the application is running on. This can be used for a variety of<br />

things. In AutoPrnt, I check the network ID with GetNetID() to see if the application print<br />

routine is running on the automatic scheduler machine. If so, I re-map the printer back to the<br />

default queue so normal output that doesn't need to be specifically redirected can be printed on<br />

the central printer.


Listing 1. The AutoPrnt procedure.<br />

********************************************************<br />

* PROCEDURE AutoPrnt<br />

********************************************************<br />

* Author............: Richard L. Aman<br />

*) Description.......: A scaled down version of the print<br />

*) : engine for the automatic scheduler<br />

* Calling Samples...: DO AutoPrnt WITH cSrcTable,<br />

*) : cFormName, cPrinter<br />

* Parameter List....: cSrcTable - table containing<br />

*) : records to print<br />

* : cFormName - name of form to use<br />

* : cPrinter - report destination<br />

PROCEDURE AutoPrnt<br />

PARAMETERS cSrcTable, cFormName, cPrinter<br />

*-- define variables<br />

PRIVATE lcFormName, lcPrinter, lcReport, lcOldPrinter, ;<br />

lcOldLibrary, lnAddConn, lnDelConn, lnGetConn, ;<br />

lcDeviceName, lcConnName, lnBuffLen, ;<br />

llVersion3, lcConnTo, lnRetVal<br />

*-- init variables<br />

lcFormName = cFormName<br />

lcPrinter = cPrinter<br />

lcReport = lcFormName<br />

lcOldPrinter = ''<br />

lcOldLibrary = SET('LIBRARY')<br />

lcDeviceName = 'LPT1'<br />

lcConnName = SPACE(254)<br />

lnBuffLen = LEN(lcConnName)<br />

llVersion3 = '3.0' $ VERSION()<br />

lcConnTo = ''<br />

lnRetVal = 0<br />

*--ensure that foxtools library is loaded<br />

IF NOT llVersion3<br />

IF NOT 'FOXTOOLS' $ UPPER( lcOldLibrary )<br />

SET LIBRARY TO SYS( 2004 ) + 'FOXTOOLS.FLL' ADDITIVE<br />

ENDIF<br />

ENDIF<br />

*-- register the Windows API functions<br />

IF llVersion3<br />

DECLARE INTEGER WNetAddConnection IN win32API ;<br />

STRING, STRING, STRING<br />

DECLARE INTEGER WNetCancelConnection IN win32API ;<br />

STRING, INTEGER<br />

DECLARE INTEGER WNetGetConnection IN win32API ;<br />

STRING @, STRING @, INTEGER @<br />

ELSE<br />

lnAddConn = RegFn('WNetAddConnection','CCC','I')


Things I learned the hard way about using Windows API functions<br />

If the function declaration calls for a value to be passed by reference, you must use a variable<br />

and preface it with the "@" symbol. I thought that for the buffer length, I could set a variable<br />

with the length of the buffer, then just pass the variable, but not in this case. I still had to use the<br />

"@" symbol.<br />

If you're using the Visual FoxPro calling convention with the DECLARE command, the<br />

Windows API function names are case-sensitive.<br />

Always remember to check the return value for any error codes. Unfortunately, I didn't have<br />

enough space in this article to go into detail about handling errors, but at the very least you<br />

should determine what the function returns when it's successful and test for that. Don't forge<br />

ahead in your code just assuming that the API function executed successfully.<br />

Conclusion<br />

FoxPro for Windows and Visual FoxPro provide a rich programming language that allows the<br />

developer to create applications of amazing power and complexity. However, even with all the<br />

commands included, there are still times when a task either can't be accomplished with native<br />

FoxPro code, or the overhead associated with the procedure written in native FoxPro causes too<br />

great a performance hit. When you hit a brick wall in your development and FoxPro just won't<br />

cooperate, take time to browse through the Windows API help file (included with the<br />

Professional Edition of Visual FoxPro and other Microsoft "visual" development products). You<br />

may find just what you need.<br />

Having easy access to around 75 percent of the Windows API functions (the rest require abstract<br />

data types such as C structures so you have to either be very tricky or write C routines to access<br />

them), opens up a world of possibilities for developers who do a little research. I hope these<br />

examples help you, and let you build on what I've presented here. I look forward to comments,<br />

questions or suggestions.<br />

Richard Aman is director of software engineering at Loren Industries, a jewelry manufacturing company<br />

with headquarters in Hollywood, Florida. Richard has been developing business solutions in<br />

FoxBASE/FoxPro since 1988 and regularly gives presentations at his local Fox User Group.<br />

CompuServe 73700,141.<br />

Create Compound Boolean Searches<br />

Jim Haentzschel<br />

One solution to the problem of user-friendly query facilities is limiting the search criteria to valid<br />

(4)


values, then translating those values into something the underlying code can use. This article gives<br />

ideas for implementing such an interface.<br />

How often has a client asked you to write a compound Boolean search routine? For example, say<br />

your client has a list of customers who order items like jams, jellies, and butter. Furthermore,<br />

these items are keywords you attach to each customer so you can track what they order.<br />

What your client needs is a simple report showing what customers have ordered jams AND<br />

jellies AND butter. You might also want a report showing what customers have ordered jams OR<br />

jellies OR butter.<br />

It turns out that some of the third-party reporting packages have trouble with compound Boolean<br />

searches (especially "AND"). Most of these third-party packages have a way to link a FoxPro<br />

application to the engine to help overcome problems with compound Boolean searches<br />

(pre-processing and post-processing). Pure SQL would be very cumbersome (or nearly<br />

impossible) for the AND case. Therefore, in this article, we'll use just FoxPro and FoxPro's<br />

report writer. Although I've coded the solution for FoxPro for DOS, the concept is equally usable<br />

in all versions of FoxPro, including Visual FoxPro.<br />

I'll show you how to create a Boolean compound search program combining search terms with<br />

logical "ANDs" or "ORs". In this implementation, the user can enter from one to three search<br />

terms, which I like to call "keywords," in a "mover" screen. I use three tables to implement this:<br />

Keywords, Customer and CustLink. The Keywords table (see Table 1) is a simple list of valid<br />

keywords the user can search for. The Customer table is a typical table containing customer ID,<br />

name, and address information. It has a single index tag based on the CustomerID field.<br />

Table 1. The Keywords Table.<br />

KEYWORDS.DBF (Indexed on KEYID, tag name KEY)<br />

Field Name Type Purpose<br />

KEYID C(3) Holds a unique ID for the keyword<br />

KEYWORD C(20) Holds the keyword text<br />

Typical KEYWORDS records:<br />

KEYID KEYWORD<br />

001 Jams<br />

002 Jellies<br />

003 Preserves<br />

The CustLink table links customers to keywords (see Table 2). Since each customer can have<br />

many keywords and each keyword can appear with many customers, there is a many-to-many<br />

(M:M) relationship between keywords and customers.


Table 2. The CustLink table.<br />

CUSTLINK.DBF (Indexed on CUSTOMERID, tag name CUSTOMERID)<br />

Field Name Type Purpose<br />

CUSTOMERID C(3) Customer ID<br />

KEYID C(3) Keyword ID<br />

Typical CUSTLINK records:<br />

CUSTOMERID KEYID<br />

001 001<br />

001 003<br />

001 004<br />

002 001<br />

002 006<br />

003 002<br />

003 005<br />

004 002<br />

004 004<br />

004 005<br />

The programs I'll describe in this article include the following:<br />

BOOLMAIN.PRGMain calling program (includes Boolean search logic)<br />

BOOLPICK.PRGProgram that puts up the "mover" screen for keyword entry<br />

BOOLPICK.SPRThe "mover" screen<br />

BOOLKEY.FRXThe report form.<br />

For this article, I'm not going to explain the mover screen (if you're interested in learning more<br />

about movers, the full source code is in the accompanying Download file, or see the "Cool Tool"<br />

on mover screens in the June 1995 issue). It's important to note, however, that a mover screen is<br />

just one way to enter keywords from a fixed list. In some cases, it might be more appropriate to<br />

let users themselves enter keywords in a text input field.<br />

The main program is BOOLMAIN.PRG. The basic flow of the program is as follows:<br />

1. The user enters from one to three keywords. This is handled by BOOLPICK.PRG and<br />

BOOLPICK.SPR.<br />

2. The user selects the "AND" or "OR" radio button to define the search type.<br />

3. The user clicks "OK" to begin the search, which occurs in BOOLMAIN.PRG.


4. The user sees any customers found in the search in a FoxPro report "PREVIEW" screen.<br />

5. If desired, the user can print any results.<br />

The screen that allows the user to pick keywords is shown in Figure 1. As you can see, the user<br />

has already selected three keywords. Notice too that "AND" is the default logical search<br />

expression. In my experience, customers are generally more interested in AND than OR.<br />

Once the user selects from one to three keywords and presses "OK," control returns to<br />

BOOLMAIN.PRG. The mover screen returns an array, laBoolArray[ ], to BOOLMAIN.<br />

laBoolArray[ ] is a two-dimensional array For each row, it lists the keyword, and the keyword ID<br />

from KEYWORDS.DBF.<br />

Handling the AND case<br />

To search for customers who have one to three keywords joined with "AND," the basic idea is to<br />

search for all customer IDs in the CustLink table with all the keywords listed in laBoolArray[ ]<br />

selected. Each customer ID that meets the criteria is then added to another array, laReportArray[<br />

], which will then be used to select records from the Customer table to include in the report.<br />

Handling the OR case<br />

The OR case is similar to the AND case, except that instead of selecting customers who match<br />

all the keywords, OR selects customers who match any of the keywords; otherwise, the two<br />

cases are identical.<br />

The code in Listing 1 is from BOOLMAIN.PRG. At the start of the listing, the criteria selection<br />

screen BOOLPICK.SPR has just returned laBoolArray[ ]. The next task is to begin searching<br />

CustLink for matching Customer IDs. You'll notice two DO WHILE loops. The outer DO<br />

WHILE loop controls the scanning of records in CustLink. The inner DO WHILE controls<br />

looping though records for a given CustomerID. In other words, the inner loop makes sure each<br />

CustomerID is examined individually.


Listing 1. BOOLMAIN.PRG.<br />

WAIT WINDOW "Searching for matching customers..." NOWAIT<br />

SELECT custlink<br />

SET ORDER TO CUSTOMERID<br />

GO TOP<br />

lnNumKeysFound=0<br />

lnReportSize="0"<br />

lcOldEx=SET ("EXACT")<br />

SET EXACT ON<br />

* Outer Do While loop controls going thru the<br />

*entire CUSTLINK.DBF<br />

DO WHILE !EOF("custlink") and !llDone<br />

lcCustid = ALLTRIM(custlink.customerid)<br />

DO WHILE ( lcCustid = ;<br />

ALLTRIM(custlink.customerid) ) AND ;<br />

!llDone<br />

IF ( ASCAN(laboolArray, ;<br />

ALLTRIM(custlink.keyid) ) !=0 )<br />

* if the user selected "OR" and we just<br />

* found a matching keyword, then add this<br />

* customer to the report array now. ELSE,<br />

* increment counter for later and continue<br />

* (if lnBoolType=2, they selected "OR")<br />

lcKeyid=custlink.keyid<br />

IF lnBoolType=2<br />

=AddToReport(lcKeyid, lcCustid)<br />

LOCATE REST FOR ;<br />

lcCustid # ALLTRIM(custlink.customerid)<br />

ELSE<br />

lnNumKeysFound = lnNumKeysFound + 1<br />

SKIP IN custlink<br />

ENDIF<br />

ELSE<br />

SKIP IN custlink<br />

ENDIF<br />

IF (EOF("custLink"))<br />

SKIP -1<br />

llDone=.T.<br />

ENDIF<br />

ENDDO<br />

* Customer just changed. Add customer to<br />

* report if this is an AND search and we<br />

* found *all* the keywords.<br />

IF lnBoolType = 1 AND ;<br />

lnNumKeysFound = ALEN(laBoolArray,1)<br />

=AddToReport(lcKeyid, lcCustid)


The call to AddToReport() will be made to add this customer to the report if this is an OR search,<br />

or if it's an AND search and all the keywords have been found:<br />

PROCEDURE AddToReport<br />

PARAMETERS Keyid, Custid<br />

* Add customer to report<br />

lnReportSize = lnReportSize + 1<br />

DIMENSION laReportArray[1,lnReportSize]<br />

laReportArray[1,lnReportSize] = custid<br />

RETURN<br />

If you have anything to report, the variable lnReportSize will be positive; otherwise, the WAIT<br />

WINDOW, which follows, will indicate that no matching Customers were found, and the<br />

program will exit. However, if one or more customers were found, BOOLMAIN builds a string<br />

to display the Boolean search string that will be displayed at the top of the report. A report cursor<br />

is then created from laReportArray[]:<br />

* CreateCursor() creates the empty cursor<br />

=CreateCursor()<br />

lcOldExact=SET("EXACT")<br />

SET EXACT OFF<br />

SELECT CUSTOMER<br />

SET ORDER TO customerid<br />

jnNumCols=ALEN(laReportArray,2)<br />

FOR jnCount= 1 to jnNumCols<br />

IF SEEK laReportArray[1,jnCount]<br />

SCATTER MEMVAR<br />

SELECT curImport<br />

APPEND BLANK<br />

GATHER MEMVAR<br />

SELECT customer<br />

ENDIF<br />

ENDFOR<br />

SELECT curImport<br />

GO TOP<br />

REPORT FORM BOOLKEY.FRX PREVIEW<br />

Figure 2 shows the report preview screen.<br />

The customer and associated tables in the accompanying Download file are simple and<br />

straightforward. I've used this code on large (real-life) tables with very good throughput. I'm


interested in hearing how this code works for you. I'm also interested in hearing from you if you<br />

have faster or different suggestions for Boolean searches.<br />

If you expect to find a lot of matching customers in your application, you might want to create<br />

the report cursor in advance and add records to it in AddToReport, rather than add to an array of<br />

"hits" and build the cursor later. Otherwise, the "hits" array, laReportArray[], could get<br />

unwieldy. I designed this demonstration, as I did for tutorial purposes, to separate and simplify<br />

the individual steps.<br />

I hope that you can see from this article that selecting customer records with a compound<br />

Boolean search string is straightforward. There are plenty of places you could add improvements<br />

to my code. For example, you could add another set of radio buttons to let the user select the<br />

grouping on the report (or some other useful application-specific feature). If you're really brave<br />

(and have lots of time), you could let the user mix "AND" and "OR" Boolean operators in a<br />

single search expression. Since most users seldom need to do this, I've focused only on one<br />

Boolean operator per search expression.<br />

Jim Haentzschel is president of Hurricane Technology, a firm specializing in database applications<br />

development in Visual FoxPro, FoxPro 2.x, and in user training. 703-684-1393, CompuServe 75166,236.<br />

Calling Win32 and 32-bit DLL Functions<br />

from FoxPro 2.X<br />

Rick Strahl<br />

Gain Visual FoxPro-like access to Windows internals through this set of library functions that you<br />

can use in FoxPro 2.x.<br />

With Windows 95 and Windows NT moving into the mainstream, the new 32-bit implementation<br />

of the Windows API -- Win32 -- is becoming increasingly important to developers. This version<br />

of the API provides many enhanced operating system features that were previously unavailable.<br />

Visual FoxPro, by using the new DECLARE-DLL syntax, has no problem taking advantage of<br />

the features and functionality provided within Win32, but FoxPro 2.x doesn't support access to<br />

this new 32-bit API since it can't call 32-bit DLLs directly.<br />

Although this lack of support has little effect on FoxPro's internal operation, it does make a<br />

difference for some system functionality you might need to provide in your applications. For<br />

example, access to the system registry requires use of the Win32 API. Several of the system<br />

information functions have 16-bit API counterparts that return incorrect values. You have to call<br />

the Win32 versions of those functions if you want reliable results. Finally, Win32 provides a<br />

host of useful functionality that previously required add on libraries or DLLs to accomplish. But


don't feel left out. In this article I'll describe a DLL that bridges the gap between your FoxPro<br />

programs and 32-bit DLLs, using an intermediate translation interface called Call32.<br />

What's with Win32?<br />

Both Windows 95 and Windows NT implement their base operating systems service APIs based<br />

on a 32-bit version of the Windows API called Win32. Win32 provides many features that were<br />

previously left out of the Windows API and many new features that weren't available or were<br />

implemented differently in Windows 3.1. The main reason for the enhanced functionality is that<br />

Win32 essentially is the programmer's interface to the operating system, replacing the reliance<br />

on a separate MS-DOS and BIOS layer. For this reason Win32 provides a wider variety of<br />

services that previously were provided only through DOS and BIOS function calls, which meant<br />

you had to have low-level access to the hardware and system interrupts using a language such as<br />

C.<br />

Win32 has many functions that give you much more control and information about the operating<br />

system. For example: Have you ever wanted to set file attributes? Try SetFileAttributes. How<br />

about retrieving the name of the current computer under Windows 95 or NT? You can't do it<br />

without GetComputerName in the Win32 API. In fact, many network-specific information keys<br />

that used to be stored in SYSTEM.INI are no longer stored there under NT and Windows 95 and<br />

now need to be retrieved via API calls. How about finding the current, correct operating system<br />

version? FoxPro's OS() and the 16-bit GetVersion API function return wrong results for both<br />

Windows NT and Windows 95, while the Win32 version returns the correct value. The list could<br />

go on and on.<br />

The single most important feature that you are likely to need from the Win32 API though is the<br />

registry. The registry is Microsoft's replacement solution for the slow .INI file interface for<br />

configuration files that store information about system services in a registration database. The<br />

registry's database approach allows faster access to the system configuration information making<br />

it possible to store a larger number of entries without suffering a performance hit. The registry is<br />

supported by 16-bit Windows, but is extremely limited to OLE and file extension registration via<br />

the HKEY_CLASSES_ROOT key, which pretty much makes it useless. Only by using Win32<br />

can you retrieve any registry information about the computer and the applications installed on it.<br />

The CALL32.DLL I'll present here (available in the accompanying Download file), along with<br />

the examples, provides read and write access to the registry from your FoxPro 2.x applications.<br />

Using CALL32.DLL with FoxPro<br />

A while back I ran into several problems that required the use of Win32 API calls in order to<br />

solve a particular problem. One of the things I needed to do was to figure out the Windows<br />

version number correctly for all Windows platforms. The other was reading several values from<br />

the system registry. I couldn't find a way to do either using Win16 API calls or any other method<br />

I knew of at the time. While searching for a solution, I ran into some public domain C DLL code<br />

specific to Visual Basic that allowed VB to call Win32 API functions. With some tweaking of<br />

the C code and by creating a pair of FoxPro front end routines, I was able to get the DLL to work<br />

under FoxPro 2.x, allowing me to access many Win32 API functions from my programs.


The Call32 DLL interface consists of two DLL functions that work similar to the way Foxtools<br />

uses RegFn() and CallFn() by registering the DLL function and then calling it with the function<br />

handle that is retrieved. A function called Declare32 registers functions much like the Foxtools<br />

RegFn() function does, registering the DLL function by describing the function name and the<br />

parameters it uses. The other function named Call32 acts like a router that takes the original<br />

arguments and passes them on to the actual 32-bit DLL, translating the parameter types from 16<br />

bit to 32 bit in the process.<br />

When using Call32 from FoxPro you end up having to register the W32 function twice: once for<br />

using the Call32 interface and its registration rules, which are slightly different from Foxtools,<br />

and once for the actual function that you end up calling with CallFn(). Because of this double<br />

registration of each 32-bit function, it's easy to get tangled up in the registration and calling<br />

logic, which all together requires five separate lines of code for a single 32-bit DLL call. For this<br />

reason I created a pair of front-end routines that simplify the job, leaving the user with only three<br />

simple function calls instead of five that require intimate knowledge of the process.<br />

Let's start with an example. The following code calls the SetFileAttributes function in the Win32<br />

API:<br />

* Call WIN32 SetFileAttributes<br />

* BOOL SetFileAttributes(lpFileName,dwFileAttributes)<br />

#DEFINE FILE_READONLY1<br />

#DEFINE FILE_HIDDEN 2<br />

#DEFINE FILE_SYSTEM4<br />

#DEFINE FILE_NORMAL128<br />

*** Register 32-bit function with Call32<br />

lhcall32=Reg32("SetFileAttributes",;<br />

"Kernel32.dll","pi")<br />

*** get a handle for use with Foxtools<br />

lhsetattr=RegFP("CL","L")<br />

*** Now actually call the 32-bit function<br />

*** Note the final parm: Handle from Reg32 call<br />

=callfn(lhsetattr,"Test.txt",file_readonly,lhcall32)<br />

The calls the Reg32 and RegFP functions in the previous code are the FoxPro front-end routines<br />

that simplify the interface to the actual Call32 DLL functions. In a nutshell, these two functions<br />

are responsible for registering the Win32 function, once for the Call32 DLL (Reg32) and once in<br />

normal Foxtools fashion using RegFn (RegFP). The final call to the Win32 DLL function is<br />

accomplished by using the familiar Foxtools CallFn function with one important addition: The<br />

final parameter must be the handle returned from the Reg32 function. As you can see, two<br />

function handles are passed to this final call of CallFn() -- the first parameter is to satisfy<br />

Foxtools, the last for the Call32 handle.<br />

Reg32 registers the Win32 function with the Call32 interface. It takes the name of the 32-bit


DLL function, the DLL it's contained in, and a list parameter types as parameters. As with<br />

Foxtools and RegFn, the parameter types are passed as individual characters, which are similar<br />

to, but not the same as, those used by RegFn. Reg32 returns a handle to the 32-bit function,<br />

which must be used as the last parameter of the final function call with CallFn(). Here's the full<br />

syntax for Reg32():<br />

lh32Handle=reg32(,,)<br />

Table 1 contains a list of the parameter types.<br />

Table 1. Parameter types.<br />

Parameter Description<br />

I 32-bit Integer. FoxPro must pass a Long when actuallyusing or returning parameters of this<br />

type unless the function explicitlyreturns a short integer type such as SHORT or BYTE.<br />

P Pointers. Use this type for all string values(even string constants!) and any values that need<br />

to be passed by reference.<br />

W Window handles. Use this type whenever you need topass a window handle. It automatically<br />

translates the 16-bit handle to a32-bit handle. You can also use this for passing DWORD<br />

type parameters andother unsigned integers if a plain integer type fails.<br />

Setting up the parameter types in this step is separate from setting up the parameters used by<br />

Foxtools and RegFn in the next step. While Reg32 registers the function with the Call32 DLL,<br />

RegFP registers the function with FoxPro using the standard Foxtools interface. RegFP expects<br />

the function parameter and return types in typical Foxtools fashion. Here's the syntax:<br />

lhHandle=RegFP(,)<br />

These are the types you pass and receive from RegFP map to standard Foxtools types, so you can<br />

use Long and Character both by value or by reference by pre-pending the variable name with<br />

'@'. Keep in mind that in the Win32 API all integers are 32 bit, so usually you must pass them as<br />

Longs, unless the API call explicitly calls for a SHORT or BYTE value. RegFP automatically<br />

adds a final parameter of type Long to support the required function handle that must be passed<br />

as the final parameter when using CallFn().<br />

Once the function is registered, you can now call it using a standard CallFn() call. The syntax for<br />

the call looks like this:<br />

lvResult=CallFn(lhHandle,Parm1,Parm2,ParmN,lh32Handle)<br />

It's very important that the last parameter in the CallFn() statement is the 32-bit function handle<br />

that was retrieved with Reg32 in order for the function to work correctly.


Let's take a look at another example. The following retrieves the correct Windows version under<br />

Windows 95 or Windows NT. This code uses a set of additional bit shifting functions I added to<br />

CALL32.DLL to make sense of the result returned from the GetVersion API call:<br />

*** Win32 API call - INTEGER GetVersion(Void)<br />

lhcall32=reg32("GetVersion","Kernel32.dll","")<br />

lhwinversion=RegFP("","L")<br />

lnversion=callfn(lhwinversion,lhcall32)<br />

*** Large Number shown in Scientific Expression<br />

? "Win32 Getversion result:",lnversion<br />

*** Now decode the version number with<br />

*** bit shifting function provided in CALL32.DLL<br />

*** Result is returned in a single LONG<br />

*** LoWord contains version<br />

*** low byte=Major - HiByte=Minor<br />

lhLoword=regfn("LoWord","L","I")<br />

lhLoByte=regfn("LoByte","I","I")<br />

lhHiByte=regfn("HiByte","I","I")<br />

lnversion=callfn(lhLoWord,lnversion)<br />

lnPlatForm=callfn()<br />

lnmajor=callfn(lhLoByte,lnversion)<br />

lnminor=callfn(lhHiByte,lnversion)<br />

? "Win32 GetVersion (Converted Version): Major ",;<br />

lnmajor," - Minor ",lnminor<br />

The code starts by registering the GetVersion API call using Reg32 and specifying the<br />

parameters types to pass and return. In this case there's no parameter, so the parameter type is<br />

passed as a null string. Next the call is registered with Foxtools using the RegFP function, which<br />

again shows no parameters and a return type of Long. The actual API call returns an Integer, but<br />

remember that 32-bit integers are Longs to FoxPro and Foxtools. Finally, you make the actual<br />

call to the API function passing the Foxtools handle and the handle returned from Reg32 using<br />

the CallFn function.<br />

GetVersion returns a large Long that is encoded to contain a Windows platform flag and version<br />

information. The low WORD (a word is a 16-bit half of a Long or DWORD value) of the<br />

returned Long contains the version number, of which the low byte (or half a WORD) contains<br />

the major version number with the high byte containing the revision number. In order to decode<br />

the version numbers, you need to do some bit shifting in order to get at the individual version<br />

numbers. HiWord and LoWord, which take a Long as a parameter, and HiByte and LoByte,<br />

which takes an Integer as a parameter, are all contained in the CALL32.DLL file as individual<br />

functions that you can use for retrieving individual WORDs or BYTEs from a Long or Integer<br />

value. This a common operation for API calls that encode multiple values in a single return value<br />

to conserve memory and keep functions compact.


The previous example is provided for demonstration of Call32's functionality only. If you need<br />

to get the Windows version number, it would be much easier to use W32Version provided in<br />

CALL32.DLL instead. I created this abstracted custom function so that it returns the Windows<br />

version number as an integer where the major version is multiplied by 100, adding the revision<br />

number to it. To call it use the following code:<br />

PROCEDURE WinVersion<br />

lhw32ver=regfn("W32Version","","I","call32.dll")<br />

RETURN callFn(lhw32ver)<br />

It returns 400 for Windows 95 and 351 for Windows NT on my machine for example.<br />

How it works<br />

The hard work for the Call32 interface is handled by the code in the C functions contained<br />

within CALL32.DLL. The Call32 function performs the thunking and function aliasing that<br />

make it possible to call 32-bit functions. If you're interested in the source code for Call32, it's<br />

included in the accompanying Download file. I can't take credit for the actual Call32 code; Peter<br />

Golde created the thunking interface and put the code into the public domain.<br />

Calling the Win32 functions from C is pretty messy, and if you're interested in this, take a look at<br />

the abstracted functions that I added in the C program file. W32Version and Read/WriteRegistry<br />

both use the Call32 function to provide their functionality.<br />

On the FoxPro end I created the Reg32 and RegFP functions to reduce the number of lines<br />

required to make a 32-bit DLL call from five to three and hide the implementation details. The<br />

user doesn't need to know how it works, but simply pass the parameters. The only rule to<br />

remember is that the final CallFn() call must include the 32-bit function handle as the last<br />

parameter.<br />

Here's the code to the Reg32 and RegFP functions:


*****************************************************<br />

PROCEDURE Reg32<br />

******************<br />

*** Author: Rick Strahl<br />

*** Function: Registers a 32-bit DLL function using<br />

*** CALL32.DLL. Thunk interface<br />

*** Assume: Foxtools is loaded. Uses CALL32.DLL<br />

*** Pass: pcDLLFunction - Name of 32-bit funct<br />

*** pcDLLName - DLL container<br />

*** pcParmTypes - Parameter types<br />

*** I - Integer (FP Long)<br />

*** P - Pointer<br />

*** Strings,Reference<br />

*** W - Handles<br />

*** Return: Function handle that must be used to<br />

*** CALL32 function<br />

*****************************************************<br />

PARAMETERS pcdllfunction,pcdllname,pcparmtypes<br />

PRIVATE lhcall32,lncall32<br />

lhcall32=regfn("Declare32","CCC","L","CALL32.DLL")<br />

lncall32=callfn(lhcall32,pcdllfunction,;<br />

pcdllname,pcparmtypes)<br />

RETURN lncall32<br />

*****************************************************<br />

PROCEDURE RegFP<br />

******************<br />

*** Function: Registers 32-bit DLL function<br />

*** with Foxtools!<br />

*** Assume: Foxtools loaded, uses CALL32.DLL<br />

*** Pass: phFunction - Function handle provided<br />

*** via Reg32<br />

*** pcParms - Parameter types<br />

*** Return: Function Return value<br />

*****************************************************<br />

PARAMETER pcparms,pcrettype<br />

RETURN regfn("Call32",pcparms+"L",;<br />

pcrettype,"CALL32.DLL")<br />

Accessing the registry<br />

With 32-bit DLL access in place, my next problem was to access the system registry. For those<br />

not familiar with the system registry, it is accessed by providing a registry root key (HKEY<br />

values if you bring up the registry editor), a key name (which looks like a path<br />

"\SOFTWARE\Microsoft\Windows\CurrentVersion") and an entry ("Version") to work with.<br />

Registry paths take on a hierarchical structure very similar to DOS file paths, where the files are<br />

represented as the actual values stored in an entry. The registry API consists of a set of more<br />

than 10 functions, which allow reading and writing of both keys and node values. I don't expect<br />

my FoxPro 2.x programs to do much writing to the registry, so I created only basic read and


write functions that are described below.<br />

This should be easy now that we can call Win32 API functions, right? Unfortunately, the answer<br />

didn't turn out to be quite so easy because there appear to be some problems with Foxtool's use<br />

of large long integer values. FoxPro actually uses signed Longs while the registry uses unsigned<br />

integers, which causes some problems at the extreme end of the number range for these values.<br />

Registry access requires use of very large negative values for the root registry keys and several<br />

of these simply wouldn't work when passing them as parameters via CallFn. For example, the<br />

key value for HKEY_LOCAL_MACHINE is -2147483646 (( HKEY ) 0x80000002 ). This value<br />

causes FoxPro to bomb when calling the RegOpenKey API function directly with CallFn.<br />

I had to use a workaround by creating a wrapper DLL function for RegOpenKey and passing the<br />

root registry keys as strings to the intermediate function. The function decodes the string and<br />

passes the resulting Long value on to the API function, which returns a key handle. Once this<br />

routine was in place, I was able to create the individual registry access function wrappers using<br />

the Win32 extensions.<br />

Here's the code for basic registry access wrapper functions:


*****************************************************<br />

PROCEDURE OpenKey<br />

******************<br />

*** Author: Rick Strahl<br />

*** Function: Opens a registry key before reading<br />

*** writing entries.<br />

*** Assume: Calls 16-bitRegOpen in CALL32.DLL<br />

*** because of limitations in Longs & HKEY<br />

*** Pass: tcHkey - "HKEY_..." strings<br />

*** tcSubkey - Reg path "\Software\version"<br />

*** Return: key handle<br />

*****************************************************<br />

PARAMETER lchkey,lcsubkey<br />

lnrethandle=0<br />

lhropen=regfn("RegOpen","CC","L","CALL32.DLL")<br />

lnkey=callfn(lhropen,lchkey,lcsubkey)<br />

RETURN lnkey<br />

*****************************************************<br />

PROCEDURE CloseKey<br />

******************<br />

*** Function: Close registry key.<br />

*** Calls 16-bit WinAPI<br />

*** Pass: thHandle - Key handle<br />

*****************************************************<br />

PARAMETER thHandle<br />

lhClose=RegFn("RegCloseKey","L","L")<br />

RETURN callfn(lhClose,thHandle)<br />

*****************************************************<br />

FUNCTION querystr<br />

******************<br />

*** Function: Reads a registry string<br />

*** Notes: Also works with Binary types<br />

*** as long as it doesn't contain<br />

*** NULL values.<br />

*** Calls Win32 API - uses CALL32.DLL<br />

*** Pass: tnHandle - Key Handle<br />

*** tcEntry - Entry to retrieve<br />

*** Return: string or "" if empty or "*ERROR*"<br />

*****************************************************<br />

PARAMETER tnhandle,tcentry<br />

PRIVATE lhCall32,lhFP,lcDataBuffer<br />

*** Register function with Win32 and Foxtools<br />

lhcall32=;<br />

reg32("RegQueryValueEx","ADVAPI32.dll","ipippp")<br />

lhfp=regfp("LCL@L@C@L","L")<br />

*** Return buffer to receive value<br />

lcdatabuffer=SPACE(1024)<br />

lnsize=LEN(lcdatabuffer)<br />

lntype=1 && REG_SZ<br />

lnresult=callfn(lhfp,tnhandle,tcentry,0,@lntype,;<br />

@l d t b ff @l i lh ll32)


Here's an example of how you use these functions (this accesses the Windows 95 registry; you<br />

might have to change the keys to make this work under NT):<br />

SET LIBRARY TO home()+"FOXTOOLS.FLL"<br />

SET PROCEDURE TO call32<br />

lcroot="HKEY_LOCAL_MACHINE"<br />

lcsubkey="SOFTWARE\Microsoft\Windows\CurrentVersion"<br />

*** Must open the key first<br />

lhreg=openkey(lcroot,lcsubkey)<br />

lcOldSetting=querystr(lhreg,"RegisteredOwner")<br />

? "Old Value: " + lcoldsetting<br />

? "Setting Value: ", writestr(lhreg,"RegisteredOwner","New Owner")<br />

? "Showing New Value: ",querystr(lhreg,"RegisteredOwner")<br />

? "Rewriting old value: ", writestr(lhreg,"RegisteredOwner",lcOldSetting)<br />

? "Showing Old Value: ",querystr(lhreg,"RegisteredOwner")<br />

*** Don't forget to close the key<br />

=closekey(lhreg)<br />

SET LIBRARY TO<br />

OpenKey calls the custom function I created in CALL32.DLL in order to work around the Long<br />

limitation I mentioned earlier. Both the RegOpenKey and RegCloseKey API calls are available<br />

in the Win16 API, so neither one of these actually needs to access Win32. The QueryStr function<br />

uses the RegQueryValueEx Win32 API function to read a value from the registry. Once a value<br />

is retrieved, the Null is stripped off. If the value can't be found, the function returns "*ERROR*"<br />

to differentiate between missing keys/values and an empty ("") value. WriteStr also uses a<br />

Win32 API call using the RegSetValueEx Win32 function. If you write to an entry that doesn't<br />

exist, it will be created, but only if the key (that is, the directory) exists. The function returns .T.<br />

on success or false if it fails.<br />

You can find Integer versions of the Read and Write functions in the accompanying Download<br />

file. If you plan on working with the registry extensively you'll likely want to add support for<br />

adding and deleting keys and values using RegCreateKey, RegDeleteKey, and RegDeleteEntry. I<br />

don't foresee using the registry in FoxPro 2.x for much more than simple extraction and<br />

occasional value modification, so I haven't bothered to implement them. You can use these<br />

registry functions I described earlier as templates.<br />

In addition CALL32.DLL includes a simplified ReadRegistry function to retrieve registry<br />

values. It's easier to use for simple registry reads since you don't have to mess with opening and<br />

closing registry keys or key handles. But keep in mind that if you read or write multiple entries<br />

on the same key, it's faster to open the key then do the reads consecutively, rather than opening


and closing the key for each individual access.<br />

Here's the code:<br />

*****************************************************<br />

PROCEDURE rdregstr<br />

******************<br />

*** Author: Rick Strahl<br />

*** Function: Reads String value from the Registry<br />

*** Assume: Requires CALL32.DLL<br />

*** Works on Binary entries as well<br />

*** as long as no NULLs are part of val<br />

*** Pass: pcRoot - string registry key value<br />

*** "HKEY_CLASSES_ROOT"<br />

*** "HKEY_CURRENT_USER"<br />

*** "HKEY_LOCAL_MACHINE"<br />

*** "HKEY_USERS"<br />

*** pcSubKey- Subkey 'path'<br />

*** pcValue - Actual entry to read<br />

*** Return: key value, "", "*ERROR*"<br />

*****************************************************<br />

PARAMETERS pcroot,pcsubkey,pckey,pnlength<br />

PRIVATE lcresult,lnlength,lnresult<br />

IF PARAMETERS()


lcroot="HKEY_LOCAL_MACHINE"<br />

lcsubkey="SOFTWARE\Microsoft\Windows\CurrentVersion"<br />

? rdRegStr(lcRoot,lcSubkey,"RegisteredOwner")<br />

I hope the tools I've presented here are useful to you and help you extend the life of your FoxPro<br />

2.x applications on 32-bit Windows platforms.<br />

Rick Strahl is president of West Wind Technologies in Hood River, Oregon (Portland area), a company<br />

providing Visual FoxPro and FoxPro 2.x programming services, specializing in application add-ins,<br />

communications, Internet connectivity, and interfacing FoxPro with external applications and libraries.<br />

Rick is author of the popular shareware applications Time Trakker Plus, West Wind E-Mail, and Web<br />

Connection. 503-386-2087, e-mail 76427.2363@compuserve.com.<br />

Colorize Your Editor with XILIGHTS<br />

Whil Hentzen (5)<br />

Now that Xitech's XILIGHTS is here, there's one less reason to envy those fancy and expensive<br />

third-party editors. Learn how easy it is to add color highlighting to FoxPro's native editor with an<br />

inexpensive shareware product that works with both FoxPro for Windows 2.6 and Visual FoxPro!<br />

As big as Microsoft is, even it has limited resources. As a result, a few things didn't make it into<br />

the first release of Visual FoxPro: outer joins, a more robust debugger, object-oriented menus,<br />

and an editor with color coding. We may be waiting a while for some of these features, but you<br />

can have color coding in your editor now with Xitech's XILIGHTS library. Perhaps the best part<br />

of XILIGHTS is that it works both with FoxPro 2.6 for Windows and Visual FoxPro.<br />

XILIGHTS comes packaged in two FLLs, one for each platform, and a DLL for the Visual<br />

FoxPro version that goes into the WINDOWS\SYSTEM directory. In either case, simply place<br />

the FLL in your FoxPro path and issue this command:<br />

SET LIBRARY TO Xilights<br />

From then on, the various components of an editing window will be shown in color. Here are the<br />

default colors:<br />

Text Black<br />

Keywords (commands and functions) Blue<br />

Strings Green<br />

Numeric values Aqua


Constructs (DO WHILE, SCAN) Light Purple<br />

Comments Mustard<br />

Compiler directives Dark Purple<br />

System memory variables Red<br />

These colors are used in regular .PRG file editing windows as well as in the snippet editing<br />

windows in FoxPro 2.6, and the code windows in Visual FoxPro.<br />

Customizing XILIGHTS<br />

XILIGHTS looks for a text file called XILIGHTS.INI in your WINDOWS directory. This file<br />

controls the color scheme used for each component in the editing window. You can manually<br />

modify the file to specify the color of each component. You can also use the SETCOLOR<br />

function (part of the XILIGHTS library) to set the color of a specific component. The first<br />

parameter to SETCOLOR is the component; the second is the RGB value the component should<br />

be set to. For example, the following commands turn keywords to red and comments to blue,<br />

respectively:<br />

= SetColor(2,RGB(192,0,0))<br />

= SetColor(6,RGB(0,0,192))<br />

For easier color specification, the 2.6 version of XILIGHTS also has an RGB() function that<br />

corresponds to Visual FoxPro's RGB() function.<br />

Caveats<br />

This is shareware, and the folks at Xitech have decided to incorporate a splash screen that<br />

appears every few minutes to remind you to register. It's annoying, but it doesn't prevent you<br />

from realistically evaluating the product. Second, in FoxPro 2.6, XILIGHTS expects the name of<br />

FoxPro's main window. If you've changed the name of the window, either via your CONFIG.FP<br />

file or with the MODIFY WINDOW SCREEN command, XILIGHTS will appear not to work.<br />

You can use the FOXTITLE function to identify the name of the main window. For example, I<br />

typically change the title of the window to something like the following:<br />

"We're developing in FoxPro 2.6a!"<br />

Then I can keep track of exactly what I'm using. As a result, XILIGHTS doesn't recognize<br />

FoxPro. After installing XILIGHTS, the following command ensures that XILIGHTS recognizes<br />

the window and functions correctly:<br />

=FOXTITLE( "We're developing in FoxPro 2.6a!")


Where to find XILIGHTS<br />

The file XLGHT105.ZIP contains both 2.6 and 3.0 versions of XILIGHT. It can be found in the<br />

accompanying Download file, as well as in Library 7 of VFOX. Registration is about $45 (U.S.),<br />

which you can do through CompuServe's SWREG mechanism. Details are contained in the<br />

XILIGHT documentation that comes with the product.<br />

Whil Hentzen is president of Henzenwerke, a Milwaukee, Wisconsin, software development firm that<br />

specializes in strategic FoxPro-based business applications for Fortune 500 companies. He is also the<br />

editor of FoxTalk. 414-224-7654, fax 414-224-7650, CompuServe 70651,2270.<br />

Sidebar: Book of the Month<br />

Rounding out the series of three Microsoft Press books on software development is Steve<br />

Maguire's Debugging the Development Process (ISBN 1-55615-650-2). The title says<br />

"Debugging" but the key words are "Development Process." In Writing Solid Code, Maguire<br />

focused on the bug hunting process and on programming techniques aimed at their prevention; in<br />

this book, he takes a step back to look at the management process of software development. He<br />

examines the ultimate goal -- getting quality products out the door on time and within budget -then<br />

analyzes the reasons why this rarely happens. Part of the problem, of course, is bugs, but<br />

just as much a factor is the rest of the ingredients that go into software development.<br />

A recurring theme is that many programmers spend too much time working on unnecessary<br />

tasks. For example, Maguire stresses that programmers should spend their time programming,<br />

not preparing for meetings, attending meetings, writing up follow-ups on meetings, and so forth.<br />

Another waste is the infamous "feature creep" -- that syndrome of additional project features that<br />

are added without a corresponding adjustment in the schedule or resources. Of course, it's easy to<br />

say "Don't allow new features to be added" but quite another to put this into practice when the<br />

person making the request is your boss (or the boss's boss!). Maguire makes a number of<br />

practical suggestions and strategies for dealing with this.<br />

Another repeated advice from the author is that delivering high-quality products on time doesn't<br />

require super-human efforts and 80-hour weeks as the norm, but rather careful attention to detail<br />

and keeping one's eye on the ball. Maguire provides a number of examples of having brought<br />

out-of-control projects back in line without working miracles. Experienced developers will find<br />

his examples engaging and true-to-life. You'll want to put his solutions to work immediately.<br />

Sleeping with the Enemy<br />

Les Pinter


I just lost my first bid to a Visual Basic competitor. It was a cold shower, I can tell you. And the<br />

worst part is that I'm not entirely sure who's right.<br />

The application is in the communications industry and involves a large database. The output is<br />

graphical; maps are used to display the results of a series of "what-if" scenarios. Doug Blank of<br />

Blank Software sells GeoGraphics (available through HALLoGRAM Software), which can<br />

easily do the map display part. And FoxPro is the only database in town, right?<br />

Wrong. Visual Basic 4.0 comes with a manual that describes how to build database applications<br />

in VB. Never mind that the same database functionality in FoxPro would pretty much consist of<br />

25 commands. With Microsoft's encouragement, Visual Basic programmers are ready to expand<br />

into the database world.<br />

It's always been possible to write database applications in BASIC. I wrote some using Dartmouth<br />

BASIC on a mainframe almost 30 years ago. But BASIC has had 30 years to become the<br />

database programmer's language of choice, and it hasn't made any significant inroads. Until now.<br />

How good is the "access" to data in VB? There are two ways to access your data in VB4. One is<br />

the ACCESS JET Engine; the other is SQL. Both are given equal time in the documentation -- as<br />

in "if you don't like the JET engine, try SQL."<br />

My experience with SQL is that about two-thirds of Visual FoxPro projects that start as SQL are<br />

converted to .DBF-based systems, based on complexity and poor performance -- as in "don't do<br />

this unless you absolutely have to." The equivalency of SQL and the JET Engine is correct in<br />

that regard. Recent optimization notwithstanding, VB's JET engine is considerably slower than<br />

FoxPro.<br />

How about interface design? I love Visual FoxPro's object orientation. Jack Hakim at SBT<br />

assures me that it's not as robust as it might be, but it clearly saves a huge amount of time in the<br />

development work we're doing for clients, reducing costs by as much as two-thirds! Believe it or<br />

not, VB's not object-oriented. You reinvent the wheel on each screen. How quickly I've become<br />

spoiled. I'm sure that, as of this moment, Visual FoxPro is the most advanced development tool<br />

on the market.<br />

So why did Delphi win the Technical Excellence award from PC Magazine for development<br />

environments? It's based on SQL, costs several thousand dollars, and requires coding in<br />

PASCAL to build and use classes -- that sexy visual interface doesn't support visual objects. As<br />

Alan Griver likes to say, "I just don't get it."<br />

Why was Access chosen in the same magazine as the best database product available? If<br />

performance were the criteria, Access would lose to FoxPro in a minute. Actually, it might take<br />

an hour to lose to FoxPro if I remember some of the benchmarks correctly. It's excellent for users<br />

and light development tasks, but in the reports I hear, developers talk about running into one wall<br />

after another. "It's like programming in a maze," a frustrated developer told me.<br />

And yet, each of these products is light-years ahead of the tools we used just a couple of years<br />

ago. And, to tell the truth, any of them will build pretty good software. They're more similar than<br />

they are different. I can see why a developer might be indifferent about choosing between saving


half the cost by writing an application in VB, or saving 80 percent of the cost by developing in<br />

Visual FoxPro.<br />

But, just in case, I'm going to allocate a little of my limitless spare time each week to exploring<br />

VB, PowerBuilder, Delphi, and even the lowly Access. Each has some good ideas. And, some<br />

fine morning, Microsoft just may bring out Visual FoxPro 4.0 and call it VB5. It's like Vietnam:<br />

They could declare a victory and go home.<br />

Les Pinter publishes the Pinter FoxPro Letter in the United States and Russia. 916-582-0595.<br />

Fox World News<br />

Bel Consulting has shipped FoxWord for Visual FoxPro. FoxWord is a report capture module<br />

that allows the capture of FoxPro report output in a rich text format (.RTF) file, preserving font<br />

and formatting information including lines, colors, and bitmaps. FoxWord can be embedded in a<br />

FoxPro application and can be distributed with the application to users. The FoxWord module is<br />

invoked with simple command-line syntax similar to FoxPro's REPORT FORM command. Bel<br />

Consulting, 172 West 79th St., Suite 9D, New York, NY 10024, 212-799-0123. $99.<br />

Azalea Software is shipping carrick, a new Windows-based encryption tool. carrick works as<br />

both a standalone application or from within any Windows application that accesses a DLL,<br />

including most popular databases, word processors, and spreadsheets. With carrick, developers<br />

can implement industrial strength encryption using the new Blowfish algorithm, which allows a<br />

448-bit password. DES, the current standard private key encryption algorithm, only allows a<br />

56-bit password. Azalea Software Inc., PO Box 16745, Seattle, WA 98116. 800-ENCRYPT, ,<br />

carrick@azalea.com. $159 single user, $199 two-copy bundle.<br />

Book of the Month<br />

Rounding out the series of three Microsoft Press books on software development is Steve<br />

Maguire's Debugging the Development Process (ISBN 1-55615-650-2). The title says<br />

"Debugging" but the key words are "Development Process." In Writing Solid Code, Maguire<br />

focused on the bug hunting process and on programming techniques aimed at their prevention; in<br />

this book, he takes a step back to look at the management process of software development. He<br />

examines the ultimate goal -- getting quality products out the door on time and within budget -then<br />

analyzes the reasons why this rarely happens. Part of the problem, of course, is bugs, but<br />

just as much a factor is the rest of the ingredients that go into software development.<br />

A recurring theme is that many programmers spend too much time working on unnecessary<br />

tasks. For example, Maguire stresses that programmers should spend their time programming,<br />

not preparing for meetings, attending meetings, writing up follow-ups on meetings, and so forth.<br />

Another waste is the infamous "feature creep" -- that syndrome of additional project features that


are added without a corresponding adjustment in the schedule or resources. Of course, it's easy to<br />

say "Don't allow new features to be added" but quite another to put this into practice when the<br />

person making the request is your boss (or the boss's boss!). Maguire makes a number of<br />

practical suggestions and strategies for dealing with this.<br />

Another repeated advice from the author is that delivering high-quality products on time doesn't<br />

require super-human efforts and 80-hour weeks as the norm, but rather careful attention to detail<br />

and keeping one's eye on the ball. Maguire provides a number of examples of having brought<br />

out-of-control projects back in line without working miracles. Experienced developers will find<br />

his examples engaging and true-to-life. You'll want to put his solutions to work immediately.

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!