MFC
GDI
Tutorials: GDI
Printing, GDI+ Printing
|
|
|
This
articles gives some hints on printing
figure made with GDI+.
As anybody knows, printing
is one of the most mysterious feature in MFC
(this is my point of view). It is not well (at all)
documented and in fact, the best examples
can be found in Printing
section.
Since GDI+
is a new technology, it brings new problems
when trying to print.
In the
following, I will try to give some hints encountered when
trying to print GDI+
stuff. I will suppose that you have some basic knowledge
about printing,
especially that you have read one of the following articles,
so I won't have to discuss about getting a printer DC
working.
In the
following, dc denotes the printer dc and graphics
denotes the GDI+
graphic context:
CDC dc;
Graphics graphics;
Setting the
mapping modes
The mapping
modes of dc and graphics must be
tuned together:
dc.SetMapMode(MM_TEXT);
graphics.SetPageUnit(UnitDocument);
With those
setting, each logical unit is converted to 1 device unit (MM_TEXT )
and one device unit is 1/300 inch (UnitDocument). So we
get 300dpi printing,
great.
What about
other DPIs?
Gulp, we got
it working for 300dpi, but what about 600 dpi? or 1200 ?
After a few
try and error, I figured out we had to do the dirty job
ourselves, that is check for the dpi of printer and scale
accordingly the graphic:
-
Get the
dpi ratio dpiRatio :
CPrintDialog MyPrintDialog;
...
DEVMODE* pDev=MyPrintDialog.GetDevMode();
double dpiRatio=300.0/pDev->dmPrintQuality;
VERIFY(GlobalUnlock(pDev));
-
Setting
page scale in to graphics
graphics.SetPageScale(dpiRatio);
That ugly
hack should do the work. Of course, any nicer method is
welcome :-)
What about
text?
Getting
font size
Unfortunately,
scaling the graphic is not sufficient and Dpi scaling has
to be taken in account when computing the font size!
Here's a
modification of the CreateFontSize
of Barnhart article. As you see, the user has to pass the dpiRatio
to scale the font accordingly:
int CreateFontSize(HDC hDC, int points, double dpiRatio)
{
ASSERT(hDC);
POINT size;
int logPixelsY=::GetDeviceCaps(hDC, LOGPIXELSY);
size.x = size.y = MulDiv(points, logPixelsY, 72);
return (float)floor(size.y*dpiRatio);
}
Create font
for printing
When
creating a font, use the following unit:
Unit fontUnit = m_pGraphics->GetPageUnit();
if (fontUnit == UnitDisplay)
fontUnit = UnitPixel;
Font font(&fontFamily, CreateFontSize(hDC,
lfHeight, dpiRatio), FontStyleRegular, fontUnit);
Using the default
properties supplied for printer output does not give
consistent results. The printable region and the pixel
density cause variations. If you are outputting pages
for a formal report, consistent margins and font size
are often required. The following functions provide a
method for obtaining consistent output between printers.
Included functions are:
UserPage
which returns a CRect which defines a
consistent printable area for each printer (Your
margins must be within the printable region of all
printers of course!)
CreateFontSize
which returns a CSize which defines the
font attribute with respect to the desired point
size and printer characteristics.
The remaining code
shows sample usage of these functions.
//Input: desired margin
//Output: CRect to use for printing area
CRect CChildView::UserPage(CDC * pDC, float margin)
{
// This function returns the area in
device units to be used to
// prints
a page with a true boarder of "margin".
//
// You could use individual margins
for each edge
// and apply below as needed.
//
// Set Map Mode - We do not want
device units
// due to lack of consistency.
// If you do not use TWIPS you will
have to change
// the scaling factor below.
int OriginalMapMode = pDC->SetMapMode(MM_TWIPS);
// Variable needed to store printer
info.
CSize PrintOffset,Physical,Printable;
// This gets the Physical size of the
page in Device Units
Physical.cx = pDC->GetDeviceCaps(PHYSICALWIDTH);
Physical.cy = pDC->GetDeviceCaps(PHYSICALHEIGHT);
// convert to logical
pDC->DPtoLP(&Physical);
// This gets the offset of the
printable area from the
// top corner of the page in Device
Units
PrintOffset.cx = pDC->GetDeviceCaps(PHYSICALOFFSETX);
PrintOffset.cy = pDC->GetDeviceCaps(PHYSICALOFFSETY);
// convert to logical
pDC->DPtoLP(&PrintOffset);
// Set Page scale to TWIPS, Which is
1440 per inch,
// Zero/Zero is the upper left corner
// Get Printable Page Size (This is
in MM!) so convert to twips.
Printable.cx = (int)((float)pDC->GetDeviceCaps(HORZSIZE)*56.69);
Printable.cy = (int)((float)pDC->GetDeviceCaps(VERTSIZE)*56.69);
// Positive X -> RIGHT
// Positive Y -> UP
// Ref Zero is upper left corner
int inch = 1440; // Scaling Factor
Inches to TWIPS
int Dx1, Dx2, Dy1, Dy2; // Distance
printable area is from edge of paper
Dx1 = PrintOffset.cx;
Dy1 = PrintOffset.cy;
// calculate remaining borders
Dy2 = Physical.cy-Printable.cy-Dy1;
Dx2 = Physical.cx-Printable.cx-Dx1;
//
// Define the User Area's location
CRect PageArea;
PageArea.left = (long)(margin*inch-Dx1);
PageArea.right = (long)(Printable.cx-margin*inch+Dx2);
PageArea.top = (int)-(margin*inch-Dy1);
// My scale is inverted for y
PageArea.bottom = (int)-(Printable.cy-margin*inch+Dy2);
// now put back to device units to
return to the program.
pDC->LPtoDP(&PageArea);
//
// return
return PageArea;
}
Input: pointer to device context for printer
// Input: desired point size of font (in points).
// Output: integer height to send to CreateFont function.
int CChildView::CreateFontSize(CDC *pdc, int points)
{
// This will calculate the font size for the printer that is specified
// by a point size.
//
// if points is:
// (-) negative uses height as value for Net Font Height
// (ie. point size)
// (+) positive height is Total Height plus Leading Height!
CSize size;
int perinch = pdc->GetDeviceCaps(LOGPIXELSY);
size.cx = size.cy = (perinch*points)/72;
pdc->DPtoLP(&size);
return size.cy;
}
To use CreateFontSize ,
just insert the function call into the CreateFont
function:
BaseFont.CreateFont( -CreateFontSize(pdc,11), 0, 0, 0, FW_MEDIUM,
FALSE, FALSE, 0, ANSI_CHARSET,
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH , "Courier New" );
The UserPage
function can be used internal to your OnPrint
function. Where you call it, use the returned area
rather than the region found from the pDC 's
GetDeviceCaps function. (Usually sent in
the PrintInfo data.) Or you can call it to
set the region in to be passed.
void CChildView::PrintSetup(int item)
{
// Create Standard windows dialog.
BOOL bStdSetUpDlg = TRUE;
// See PRINTDLG for flags to set defaults in dialog.
// DWORD dwFlags = PD_ALLPAGES | PD_USEDEVMODECOPIES |
PD_NOPAGENUMS | PD_HIDEPRINTTOFILE | PD_NOSELECTION;
DWORD dwFlags = PD_ALLPAGES;
// Parent (may be NULL)
CWnd *pParent = this;
CPrintDialog MyPrintDlg(bStdSetUpDlg,dwFlags,pParent);
// Print Info
CPrintInfo MyPrintInfo;
// first link with dialog so data is shared
// Your input into min and max pages is now shared.
MyPrintInfo.m_pPD = &MyPrintDlg;
//
// Get Users Answer;
int MyAnswer;
MyAnswer = MyPrintDlg.DoModal();
// Allow the user to cancel
if(MyAnswer==IDCANCEL) return;
//
// Get the mode the printer is in from the Print Dialog.
// This memory block must be unlocked later.
DEVMODE *MyPrintMode;
MyPrintMode = MyPrintDlg.GetDevMode();
//
// Create our Printer Context
CDC MyPrintDC;
MyPrintDC.CreateDC(MyPrintDlg.GetDriverName(), // Ignored for Printer DC's
MyPrintDlg.GetDeviceName(), // The only required item for Printer DC's
MyPrintDlg.GetPortName(), // Ignored for Printer DC's
MyPrintMode); // Optional Item for Printer DC's
//
// Start the Document for our document
DOCINFO MyDocInfo;
MyDocInfo.cbSize=sizeof(DOCINFO);
CString DocName;
DocName.LoadString(AFX_IDS_APP_TITLE);
MyDocInfo.lpszDocName="DocName";
MyDocInfo.lpszOutput="";
//
// Start the document
int iErr = MyPrintDC.StartDoc(&MyDocInfo);
if(iErr < 0)
{
//success returns positive value
MyPrintDC.AbortDoc();
GlobalUnlock(MyPrintMode); // Release the print mode.
return;
}
// success so set flag to printing
MyPrintDC.m_bPrinting=TRUE;
// Most programs us the device's printable region found with
// MyPrintDC.GetDevicecaps(****) functions.
// However this is not consistent between printers so -->
// The UserPage functions calculates what margins
// to specify so we have the
// actual distance from the edge of the page
// to be consistent between printers.
CRect MyArea;
// fixed margin in inches (you can change this)
MyArea = UserPage(&MyPrintDC, 0.9f);
MyPrintInfo.m_rectDraw.SetRect(MyArea.left,
MyArea.top,MyArea.right,MyArea.bottom);
//
// We are now into personal preferences based on your program needs.
//
// We can call OnBeginPrinting and OnEndPrinting functions
// to initialize and clean up
// and loop through calls to OnPrint
// (calling Startpage and EndPage functions)
//
// or as I have done here->
// Call the StartPage the first time EndPage at the end
// with the print fnuction handling the begin
// and end when needed internally.
//
// Start the page. (This allways sets the DC to device units!)
MyPrintDC.StartPage();
// Set mode.
MyPrintDC.SetMapMode(MM_TEXT);
//
// We are now ready to print our data. Switch to the options allowed.
// For our usage we will end and restart
// each page in the functions called
// based on the location of the current print location on the page.
//
// Internal to the fucntions we need to call:
// pdc->EndPage();
// pdc->StartPage(); // Returns in Device units
// pdc->SetMapMode(MM_LOENGLISH); // Reset to our desired mode
// Reset position to draw, etc....
// as needed.
//
switch(item)
{
case(1):
PrintLoose(&MyPrintDC,MyArea);
break;
case(2):
PrintRecord(&MyPrintDC,MyArea);
break;
}
// We are all done. Clean up
MyPrintDC.m_bPrinting=FALSE;
// end last page
MyPrintDC.EndPage();
// end the document
MyPrintDC.EndDoc();
// Release the device context
GlobalUnlock(MyPrintMode); // Release the print mode.
return;
}
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") ;
Getting a PrinterDC in OnPreparePrinting()
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
Getting the size of the printable page area
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.
Margins
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.
Choosing a suitable font size for printing
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.
If you do not know how many pages you are going to print use
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 ;
Use DIB's instead of DDB's
When 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!
HANDLE ImageToDIB( CImageList* pImageList, int iImageNumber, CWnd* pWnd)
{
CBitmap bitmap;
CWindowDC dc( pWnd );
CDC memDC;
CRect rect;
CPalette pal;
IMAGEINFO imageInfo;
if (!pImageList->GetImageInfo( iImageNumber, &imageInfo ))
{
return NULL;
}
if (!memDC.CreateCompatibleDC(&dc ))
{
return NULL;
}
if (!bitmap.CreateCompatibleBitmap(&dc,
imageInfo.rcImage.bottom-imageInfo.rcImage.top,
imageInfo.rcImage.right-imageInfo.rcImage.left))
{
memDC.DeleteDC() ;
return NULL;
}
CBitmap* pOldBitmap = memDC.SelectObject( &bitmap );
if( NULL == pOldBitmap )
{
memDC.DeleteDC() ;
return NULL;
}
CPoint point( 0, 0);
UINT nStyle = ILD_NORMAL;
if(!pImageList->Draw( &memDC, iImageNumber, point, nStyle ))
{
memDC.SelectObject(pOldBitmap) ;
VERIFY(bitmap.DeleteObject()) ;
memDC.DeleteDC() ;
return NULL;
}
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 );
pal.CreatePalette( pLP );
delete[] pLP;
}
memDC.SelectObject( pOldBitmap );
memDC.DeleteDC() ;
HANDLE h = DDBToDIB(bitmap, BI_RGB, &pal );
VERIFY(bitmap.DeleteObject()) ;
return h ;
}
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() );
if( dwCompression == BI_BITFIELDS )
return NULL;
hPal = (HPALETTE) pPal->GetSafeHandle();
if (hPal==NULL)
hPal = (HPALETTE) GetStockObject(DEFAULT_PALETTE);
bitmap.GetObject(sizeof(bm),(LPSTR)&bm);
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;
int nColors = 0;
if(bi.biBitCount <= 8)
{
nColors = (1 << bi.biBitCount);
}
dwLen = bi.biSize + nColors * sizeof(RGBQUAD);
hDC = ::GetDC(NULL);
hPal = SelectPalette(hDC,hPal,FALSE);
RealizePalette(hDC);
hDIB = GlobalAlloc(GMEM_FIXED,dwLen);
if (!hDIB){
SelectPalette(hDC,hPal,FALSE);
::ReleaseDC(NULL,hDC);
return NULL;
}
lpbi = (LPBITMAPINFOHEADER)GlobalLock(hDIB);
*lpbi = bi;
GetDIBits(hDC, (HBITMAP)bitmap.GetSafeHandle(), 0L, (DWORD)bi.biHeight,
(LPBYTE)NULL, (LPBITMAPINFO)lpbi, (DWORD)DIB_RGB_COLORS);
bi = *lpbi;
if (bi.biSizeImage == 0){
bi.biSizeImage = ((((bi.biWidth * bi.biBitCount) + 31) & ~31) / 8)
* bi.biHeight;
if (dwCompression != BI_RGB)
bi.biSizeImage = (bi.biSizeImage * 3) / 2;
}
dwLen += bi.biSizeImage;
handle = GlobalReAlloc(hDIB, dwLen, GMEM_MOVEABLE) ;
if (handle != NULL)
hDIB = handle;
else
{
GlobalFree(hDIB);
SelectPalette(hDC,hPal,FALSE);
::ReleaseDC(NULL,hDC);
return NULL;
}
lpbi = (LPBITMAPINFOHEADER)hDIB;
BOOL bGotBits = GetDIBits( hDC, (HBITMAP)bitmap.GetSafeHandle(),
0L,
(DWORD)bi.biHeight,
(LPBYTE)lpbi
+ (bi.biSize + nColors * sizeof(RGBQUAD)),
(LPBITMAPINFO)lpbi,
(DWORD)DIB_RGB_COLORS);
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(ℑ_list, iImage, this) ;
BITMAPINFOHEADER *pBMI ;
pBMI = (BITMAPINFOHEADER*)GlobalLock(hDib) ;
int nColors = 0;
if (pBMI->biBitCount <= 8)
{
nColors = (1 << pBMI->biBitCount);
}
::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);
GlobalUnlock(hDib) ;
GlobalFree(hDib) ;
}
|