Printing tips and tricks from the trenches






4.75/5 (24 votes)
A selection of printing tips and tricks that can help make printing in an MFC application easier
Introduction
It seems from many of my other articles that I have become the "Print" guru here at CodeProject. So I though I would gather together a list of printing Tricks and Tips that I have collected together over a period of time through trial and error.
- Only ever print inside the
CPrintInfo::m_rectDraw
area
Your printing code should not make assumptions about where it should print on the page,
and make proper use of the CPrintInfo::m_rectDraw
variable. This ensures that
you will not overwrite margins/headers/footers that may be printed outside of your main
OnPrint
procedure.
pDC->TextOut(pInfo->m_rectDraw.left, pInfo->m_rectDraw.top,
"Only draw inside the reported m_rectDraw area") ;
When OnPreparePrinting()
is called in your CView
derived class,
you are generally required to setup the number of pages of output your document will need
when printing, unless you are using the CPrintInfo::m_bContinuePrinting
method.
But it can be difficult to do this if you have no information on the printer resolution or page size.
So at this point you need to get hold of the printer DC object that will be used. As the MFC print
architecture would not create this until the OnBeginPrinting()
function would be called,
you have to create one yourself and release it after calculating the number of pages you want to print.
To create such a printer DC you can use this code:
CDC dc ;
AfxGetApp()->CreatePrinterDC(dc) ;
...
// when finished with the DC, you should delete it
dc.DeleteDC() ;
This will create a printer DC for the default printer selected for you application. To switch to a different printer in code you should see my article Setting the default printer programmatically in an MFC application
The printable area of a page on a printer is normally contained in the CPrintInfo::m_rectDraw
member variable.
A CPrintInfo
object gets passed through to your CView
overridden virtual functions. But in some cases, like
in OnPreparePrinting(), OnBeginPrinting()
, this member variable will not yet have been intialised. So you have to do it
yourself.
pInfo->m_rectDraw.SetRect(0, 0, pDC->GetDeviceCaps(HORZRES), pDC->GetDeviceCaps(VERTRES)) ;
This gets the printable area of a printers page.
In many cases you may want to have a user programmable margin around a page so that you do not over-print company logo's etc on
headed paper, so you can set a user programmable range for you margins in inches
. You can then convert these to device
units and reserve that space on the page by changing the dimensions of the CPrintInfo::m_rectDraw
variable. For example:
double LeftOffset = 0.5 ; // in imperial inches! double TopOffset = 0.5 ; // in imperial inches! double RightOffset = 0.5 ; // in imperial inches! double BottomOffset = 0.5 ; // in imperial inches! pInfo->m_rectDraw.DeflateRect( (int)(pDC->GetDeviceCaps(LOGPIXELSX) * LeftOffset), (int)(pDC->GetDeviceCaps(LOGPIXELSY) * TopOffset), (int)(pDC->GetDeviceCaps(LOGPIXELSX) * RightOffset), (int)(pDC->GetDeviceCaps(LOGPIXELSY) * BottomOffset)) ;
You will need to apply these changes to the m_rectDraw
variable for every page printed, as the rectangle gets reset
for every page loop in the MFC stock library code.
When printing, choosing a font size that is suitable for the resolution of the printer in the past has been a hit and miss affair. I have had code that worked correctly on my development PC/printer setup, only to die horribly on a users PC/printer in Japan (e.g. the text generated was 1 pixel in height). Getting consistent output across printers can be done by selecting the font size based on the resolution reported by the printer:
CFont font ; LOGFONT lf ; ::ZeroMemory(&lf, sizeof(LOGFONT)); // This aims to get a 12-point size font regardless of the // printer resolution lf.lfHeight = -MulDiv(12, pDC->GetDeviceCaps(LOGPIXELSY), 72); strcpy(lf.lfFaceName, "Arial"); // with face name "Arial". // make use of the font....
We set the LOGFONT::lfHeight
member to a -ve value as this will get windows to select a good width
for us which will give a nice proportional font.
CPrintInfo::m_bContinuePrinting
If, when printing your document, you did not know how many pages you were going to print until you actually printed
(as calculating the actual page usage can be difficult), you can set the MFC print architecture to continue to request
pages to print until you have finished with all your output. To do this, you should not sent a maximum page in your
CView::OnPreparePrinting()
function.
There are 2 places where you can choose to end the printing:
1: In your CView::OnPrepareDC()
override
2: At the end of your CView::OnPrint()
function when you have printed the last of your output
pInfo->m_bContinuePrinting = FALSE ;
DIB
's instead of DDB
'sWhen printing bitmaps or icons to a printer DC, you should use a DIB
(Device Independant Bitmap) rather than
a DDB
(Device Dependant Bitmap). This is because printer device drivers tend not to support BitBlt
.
You can end up spending a lot of time wondering why the bitmap appears in Print Preview (because the screen DC supports BitBlt
)
and not on your printed output (becuase the printer driver does not). So when printing, convert your image to a DIB
and
use StretchDIBBits
to print the image. I have yet to find a printer where this technique would not work.
Here are some helpful functions that I have acquired from the web. I am not the original author of these, but I forget just where I got them from. But they are free source!
// this procedure extracts a single image from an image list into a DIB HANDLE ImageToDIB( CImageList* pImageList, int iImageNumber, CWnd* pWnd) { // Local Variables CBitmap bitmap; CWindowDC dc( pWnd ); CDC memDC; CRect rect; CPalette pal; IMAGEINFO imageInfo; if (!pImageList->GetImageInfo( iImageNumber, &imageInfo )) { // Getting of the Imageinfos failed return NULL; } // Create compatible stuff and select Bitmap if (!memDC.CreateCompatibleDC(&dc )) { // Create failed return NULL; } if (!bitmap.CreateCompatibleBitmap(&dc, imageInfo.rcImage.bottom-imageInfo.rcImage.top, imageInfo.rcImage.right-imageInfo.rcImage.left)) { // Create failed memDC.DeleteDC() ; return NULL; } CBitmap* pOldBitmap = memDC.SelectObject( &bitmap ); if( NULL == pOldBitmap ) { // Select failed memDC.DeleteDC() ; return NULL; } // Local Variables for Draw CPoint point( 0, 0); UINT nStyle = ILD_NORMAL; // Draw Image to the compatible DC if(!pImageList->Draw( &memDC, iImageNumber, point, nStyle )) { // Drawing of the Image failed memDC.SelectObject(pOldBitmap) ; VERIFY(bitmap.DeleteObject()) ; memDC.DeleteDC() ; return NULL; } // Create logical palette if device support a palette if( dc.GetDeviceCaps( RASTERCAPS ) & RC_PALETTE ) { UINT nSize = sizeof(LOGPALETTE) + ( sizeof(PALETTEENTRY) * 256 ); LOGPALETTE* pLP = (LOGPALETTE*)new BYTE[nSize]; pLP->palVersion = 0x300; pLP->palNumEntries = (unsigned short)GetSystemPaletteEntries( dc, 0, 255, pLP->palPalEntry ); // Create the palette pal.CreatePalette( pLP ); // Free memory delete[] pLP; } memDC.SelectObject( pOldBitmap ); memDC.DeleteDC() ; // Convert the bitmap to a DIB HANDLE h = DDBToDIB(bitmap, BI_RGB, &pal ); VERIFY(bitmap.DeleteObject()) ; return h ; } // DDBToDIB - Creates a DIB from a DDB // bitmap - Device dependent bitmap // dwCompression - Type of compression - see BITMAPINFOHEADER // pPal - Logical palette HANDLE DDBToDIB( CBitmap& bitmap, DWORD dwCompression, CPalette* pPal ) { BITMAP bm; BITMAPINFOHEADER bi; LPBITMAPINFOHEADER lpbi; DWORD dwLen; HANDLE hDIB; HANDLE handle; HDC hDC; HPALETTE hPal; ASSERT( bitmap.GetSafeHandle() ); // The function has no arg for bitfields if( dwCompression == BI_BITFIELDS ) return NULL; // If a palette has not been supplied use defaul palette hPal = (HPALETTE) pPal->GetSafeHandle(); if (hPal==NULL) hPal = (HPALETTE) GetStockObject(DEFAULT_PALETTE); // Get bitmap information bitmap.GetObject(sizeof(bm),(LPSTR)&bm); // Initialize the bitmapinfoheader bi.biSize = sizeof(BITMAPINFOHEADER); bi.biWidth = bm.bmWidth; bi.biHeight = bm.bmHeight; bi.biPlanes = 1; bi.biBitCount = (unsigned short)(bm.bmPlanes * bm.bmBitsPixel) ; bi.biCompression = dwCompression; bi.biSizeImage = 0; bi.biXPelsPerMeter = 0; bi.biYPelsPerMeter = 0; bi.biClrUsed = 0; bi.biClrImportant = 0; // Compute the size of the infoheader and the color table int nColors = 0; if(bi.biBitCount <= 8) { nColors = (1 << bi.biBitCount); } dwLen = bi.biSize + nColors * sizeof(RGBQUAD); // We need a device context to get the DIB from hDC = ::GetDC(NULL); hPal = SelectPalette(hDC,hPal,FALSE); RealizePalette(hDC); // Allocate enough memory to hold bitmapinfoheader and color table hDIB = GlobalAlloc(GMEM_FIXED,dwLen); if (!hDIB){ SelectPalette(hDC,hPal,FALSE); ::ReleaseDC(NULL,hDC); return NULL; } lpbi = (LPBITMAPINFOHEADER)GlobalLock(hDIB); *lpbi = bi; // Call GetDIBits with a NULL lpBits param, so the device driver // will calculate the biSizeImage field GetDIBits(hDC, (HBITMAP)bitmap.GetSafeHandle(), 0L, (DWORD)bi.biHeight, (LPBYTE)NULL, (LPBITMAPINFO)lpbi, (DWORD)DIB_RGB_COLORS); bi = *lpbi; // If the driver did not fill in the biSizeImage field, then compute it // Each scan line of the image is aligned on a DWORD (32bit) boundary if (bi.biSizeImage == 0){ bi.biSizeImage = ((((bi.biWidth * bi.biBitCount) + 31) & ~31) / 8) * bi.biHeight; // If a compression scheme is used the result may infact be larger // Increase the size to account for this. if (dwCompression != BI_RGB) bi.biSizeImage = (bi.biSizeImage * 3) / 2; } // Realloc the buffer so that it can hold all the bits dwLen += bi.biSizeImage; handle = GlobalReAlloc(hDIB, dwLen, GMEM_MOVEABLE) ; if (handle != NULL) hDIB = handle; else { GlobalFree(hDIB); // Reselect the original palette SelectPalette(hDC,hPal,FALSE); ::ReleaseDC(NULL,hDC); return NULL; } // Get the bitmap bits lpbi = (LPBITMAPINFOHEADER)hDIB; // FINALLY get the DIB BOOL bGotBits = GetDIBits( hDC, (HBITMAP)bitmap.GetSafeHandle(), 0L, // Start scan line (DWORD)bi.biHeight, // # of scan lines (LPBYTE)lpbi // address for bitmap bits + (bi.biSize + nColors * sizeof(RGBQUAD)), (LPBITMAPINFO)lpbi, // address of bitmapinfo (DWORD)DIB_RGB_COLORS); // Use RGB for color table if( !bGotBits ) { GlobalFree(hDIB); SelectPalette(hDC,hPal,FALSE); ::ReleaseDC(NULL,hDC); return NULL; } SelectPalette(hDC,hPal,FALSE); ::ReleaseDC(NULL,hDC); return hDIB; }
To use the above function(s) as an example code may be:
if (iImage >= 0) { HANDLE hDib ; hDib = ImageToDIB(8465;_list, iImage, this) ; // this is a dialog window in this example BITMAPINFOHEADER *pBMI ; pBMI = (BITMAPINFOHEADER*)GlobalLock(hDib) ; int nColors = 0; if (pBMI->biBitCount <= 8) { nColors = (1 << pBMI->biBitCount); } // print the correct image ::StretchDIBits(dc.m_hDC, pInfo.m_rectDraw.left, pInfo.m_rectDraw.top + cs.cy * j, cs.cy, cs.cy, 0, 0, pBMI->biWidth, pBMI->biHeight, (LPBYTE)pBMI + (pBMI->biSize + nColors * sizeof(RGBQUAD)), (BITMAPINFO*)pBMI, DIB_RGB_COLORS, SRCCOPY); // free resources GlobalUnlock(hDib) ; GlobalFree(hDib) ; }
Updates
- 25/7/2002 - Additional tips added, and some document re-wording.
I will update this document with additional tips as I acquire them, so if you have any of your own, please send them in!
Enjoy!