Enumerating printer forms

Enumerating of printer forms can be done with Windows API EnumForms function. Contrary to what MSDN says, it returns the list of all printer forms on PC, not just for the specific printer. On other hand, DeviceCapabilities can return a list of supported paper sizes for the printer.

This is sample code. Add error handling and adjust to your requirements as necessary.

Requires Windows 2000 or later. The WinApiErrMsg function in the code below is from Retrieving Windows system error message. The Windows API support class is used to handle Windows API structures.

The code will run 'as is' in VFP8 or later because it uses Collection class to store the list of forms. It can be easily adapted to run in earlier VFP versions.

CLEAR
* Form flag values
#define FORM_USER       0x00000000
#define FORM_BUILTIN    0x00000001
#define FORM_PRINTER    0x00000002
ooo = NEWOBJECT("EnumPrinterForms", "EnumPrinterForms.fxp")
ooo.cUnit = "English"
ooo.nRound = 2
 
* Enumerate forms for default VFP printer
lcPrinter = SET("Printer",3)
 
* Enumerate forms for specified Windows printer
*lcPrinter = "Adobe PDF"
 
* Enumerate all print forms and get info about specific for stored in the properties of the object
IF NOT ooo.GetFormList(lcPrinter, "Envelope")
	? ooo.cErrorMessage
	? ooo.cApiErrorMessage
	* Error
ENDIF
 
? ooo.cFormName, ooo.nFormNumber
 
CREATE CURSOR crsPrintrForms ( ;
		FormID I, ;
		FormName C(30), ;
		Width N(9, ooo.nRound), ;
		Height  N(9,ooo.nRound), ;
		FormFlags I, ;
		IsSupported L)
 
FOR i=1 TO ooo.oFormList.Count
	loOneForm = ooo.oFormList.Item(i)
	*? loOneForm.FormID, loOneForm.FormName, loOneForm.Width, loOneForm.Height, loOneForm.FormFlags
	INSERT INTO crsPrintrForms FROM NAME loOneForm
ENDFOR
 
GO TOP
*BROWSE NOWAIT
BROWSE NOWAIT FOR IsSupported
 
ooo = Null
RETURN

The EnumPrinterForms class code

* EnumPrinterForms.prg
DEFINE CLASS EnumPrinterForms AS Custom
	HIDDEN hHeap, nInch2mm, nCm2mm, nCoefficient
 
	* Specified a Printer name for which the list of supported forms is retrieved
	*	If empty, it would retrieve the list of forms defined on local PC
	cPrinterName = ""
	* The form attributes are stored in thousands of millimeters
	* It can be converted by class to inches ("English") or centimeters ("Metric")
	cUnit = "Internal"
	* Specified how to round result of conversion
	nRound = 0
	* Conversion Coefficients
	nInch2mm = 25.4
	nCm2mm = 10
	nCoefficient = 1
 
	* Error code and Error message returned by Win API
	cApiErrorMessage = ""
	* Error message returned by class itself (none-API error)
	cErrorMessage = ""
 
	hHeap = 0
	* Collection of Print Forms retrieved
	oFormList = Null
	* Win API support class
	oWas = NULL
 
	* Store Form # for the form
	cFormName = ""
	nFormNumber = 0
 
	PROCEDURE Init(tcUnit, tnRound)
 
	IF PCOUNT() >= 1 
		This.cUnit = PROPER(tcUnit)
	ENDIF
	IF PCOUNT() = 2
		This.nRound = tnRound
	ENDIF
	This.oFormList = CREATEOBJECT("Collection")
	This.oWas = NEWOBJECT("WinApiSupport", "WinApiSupport.fxp")
 
	* Load DLLs
	This.LoadApiDlls()
	* Allocate a heap
	This.hHeap = HeapCreate(0, 4096*10, 0)
	ENDPROC
 
	PROCEDURE cUnit_Assign(tcUnit)
	IF INLIST(tcUnit, "English", "Metric", "Internal")
		This.cUnit = PROPER(tcUnit)
	ELSE
		RETURN	
	ENDIF
	* Calculate conversion coefficient
	DO CASE
	CASE PROPER(This.cUnit) = "English"
		This.nCoefficient = This.nInch2mm * 1000
	CASE PROPER(This.cUnit) = "Metric"
		This.nCoefficient = This.nCm2mm * 1000
	OTHERWISE
		This.cUnit = "Internal"
		This.nCoefficient = 1
	ENDCASE
	ENDPROC
 
	PROCEDURE Destroy
	IF This.hHeap <> 0
		HeapDestroy(This.hHeap)
	ENDIF
	ENDPROC
 
	*PROCEDURE GetFormNumber(tcPrinterName, tcFormName)
	*ENDPROC
 
	PROCEDURE GetFormList(tcPrinterName, tcFormName)
	LOCAL lhPrinter, llSuccess, lnNeeded, lnNumberOfForms, lnBuffer, i, lcFormName, lcFormName 
 
	IF NOT EMPTY(tcPrinterName)
		This.cPrinterName = tcPrinterName
	ENDIF	
 
	IF NOT EMPTY(tcFormName)
		This.cFormName = tcFormName
	ENDIF
 
	This.ClearErrors()
	This.nFormNumber = 0
	This.oFormList.Remove(-1)
 
	* Open a printer
	lhPrinter = 0
	lnResult = OpenPrinter( IIF(EMPTY(This.cPrinterName),0,This.cPrinterName), @lhPrinter, 0)
	IF  lnResult = 0
		This.cErrorMessage = "Unable to get printer handle for '" + This.cPrinterName 
		This.cApiErrorMessage = WinApiErrMsg(GetLastError())
		RETURN .F.
	ENDIF
 
	lnNeeded = 0
	lnNumberOfForms = 0
 
	* Get the size of the buffer required to fit all forms in lnNeeded
	IF EnumForms(lhPrinter, 1,  0, 0, @lnNeeded, 	@lnNumberOfForms  ) = 0
		IF GetLastError() <> 122   && The buffer too small error
			This.cErrorMessage = "Unable to Enumerate Forms"
			This.cApiErrorMessage = WinApiErrMsg(GetLastError())
			RETURN .F.
		ENDIF
	ENDIF
 
	* Get the list of forms
	lnBuffer = HeapAlloc(This.hHeap, 0, lnNeeded)
	llSuccess = .T.
	IF EnumForms(lhPrinter, 1, lnBuffer, @lnNeeded, @lnNeeded, 	@lnNumberOfForms  ) = 0
		This.cErrorMessage = "Unable to Enumerate Forms."
		This.cApiErrorMessage = WinApiErrMsg(GetLastError())
		llSuccess = .F.
	ENDIF
 
	IF llSuccess
		* Put list of the forms into collection with Form number (i) as a key
		* A collection here can be replaced with an array or a cursor.
		FOR i=1 TO lnNumberOfForms
			loOneForm = This.OneFormObj()
			WITH loOneForm
				lnPointer = lnBuffer + (i-1) * 32
				.FormID 	= i
				.FormFlags 	= This.oWas.Long2NumFromBuffer(lnPointer)
				.FormName 	= This.oWas.StrZFromBuffer(lnPointer+4)
				.Width 		= This.ConvertFormDimension(lnPointer+8) 
				.Height		= This.ConvertFormDimension(lnPointer+12)
				.Left 		= This.ConvertFormDimension(lnPointer+16)
				.Top 		= This.ConvertFormDimension(lnPointer+20)
				.Right 		= This.ConvertFormDimension(lnPointer+24)
				.Bottom 	= This.ConvertFormDimension(lnPointer+28)
				* Store form # for requested form
				IF UPPER(.FormName) == UPPER(This.cFormName )
					This.nFormNumber = .FormID
				ENDIF
 
			ENDWITH
			This.oFormList.Add(loOneForm, TRANSFORM(i))
		ENDFOR
 
		* Mark forms that are supported by the printer
		llSuccess = This.MarkSupportedForms()
 
	ENDIF
 
	= HeapFree(This.hHeap, 0, lnBuffer )
	= ClosePrinter(lhPrinter)
 
	RETURN llSuccess
 
	FUNCTION ConvertFormDimension(tnPointer)
		RETURN ROUND(This.oWas.Long2NumFromBuffer(tnPointer) / This.nCoefficient, This.nRound)
	ENDFUNC
 
	PROCEDURE MarkSupportedForms
	#DEFINE DC_PAPERS     2 
	LOCAL lnCount, lcBufferPapers, lnIndex, lcStr, lnFormID, loOneForm
	lcBufferPapers = REPLICATE(CHR(0), 2*512) 
	* Get the list of supported paper sizes, 2 bytes per item
	lnCount = DeviceCapabilities(This.cPrinterName, "", DC_PAPERS, @lcBufferPapers, 0) 
	IF lnCount <= 0
		* Call to DeviceCapabilities failed
		This.cErrorMessage = "DeviceCapabilities failed."
		This.cApiErrorMessage = WinApiErrMsg(GetLastError())
		RETURN .F.
	ENDIF	
 
	FOR lnIndex=1 To lnCount 
	    lcStr  = SUBSTR(lcBufferPapers, (lnIndex-1)*2+1, 2)  
	    lnFormID = This.oWas.Short2Num(lcStr)
	    IF NOT EMPTY(This.oFormList.Getkey(TRANSFORM(lnFormID)))
		    loOneForm = This.oFormList.Item(TRANSFORM(lnFormID))
		    loOneForm.IsSupported = .T.
	    ENDIF		    
	ENDFOR 
	ENDPROC
 
	* Create an object with forms attributes
	PROCEDURE OneFormObj
	LOCAL loOneForm
	loOneForm = NEWOBJECT("Empty")
	ADDPROPERTY(loOneForm, "FormFlags", 0)
	ADDPROPERTY(loOneForm, "FormId", 0)
	ADDPROPERTY(loOneForm, "FormName", "")
	ADDPROPERTY(loOneForm, "Width", 0)
	ADDPROPERTY(loOneForm, "Height", 0)
	ADDPROPERTY(loOneForm, "Left", 0)
	ADDPROPERTY(loOneForm, "Top", 0)
	ADDPROPERTY(loOneForm, "Right", 0)
	ADDPROPERTY(loOneForm, "Bottom", 0)
	* Indicates if printer supports the form 
	ADDPROPERTY(loOneForm, "IsSupported", .F.)
	RETURN loOneForm
	ENDPROC
 
	PROCEDURE ClearErrors
	This.cErrorMessage = ""
	This.cApiErrorMessage = ""
	ENDPROC
 
	HIDDEN PROCEDURE LoadApiDlls
		DECLARE Long HeapCreate IN WIN32API Long dwOptions, Long dwInitialSize, Long dwMaxSize
		DECLARE Long HeapAlloc IN WIN32API Long hHeap, Long dwFlags, Long dwBytes
		DECLARE Long HeapFree IN WIN32API Long hHeap, Long dwFlags, Long lpMem
		DECLARE HeapDestroy IN WIN32API Long hHeap
		DECLARE Long GetLastError IN kernel32
	ENDPROC
 
ENDDEFINE
*----------------------------------------------------------------------------------------------
 
FUNCTION OpenPrinter(tcPrinterName, thPrinter, tcDefault)
DECLARE Long OpenPrinter IN WinSpool.Drv ;
	String pPrinterName, Long @ phPrinter, String pDefault
RETURN 	OpenPrinter(tcPrinterName, @thPrinter, tcDefault)
 
FUNCTION ClosePrinter (thPrinter)
DECLARE Long ClosePrinter IN WinSpool.Drv Long hPrinter
RETURN ClosePrinter(thPrinter)
 
FUNCTION EnumForms(thPrinter, tnLevel, tnForm, tnBuf, tnNeeded, tnReturned)
DECLARE Long EnumForms IN winspool.drv ;
	Long hPrinter, Long Level, Long pForm, ;
	Long cbBuf, Long @pcbNeeded, Long @ pcReturned
RETURN EnumForms(thPrinter, tnLevel, tnForm, tnBuf, @tnNeeded, @tnReturned)
 
FUNCTION DeviceCapabilities(pDevice, pPort, fwCapability, pOutput, pDevMode)
DECLARE Long DeviceCapabilities IN winspool.drv ; 
    String pDevice, String pPort, Long fwCapability, ; 
   	String @pOutput, Long pDevMode
RETURN DeviceCapabilities(pDevice, pPort, fwCapability, @pOutput, pDevMode)

I want to try this code but

I want to try this code but an error occurs in first before any execution:
what is : EnumPrinterForms.fxp ?

ooo = NEWOBJECT("EnumPrinterForms", "EnumPrinterForms.fxp")

I split code into 2 parts for clarity

Put the EnumPrinterForms class (the second code block) into EnumPrinterForms.prg. After that you will be able to play with test code (the first code block)

winapisupport error

I use this code its generating a error ' winapisupport.fxp ' not found .. how can i solve this problem

Windows API support class

Can I change a printer form ID?

Thanks Sergey

All problem are solved. How can I change a printer form ID of a form is it possible?

You cannot change a printer form ID

Hi Hyder,

You cannot change a printer form ID in Windows.

Custom print form ID problem

hi...

i have a problem with vfp9 reports. I'm trying to use a custom print form, but the form id may be different on each PC,.
On a pc is 131, other 148, oher 126, but the report is created at with id 148. A solution may be change the form id to 148 in each pc? Any solution for it ?

Thanks

Re: Custom print form ID problem

Hi Deyvis,

Here's how the problem can be worked around:

  1. Use code from this article to retrieve Custom form ID before running a report
  2. Make a temporary copy of the report
  3. Open the copy of report as a table
  4. Update PAPERSIZE=nnn value in the Expr memo field of the first report record with new Custom form ID. Alternatively, you can put new PAPERSIZE=nnn in the Picture memo field of the same record.
  5. Close the report table
  6. Run updated temporary copy of the report.

lcPrinter

In EnumPrinterForms.MarkSupportedForms(), there is a reference to variable "lcPrinter", which is undefined within the EnumPrinterForms class.

I believe the correct reference should be, instead, the EnumPrinterForms class's own "THIS.cPrinterName".

lcPrinter Typo fixed

Thanks, Willson. I fixed it.

Thanks, code being used at FoxyPreviewer

Thanks a lot again Sergey !
The printing samples are all great. I'm using an adapted code from this sample in FoxyPreviewer(http://foxypreviewer.codeplex.com), to get the selected "Form" dimensions.

Once again....
The Bereznikeropedia Rocks !!!

What should I do to select the format found in the printer?

What should I do to select the format found in the printer?

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <java>, <powershell>, <tsql>, <visualfoxpro>. The supported tag styles are: <foo>, [foo].
  • Lines and paragraphs break automatically.
  • Web page addresses and e-mail addresses turn into links automatically.

More information about formatting options

By submitting this form, you accept the Mollom privacy policy.