A discussion came up in the office recently about who sends the most paper through the printer: the developers or the sales folks. I leaned toward the latter simply because of all the multi-page proposals that that group creates. In either case, I thought it would be a good exercise to conjure up some sort of "monitoring" tool. My journey follows...
Getting the List of Connected Printers
Getting the names of the printers I'm connected to is an easy task, but to get things going quickly I initially just hard-coded the name of the printer I was working with when calling OpenPrinter(). When the time was right, however, I changed this such that I could select a printer from a list. The code to do that looks like:
EnumPrinters(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS,
NULL, 1, NULL, 0, &dwNeeded, &dwReturned);
LPBYTE lpBuffer = new BYTE[dwNeeded];
EnumPrinters(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS,
NULL, 1, lpBuffer, dwNeeded, &dwNeeded, &dwReturned);
PPRINTER_INFO_1 p1 = (PPRINTER_INFO_1) lpBuffer;
for (DWORD x = 0; x < dwReturned; x++)
Now I could easily switch between multiple printers and not have to worry about spelling them correctly. I also added Start and Stop buttons to the UI so that notifications could be turned off and on without having to restart the app each time.
Creating the Secondary Thread
Some tasks require additional threads, some benefit from them but don't necessarily require them, and others might even see a degrade in performance when additional threads are introduced (e.g., context switching). At first glance, it would seem that this project would fall into the second category because the secondary thread is never busy enough that it completely blocks the primary thread and its UI responsibilities. However, since we'll be using two of the wait functions, they will be blocking the thread until some event is signalled. If that thread is the primary thread, the UI becomes frozen. Hence the need for a secondary thread.
When the Start button is clicked, a secondary thread is created that will handle the notifications, thus leaving the primary thread to deal with UI issues.
OpenPrinter((LPTSTR) (LPCTSTR) strPrinter, &hPrinter, NULL);
The use of AfxBeginThread(), namely the first and second arguments, has always been a source of confusion here at CodeProject and on the microsoft.public.vc.mfc newsgroup. I'm certainly no expert with it, but I'll offer what I know. The first argument is the address of the thread function. This can either be a static member of some class or a global function. There are pros and cons to each. While the function's signature is the same either way, the former also requires the static modifier. This is to disassociate the function with any specific instance of the class (i.e., no this pointer). Using this approach, the function can only access static members of the class. This may or may not be an issue with your design. With the latter, the function cannot directly access any members of the class. That said, a common implementation of the thread function is to simply cast the
LPVOID parameter to a class pointer and use it to call some other member function that actually does the thread work.
The second argument can be any value that you want to pass to the thread function. Per MSDN, the thread function can interpret this value in any manner it chooses. It can be treated as a scalar value, or a pointer to a structure containing multiple parameters, or it can be ignored. If the parameter refers to a structure, the structure can be used not only to pass data from the caller to the thread, but also to pass data back from the thread to the caller. In the above code snippet, I passed the
this pointer so that I could then access the instance of the dialog class and any of its members. We could have also done it the other way around: pass a pointer to the
CThreadInfo object and add a
CWatchPrinterDlg* member to it. I opted for the former, so the thread function simply looks like:
UINT ThreadFunc( LPVOID pParam )
CWatchPrinterDlg *pDlg = (CWatchPrinterDlg *) pParam;
Monitoring and Getting Notified
In the "real" thread function is where the actual monitoring takes place. The first thing we do is call FindFirstPrinterChangeNotification() to get a notification handle that will get signalled when a specified set of events occur on a specified print queue. In the call, we specify the printer events for which notification is being requested,
PRINTER_CHANGE_ALL in this case. At this point, we can set up some sort of loop and wait for the event to get signalled. You'll notice that there are two events here that are being monitored: the handle to a change notification object, and a "stop request" from the primary thread. The latter will be discussed later in the article.
UINT CWatchPrinterDlg::ThreadFunc( void )
PPRINTER_NOTIFY_INFO pNotification = NULL;
HANDLE hChange = FindFirstPrinterChangeNotification(m_ThreadInfo.GetPrinter(),
aHandles = hChange;
aHandles = m_ThreadInfo.GetStopRequestedEvent();
while (hChange != INVALID_HANDLE_VALUE)
WaitForMultipleObjects(2, aHandles, FALSE, INFINITE);
if (WaitForSingleObject(hChange, 0U) == WAIT_OBJECT_0)
(LPVOID *) &pNotification);
if (pNotification != NULL)
if (pNotification->Flags & PRINTER_NOTIFY_INFO_DISCARDED)
DWORD dwOldFlags = NotificationOptions.Flags;
NotificationOptions.Flags = PRINTER_NOTIFY_OPTIONS_REFRESH;
(LPVOID *) &pNotification);
NotificationOptions.Flags = dwOldFlags;
for (DWORD x = 0; x < pNotification->Count; x++)
ASSERT(pNotification->aData[x].Type == JOB_NOTIFY_TYPE);
pNotification = NULL;
else if (WaitForSingleObject
(m_ThreadInfo.GetStopRequestedEvent(), 0U) == WAIT_OBJECT_0)
hChange = INVALID_HANDLE_VALUE;
For the most part, the secondary thread will be in a wait state within WaitForMultipleObjects(). It uses no processor time while waiting for the criteria to be met. Once an event becomes signalled, we can call WaitForSingleObject() to find out which one. If the "stop request" event was signalled, the change notification object is closed and the loop is exited. This causes the "thread done" event to become signalled, which lets the primary thread know that the secondary thread is done.
If the change notification object was signalled, we make a call to
FindNextPrinterChangeNotification() to retrieve information about the most recent change notification. Some changes may get combined into a single notification. For each notification, its type is either
PRINTER_NOTIFY_TYPE, depending on what was requested. Since
JOB_NOTIFY_TYPE was indirectly specified in the call to
FindFirstPrinterChangeNotification(), we can simply assert that the types match.
For this particular demonstration, information about the change notification was added to a list control. As the list control is owned by the primary thread, we should not interact with it directly. Doing so could cause a deadlock situation. Instead, create an object on the heap and post a user-defined message to the primary thread. That looks like:
CJobInfo *pJobInfo = NULL;
if (! m_mapJobInfo.Lookup(pNotification->aData[x].Id, pJobInfo))
pJobInfo = new CJobInfo(pNotification->aData[x].Id);
ASSERT(pJobInfo != NULL);
::PostMessage(m_ThreadInfo.GetHwnd(), UDM_UPDATE_JOB_LIST, 0, 0);
At this point, a
CJobInfo object was either created or a match was found in the map based on the job identifier. The
UpdateInfo() member is then called to update the other members from the change notification object.
The fourth argument to
PostMessage() could have been the address of the heap object. It's not in this case because the map that holds those objects is already accessible. Regardless, the primary thread will be in charge of freeing the heap object from memory.
Using Events to Communicate Between Threads
Cross-thread communication can be a tricky issue depending on your requirements. Fortunately for this example, the requirements were fairly simple:
- Notify the secondary thread that it needs to end (stop request), and
- Notify the primary thread that it has ended (thread done). It's not enough to notify the secondary thread that it needs to end and not hang around long enough to see if it actually did. While the primary thread may end and take the UI with it, the secondary thread may still be running or be in a blocked state.
When the application is first started, the "thread done" event is set to signalled so that clicking the Cancel button (or ALT+F4) behaves as expected (i.e., the app closes). Right before spawning the secondary thread, both events are set to non-signalled because neither event has happened.
If the secondary thread is running and the Stop button is clicked, the "stop request" event is set to signalled (this is one of the events that
WaitForMultipleObjects() is waiting for), then the "thread done" event is waited on. Once the secondary thread is done, the UI controls are re-enabled and the handle to the printer we were monitoring is closed. At this point, a different printer can be selected from the combobox and the monitoring process started over.
If the Cancel button is clicked, all of the above happens, memory is cleaned up, and the app closes. Code for this looks like:
if (m_ThreadInfo.GetPrinter() != INVALID_HANDLE_VALUE)
Something that I noticed fairly early in my testing was that the printer driver was not (always) sending me all of the notification information that I had expected: the
JOB_NOTIFY_FIELD_BYTES_PRINTED fields, and the
JOB_STATUS_PRINTED status. I read someplace that not all printer drivers send these notifications, and if you really need them, other options must be used. Periodically calling EnumJobs() to "poll" the printer is one of those options.
One thing I did not do when displaying the current jobs in the queue was to remove them from the list control when they were done printing (like Windows does). This would have defeated the whole purpose behind being able to see who was sending the most paper through the printer. Admittedly, there are several other ways I could have designed the UI so that grouping by user or pages printed could have told me those numbers at a glance.
- 20th April, 2010: Initial post