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.