从打印机请求和接收更改通知





5.00/5 (10投票s)
本文简要讨论了如何使用FindFirstPrinterChangeNotification。

引言
最近办公室里讨论了一个问题,究竟是开发人员还是销售人员打印的纸最多。我倾向于后者,因为他们会创建大量的多页提案。无论如何,我觉得做一个“监控”工具会是个好主意。我的探索之旅由此开始...
获取已连接打印机列表
获取我已连接的打印机名称是一项简单的任务,但为了快速开始,我最初在调用 OpenPrinter() 时只是硬编码了正在使用的打印机名称。然而,到了适当的时候,我将其更改为可以从列表中选择打印机。执行此操作的代码如下:
DWORD dwNeeded,
dwReturned;
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++)
{
m_cbPrinters.AddString(p1->pName);
p1++;
}
delete lpBuffer;
现在我可以轻松地在多台打印机之间切换,而无需担心拼写错误。我还向 UI 添加了“开始”和“停止”按钮,以便可以在不重启应用程序的情况下打开和关闭通知。
创建辅助线程
有些任务需要额外的线程,有些任务从线程中受益但并非必需,而另一些任务在引入额外线程时性能可能会下降(例如,上下文切换)。乍一看,这个项目似乎属于第二类,因为辅助线程的繁忙程度从未完全阻塞主线程及其 UI 职责。但是,由于我们将使用两个 等待函数,它们会阻塞线程直到某个事件被发出信号。如果该线程是主线程,UI 就会冻结。因此需要一个辅助线程。
当单击“开始”按钮时,会创建一个辅助线程来处理通知,从而使主线程可以处理 UI 问题。
void CWatchPrinterDlg::OnStart()
{
// adjust the UI controls such that the user cannot interact with the wrong ones
m_btnStart.EnableWindow(FALSE);
m_btnStop.EnableWindow(TRUE);
m_cbPrinters.EnableWindow(FALSE);
// set the events to non-signalled (stop has not been requested
// and the thread is not done)
m_pEventStopRequested->ResetEvent();
m_pEventThreadDone->ResetEvent();
HANDLE hPrinter;
CString strPrinter;
m_cbPrinters.GetWindowText(strPrinter);
OpenPrinter((LPTSTR) (LPCTSTR) strPrinter, &hPrinter, NULL);
m_ThreadInfo.SetPrinter(hPrinter);
AfxBeginThread(::ThreadFunc, this);
}
在此处,AfxBeginThread() 的使用,特别是第一个和第二个参数,一直是 CodeProject 和 microsoft.public.vc.mfc 新闻组中的一个困惑之源。我当然不是这方面的专家,但我会分享我所知道的。第一个参数是线程函数的地址。它可以是某个类的 **静态** 成员,也可以是全局函数。两者都有优缺点。虽然 函数的签名 无论哪种方式都相同,但前者还需要 static 修饰符。这是为了将函数与类的任何特定实例分离开(即,没有 this 指针)。使用此方法,该函数只能访问类的 **静态** 成员。这可能对您的设计是或不是一个问题。对于后者,该函数不能直接访问类的 **任何** 成员。话虽如此,线程函数的常见实现方法是简单地将 LPVOID
参数转换为类指针,并使用它来调用另一个实际执行线程工作的成员函数。
第二个参数可以是您想传递给线程函数的任何值。根据 MSDN 的说法,*线程函数可以以任何它选择的方式解释此值。它可以被视为标量值,或指向包含多个参数的结构的指针,或者可以忽略它。如果参数引用结构,则该结构不仅可以用于将数据从调用者传递到线程,还可以用于将数据从线程传递回调用者。* 在上面的代码片段中,我传递了 this
指针,以便然后可以访问对话框类的实例及其任何成员。我们也可以反过来做:传递 CThreadInfo
对象的指针,并向其中添加一个 CWatchPrinterDlg*
成员。我选择了前者,因此线程函数如下所示:
UINT ThreadFunc( LPVOID pParam )
{
CWatchPrinterDlg *pDlg = (CWatchPrinterDlg *) pParam;
return pDlg->ThreadFunc();
}
监控和接收通知
在实际的线程函数中,实际的监控在那里进行。我们首先调用 FindFirstPrinterChangeNotification() 来获取一个通知句柄,当指定打印队列上发生指定的一组事件时,该句柄将被发出信号。在调用中,我们指定了请求通知的打印机事件,在这种情况下是 PRINTER_CHANGE_ALL
。此时,我们可以设置某种循环并等待事件被发出信号。您会注意到这里有两个被监控的事件:更改通知对象的句柄,以及来自主线程的“停止请求”。后者将在本文稍后讨论。
UINT CWatchPrinterDlg::ThreadFunc( void )
{
PPRINTER_NOTIFY_INFO pNotification = NULL;
// get a handle to a printer change notification object
HANDLE hChange = FindFirstPrinterChangeNotification(m_ThreadInfo.GetPrinter(),
PRINTER_CHANGE_ALL,
0,
&NotificationOptions);
DWORD dwChange;
HANDLE aHandles[2];
aHandles[0] = hChange;
aHandles[1] = m_ThreadInfo.GetStopRequestedEvent();
while (hChange != INVALID_HANDLE_VALUE)
{
// sleep until a printer change notification wakes this thread or the
// event becomes set indicating it's time for the thread to end.
WaitForMultipleObjects(2, aHandles, FALSE, INFINITE);
if (WaitForSingleObject(hChange, 0U) == WAIT_OBJECT_0)
{
FindNextPrinterChangeNotification(hChange,
&dwChange,
&NotificationOptions,
(LPVOID *) &pNotification);
if (pNotification != NULL)
{
// if a notification overflow occurred, ...
if (pNotification->Flags & PRINTER_NOTIFY_INFO_DISCARDED)
{
DWORD dwOldFlags = NotificationOptions.Flags;
// ...we must refresh to continue
NotificationOptions.Flags = PRINTER_NOTIFY_OPTIONS_REFRESH;
FreePrinterNotifyInfo(pNotification);
FindNextPrinterChangeNotification(hChange,
&dwChange,
&NotificationOptions,
(LPVOID *) &pNotification);
NotificationOptions.Flags = dwOldFlags;
}
// iterate through each notification
for (DWORD x = 0; x < pNotification->Count; x++)
{
ASSERT(pNotification->aData[x].Type == JOB_NOTIFY_TYPE);
// do something here with the notification
}
}
FreePrinterNotifyInfo(pNotification);
pNotification = NULL;
}
else if (WaitForSingleObject
(m_ThreadInfo.GetStopRequestedEvent(), 0U) == WAIT_OBJECT_0)
{
FindClosePrinterChangeNotification(hChange);
hChange = INVALID_HANDLE_VALUE;
}
}
// signal the event to let the primary thread know that this thread is done
SetEvent(m_ThreadInfo.GetThreadDoneEvent());
return 0;
}
在大多数情况下,辅助线程将在 WaitForMultipleObjects() 中处于等待状态。在等待条件满足期间,它不占用处理器时间。一旦某个事件被发出信号,我们就可以调用 WaitForSingleObject() 来找出是哪个事件。如果“停止请求”事件被发出信号,则关闭更改通知对象并退出循环。这会导致“线程完成”事件被发出信号,从而让主线程知道辅助线程已完成。
如果更改通知对象被发出信号,我们将调用 FindNextPrinterChangeNotification()
来检索有关最近一次更改通知的信息。某些更改可能会合并到单个通知中。对于每个通知,其类型是 JOB_NOTIFY_TYPE
或 PRINTER_NOTIFY_TYPE
,具体取决于请求的内容。由于在调用 FindFirstPrinterChangeNotification()
时间接指定了 JOB_NOTIFY_TYPE
,因此我们可以断言类型匹配。
在此演示中,有关更改通知的信息已添加到列表控件中。由于列表控件归主线程所有,我们不应直接与其交互。这样做可能导致死锁。而是,在堆上创建一个对象,并将一个用户定义的 I 消息 **发布** 给主线程。看起来像这样:
CJobInfo *pJobInfo = NULL;
// if the job info item does not exist, create a new one
if (! m_mapJobInfo.Lookup(pNotification->aData[x].Id, pJobInfo))
{
pJobInfo = new CJobInfo(pNotification->aData[x].Id);
m_mapJobInfo.SetAt(pNotification->aData[x].Id, pJobInfo);
}
ASSERT(pJobInfo != NULL);
pJobInfo->UpdateInfo(&pNotification->aData[x]);
::PostMessage(m_ThreadInfo.GetHwnd(), UDM_UPDATE_JOB_LIST, 0, 0);
此时,已创建 CJobInfo
对象,或在地图中找到了基于 作业标识符 的匹配项。然后调用 UpdateInfo()
成员来使用更改通知对象更新其他成员。
PostMessage()
的第四个参数可以是堆对象的地址。在本例中不是,因为保存这些对象的地图已经可用。无论如何,主线程将负责从内存中释放堆对象。
使用事件在线程之间进行通信
跨线程通信可能是一个棘手的问题,具体取决于您的要求。幸运的是,在这个例子中,要求相当简单:
- 通知辅助线程它需要结束(停止请求),以及
- 通知主线程它已结束(线程完成)。仅仅通知辅助线程它需要结束,而不等待它实际完成,这是不够的。主线程可能会结束并带走 UI,但辅助线程可能仍在运行或处于阻塞状态。
当应用程序首次启动时,“线程完成”事件被设置为已发出信号,以便单击“取消”按钮(或 ALT+F4)能够按预期工作(即,应用程序关闭)。在生成辅助线程之前,两个事件都被设置为未发出信号,因为这两个事件都尚未发生。
如果辅助线程正在运行并且单击了“停止”按钮,“停止请求”事件被设置为已发出信号(这是 WaitForMultipleObjects()
正在等待的事件之一),然后等待“线程完成”事件。一旦辅助线程完成,“UI 控件”被重新启用,我们正在监控的打印机句柄被关闭。此时,可以从组合框中选择另一台打印机,并重新开始监控过程。
如果单击“取消”按钮,则会发生上述所有操作,清理内存,然后应用程序关闭。此操作的代码如下:
void CWatchPrinterDlg::OnStop()
{
// signal and wait for ThreadFunc() to end
m_pEventStopRequested->SetEvent();
WaitForSingleObject(m_pEventThreadDone->m_hObject, 8000U);
if (m_ThreadInfo.GetPrinter() != INVALID_HANDLE_VALUE)
ClosePrinter(m_ThreadInfo.GetPrinter());
m_btnStart.EnableWindow(TRUE);
m_btnStop.EnableWindow(FALSE);
m_cbPrinters.EnableWindow(TRUE);
}
//====================================================================
void CWatchPrinterDlg::OnCancel()
{
OnStop();
delete m_pEventStopRequested;
delete m_pEventThreadDone;
m_mapJobInfo.Cleanup();
CDialog::OnCancel();
}
结语
我在测试早期注意到的一点是,打印机驱动程序(并非总是)没有发送所有预期的通知信息:JOB_NOTIFY_FIELD_PAGES_PRINTED
和 JOB_NOTIFY_FIELD_BYTES_PRINTED
字段,以及 JOB_STATUS_PRINTED
状态。我读到过并非所有打印机驱动程序都会发送这些通知,如果您确实需要它们,则必须使用其他选项。定期调用 EnumJobs() 来“轮询”打印机是这些选项之一。
在显示队列中的当前作业时,我没有做的一件事是当它们打印完成后从列表控件中删除它们(就像 Windows 做的那样)。这将违背能够看到谁打印了最多纸张的整个目的。诚然,我可以通过其他几种方式设计 UI,例如按用户或打印页数分组,这样可以一目了然地得知这些数字。
尽情享用!
历史
- 2010 年 4 月 20 日:初始发布