Use the Windows API to Print to Multiple Print Queues

Richard Aman

Richard Aman is director of software engineering at Loren Industries, a jewelry manufacturing company with headquarters in Hollywood, Florida. Richard has been developing business solutions in FoxBASE/FoxPro since 1988 and regularly gives presentations at his local Fox User Group. CompuServe 73700,141.

This article originally appeared in the February 1996 issue of FoxTalk, published by Pinnacle Publishing Inc., PO Box 888, Kent, WA 98035-0888; 800-788-1900; 206-251-1900; http://www.pinpub.com.

Windows provides a wealth of services for controlling network connections. Richard Aman shows how to use these services in either FoxPro 2.x or Visual FoxPro to easily send a report to any available output device. In the process, you'll learn about Visual FoxPro's new variant of DECLARE, which allows you to call external DLL routines as if they were native UDFs. You'll also learn how to use several useful Windows API routines.

ONE of the more difficult application types I've implemented is an automatic scheduler. A scheduler performs procedures at various scheduled and unscheduled times throughout the day. The procedures must perform in unattended mode and make decisions without user input. Along with software-related decisions like branching and looping, the procedures must make hardware decisions. For example, the software must decide which printer to send output to, which network connection to use, when to reset the software, and which users are logged on when.

When I implemented the scheduler in Windows, the main design decision was how to allow the system to use different printers in different locations for the various output reports—key in an order-entry and work progress-based system like this one. I designed the system to make decisions based on orders entered, and by the various stages of manufacture that pieces have achieved.

The Win.INI route

To choose an output printer, I initially used sample code from the Microsoft Developer's Network (MSDN) CD. That sample code had multiple printers installed in Windows, then modified the WIN.INI file to change the default printer. The method worked fine, although it had limitations. First, it required that all printers to be used be installed in Windows—a problem if you want to use the application on different machines. The second limitation was that Windows has only a limited number of printer ports. Though it's possible to have several printers assigned to the same port, keeping track of them can get messy. The third limitation is that it takes time to modify WIN.INI and notify running applications to update themselves with the changes.

The method detailed on the MSDN CD used the Windows API functions GetProfileString() and PutProfileString() to access WIN.INI. This made me wonder what other functions for switching printers might be buried in the Windows API. I started looking through the Microsoft Developer's Network CD and the TechNet CD (both excellent resources) and the Windows API help file that comes with Visual FoxPro Professional Edition for other approaches to this problem.

A better way

That's when I came across the WNetGetConnection(), WNetAddConnection(), and WNetCancelConnection() functions. These three Windows API functions combine to give you almost unlimited programmatic control over the network connections through an application. WNetGetConnection() returns the name of the network connection that a local device is mapped to, or NULL if the device isn't mapped on the network. WNetAddConnection() maps a local device to a network connection if it's not already connected. And WNetCancelConnection() disconnects a local device from a network connection. With these three functions, I implemented multiple printer output through LPT1 alone. I also found two other useful Windows API functions: WNetGetUser(), which returns the user network login ID, and ExitWindows(), which can be used to restart Windows from within FoxPro.

My automatic scheduler application scans the orders table at a recurring interval for any new orders. When new orders are found, they're copied to separate temporary tables for printing. Based on the type, orders can print at one of three places in the plant. The print routine is passed through three parameters, which tell the routine the name of the temporary table, the report form to use, and where to send output. When the routine processes the print location decision, it calls WNetGetConnection() to check whether LPT1 is connected to a network print queue. If a current connection exists, it calls WNetCancelConnection() to disconnect LPT1 from the current network print queue. It then calls WNetAddConnection() to connect LPT1 to the proper network print queue. (To prevent a connection error, you'll need to cancel the existing connection before creating a new connection..)

The basics

Before I get to my sample code, I want to quickly cover some basic requirements for using this method of printer control and for using any of the Windows API functions (or functions in any Windows DLL).

If you're using FoxPro 2.6 for Windows, first load in the library file FoxTools.fll, (supplied with FoxPro 2.6). This library file loads with the command SET LIBRARY TO FoxTools. Once the library is loaded, your program has access to a pair of functions called RegFn() and CallFn(). RegFn() is used to register Windows API functions to FoxPro. CallFn() is used to call the functions previously registered with RegFn(). In Visual FoxPro 3.0, the DECLARE command replaces the need for FoxTools.fll and the RegFn() and CallFn() routines . This new DECLARE command is also used to register Windows API functions. Once the functions have been registered, they're called just like the internal FoxPro functions. However, for backward compatibility, FoxTools.fll, RegFn() and CallFn() can still be used in FoxPro 3.0. (Refer to the Visual FoxPro 3.0 help file for more information on the DECLARE command.)

If you're working in FoxPro 2.x and are new to FoxTools/RegFn/CallFn, you can get details from "Use Microsoft Windows Services from FoxPro" by Robert W. Lord (FoxTalk, March 1994). Back issues can be ordered at 800-788-1900. In the meantime, here's a brief rundown of these commands and the new Visual FoxPro replacements for them:

Loading the FoxTools.fll library

First, load the FoxTools.fll library for FoxPro 2.6:


 SET LIBRARY TO SYS(2004) + 'FoxTools.fll' ADDITIVE

This command line uses the FoxPro function SYS(2004) to get FoxPro's home directory, which is where FoxTools.fll is installed during normal installation. Also, use the ADDITIVE clause to add the library to any existing loaded libraries; otherwise, FoxTools.fll will replace any existing loaded libraries.

Using RegFn()

The FoxTools function RegFn() registers a Windows API function with FoxPro. RegFn() takes three required parameters and one optional parameter. The first parameter is the name of the Windows API function you want to register. The second parameter is a string containing letter designations for the types of parameters the Windows API function requires ( 'C' for character or string, 'I' for integer, and so forth). The third parameter is a letter designation for the type of value the Windows API function will return to FoxPro. The fourth and optional parameter is the name of the Windows DLL that contains the function you want to register. If you don't include the DLL name, FoxPro automatically looks in the standard Windows libraries (USER.EXE, KRNL386.EXE, and GDI.EXE in Windows 3.x) to try to find the Windows API function. If the function isn't found, an error code is returned. If the function is successfully registered, a function handle is returned. That handle is then used by CallFn() to access the Windows API function, as I describe later.

Using CallFn()

The FoxTools function CallFn() is used to access a Windows API function from within FoxPro once it has been registered with RegFn(). The parameters passed to CallFn() are the Windows API function handle returned by RegFn(), and the parameters specified in the second parameter in RegFn() when the Windows API function was registered.

Using DECLARE in Visual FoxPro

DECLARE is an enhanced command in Visual FoxPro 3.0. In addition to defining arrays, the DECLARE command also removes the need for using FoxTools to access the Windows API functions. DECLARE allows the application to directly register Windows API functions with FoxPro. Once the functions are registered, they can be called like any other FoxPro internal function. The first parameter to the DECLARE command is the Windows API function return type. The second parameter is the name of the function you're registering. The third parameter is the DLL containing the Windows API function. The remainder of the parameters are the parameter types that FoxPro will pass to the Windows API function.

The Windows API functions

WNetGetConnection()

WNetGetConnection() is used to retrieve the network connection to which a local device is mapped. The first parameter is a variable containing the name of the local device you want to check. The second parameter is a variable initialized to spaces ( I use 255 ) and will be supplied with the connection name by WNetGetConnection(). The last parameter is a variable containing the length of the second parameter ( in this case 255 ). All three parameters need to be passed by reference. After initializing the variables, register the function with FoxPro. This lets FoxPro know that the function will be passed three parameters by reference, two strings and one integer, and the function will return an integer. Ensure the second parameter buffer is empty before calling this function, because an error won't clear the buffer and you might get incorrect results.

Here's the syntax for setting up the WNetGetConnection call, first for FoxPro 2.x, then for Visual FoxPro:


 * FoxPro 2.x
lnGetConn = RegFn('WNetGetConnection','@C@C@I','I')
* Visual FoxPro
DECLARE INTEGER WNetGetConnection IN win32api ;
       STRING @, STRING @, INTEGER @

Call the function to return the current connection for the specified device:


 * FoxPro 2.x
lnRetVal = CallFn(lnGetConn, @lcDeviceName, ;
         @lcConnName, @lnBuffLen)
* Visual FoxPro
lnRetVal = WNetGetConnection(@lcDeviceName, ;
               @lcConnName, @lnBuffLen)

After the call to WNetGetConnection(), the buffer lcConnName will either contain the name of the network connection for the specified local device or will be empty if no connection currently exists for the local device. It will also be empty if an error occurred. Also, be sure to check the return value for any error codes. The Windows API return codes for WNetGetConnection() follow, as found in the Windows SDK:

0 The function was successful.

8 The system was out of memory.

50 The function was not supported.

59 An error occurred on the network.

87 The local device name parameter was not a valid local device.

234 The buffer was too small. ( The connection name is longer than

the allotted buffer length. )

487 The pointer was invalid.

2250 The local device name parameter was not a redirected local

device.

WNetAddConnection()

WNetAddConnection() is used to map a local device to a network connection. The first parameter is the network connection to map the device to. The second parameter is the password to use and should be a null string to use the default password. Finally, the third parameter is the local device to map. After initializing the variables, register the function with FoxPro. This lets FoxPro know that the function will be passed three parameters as strings, and will return one integer:


 * FoxPro 2.6
lnAddConn = RegFn('WNetAddConnection','CCC','I')

* Visual FoxPro
DECLARE INTEGER WNetAddConnection IN win32api ;
    STRING, STRING, STRING

Call the function to map the local device to the network connection:


 * FoxPro 2.x
lnRetVal = CallFn(lnAddConn,'\\SERVER1\HP4','','LPT1:')

* Visual FoxPro
lnRetVal = WNetAddConnection('\\SERVER1\HP4','','LPT1:')

This function returns 0 if successful, or an error number for any error that occurs while attempting to create the network connection. I've listed the Windows API return codes for WNetAddConnection(), which follow, as found in the Windows SDK.

0 The function was successful.
5 A security violation occurred.
8 The system was out of memory.
50 The function was not supported.
59 An error occurred on the network.
67 The network resource name was invalid.
85 The local device was already connected to a remote resource.
86 The password was invalid.
487 The pointer was invalid.
1200 The local device name was invalid.


WNetCancelConnection()

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 that you are trying to map is already mapped to a network connection. For this reason, I recommend checking the network mapping for the local device before attempting to create a new connection. Also, I recommend releasing any network mapping that exists first. The first parameter is the local device to cancel the network connection to. The second parameter tells Windows whether to close any open files, or simply to return an error. The second parameter should be 0 to close open files before disconnecting. After initializing the variables, register the function with FoxPro. This lets FoxPro know that the function will be passed two parameters, one string and one integer, and will return one integer.


 * FoxPro 2.6
lnCancelConn = RegFn('WNetCancelConnection','CI','I')

* Visual FoxPro
DECLARE INTEGER WNetCancelConnection IN win32api ;
    STRING, INTEGER

Call the function to cancel the connection for the specified local device:


 * FoxPro 2.6
lnRetVal = CallFn(lnCancelConn,'LPT1:',0)

* Visual FoxPro
lnRetVal = WNetCancelConnection('LPT1:',0)

This function returns 0 if successful, or it returns an error number for any error that occurs while attempting to cancel the network connection. Following are the Windows API return codes for WNetCancelConnection(), as found in the Windows SDK:

0 The function was successful.
8 The system was out of memory.
50 The function was not supported.
59 An error occurred on the network.
87 The local device name parameter was not a valid local device
  or network name.
487 The pointer was invalid.
2250 The local device name parameter was not a redirected local
  device or currently accessed network resource.
2401 Files were open and the fForce parameter was 0.
  The connection was not canceled.


WNetGetUser()

WNetGetUser() is used to return the network login ID of the machine the application is running on. The first parameter is a variable containing the local name to return the network login ID for. 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 third parameter is the length of the second parameter. All three parameters need to be passed in by reference in order for the function to operate correctly. After initializing the variables, register the function with FoxPro. This lets FoxPro know that the function will be passed two parameters by reference, one string and one integer, and the function will return one integer. Use NULL for the first parameter to get the current sign-on name because if the user is signed on more than once, the system makes a random choice of which login name to return:


 * FoxPro 2.x
lnGetUser = RegFn('WNetGetUser','@C@C@I','I')

* Visual FoxPro
DECLARE INTEGER WNetGetUser IN win32api ;
    STRING @, STRING @, INTEGER @

Call the function to get the network login ID of the machine:


 * FoxPro 2.x
lnRetVal = CallFn(lnGetUser,@lcUserID,@lnBuffLen)
lnRetVal = WNetGetUser(@lcUserID,@lnBuffLen)

After the call to WNetGetUser(), the buffer lcUserID will contain either the network login ID or will be empty if the machine isn't logged in to the network. Also, check the return value for any error codes. Following are the Windows API return codes for WNetGetUser(), as found in the Windows SDK:

8 The function could not allocate sufficient memory to
  complete its operation.
50 This function is not supported.
59 A network error occurred.
234 The buffer was too small to hold the complete user name.
487 The pointer is invalid.
2202 The user is not logged in; there is no current user name.


ExitWindows() and ExitWindowsEx()

ExitWindows() has two uses of interest, based on the first parameter passed in. The first parameter is a flag to tell Windows to either reboot or exit. The second parameter is reserved and should be 0. If you pass in a 67 to the first parameter, Windows will close down any running applications and exit to the DOS prompt. If you pass in a 66, Windows will close down any running applications and restart Windows. (the function is called ExitWindowsEx() in 32-bit Windows; the first parameter should be 0 to restart Windows.) I use a 66 in the automatic scheduler application . After initializing the variables, register the function with FoxPro. This lets FoxPro know that the function will be passed two parameters, both integers, and the function will return one integer:


 * FoxPro 2.x
lnExitWin = RegFn('ExitWindows','II','I')

* Visual FoxPro
DECLARE INTEGER ExitWindowsEx IN user32 ;
    INTEGER, INTEGER

Call the function to restart Windows:


 * FoxPro 2.x
lnRetVal = CallFn(lnExitWin,66,0)

* Visual FoxPro
lnRetVal = ExitWindowsEx(0,0)

ExitWindows() and ExitWindowsEx() have only two return codes: 0 or FALSE if an error occurred, and 1 or TRUE if the function call was successful. In this use of ExitWindows() or ExitWindowsEx(), you probably need only check for a failure return code, since a successful call would have restarted Windows and FoxPro, meaning the return code value would have been released. But a check for success might be important in other uses.

Why use ExitWindows() or ExitWindowsEx()? I use ExitWindows() because of a "memory leak" in FoxPro for Windows 2.6, which my scheduler runs under. When FoxPro generates a report, some of the memory used isn't released back to the pool of free memory. Consequently, after a certain number of reports, FoxPro can run out of memory. In my case, problems occur after approximately 75 work production tickets, each with a bitmap of the item and several different fonts. To prevent this, I set up FoxPro in the Windows StartUp group with a command line call to the automatic scheduler. Then, every 15 minutes I call a procedure called ResetWin (included on this month's Companion Disk), which closes down FoxPro and restarts Windows. When Windows starts up, it runs FoxPro from the StartUp group, which then restarts the automatic scheduler, which picks up where it left off—but with refreshed memory. Everything runs smoothly with this scheme in place.

Presumably, this memory leak has been fixed in Visual FoxPro, but you may still want to use ExitWindowsEx() to automatically shut down or restart some application types, perhaps at particular times of the day when demand is minimal. It often helps to get a "clean slate" periodically for an automated server application on a dedicated workstation, particularly if the application has the potential of running unattended for days or weeks.

Listing 1 illustrates my basic method of printer control. Prior to this procedure, the application will have selected the records to be printed, the report form to use, and the network print queue destination, all of which are passed in through parameters. I first initialize my variables and save the current library setting. I then make sure the Foxtools library is loaded for version 2.x. Next, I register the Windows API functions with FoxPro ( using the DECLARE command for version 3.0 or the RegFn() for version 2.x ). Then I check for and release any existing connection for LPT1: using WNetGetConnection(). I then set LPT1: to the desired network print queue with WNetAddConnection(). After all is set up properly, I loop through the source table and generate a full-page form for each record (this allows other users to insert print jobs between my frequent multi-page output). Finally, I make a call to GetNetID() to see if I'm on the automatic scheduler. If so, I reset LPT1: to a default network print queue. I then restore the previous library setting and exit the routine.

The call to the GetNetID UDF deserves a little more explanation. GetNetID() is included on this month's Companion Disk. It calls the Windows WNetGetUser() function to return the network login ID of the machine the application is running on. This can be used for a variety of things. In AutoPrnt, I check the network ID with GetNetID() to see if the application print routine is running on the automatic scheduler machine. If so, I re-map the printer back to the default queue so normal output that doesn't need to be specifically redirected can be printed on the central printer.

Listing 1. The AutoPrnt procedure.


 ********************************************************
* PROCEDURE AutoPrnt
********************************************************
* Author............: Richard L. Aman
*) Description.......: A scaled down version of the print
*)          : engine for the automatic scheduler
* Calling Samples...: DO AutoPrnt WITH cSrcTable,
*)          : cFormName, cPrinter
* Parameter List....: cSrcTable - table containing
*)          : records to print
*          : cFormName - name of form to use
*          : cPrinter - report destination

PROCEDURE AutoPrnt
PARAMETERS cSrcTable, cFormName, cPrinter

*-- define variables
PRIVATE lcFormName, lcPrinter, lcReport, lcOldPrinter, ;
    lcOldLibrary, lnAddConn, lnDelConn, lnGetConn, ;
    lcDeviceName, lcConnName, lnBuffLen, ;
    llVersion3, lcConnTo, lnRetVal
*-- init variables
lcFormName = cFormName
lcPrinter = cPrinter
lcReport = lcFormName
lcOldPrinter = ''
lcOldLibrary = SET('LIBRARY')
lcDeviceName = 'LPT1'
lcConnName = SPACE(254)
lnBuffLen = LEN(lcConnName)
llVersion3 = '3.0' $ VERSION()
lcConnTo = ''
lnRetVal = 0
*--ensure that foxtools library is loaded
IF NOT llVersion3

 IF NOT 'FOXTOOLS' $ UPPER( lcOldLibrary )
  SET LIBRARY TO SYS( 2004 ) + 'FOXTOOLS.FLL' ADDITIVE
 ENDIF

ENDIF
*-- register the Windows API functions
IF llVersion3
 DECLARE INTEGER WNetAddConnection IN win32API ;
     STRING, STRING, STRING
 DECLARE INTEGER WNetCancelConnection IN win32API ;
     STRING, INTEGER
 DECLARE INTEGER WNetGetConnection IN win32API ;
     STRING @, STRING @, INTEGER @
ELSE
 lnAddConn = RegFn('WNetAddConnection','CCC','I')
 lnDelConn = RegFn('WNetCancelConnection','CI','I')
 lnGetConn = RegFn('WNetGetConnection','@C@C@I','I')
ENDIF
*-- check for an existing connection
IF llVersion3
 lnRetVal = WNetGetConnection(@lcDeviceName, ;
                @lcConnName,@lnBuffLen)
ELSE
 lnRetVal = CallFn(lnGetConn,@lcDeviceName, ;
          @lcConnName,@lnBuffLen )
ENDIF

IF NOT EMPTY( lcConnName )

 IF llVersion3
  lnRetVal = WinNetCancelConnection('LPT1:',0)
 ELSE
  lnRetVal = CallFn(lnDelConn,'LPT1:',0)
 ENDIF

ENDIF

*-- set the printer to the correct queue
DO CASE

 CASE lcPrinter = 'MIS'
  lcConnTo = '\\SERVER1\MIS'
        
 CASE lcPrinter = 'QC'
  lcConnTo = '\\SERVER1\HP4'
        
 CASE lcPrinter = 'PRODUCTION'
  lcConnTo = '\\SERVER2\PRODPRINT'
        
ENDCASE

IF llVersion3
 lnRetVal = WNetAddConnection( lcConnTo, '','LPT1:')
ELSE
 lnRetVal = CallFn(lnAddConn,lcConnTo,'','LPT1:')
ENDIF

*-- print the report for each record
GO TOP

SCAN WHILE LASTKEY() <> 27 && allow ESC
 REPORT FORM &lcReport TO PRINTER NEXT 1 NOCONSOLE
ENDSCAN

*-- clean up and return
IF GetNetID() = 'SCHEDULE'

 IF llVersion3
  lnRetVal = WNetCancelConnection('LPT1:',0)
  lnRetVal = WNetAddConnection('\\SERVER1\PRINTQ_0',;
                 '','LPT1:')
    ELSE
  lnRetVal = CallFn(lnDelConn,'LPT1:',0)
  lnRetVal = CallFn(lnAddConn,'\\SERVER1\PRINTQ_0',;
           '','LPT1:')
 ENDIF

ENDIF

SET LIBRARY TO &lcOldLibrary
RETURN

Things I learned the hard way about using Windows API functions

If the function declaration calls for a value to be passed by reference, you must use a variable and preface it with the "@" symbol. I thought that for the buffer length, I could set a variable with the length of the buffer, then just pass the variable, but not in this case. I still had to use the "@" symbol.

If you're using the Visual FoxPro calling convention with the DECLARE command, the Windows API function names are case-sensitive.

Always remember to check the return value for any error codes. Unfortunately, I didn't have enough space in this article to go into detail about handling errors, but at the very least you should determine what the function returns when it's successful and test for that. Don't forge ahead in your code just assuming that the API function executed successfully.

Conclusion

FoxPro for Windows and Visual FoxPro provide a rich programming language that allows the developer to create applications of amazing power and complexity. However, even with all the commands included, there are still times when a task either can't be accomplished with native FoxPro code, or the overhead associated with the procedure written in native FoxPro causes too great a performance hit. When you hit a brick wall in your development and FoxPro just won't cooperate, take time to browse through the Windows API help file (included with the Professional Edition of Visual FoxPro and other Microsoft "visual" development products). You may find just what you need.

Having easy access to around 75 percent of the Windows API functions (the rest require abstract data types such as C structures so you have to either be very tricky or write C routines to access them), opens up a world of possibilities for developers who do a little research. I hope these examples help you, and let you build on what I've presented here. I look forward to comments, questions or suggestions.

To find out more about FoxTalk and Pinnacle Publishing, visit their website at
http://www.pinpub.com/foxtalk/

Note: This is not a Microsoft Corporation website.
Microsoft is not responsible for its content.

This article is reproduced from the February 1996 issue of FoxTalk. Copyright 1995, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. FoxTalk is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call (800)788-1900 or (206)251-1900.