Saturday, February 26, 2011

Printing text with wxWidgets

A common requirement for an accounting program is printing some text, often for reports.

One possible solution is creating an HTML file and printing it using the wxHtmlEasyPrinting. I used it and it is a very simple and quick solution, but it does not give you precise control on the text layout.

For those who want to print using a more traditional approach there is the wxWidgets printing framework: it shields some of the low level tasks but it leaves you full control on the page layout.
I spent a lot of time trying to understand how to use it to print text lines and I found it difficult to understand the documentation and the printing sample (as usual, the wxWidgets samples are the best place to look for inspiration). Samples and documentation are more geared towards printing graphics, not text.
In the end I found a satisfactory solution and I will describe it here.

To use the printing framework you need to derive your own class from wxPrintout, and at least to implement the OnPrintPage() and HasPage() functions.

The OnPrintPage() function is called by the printing framework for each page that will be printed. It will contain the actual print code.
The problem, in the print code, is that each function that prints something requires coordinates to tell it where to print in the page. Those coordinates are not in millimeters or inches, so we need to find a way to convert millimeters (for example) to the units used by the device context. I used the following code to determine a conversion factor from millimeters to device units. The code is based on the printing sample.

    wxDC *dc = GetDC();
    if( dc == NULL || !dc->IsOk() ) {

        // report error and...
        return false;
    }

    // Get the logical pixels per inch of screen and printer
    int ppiScreenX, ppiScreenY;
    GetPPIScreen(&ppiScreenX, &ppiScreenY);
    int ppiPrinterX, ppiPrinterY;
    GetPPIPrinter(&ppiPrinterX, &ppiPrinterY);

    // This scales the DC so that the printout roughly represents the the screen
    // scaling. The text point size _should_ be the right size but in fact is
    // too small for some reason. This is a detail that will need to be
    // addressed at some point but can be fudged for the moment.
    float scale = (float)((float)ppiPrinterX/(float)ppiScreenX);

    // Now we have to check in case our real page size is reduced (e.g. because
    // we're drawing to a print preview memory DC)
    int pageWidth, pageHeight;
    int w, h;
    dc->GetSize(&w, &h);
    GetPageSizePixels(&pageWidth, &pageHeight);

    // If printer pageWidth == current DC width, then this doesn't change. But w
    // might be the preview bitmap width, so scale down.
    float overallScale = scale * (float)(w/(float)pageWidth);
    dc->SetUserScale(overallScale, overallScale);

    // Calculate conversion factor for converting millimetres into logical
    // units. There are approx. 25.4 mm to the inch. There are ppi device units
    // to the inch. Therefore 1 mm corresponds to ppi/25.4 device units. We also
    // divide by the screen-to-printer scaling factor, because we need to
    // unscale to pass logical units to DrawLine.
    float logUnitsFactor = (float)(ppiPrinterX/(scale*25.4));



now we can, for example, convert 20 millimeters to device units by computing 20*logUnitsFactor.
Font size in points will be respected, so the code for printing some text lines will be the following.

    dc->SetFont( m_ReportFont );
    dc->SetBackgroundMode( wxTRANSPARENT );
    dc->SetTextForeground( *wxBLACK );
    dc->SetTextBackground( *wxWHITE );

    // line height
    float charH = dc->GetCharHeight();

    // coordinates of the first line
    float x = m_MarginLeft*logUnitsFactor;
    float y = m_MarginTop*logUnitsFactor;
   
    // print lines
    dc->DrawText( "First line", x, y );
    y += charH;
    dc->DrawText( "Second line", x, y );
    y += charH;

One problem of the printing framework when printing reports from a database if that it seems that it requires the previous knowledge of how many pages will be printed. Of course this is not the case for a database report: the most efficient solution is reading the rows while printing the report, so there is no previous knowledge of how many pages will be printed.
The documentation says that you must provide an implementation of the GetPageInfo() function, telling the framework how many pages you are going to print. This is not completely true: if you do not provide that function the printing framework will assume a default of 32000 pages.
You can start with that default, then you can use your implementation of the HasPage() function to stop printing when it will be time. Just return false when you have printed all the needed rows.

Once you have created your custom printout class just use something like the following code to print your pages.

    MyPrintout printout( "Print name" );
   
    wxPrintDialogData printDialogData(* m_PrintData);
    wxPrinter printer( &printDialogData );
    bool success = printer.Print( NULL, &printout, true );

    if( !success ) {
        if (wxPrinter::GetLastError() == wxPRINTER_ERROR) {
            CUtils::MsgErr( "Printing error" );
        }
        else {
            // print has been canceled
        }
    }

That code will show a common dialog to select the desired printer, then it will print to that printer.