Introduction
Windows is really fun, mostly. But one of the things I miss about UNIX (or the
IBM 1401) is the handy way it writes to a line printer. No sissy graphics, no
obsequious "Page n of m" ( unless you want to write a lot of extra code), just
thousands of sturdy fixed width lines of ASCII characters. Gayle Manufacturing
Company (The GM in all the names and labels below) is the steel fabricating
company I work for. Most of the routine reports we generate are in line
printer format, nothing fancy - just the numbers. Did you ever wonder why
there isn't some easy way to simply iterate through your data and print a line
at a time (nostalgic sigh)?? Maybe it's just been too obvious and that's why I
missed it.
I'm immersed in a project that is a redesign of a system that presently runs
on UNIX. Most of the reports are fine the way they are so I want a simple way
to re-create them from Windows apps. I need a line printer!! I want printf()
to go to 'stdout' so I can pipe it to 'lp'. Is that so wrong? The idea is to
loop throught the data in a query and print a line or two for each row of data
with the odd total here and there.
Ah, well. Things have changed and there isn't a line printer so I guess I had
to make my own. I designed a COM class that acts sort of like a line printer.
It's written in VC++ 5 with MFC4.2, ATL and STL. Since it is a dual inteface
COM component, it may be used from C++ or VBA. It makes it trivially easy to
create line printer style reports and it runs under MTS. It is another example
of a way to print without using a CView class. It compiles under UNICODE. The
error checking is rather minimal and some of the code is probably BFI (Brute
Force and Ignorance, but if I'm ignorant how can I tell?).
The COM Interface has the following methods
write(LINE_TYPE, BSTR text)
reset(RESET_TYPE)
print(PRINT_TYPE)
These properties
font_size ( GMP_FONT_CPI)
orientation ( GMP_ORIENTATION)
title(BSTR)
print_heading(bool)
page_breaks(bool)
punch_margin(double )
and these enumerations
[helpstring("enum line types")]
typedef enum
{
GMP_LT_DEFAULT,
GMP_LT_HEAD,
GMP_LT_BODY,
GMP_LT_NEWPAGE
} LINE_TYPE;
[helpstring("enum reset types")]
typedef enum
{
GMP_RS_NONE,
GMP_RS_HEAD,
GMP_RS_BODY,
GMP_RS_ALL
} RESET_TYPE;
[helpstring("enum print types")]
typedef enum
{
GMP_DEFAULT,
GMP_PAGE_RANGE,
GMP_NO_PAGE_BREAK,
GMP_NO_HEADING
} PRINT_TYPE;
[helpstring("enum orientation")]
typedef enum
{
GMP_PORTRAIT,
GMP_LANDSCAPE
} GMP_ORIENTATION;
[helpstring("enum font cpi selection")]
typedef enum
{
GMP_FONT_10,
GMP_FONT_15,
GMP_FONT_12
} GMP_FONT_CPI;
It works like this Use the usual means of getting a pointer to an IGMPrintEZ
instance. I like the #import method so
IGMPrintEZPtr p_prt;
HRESULT hr = p_prt.CreateInstance(__uuidof(GMPrintEZ));
The printer object starts out empty but just to be sure I can reset it
p_prt->reset(GMP_RS_ALL);
Write the heading lines to the heading line table
BSTR text = SysAllocString(L"HeadingLine 1");
CString str_blankline = "";
p_prt->write(GMP_LT_HEAD, text);
p_prt->write(GMP_LT_HEAD, str_blankline.AllocSysString());
p_prt->write(GMP_LT_HEAD,
SysAllocString(L"Column 1 Column 2 Column3"));
You can write heading lines any time during the process. There is a separate
array for the heading and the body lines. The driving program could write the
heading lines AFTER processing all the body lines so that totals appear on
each page heading. This opens a number of possibilities. In fact the title,
orientation, and font can all be determined after the body lines have been
written to the body line array, and set just prior to actually printing.
The other operation is to write the body lines
LINE_TYPE lt = GMP_LT_BODY;
while(! data.IsEOF())
{
CString data;
data.Format("%25s %10d", data.item, data.quantity);
p_prt->write(lt, data.AllocSysString());
data.MoveNext();
}
Before we print we set the title, the format to landscape and the font to 12
characters per inch
p_prt->title = _T("Test Job");
p_prt->orientation = GMP_LANDSCAPE;
p_prt->font_size = GMP_FONT_12;
And then we print
p_prt->print(GMP_DEFAULT);
The print object iterates through the BODY line array and prints a line for
each entry in the array. The heading and footer are separated from the body by
horizontal lines. The date and time print in the lower left, the page number
in the center and the title in the lower right.
The main elements of the program are a pair of vectors ( vector), one for the
heading lines and one for the body lines, and a print loop. Here's what the
print loop code looks like
bool GMLinePrintpage_loop()
{
try
{
CPrintDialog pd(FALSE);
DOCINFO di;
m_line = 0;
m_max_lines = 20;
m_last_body_line = m_max_lines;
m_line_height = LINEHEIGHT_10;
if(0 == body_lines->size())
return true;
memset(di, 0, sizeof(DOCINFO));
di.cbSize = sizeof(DOCINFO);
di.lpszDocName = m_title;
pd.GetDefaults();
DEVMODE *dm = pd.GetDevMode();
dm->dmOrientation = m_orientation + 1;
dm->dmFields |= DM_ORIENTATION;
switch(m_orientation)
{
case GMP_PORTRAIT
m_top_offset = 0;
m_left_offset = INCH * m_punch_margin;
break;
case GMP_LANDSCAPE
m_top_offset = INCH * m_punch_margin;
m_left_offset = 0;
break;
}
CDC dc;
if(! dc.CreateDC(pd.GetDriverName(), pd.GetDeviceName(),
pd.GetPortName(), dm))
{
AfxMessageBox(_T("Can't create DC in print_loop"));
return false;
}
dc.StartDoc();
m_page_height = dc.GetDeviceCaps(VERTSIZE) * MM_TO_INCH;
m_page_width = dc.GetDeviceCaps(HORZSIZE) * MM_TO_INCH;
CFont *oldfont;
switch(m_font)
{
case GMP_FONT_12
oldfont = dc.SelectObject(C12);
m_line_height = LINEHEIGHT_12;
m_cpi = 12;
break;
case GMP_FONT_15
oldfont = dc.SelectObject(C15);
m_line_height = LINEHEIGHT_15;
m_cpi = 15;
break;
case GMP_FONT_10
default
oldfont = dc.SelectObject(C10);
m_line_height = LINEHEIGHT_10;
m_cpi = 10;
break;
}
m_max_lines = -(( m_page_height - m_top_offset) /
m_line_height);
if(m_b_print_head)
{
m_last_body_line = m_max_lines - FOOTER_LINES -
head_lines->size() - HEADER_SEPARATOR;
}
else
{
m_last_body_line = m_max_lines;
}
vectoriterator itext;
m_page = 0;
for(itext = body_lines->begin();
itext < body_lines->end();
++itext)
{
if("@@FF" == itext->Left(4))
{
if(m_b_use_page_breaks)
{
m_line = 0;
dc.EndPage();
new_page(&dc);
}
}
else
{
if (m_line >= m_last_body_line)
{
m_line = 0;
dc.EndPage();
}
if (m_line == 0)
new_page(&dc);
dc.TextOut(
m_left_offset,
(m_line++ * m_line_height) - m_top_offset,
*itext
);
}
}
dc.EndPage();
dc.EndDoc();
dc.DeleteDC();
return true;
}
catch(...)
{
return false;
}
}
There is no user interface to this object (that's a feature!!) so there is no
print preview or printer dialog. This makes it it possible to install this COM
class on MTS and run the printer from a process on a remote system.
This article was contributed by Richard Warg .
There are many applications where I don't really want to use the usual windows page print.
Instead I want output to go directly to the printer using standard print i/o. It's actually a topic that
is hard to find in any of the books on Windows, at least I've never found anything on it. But to my
surprise I recently learned that standard (DOS/UNIX) printing is alive and well underneath
windows. All we need to do is open a printer port and print to it.
If the printer is directly attached to the computer it's trivial. The method for obtaining a printer
port when the printer is on the network isn't hard either.
The example below shows how I use the Windows NET USE command to re-direct LPT1 to a
shared printer on an NT Server. The same technique applies for Novell networks with a slightly
different syntax.
Try this out by creating a new MFC Form-based project. Put a button on the form and attach
this code to it. You can actually print with only 3 lines of code:
FILE *fp = fopen("LPT1", "w");
fprintf(fp,"What's up, Doc?\n");
fclose(fp);
Instant print gratification!!
While the program is open it hogs the printer port. In my shop that isn't a
problem but be aware of the effect on your windows spooled output.
*********************************************************
THE CODE
*********************************************************
#include
#include
#include
using namespace std;
void CLineprtView::OnButton1()
{
char buff[MAX_BUFF_SIZE];
if (PRINTER_IS_REMOTE)
{
system("NET USE LPT1 /d");
system("net use lpt1 \\\\green\\hp5annex");
}
FILE *ptr = fopen("LPT1","w");
sprintf(buff,"\033E\033(s0p4102t1b16.66H\033&l1O");
fprintf(ptr,buff);
fprintf(ptr,"Who of late doth make a thimble.\n");
fprintf(ptr,"Is a lower bunk a status symbol??\n");
fclose(ptr);
ofstream optr("LPT1", ios::out);
string str_text = "Hey Doc, Ain't this a print test from windows\n";
str_text += "with more lines to follow?\n";
optr << str_text << endl;
optr << "Quiet, wabbit. I'm conversing with my muse!!\n";
optr << "That's all folks." << "\f" << flush;
optr.close();
if (PRINTER_IS_REMOTE)
{
system("net use lpt1 /d");
}
}
In practice I get printer path information from the registry on each machine,
so the real live code is a little busier than this example, but not much.