Introduction
I have been using Code Project for several years now, and I've always felt that I should give back
something to help my fellow software developers. Therefore, when I finished my last project that required
some research into ActiveX control printing, I thought it would be a good topic to create a demo program.
This project demonstrates several different ways to print an ActiveX control. I've tried to show some of
the most common alternatives, and in turn, describe some of the problems that you might encounter when printing.
This is by no means an exhaustive coverage of printing.
Background
This article assumes that you have basic familiarity with ActiveX controls in C++. In addition, it
is assumed that you understand how to use QueryInterface
on a COM object.
The demo program was created as a simple "Single document" MFC application using the AppWizard.
CFormView
was
selected to implement the view since it provided a place to put dialog controls. I decided to use this approach
because it simplifies the details of printing, and the reader can focus on the MFC
OnPrint
function
and the functions that it calls.
To show how a "standard" ActiveX control operates, the demo app uses the MS Chart control and the MS Calendar control. These
controls show all of the basic concepts and they appear to ship with Windows XP. The picture control is thrown in to show
one alternative (non COM) method of printing (WM_PRINT
). The picture control is a standard Win32 control (not ActiveX).
Printing Alternatives
There are several ways to print an ActiveX control. This article demonstrates the following methods:
- The
IViewObject
interface.
- The
IDataObject
interface.
- Using
WM_PRINT
messages.
Each of these methods has its advantages and disadvantages. In fact, not all ActiveX controls will support
all of these methods, and even when they do, the results might be less than ideal.
The sections that follow describe how to use each method. The demo program can be used to play with the different
alternatives and see how the ActiveX controls react to the different methods.
The IViewObject Interface (Screen or Printer Device Context)
Many ActiveX controls support this interface. This interface allows an object to place a representation of
itself onto a device context. The IViewObject::Draw
method is used for this purpose.
Since the caller is responsible for setting up the device context to draw into, the caller has several choices. The
printer device context can be used directly, or an off-screen memory device context can be used. Internet Explorer 4.0 uses this
method to print, passing the device context of the printer.
During my own testing with a third party control, I tried to use the IViewObject::Draw
method with the
printer device context directly. My goal was to get a very high resolution printout of the control with a minimum of
work. Unfortunately, after several tries, I could not get the control to print itself correctly using this method.
After examining some of the underlying source code for the control, I found that it was making some
assumptions about the size (in pixels) of the drawing area. As a result, the control did not print out correctly to the
printer which had a much higher resolution.
The alternative to printing directly into the printer device context was to create an off-screen memory area that I
passed into the IViewObject::Draw
method. I sized this memory area (a device dependent bitmap) to match the size of the
control so that the drawing would behave exactly the same as on screen. This resulted in a reasonably good printout of the
control, although you can see some problems when the image is scaled up.
Printing to the off-screen memory is very similar to doing a "screen capture" of the control. However, you don't have to worry
about whether the control is hidden or obscured, etc.
The demo program will allow you to test the IViewObject::Draw
method for these two alternatives. The first two print
options demonstrate how this is done. In both cases, the code queries the control for the
IViewObject
interface and then
calls the Draw method with the appropriate device context as shown below.
LPUNKNOWN pUnk = pWnd->GetControlUnknown();
if (!pUnk)
{
AfxMessageBox(_T("Not an ActiveX Control"));
return;
}
IViewObjectPtr spViewObj;
hRes = pUnk->QueryInterface(__uuidof(spViewObj),
(void **) &spViewObj);
if (FAILED(hRes)) _com_issue_error(hRes);
hRes = spViewObj->Draw(DVASPECT_CONTENT, -1, NULL, NULL,
NULL, pDC->m_hDC, &rectPrn, NULL, NULL, 0);
if (FAILED(hRes)) _com_issue_error(hRes);
Note: if you attempt use the IViewObject
method on the picture control, the
GetControlUnknown
call will fail because the
picture control is not an ActiveX control.
One additional note. When I was attempting to transfer the off-screen memory onto the printer device context, I found
that on some computers/printers the printout would not show up. It would show up blank and there did not appear to be any errors.
After doing some research on this, I found the MSDN article Q195830 - "INFO: Blitting Between DCs for Different Devices
Is Unsupported". This explained why my direct use of StretchBlt
from the memory bitmap to the printer did not work. As
a result, I changed the routine so that it converted the device dependent bitmap to a device independent bitmap
and then transferred the results using StretchDIBits
instead of
StretchBlt
. This can be seen in function PrintControlUsingScreen
.
The IViewObject Interface (Metafile)
This method uses the same interface as described in the previous section, however in this case a metafile device context is
passed in. The control then draws into the metafile and the client can use the metafile to draw into the printer
device context. This is the method used by Visual Basic 6.0.
An excerpt from the code is shown below.
CMetaFileDC dcMeta;
if (!dcMeta.Create())
_com_issue_error(HRESULT_FROM_WIN32(::GetLastError()));
IViewObjectPtr spViewObj;
hRes = pUnk->QueryInterface(__uuidof(spViewObj),
(void **) &spViewObj);
if (FAILED(hRes)) _com_issue_error(hRes);
hRes = spViewObj->Draw(DVASPECT_CONTENT, -1, NULL, NULL,
NULL, dcMeta, &rectPrn, NULL, NULL, 0);
if (FAILED(hRes)) _com_issue_error(hRes);
HMETAFILE hMeta = dcMeta.Close();
if (!hMeta)
_com_issue_error(HRESULT_FROM_WIN32(::GetLastError()));
pDC->PlayMetaFile(hMeta);
DeleteMetaFile(hMeta);
The IDataObject Interface (Metafile)
This interface is also supported by many ActiveX controls. The interface is used for data transfer. The IDataObject::GetData
method
is used for this purpose. The GetData
method is called with a value of
TYMED_MFPICT
in the FORMATETC
parameter. This is the way Microsoft Word 97 works.
The code excerpt below shows the basic technique. In this case, the client queries for the
IDataObject
interface, and then
calls the GetData
routine to retrieve a metafile representation of the control. The control itself is
responsible for allocating the metafile, so the client frees it with the ReleseStgMedium
call.
IDataObjectPtr spDataObj;
hRes = pUnk->QueryInterface(__uuidof(spDataObj),
(void **) &spDataObj);
if (FAILED(hRes)) _com_issue_error(hRes);
FORMATETC Formatetc;
Formatetc.cfFormat = CF_METAFILEPICT;
Formatetc.ptd = NULL;
Formatetc.dwAspect = DVASPECT_CONTENT;
Formatetc.lindex = -1;
Formatetc.tymed = TYMED_MFPICT;
STGMEDIUM Medium = { 0 };
hRes = spDataObj->GetData(&Formatetc, &Medium);
if (FAILED(hRes)) _com_issue_error(hRes);
if (Medium.tymed & TYMED_MFPICT)
{
METAFILEPICT *pMetaPict = (METAFILEPICT *)
GlobalLock(Medium.hMetaFilePict);
pDC->SetMapMode(pMetaPict->mm);
pDC->SetViewportOrg(rectPrn.left, rectPrn.top);
pDC->SetViewportExt(rectPrn.right - rectPrn.left + 1,
rectPrn.bottom - rectPrn.top + 1);
pDC->PlayMetaFile(pMetaPict->hMF);
GlobalUnlock(Medium.hMetaFilePict);
ReleaseStgMedium(&Medium);
}
Not all controls support the IDataObject
interface. In fact, the MS Chart control does not support it. If you attempt
to print the chart control using this method, you will get a "No such interface supported" error.
Obviously, the picture control does not support this method. The picture control is not an ActiveX control.
The WM_PRINT Message
For some types of controls, you can use the WM_PRINT
or WM_PRINTCLIENT
message to pass a device context for drawing.
Unfortunately, during my testing, I did not find any ActiveX controls that handled this message very well. For a standard
Win32 control (like the Picture control), this worked very well. I included it in my demo application just to be thorough.
Using this method, I did not get acceptable results from either the MS Chart control or the MS Calendar control. For the Chart control,
the printout did not show anything. For the Calendar control, it only rendered a portion of the combo boxes.
The code used to test this method is shown below.
LRESULT lRes = pWnd->SendMessage(WM_PRINT, (WPARAM) pDC->GetSafeHdc(),
PRF_CLIENT | PRF_CHILDREN | PRF_OWNED);
Examining the code
The demo program can be used to test each of the print techniques. All of the code of interest is
in the CPrintControlView
class. Specifically, there is one function for each of the print options.
There are a couple of other support routines included; to print a title and output an error message. The content
of these routines should be self explanatory. All of the other code in the application was generated using the AppWizard.
Conclusion
My goal was to show a few different methods of printing ActiveX controls. My intent was not to show every possible way to print, but
to present a few concepts that may be helpful to others.
Hopefully these simple code snippets will be of use to others facing the same problems that I did.
History