[RESOLVED] Printer status: detecting on or off
In my shutdown program -- which defrags the harddrive with Diskeeper Lite, scans for spyware with AdAware and then scans for viruses with AVG before powering down -- I want it to warn me if I left the printer on.
Searching for existing threads uncovered this post, which links to this article. But that article is poorly constructed; it doesn't offer functional code.
Can anyone help me piece it together? This is what I have so far:
Code:
Option Explicit
Public Enum Printer_Status
PRINTER_STATUS_READY = &H0
PRINTER_STATUS_PAUSED = &H1
PRINTER_STATUS_ERROR = &H2
PRINTER_STATUS_PENDING_DELETION = &H4
PRINTER_STATUS_PAPER_JAM = &H8
PRINTER_STATUS_PAPER_OUT = &H10
PRINTER_STATUS_MANUAL_FEED = &H20
PRINTER_STATUS_PAPER_PROBLEM = &H40
PRINTER_STATUS_OFFLINE = &H80
PRINTER_STATUS_IO_ACTIVE = &H100
PRINTER_STATUS_BUSY = &H200
PRINTER_STATUS_PRINTING = &H400
PRINTER_STATUS_OUTPUT_BIN_FULL = &H800
PRINTER_STATUS_NOT_AVAILABLE = &H1000
PRINTER_STATUS_WAITING = &H2000
PRINTER_STATUS_PROCESSING = &H4000
PRINTER_STATUS_INITIALIZING = &H8000
PRINTER_STATUS_WARMING_UP = &H10000
PRINTER_STATUS_TONER_LOW = &H20000
PRINTER_STATUS_NO_TONER = &H40000
PRINTER_STATUS_PAGE_PUNT = &H80000
PRINTER_STATUS_USER_INTERVENTION = &H100000
PRINTER_STATUS_OUT_OF_MEMORY = &H200000
PRINTER_STATUS_DOOR_OPEN = &H400000
PRINTER_STATUS_SERVER_UNKNOWN = &H800000
PRINTER_STATUS_POWER_SAVE = &H1000000
End Enum
Private Type PRINTER_INFO_2
pServerName As String
pPrinterName As String
pShareName As String
pPortName As String
pDriverName As String
pComment As String
pLocation As String
pDevMode As Long
pSepFile As String
pPrintProcessor As String
pDatatype As String
pParameters As String
pSecurityDescriptor As Long
Attributes As Long
Priority As Long
DefaultPriority As Long
StartTime As Long
UntilTime As Long
Status As Long
JobsCount As Long
AveragePPM As Long
End Type
Private Const CCHDEVICENAME = 32
Private Const CCHFORMNAME = 32
Private Type DEVMODE
dmDeviceName As String * CCHDEVICENAME
dmSpecVersion As Integer
dmDriverVersion As Integer
dmSize As Integer
dmDriverExtra As Integer
dmFields As Long
dmOrientation As Integer
dmPaperSize As Integer
dmPaperLength As Integer
dmPaperWidth As Integer
dmScale As Integer
dmCopies As Integer
dmDefaultSource As Integer
dmPrintQuality As Integer
dmColor As Integer
dmDuplex As Integer
dmYResolution As Integer
dmTTOption As Integer
dmCollate As Integer
dmFormName As String * CCHFORMNAME
dmUnusedPadding As Integer
dmBitsPerPel As Long
dmPelsWidth As Long
dmPelsHeight As Long
dmDisplayFlags As Long
dmDisplayFrequency As Long
End Type
Private Type PRINTER_DEFAULTS
pDatatype As String
pDevMode As DEVMODE
DesiredAccess As Long
End Type
Private Declare Function ClosePrinter Lib "winspool.drv" (ByVal hPrinter As Long) As Long
Private Declare Function GetPrinterApi Lib "winspool.drv" Alias "GetPrinterA" (ByVal hPrinter As Long, ByVal Level As Long, buffer As Long, ByVal pbSize As Long, pbSizeNeeded As Long) As Long
Private Declare Function OpenPrinter Lib "winspool.drv" Alias "OpenPrinterA" (ByVal pPrinterName As String, phPrinter As Long, pDefault As PRINTER_DEFAULTS) As Long
Private Sub Command1_Click()
Dim lret As Long
Dim pDef As PRINTER_DEFAULTS
Dim mhPrinter As Long
Dim Index As Long
lret = OpenPrinter(Printer.DeviceName, mhPrinter, pDef)
Dim SizeNeeded As Long
Dim buffer() As Long
ReDim Preserve buffer(0 To 1) As Long
lret = GetPrinterApi(mhPrinter, Index, buffer(0), UBound(buffer), SizeNeeded)
ReDim Preserve buffer(0 To (SizeNeeded / 4) + 3) As Long
lret = GetPrinterApi(mhPrinter, Index, buffer(0), UBound(buffer) * 4, SizeNeeded)
Dim mPRINTER_INFO_2 As PRINTER_INFO_2
With mPRINTER_INFO_2 '\\ This variable is of type PRINTER_INFO_2
.pServerName = StringFromPointer(buffer(0), 1024)
.pPrinterName = StringFromPointer(buffer(1), 1024)
.pShareName = StringFromPointer(buffer(2), 1024)
.pPortName = StringFromPointer(buffer(3), 1024)
.pDriverName = StringFromPointer(buffer(4), 1024)
.pComment = StringFromPointer(buffer(5), 1024)
.pLocation = StringFromPointer(buffer(6), 1024)
.pDevMode = buffer(7)
.pSepFile = StringFromPointer(buffer(8), 1024)
.pPrintProcessor = StringFromPointer(buffer(9), 1024)
.pDatatype = StringFromPointer(buffer(10), 1024)
.pParameters = StringFromPointer(buffer(11), 1024)
.pSecurityDescriptor = buffer(12)
.Attributes = buffer(13)
.Priority = buffer(14)
.DefaultPriority = buffer(15)
.StartTime = buffer(16)
.UntilTime = buffer(17)
.Status = buffer(18)
.JobsCount = buffer(19)
.AveragePPM = buffer(20)
End With
ClosePrinter mhPrinter
End Sub
Public Function StringFromPointer(lpString As Long, lMaxLength As Long) As String
Dim sRet As String
Dim lret As Long
If lpString = 0 Then
StringFromPointer = ""
Exit Function
End If
If IsBadStringPtrByLong(lpString, lMaxLength) Then
'\\ An error has occured - do not attempt to use this pointer
StringFromPointer = ""
Exit Function
End If
'\\ Pre-initialise the return string...
sRet = Space$(lMaxLength)
CopyMemory ByVal sRet, ByVal lpString, ByVal Len(sRet)
If Err.LastDllError = 0 Then
If InStr(sRet, Chr$(0)) > 0 Then
sRet = Left$(sRet, InStr(sRet, Chr$(0)) - 1)
End If
End If
StringFromPointer = sRet
End Function
Re: Printer status: detecting on or off
Drat, I was able to get the holes filled by using a post from here, but the code doesn't tell me whether the printer is on or off. Sure, there is an enumerated value for PRINTER_STATUS_OFFLINE, but it never returns that. When the printer is powered off, the printer status from the above code is set to PRINTER_STATUS_READY.
So back to square one. Anyone know how to do this? All I want is to be able to throw up a msgbox saying "Turn off the printer, dummy!" before beginning the shutdown process, but only if the printer is actually on.
Re: Printer status: detecting on or off
Googling turned up much shorter code that actually works, which I have adapted into a generic utility function:
Code:
Public Function PrinterOffline(Optional pstrPrinter As String = "Default") As Boolean
Dim strWhere As String
Dim objWMI As Object
Dim objPrinters As Object
Dim objPrinter As Object
Set objWMI = GetObject("winmgmts:\\.\root\CIMV2")
If LCase$(pstrPrinter) = "default" Then
strWhere = "Default = True"
Else
strWhere = "Name = '" & pstrPrinter & "'"
End If
Set objPrinters = objWMI.ExecQuery("SELECT * FROM Win32_Printer WHERE " & strWhere)
For Each objPrinter In objPrinters
PrinterOffline = objPrinter.WorkOffline
Exit For
Next
Set objPrinter = Nothing
Set objPrinters = Nothing
Set objWMI = Nothing
End Function
Send this function the name of the printer you want to query, or send it nothing to query the default printer. Returns True if the printer is turned off or not ready, False if the printer is on.
Does anyone know how I can remove the For...Each loop from the code? It strike me as quite inelegant.
Re: Printer status: detecting on or off
Presumably the For loop would be equivalent to this:
Code:
PrinterOffline = objPrinters(1).WorkOffline
Re: Printer status: detecting on or off
Nope, both of the following...
objPrinters(0).WorkOffline
objPrinters(1).WorkOffline
...generate "Generic Failure" errors, whatever the heck that means.
Re: Printer status: detecting on or off
If you run that code and add a breakpoint to Watch objPrinters, it shows the sub-object of objPrinters to be named "Item 1", with a space. What the heck?
And of course, like virtually everything else I've tried...
objPrinters.Item(1).WorkOffline
...raises a "Generic Failure" error.
Re: Printer status: detecting on or off
That is the normal way a collection is shown in the watch window.
By a process of elimination I have found that objPrinters.Item(x) and just objPrinters(x) are both correct - but I can't get past the "Generic Failure" either, which is rather odd as the "For Each" does basically the same thing. :confused:
Re: Printer status: detecting on or off
For some reason I see nothing hard in this:
Code:
Public Function IsPrinterOff(Optional Printer As String) As Boolean
Dim strWhere As String
If LenB(Printer) Then
strWhere = "Name = '" & Replace(Printer, "'", "\'") & "'"
Else
strWhere = "Default = TRUE"
End If
IsPrinterOff = GetObject("winmgmts:\\.\root\CIMV2").ExecQuery("SELECT * FROM Win32_Printer WHERE " & strWhere & " AND WorkOffline = TRUE").Count
End Function
You can even modify it to see if any printer is turned on. Just change the WQL.
Code:
Public Function PrintersOn() As Long
Dim strWQL As String
strWQL = "SELECT * FROM Win32_Printer WHERE WorkOffline = FALSE"
PrintersOn = GetObject("winmgmts:\\.\root\CIMV2").ExecQuery(strWQL).Count
End Function
Re: Printer status: detecting on or off
si_the_geek: enumeration (For Each) is provided by an entirely different mechanism than the regular Item interface. You may want to search and look at SelfEnum or ObjectCollection that I posted at some point, it shows you what is required to create the For Each mechanism "manually".
Re: Printer status: detecting on or off
That makes sense I guess.. thanks for easing my confusion!
Re: Printer status: detecting on or off
I also found this resource that should make it much easier to understand WMI: Windows 2000 Scripting Guide
One of the interesting parts is how it tells you how to get a single object instead of getting all of them, by using Get instead of ExecQuery (I didn't try it yet, but I assume many of those are true for printers as well).
Re: Printer status: detecting on or off
Here is how you can get the status of the first printer installed in the system:
Code:
Public Function Test() As Boolean
Test = GetObject("winmgmts:\\.\root\CIMV2").Get("Win32_Printer='" & Printers(0).DeviceName & "'").WorkOffline
End Function
Private Sub Form_Load()
MsgBox Test
End Sub
Yeah, it makes use of VB's Printers collection :)
Basically that Get would be the same you'd give to Item of the collection returned, but why get the collection if you can get single item this way as well... so you only want to use ExecQuery with For Each and nothing else.
Re: Printer status: detecting on or off