打印 ActiveX 控件






4.55/5 (14投票s)
2004 年 5 月 4 日
7分钟阅读

127805

2721
打印 ActiveX 控件
引言
我使用 Code Project 已经好几年了,一直觉得应该回馈一些东西来帮助我的软件开发者同行。因此,当我完成了上一个需要研究 ActiveX 控件打印的项目后,我认为这将是创建一个演示程序的好主题。
该项目演示了打印 ActiveX 控件的几种不同方法。我尝试展示一些最常见的替代方案,并依次描述打印时可能遇到的一些问题。
这绝不是对打印问题的详尽论述。
背景
本文假定您对 C++ 中的 ActiveX 控件有基本的了解。此外,还假定您理解如何对 COM 对象使用 QueryInterface
。
这个演示程序是使用 AppWizard 创建的一个简单的“单文档”MFC 应用程序。选择 CFormView
来实现视图,因为它提供了一个放置对话框控件的地方。我决定使用这种方法,因为它简化了打印的细节,读者可以专注于 MFC 的 OnPrint
函数以及它调用的函数。
为了展示一个“标准”的 ActiveX 控件如何运作,该演示应用使用了 MS Chart 控件和 MS Calendar 控件。这些控件展示了所有的基本概念,并且它们似乎随 Windows XP 一起提供。图片控件的加入是为了展示一种替代的(非 COM)打印方法(WM_PRINT
)。图片控件是一个标准的 Win32 控件(不是 ActiveX)。
打印的替代方案
打印 ActiveX 控件有几种方法。本文演示了以下方法:
IViewObject
接口。IDataObject
接口。- 使用
WM_PRINT
消息。
这些方法各有优缺点。实际上,并非所有 ActiveX 控件都支持所有这些方法,即使支持,结果也可能不理想。
接下来的部分将描述如何使用每种方法。可以使用演示程序来体验不同的替代方案,并观察 ActiveX 控件对不同方法的反应。
IViewObject 接口(屏幕或打印机设备上下文)
许多 ActiveX 控件都支持此接口。该接口允许对象将其自身的表示形式绘制到设备上下文中。IViewObject::Draw
方法就是用于此目的。
由于调用者负责设置用于绘制的设备上下文,因此调用者有多种选择。可以直接使用打印机设备上下文,也可以使用屏幕外内存设备上下文。Internet Explorer 4.0 使用此方法进行打印,传递打印机的设备上下文。
在我自己对一个第三方控件的测试中,我尝试直接使用打印机设备上下文来调用 IViewObject::Draw
方法。我的目标是以最少的工作量获得控件的超高分辨率打印输出。不幸的是,经过几次尝试,我无法让该控件用这种方法正确地打印自己。在检查了该控件的一些底层源代码后,我发现它对绘图区域的大小(以像素为单位)做了一些假设。结果,该控件无法正确打印到分辨率高得多的打印机上。
直接打印到打印机设备上下文的替代方法是创建一个屏幕外内存区域,然后将其传递给 IViewObject::Draw
方法。我将这个内存区域(一个设备相关位图)的大小设置为与控件大小相匹配,这样绘制行为就会与屏幕上完全相同。这使得控件的打印输出效果相当好,尽管在图像放大时可以看到一些问题。
打印到屏幕外内存非常类似于对控件进行“屏幕截图”。然而,您不必担心控件是否被隐藏或遮挡等问题。
演示程序将允许您测试 IViewObject::Draw
方法的这两种替代方案。前两个打印选项演示了如何做到这一点。在这两种情况下,代码都会查询控件的 IViewObject
接口,然后使用适当的设备上下文调用 Draw 方法,如下所示。
// get the IUnknown pointer for the control object // (note, don't release this one per MSDN) LPUNKNOWN pUnk = pWnd->GetControlUnknown(); if (!pUnk) { AfxMessageBox(_T("Not an ActiveX Control")); return; } // ... // query for the IViewObject interface for printing // (note, some don't support this, like lite controls) IViewObjectPtr spViewObj; hRes = pUnk->QueryInterface(__uuidof(spViewObj), (void **) &spViewObj); if (FAILED(hRes)) _com_issue_error(hRes); // draw the object into the printer device context hRes = spViewObj->Draw(DVASPECT_CONTENT, -1, NULL, NULL, NULL, pDC->m_hDC, &rectPrn, NULL, NULL, 0); if (FAILED(hRes)) _com_issue_error(hRes);
注意:如果您尝试在图片控件上使用 IViewObject
方法,GetControlUnknown
调用将会失败,因为图片控件不是 ActiveX 控件。
另外一点需要注意。当我试图将屏幕外内存传输到打印机设备上下文时,我发现在某些计算机/打印机上,打印输出无法显示。它会显示为空白,并且似乎没有任何错误。在对此进行一些研究后,我找到了 MSDN 文章 Q195830 - “INFO: Blitting Between DCs for Different Devices Is Unsupported”(信息:不支持在不同设备的设备上下文之间进行位块传送)。这解释了为什么我直接使用 StretchBlt
从内存位图到打印机的操作不起作用。因此,我更改了例程,使其将设备相关位图转换为设备无关位图,然后使用 StretchDIBits
而不是 StretchBlt
来传输结果。这可以在函数 PrintControlUsingScreen
中看到。
IViewObject 接口(图元文件)
此方法使用与上一节所述相同的接口,但在这种情况下,传入的是图元文件设备上下文。然后,控件绘制到图元文件中,客户端可以使用该图元文件绘制到打印机设备上下文中。这是 Visual Basic 6.0 使用的方法。下面是部分代码摘录。// create a metafile to draw into CMetaFileDC dcMeta; if (!dcMeta.Create()) _com_issue_error(HRESULT_FROM_WIN32(::GetLastError())); // query for the IViewObject interface for printing // (note, some don't support this, like lite controls) IViewObjectPtr spViewObj; hRes = pUnk->QueryInterface(__uuidof(spViewObj), (void **) &spViewObj); if (FAILED(hRes)) _com_issue_error(hRes); // draw the object into the metafile device context hRes = spViewObj->Draw(DVASPECT_CONTENT, -1, NULL, NULL, NULL, dcMeta, &rectPrn, NULL, NULL, 0); if (FAILED(hRes)) _com_issue_error(hRes); // get the completed meta file handle HMETAFILE hMeta = dcMeta.Close(); if (!hMeta) _com_issue_error(HRESULT_FROM_WIN32(::GetLastError())); // play the meta file into the printer device context pDC->PlayMetaFile(hMeta); // free up the metafile memory DeleteMetaFile(hMeta);
IDataObject 接口(图元文件)
许多 ActiveX 控件也支持此接口。该接口用于数据传输。IDataObject::GetData
方法就是用于此目的。调用 GetData
方法时,在 FORMATETC
参数中传入 TYMED_MFPICT
值。Microsoft Word 97 就是这样工作的。
下面的代码摘录展示了基本技术。在这种情况下,客户端查询 IDataObject
接口,然后调用 GetData
例程来检索控件的图元文件表示。控件本身负责分配图元文件,因此客户端使用 ReleseStgMedium
调用来释放它。
// query for the IDataObject interface for GetData // (some controls don't support this) IDataObjectPtr spDataObj; hRes = pUnk->QueryInterface(__uuidof(spDataObj), (void **) &spDataObj); if (FAILED(hRes)) _com_issue_error(hRes); // setup the structures for retrieving the results FORMATETC Formatetc; Formatetc.cfFormat = CF_METAFILEPICT; Formatetc.ptd = NULL; Formatetc.dwAspect = DVASPECT_CONTENT; Formatetc.lindex = -1; Formatetc.tymed = TYMED_MFPICT; STGMEDIUM Medium = { 0 }; // draw the control into a metafile hRes = spDataObj->GetData(&Formatetc, &Medium); if (FAILED(hRes)) _com_issue_error(hRes); // the returned type should be a metafile since // that's what we requested if (Medium.tymed & TYMED_MFPICT) { // get the metafile picture pointer so we // can get the metafile handle METAFILEPICT *pMetaPict = (METAFILEPICT *) GlobalLock(Medium.hMetaFilePict); // scale appropriately pDC->SetMapMode(pMetaPict->mm); pDC->SetViewportOrg(rectPrn.left, rectPrn.top); pDC->SetViewportExt(rectPrn.right - rectPrn.left + 1, rectPrn.bottom - rectPrn.top + 1); // play the meta file into the printer device context pDC->PlayMetaFile(pMetaPict->hMF); // unlock the metafile picture handle GlobalUnlock(Medium.hMetaFilePict); // release the results ReleaseStgMedium(&Medium); }
并非所有控件都支持 IDataObject
接口。事实上,MS Chart 控件就不支持它。如果您尝试使用此方法打印图表控件,将会收到“不支持此接口”的错误。
显然,图片控件不支持此方法。图片控件不是 ActiveX 控件。
WM_PRINT 消息
对于某些类型的控件,您可以使用 WM_PRINT
或 WM_PRINTCLIENT
消息来传递用于绘制的设备上下文。不幸的是,在我的测试中,我没有发现任何能很好地处理此消息的 ActiveX 控件。对于标准的 Win32 控件(如图片控件),这种方法效果很好。为了全面起见,我将其包含在我的演示应用程序中。使用此方法,我从 MS Chart 控件或 MS Calendar 控件中都没有得到满意的结果。对于 Chart 控件,打印输出没有任何内容。对于 Calendar 控件,它只渲染了组合框的一部分。
用于测试此方法的代码如下所示。
// send the message to print the control // (could use pWnd->Print directly) LRESULT lRes = pWnd->SendMessage(WM_PRINT, (WPARAM) pDC->GetSafeHdc(), PRF_CLIENT | PRF_CHILDREN | PRF_OWNED);
检查代码
演示程序可用于测试每种打印技术。所有相关的代码都在 CPrintControlView
类中。具体来说,每个打印选项都有一个对应的函数。
还包括了几个其他的支持例程;用于打印标题和输出错误消息。这些例程的内容应该是不言自明的。应用程序中的所有其他代码都是使用 AppWizard 生成的。
结论
我的目标是展示几种不同的打印 ActiveX 控件的方法。我的意图不是展示所有可能的打印方式,而是提出一些可能对他人有帮助的概念。
希望这些简单的代码片段能对面临与我相同问题的人有所帮助。
历史
- 2004年5月1日 初始版本