"Enhanced" Printer control functions for FoxPro 2.x for Windows
Steve Sawyer - 75730,455

About a year ago, I was trying to figure out how to change the default windows printer from within a FoxPro/Win application.  A FoxForum sysop (Louise Simmons) passed along some information to me that showed how this can be done. This information is available in a number of places, including the Microsoft Knowledge Base, and I believe the Microsoft Developer's Network CD. I made a few changes to the code that Louise provided and uploade the resulting routines, including a "front end" to allow demonstration of these methods, and uploaded the result to Library 3 of the FOXFORUM as WINPRN.ZIP.

The contents of this archive is the result of making what I feel are improvements to the original methods.  The most significant change is that the original methods involved creation and maintenance of a global (public) array memory variable to store a list of available printers and their parameters, and a public character-type memvar to store the current default printer.  I don't honestly know whether this is poor programming practice or not, but it isn't how I feel comfortable working, and the revised routines here store this information in a "printer control" table instead.  Some folks may argue that it is perfectly Ok to store this kind of information in memory variables, but I feel that routines can be more cleanly interfaced with the rest of the system, and can be better "black boxes" if the way in which they communicate is less "volatile" than a memvar.  Maybe (no, probably) I'm wrong on this, but you can decide and make mods to this code if you feel differently.

Another change was to structure the routine which actually changes the WIN.INI file to be called as a function, and to return a logical value, depending on whether the change in the default printer was successful or not.  This can be used to prevent running a print job if changing the default printer was unsuccessful.

The core of these routines remains the same, i.e. using Windows API function calls (via FOXTOOLS.FLL) to retrieve information from, and make modifications to, the WIN.INI file.

These documents include an overview for those unfamiliar with how Windows sets and changes the default printer, and includes a discussion of the Windows API and how it is used, including a good alternative reference work for those who do not have the Windows Software Developer's Kit (SDK).  You will also find a "Quick Start" section to try these routines from the command window, with installation notes.

*---------*---------*---------*---------*---------*---------*---------
OVERVIEW
*---------*---------*---------*---------*---------*---------*---------
Open your WIN.INI file and look at three sections: [Windows], [Devices] and [PrinterPorts].  In the [Windoes] section, you will see an entry that looks something like this:

[Windows]
.
.
.
device=IBM 4039 LaserPrinter PS,pscript,LPT3:

This line tells Windows what your current, default printer is.

Your [Devices] and [PrinterPorts] sections should look something like this:

[PrinterPorts]
Epson LQ-1500=EPSON24,LPT1:,15,45
Generic / Text Only=TTY,LPT1:,15,45,LPT2:,15,45,LPT3:,15,45
IBM 4039 LaserPrinter PS=pscript,LPT3:,15,45
IBM 4039-10R Print Accelerator=IBMPCL5,LPT3:,15,45
Linotronic 200/230=pscript,LPT1:,15,90
Panasonic KX-P1124=PANSON24,LPT2:,15,45
WINFAX=WINFAX,COM2:,15,45

[devices]
Epson LQ-1500=EPSON24,LPT1:
Generic / Text Only=TTY,LPT1:,LPT2:,LPT3:
IBM 4039 LaserPrinter PS=pscript,LPT3:
IBM 4039-10R Print Accelerator=IBMPCL5,LPT3:
Linotronic 200/230=pscript,LPT1:
Panasonic KX-P1124=PANSON24,LPT2:
WINFAX=WINFAX,COM2:

Obviously I have different devices on my system than you have on yours, so the entries will be different.  Note the format of these entries:

Device entry in the [Windows] section:

	Printer_Name,driver,port
	(See p. 165 of the Windows Resource Kit for more info - "driver" is the name of the driver file, w/out extension) 

Entries in the [Devices] section:

	Device_Name = driver,port [,additional ports]
	(See p. 181 of the Windows Resource Kit for more info)

Entries in the [PrinterPorts] section:

	Device_Name = driver,port w/port parameters [,additonal ports w/port
					parameters]
	(See p. 180 of the Windows Resource Kit for more info - Note port parameters are DeviceTimeout and RetryTimeout)

The Windows API, or Application Programming Interface is a series of documented "public" functions that can be called from an application to provide a service to that application.  (If you're familiar with DOS programming, you will recall that a DOS programmer could make call to a DOS function via a software interrupt). 

As you may be aware, if a change is made to your SYSTEM.INI file, you must re-start Windows in order for those changes to take effect - SYSTEM.INI is only read once, as Windows is started.  On the other hand, WIN.INI is read from and written to constantly by Windows and Windows Applications.  If you go into the control panel and change your wallpaper, this change is actually recorded in WIN.INI in the [Desktop] section, and takes effect as soon as you close the Desktop applet in the Control Panel.  Likewise changes to the default printer, or adding and removing printers via the Control Pannel Printers applet, are made in WIN.INI and take effect immediately.  That is why you can change your default printer while an application is running and direct the output from your word processor to a file instead of the printer, and do so without having to re-start windows.

This bit of magic is done by using the two Windows API functions GetProfileString() and WriteProfileString().  The first is used to retrieve information from WIN.INI, the second to make changes in WIN.INI.

USING THE WINDOWS API FROM WITHIN FOXPRO

How do we do this from within a FoxPro/Win application?  The dynamic-link library that ships with FoxPro, FOXTOOLS.FLL is specifically intended to allow the FoxPro programmer to make these kinds of API calls.  Information on using FOXTOOLS.FLL is included in the FOXTOOLS.WRI file, and I suggest that you examine this file for a better understanding of the power of this capability.  I would also highly recommend Dan Appleman's "Visual Basic Programmer's Guide to the Windows API".  While this book is intended for use with Visual Basic, the Windows API is the same, regardless of what language you are using when you make an API call.  Dan's book provides the most complete reference I have seen to the Windows API outside of the Windows SDK itself.

While FOXTOOLS.FLL adds a number of functions to FoxPro, we are concerned with the two basic functions, REGFN() and CALLFN().

To illustrate what these functions do and how to use them, let's look at the READINI function included with this package:

* FUNCTION readini
PARAMETER pcSection, pcEntry, pcDflt, pcBuffer, pnBufLen
PRIVATE LIKE j*
IF OPENTOOL()
	jnFn = regfn("GETPROFILESTRING","CCC@CI","I")
	jnRetVal = callfn(jnFn,pcSection,pcEntry,pcDflt,@pcBuffer,pnBufLen)
ELSE
	=YNALERT("Couldn't open FOXTOOLS.FLL",3)
	jnRetVal = 0
ENDIF
RETURN jnRetVal

Look first at line above that uses regfn():

	jnFn = regfn("GETPROFILESTRING","CCC@CI","I")

This line is "registering" a Windows API function, called GetProfileString, with FoxPro.  This means that after this command is issued, we can use this API function as if it were a "native" FoxPro function.  The full syntax of this regfn() is:

RegFn(FunctionName, ArgTypes, ReturnType, DLLName) 

FunctionName is the API function being registered, the ArgTypes are the types of arguments (or parameters) passed to the function.  In this case we are telling FoxPro that getProfileString expects five parameters, three of character type, passed by reference, one of character type, passed by value (Note the "@" preceding the fourth "C" - see the FoxPro helpfile or Language Reference under SET UDFPARMS for more information about how parameters are passed)  and one of integer type.  The ReturnType ("I") tells FoxPro that the function will return an Integer value.  

There are three "libraries" that are always loaded whenever Windows is running, USER.EXE, KRNL386.EXE and GDI.EXE.  Dan Appleman's book, or the Windows SDK will tell us that GetProfileString is found in the "Kernel" library, therefore we don't have to specify a .DLL filename in the fourth parameter - as long as the .DLL is loaded, regfn() will be able to register the function if specified by name.

Regfn() returns an integer value which is a "handle" to the "registered" function, and is used by the next line by callfn() to use the registered function:

	jnRetVal = callfn(jnFn,pcSection,pcEntry,pcDflt,@pcBuffer,pnBufLen)

Here we actually get some information from WIN.INI, by calling the GetProfileString function, identified by the numeric "handle" in the variable jnFn.  We ask it to return information from the section in the parameter pcSection (in our case we're looking for stuff in the [Windows], [Devices] or [PrinterPorts] sections), specify a specific entry under the desired section that we want the information from (such as the "device=" entry in the [Windows] section), specify a "default" value (pcDflt) to return if it does not find any information in the requested section, give it a variable in which to store the information from WIN.INI (@pcBuffer), and tell it how long that variable is (pnBufLen).

In addition to the actual information from WIN.INI, recall that when we "registered" the GetProfileString() function, we told FoxPro that GetProfileString() returned an integer value!  This value is stored in the variable jnRetVal, and is returned to the code that calls this READINI() function.  What is this value?  Again, according to Dan Appleman's book (and the Windows SDK), this number is the number of bytes (therefore, characters) returned from WIN.INI by GetProfileString!

*---------*---------*---------*---------*---------*---------*---------
MEANWHILE, BACK AT THE RANCH...
(PROGRAM NOTES)
*---------*---------*---------*---------*---------*---------*---------

Now that we've seen the nuts and bolts of tickling Windows to give up some of it's secrets from within FoxPro, let's look at how we can make use of this in a practical way.

The procedure GETWPRNT makes three separate retrievals of information from, or "passes" at, WIN.INI.  Each one calls the READINI function, and the first simply compiles a list of the available printers the user has installed on his system:

jnBytes = READINI("devices",0,CHR(0),@jcRetBuf,jnBufLen)

Compare this call of our READINI() function to the parameter list above.

We are asking for information from the [devices] section of WIN.INI, and asking for *all* of the entries, hence the value of 0 for the second parameter (again this is information available from the Windows SDK or other source on the Windows API).  We are specifying that GetProfileString() (and hence our READINI()) will return a single null (CHR(0)) if no entries are found.  We pass an "empty" (not really - see below) character memory variable (jcRetBuf) as a "buffer" to hold the result, and tell READINI and GetProfileString the size of that variable (jnBufLen).

The only setup required before hitting this line is that we establish the "buffer" variable, which is initialized as a string of 2048 null characters.

So, what do we have after this line is executed?  A memory variable of 2048 bytes (characters), which has embedded in it the installed "devices" or printers (by name) that are installed, and a numerical variable, jnBytes, that tells us how much of jcRetBuf has the information we're looking for.

By doing a LEFT(jcRetBuf,jnBytes) we get a string with just the information we want!  Note that according to a suitable API reference, what we actually have is a string containing a list, delimited by null characters, and being terminated with *two* null characters.

The next DO WHILE...ENDDO loop makes use of these null "markers" to parse the string into the individual entries and to store them to the first column of an array.

Next step is to perform our "second pass" at WIN.INI.  Since we now know the entries being looked for in the [Devices] section, we can call READINI again, and instead of passing a 0 as the second parameter, this time we pass one of the printer names, and return the value on the other side of the "=" in the WIN.INI.

The "third pass" uses READINI() to look at the "device=" entry in the [Windows] section to determine the default printer.

Once this is done, GETWPRNT then transfers all of this information from it's internal array, jaPrinters, to a table, WinPrint.DBF.  Note that this is stored in the same location that the user has set to hold all temporary files (via the TMPFILES= setting in the CONFIG.FPW).  This information is available (under FoxPro/Win) using the SYS(2023) function.

You can take a look at the SAMPLE.DBF file included in this package (created by running GETWPRNT on my system) and compare it to the contents of my WIN.INI file as shown above.  Or, you can simply run GETWPRNT on your machine from the command window.  When it terminates, the file WINPRINT.DBF will have been created and left open, and (if you had no table open in the current work area when you ran GETWPRNT), it will be the currently selected workarea.  Then browse this file, do a MODI FILE C:\WINDOWS\WIN.INI and compare it to the [Windows], [Devices] and [PrinterPorts] sections of WIN.INI - Look familiar?

The other major procedure included here is SETWPRNT.  This procedure works in reverse, taking the information contained in WINPRINT.DBF and updating WIN.INI to "look like" WINPRINT.DBF.
*---------*---------*---------*---------*---------*---------*---------
QUICK START
*---------*---------*---------*---------*---------*---------*---------

Note that these routines make a couple of assumptions:

1) Your FOXTOOLS.FLL is located in your FoxPro/W startup directory

2) You have a TMPFILES= line in your CONFIG.FPW - if you don't, the default temporary directory is the one used by Windows, specified in your AUTOEXEC.BAT (SET TEMP=).

For now, you can play with this by changing your default printer in the Windows Control Panel, say to printer "A", running GETWPRNT, and BROWSEing the WINPRINT.DBF.  Then, [Assuming you *do* have more than one printer available] "copy" the contents of the cDefault field for printer "A".  Run the Windows Control Panel and change the default printer to printer "B".  Run GETWPRNT again, and note that the "non-empty" cDefault field is now on the record for printer "B".  Now, delete the contents of this field, move the cursor to the record for printer "A" from which you copied the contents of cDefault, paste in the value that you copied, and run SETWPRNT.

Open the Windows Control Panel, check your default printer, and voila! SETWPRNT has changed the default printer *back* to printer "A".

From this you can see that by manipulationg WINPRINT.DBF and running SETWPRNT, we can change the current default Windows printer either under program control, or through a dialog with the user.

There are a total of 6 procedures and functions in this package:

GETWPRNT 	- creates the WINPRINT.DBF table of available and the current 
			default printer.
			
SETWPRNT	- uses the WINPRINT.DBF (and calls GETWPRNT to create it if it
			doesn't exist) to update the WIN.INI file to change the default
			printer.
			
WRITEINI()	- Registers and invokes the API function WriteProfileString to write changes to WIN.INI

READINI()	- Registers and invokes the API function GetProfileString to read the current values in WIN.INI

OPENTOOL()	- Checks to see if FOXTOOLS.FLL has already been loaded, and if not, looks for it in the FoxPro/Win "home" directory, and if found loads it.  Returns .T. if successful, .F. if it fails.

YNALERT()	- Simple message-box routine.  Used in the event that OPENTOOL() couldn't find FOXTOOLS.FLL to inform the user that there is a problem.  Note that this too uses FOXTOOLS.FLL, however in this case the Windows MsgBox() function has been built into FOXTOOLS.FLL, so there is no need to use regfn() or callfn().  Also includes code to emulate a windows-type "Yes/No" or "Ok" message-box in DOS.  This function is also called to inform the user that SETWPRNT is going to return a .F. value, if for any reason SETWPRNT is unable to write to the WIN.INI file.




	







