Top

C++/Win32: Printing

March 2, 2008

Printing in Windows is a pretty simple task — if you know the GDI functions. Since this tutorial doesn’t (yet) cover the GDI in any great depth, you might want to read up on the GDI elsewhere. We have, however, seen enough of the GDI — namely, the TextOut function — to print the simple text our display box is showing. So that’s what we’ll do for now.

Printing is basically done in 9 steps:

1. Declare and populate a PRINTDLG structure.

2. Pass the structure to PrintDlg.

3. Create and populate a DOCINFO structure.

4. Pass the device context field of the PRINTDLG, and your DOCINFO structure, to StartDoc.

5. Call StartPage.

6. Drawn to the device context of the PRINTDLG.

7. Call EndPage. Repeat steps 5-7 until all pages are drawn.

8. Call EndDoc.

9. Delete your device context.

As you can see, it’s a very sensible procedure. And if you know the GDI, and you understand drawing to the client area of a window, then you understand printing, because it’s exactly the same. As I said, study the GDI functions if you want to do some more complicated things with the printer. For now, though, we just need to print some text.

We’ll handle printing whatever is in the output box to the printer in a member function called PrintOutput. The function will be called when we receive a WM_COMMAND message from the File->Print menu option, or the accelerator ctrl-p. We’ve already covered menus and accelerators, so I’ll leave it to you to add those yourself.

Here is a really rough version of the PrintOutput function (certainly not the one we’re going to use):

BOOL PortScannerWindow::PrintOutput() {

	// declare a PRINTDLG structure and zero it out
	PRINTDLG pd;
	ZeroMemory(&pd, sizeof(PRINTDLG));

	// populate it
	pd.lStructSize	= sizeof(PRINTDLG);
	pd.hwndOwner	= m_hWnd;
	pd.hDevMode		= NULL;
	pd.hDevNames	= NULL;
	pd.Flags		= PD_USEDEVMODECOPIESANDCOLLATE | PD_RETURNDC;
	pd.nCopies		= 1;
	pd.nFromPage	= 0xFFFF;
	pd.nToPage		= 0xFFFF;
	pd.nMinPage		= 1;
	pd.nMaxPage		= 0xFFFF;

	if (!PrintDlg(&pd)) {
		MessageBox (m_hWnd, "A printer error has occurred.\r\n\r\nPlease check the print cable and\r\n
                     make sure the printer is turned on.", "Print Error!", MB_OK);
	}

	// declare a DOCINFO structure and populate it
	DOCINFO di;

	di.cbSize		= sizeof(DOCINFO);
	di.lpszDocName	= "Glowdot Port Scanner";
	di.lpszOutput	= (LPTSTR)NULL;
	di.fwType		= 0;

	// Print the job
	StartDoc (pd.hDC, &di);
	StartPage(pd.hDC);

	char *outputText = GrabTextFromEdit(IDT_OUTPUT);

	TextOut(pd.hDC, 10, 10, outputText, lstrlen(outputText));

	EndPage(pd.hDC);
	EndDoc(pd.hDC);

	DeleteDC(pd.hDC);

	return TRUE;
}

Why won’t it work? Well, TextOut is a stupid function. It doesn’t know wordwrap and all that kind of stuff, so it attempts to write one big, long string to the screen. It doesn’t deal with newline characters either. So you’ll see the one line of text bleeding off the right side of the page. Not exactly useful. We’ll fix that in a moment. For now, let’s breakdown this function — specifically, let’s learn what the fields in a PRINTDLG and DOCINFO structure do.

The PRINTDLG structure

The name probably gives it away: PRINTDLG is used to create a dialog. And not just any dialog, but the Windows default Print dialog you’ve seen a billion times. We need to give it some info and pass it to PrintDlg. When we do, PrintDlg will assign the hDC field the address of a device context for the printer. It is to this device context that we will draw our pages. PrintDlg will also set many of these fields. We are merely providing default values here, which may be altered after PrintDlg returns.

Look up the documentation on the PRINTDLG structure to see what each field is. Some are obvious, some are beyond the scope of this tutorial. But the following are worth mentioning:

Flags:

the PD_USEDEVMODECOPIESANDCOLLATE flag indicates that the app doesn’t support multiple copies and collation.

The PD_RETURNDC specifies that we want PrintDlg to return a device context.

nFromPage: the initial value for the starting page

nToPage: the initial value for the ending page.

nMinPage: the minimum value for the starting page

nMaxPage: the maximum value for the ending page

nCopies: the default for number of copies to print

The DOCINFO structure

The only thing notable here is the lpszDocName field, in which you specify the job name. This is what will show up when you inspect the printer’s job queue.

That’s really all there is to printing. Thankfully, it’s quite simple, because actually constructing your pages is not. We will have to do several calculations to determine what will fit on a page before we send the page to the printer. We will need to handle newlines on our own, since TextOut interprets them as unknown characters, and we will have to decide when we need a new page. All of these things are critical, because otherwise data will not appear on the page.

Figuring out what is going to fit on the page requires a few things:

· Determining the size of the printable area of the page

· Determining what our margin size will be

· Determining how many characters of the current font will fit in our margin

minus printable area (horizontally)

· Determining how many lines of the current font will fit in our margin minus printable area (vertically)

That’s a lot of determining! And it’s complicated by the fact that in certain fonts, different characters have different widths. But let’s figure out our printable area for now.

First, we convert our app defined margin into device units. Let’s say we want a 1 inch margin all around. We need to convert this into pixels, since that’s what the printer understands. We use the formula inches*pixels/inch. Fortunately, we can retrieve the pixels per inch with a call to GetDeviceCaps, specifying that we want the LOGPIXELSX or LOGPIXELSY value.

leftMargin  = 1 * GetDeviceCaps(pd.hDC, LOGPIXELSX);
rightMargin = 1 * GetDeviceCaps(pd.hDC, LOGPIXELSX);

Now we can subtract the physical offset (the area of the page the printer cannot access) from these margins to figure out the actual area. If the physical offset is smaller than the margin we’ve set, we will end up with a value the printer can use to offset printing from its starting position. If our margin is smaller than the offset, we will end up with a negative number. We can either set this to zero and move on, or pop up an error box saying the margin is too small. Your call.

int leftOffset   = leftMargin  - GetDeviceCaps(pd.hDC, PHYSICALOFFSETX);
int rightOffset  = rightMargin - (GetDeviceCaps(pd.hDC, PHYSICALWIDTH) - GetDeviceCaps(pd.hDC, PHYSICALOFFSETX)
                     - GetDeviceCaps(pd.hDC, HORZRES));

We can also calculate the printable width and height:

int prnWidth  = GetDeviceCaps(pd.hDC, HORZRES) - (leftOffset + rightOffset);
int prnHeight = GetDeviceCaps(pd.hDC, VERTRES) - (topOffset  + bottomOffset);

Now we have an idea where we will be able to print. But to calculate how many lines we’ll be able to print, we need to know how tall our font is.

// Now calculate the height of our font
TEXTMETRIC tm;
HFONT hfont = (HFONT)GetStockObject(ANSI_FIXED_FONT);
int yChar;

// Setup the current device context
SetMapMode(pd.hDC, MM_TEXT);
SelectObject(pd.hDC, hfont);

// work out the character dimensions for the current font
GetTextMetrics(pd.hDC, &tm);
yChar = tm.tmHeight;

Now, finally, we can figure out our lines per page. I’m taking a cue from the Catch22 tutorial (see links below) and including a HeaderHeight value here to account for a possible logo image or some such at the top of each page. For now, I’ll set it to zero, but it will be waiting for me if I decide to make these pages prettier.

int HeaderHeight   = 0;
int LinesPerPage   = (prnHeight - HeaderHeight) / yChar;

Now we have a usable figure. We can now print out lines until we reach LinesPerPage, and all we’ll have to do to continue is execute the following code:

EndPage(pd.hDC);
StartPage(pd.hDC);

We can reset our y value counter (however we implement it) at this point, and start printing the next page.

So let’s print our lines. Now, this part is tricky, but since it involves C/C++ logic more than Win32 principles, I won’t explain it here. I’ll leave it only because I searched for something similar, and found no answers. But this is how to break the string (char *) you retrieve from the edit control into lines. In PHP & Perl, we have the lovely explode function, which I would love to use in situations like this. But… of course that won’t happen. We’ll have to process this by hand. Be aware that this could be done MUCH MUCH MUCH better than I’ve done it, but I just wanted a quick, simple solution here with no hassles. I’ve decided our page width will be 80 characters, and acted accordingly. Here’s the code block:

char thisLine[80];
int i,j;

for (i = 0; i < strlen(outputText); i++) {
	for (j = 0; j < 80; j++) {
		if (*outputText == '\r') {
			thisLine[j] = '\0';
			outputText += 2;
			break;
		} else if (j == 80) {
			thisLine[j] = '\0';
		} else {
			thisLine[j] = *outputText++;
		}
	}

	// Now we've got a line -- figure out where to print it and output it
	if (((i - 1) % LinesPerPage == 0) && (i != 1)) {
		// We should start a new page first
		EndPage(pd.hDC);
		StartPage(pd.hDC);
		TextOut(pd.hDC,leftOffset, (yChar * i + HeaderHeight + topOffset)
                    - ((i - 1)/LinesPerPage)*prnHeight, thisLine, lstrlen(thisLine));
	} else {
		TextOut(pd.hDC,leftOffset, (yChar * i + HeaderHeight + topOffset)
                    - ((i - 1)/LinesPerPage)*prnHeight, thisLine, lstrlen(thisLine));
	}
}

Again, this is C, not Win32. But if you read over that block there isn’t really anything very complicated about it. You should be able to go on from here and build a really solid string processor for your own apps. This project, however, will not, ultimately, simply dump text to the printer, so I don’t feel the need to polish this out.

If you understand the above code, you also understand that while we check to make sure we’re not printing beyond the bottom of the page, there is no such check to make sure we don’t print beyond the right margin. This is because a font’s height is retrievable for calculation, but a font’s width is not. Font width often will vary from character to character. We could try to figure out what the maximum character width is a assume that, or we could use the text height value as the width, assuming that no reasonable font would be wider than it is tall (sometimes a false assumption, but true in most cases), or we could figure out the width of our line at each iteration of the loop, and break from the for loop when we exceed the margin… any of these would work, and you might think of others. I may return later to update this page with some width checking code later (especially if the comments below demonstrate interest in it).

You should also be aware that you should be doing some error checking along the way while printing. You should also be checking to make sure the user didn’t cancel the job. Read the Catch22 tutorial for more on this.

Note: You’ll probably notice that the skinned interface continues to improve. I am not doing anything I haven’t presented here already. I generally tend to work on the interface when my brain gets fried from the more heady stuff. So the interface will continue to improve until the end. See the source if you need more examples of interface design — there will be plenty of material to go over by the time this is finished

Next: The System Tray and Balloon Tips.


Source Code:

win32tut_part14.zip [2.89MB zipped]

Additional Information:

· Catch22 - Printing

· TheForger’s Win32 API Tutorial

· FunctionX - Win32 Tutorial - Dialogs

Further Reading:

· Nitty Gritty Windows Programming with C++ by Henning Hansen.

· Thinking in C++ by Bruce Eckel.

Comments

Got something to say?





Bottom