Expand Your VB6 Printing Repertoire—Part III

n the past two 10-Minute Solutions, you’ve explored the basics of printing with VB6 and built a very simple text editor with print and preview capabilities. Now, learn how to print tabular data from a ListView control. The ListViewcontrol is very handy for displaying tabular data—though because it doesn’t allow you to edit the data, you need a tool for printing its contents.

The ListView Control
Figure 1 shows the test form of this article’s sample project, which contains two ListView controls displaying different sets of data, and a preview form for the customer data. The Print and Preview buttons create an instance of the LVPrint class, set a few properties, and then call the PrintList method to preview and print the data. The PrintList method picks up all the information from the corresponding ListViewcontrol and generates the printout. The following is the core code behind the Preview buttons:

'Printing the top ListView Private Sub bttnPreviewCustomers_Click()    Dim LVPRN As New PRNClass.LVPrint    Set LVPRN.LV = ListView1    LVPRN.ReportTitle = "Northwind Customers"    If Not LVPRN.PrintList(True) Then        MsgBox "Preview failed!"    End IfEnd Sub'Printing the bottem ListView Private Sub bttnPreviewInvoice_Click()    Dim LVPRN As New PRNClass.LVPrint    Set LVPRN.LV = ListView2    LVPRN.ReportTitle = "Simple Invoice Printout Demo"    If Not LVPRN.PrintList(True) Then        MsgBox "Preview failed!"    End IfEnd Sub
Figure 1. The test form for this article’s sample project contains two ListView controls displaying different sets of data and a preview form for the customer data.

The argument of the PrintList method determines whether the data will be previewed (True) or printed (False). The LVPrint class can handle any ListView control and you don’t really need to understand how it works; just use it to add print and preview capabilities to any form that displays data on the ListViewcontrol. To make the most of this code, review it so you are able to edit it to accommodate your application’s specific requirements.

The basic code for printing the items of a ListView control is straightforward, but the details introduce a few interesting challenges. It would be fairly easy to print the items on one line per cell, which is how the ListView control displays its data in Report mode (View property); if an item’s text is too long to fit on a single line, only part of it is visible. A decent printout, however, requires that long strings are broken into multiple lines of the text, which are printed in a tall cell. All cells on the same row must have the same height, which is determined by the height of the tallest cell. We’ll look closer at the code for wrapping each cell’s text shortly. The code also picks the alignment information from the ListViewcontrol being printed and uses it to align the text in every cell.

In addition to breaking long cells into multiple lines of text, the LVPrint class maintains the ratio of the control’s columns. The ratio of each column’s width to the total width of the control is the same on both the preview form and the printed page. A column that takes 15 percent of the ListView control’s width will take 15 percent of the printable area on the page. This means that users can resize the columns of the ListViewcontrol to affect the appearance of the printout. Printing the report sideways will give you room to fit more data on each line than across a typical monitor.

The VBPrint Project
This solution’s example consists of two projects: a PRNClass project, which is a class, and a test project. The PRNClass project contains the LVPrint class and a blank form that serves as the preview surface. Compiling the class project creates a DLL which you can use with any other project. The preview form is displayed automatically from within the class’s code as needed. All the printing and previewing functionality is cast into a DLL, which can handle any ListView. Keep in mind that the LVPrint class expects that the ListView control has a Columns collection. If you don’t set up the control’s Columnscollection, nothing will be printed.

The Printing Code
First, you need to understand the role of the form-level variables, which are used by multiple procedures in the application. These variables are declared at the form’s level with the following statements:

' Work variablesDim PRN As ObjectDim XPos As Single, YPos As SingleDim RowHeight As SingleDim CellHeight As SingleDim YMin As Single, YMax As SingleDim MorePages As BooleanDim currentRow As IntegerDim colWidths() As Single

The PRN variable represents the printing surface (the preview form, or the actual page). For more information on printing and previewing see the first installationof the series. XPos and YPos are the coordinates of the next element that will be printed on the page. Every time the code prints a cell, it adjusts the settings of these two variables for the next cell on the same row, or the following row. YMin and YMax are the coordinates of the top and bottom of the printable area on the page.

RowHeight is the default height of a row (a row that contains cells that don’t span multiple text lines). CellHeight is the current row’s height. CellHeight is larger that RowHeight for rows with cells that span multiple lines of text. Every time the code prints a cell, it adjusts the value of the CellHeight variable. Basically, this variable keeps track of the vertical space between consecutive rows. The MorePages variable is a Boolean variable that determines whether there are more pages to be printed when the end of the current page is reached. The currentRow variable holds the index of the row being printed and is increased by one after printing a row of cells. colWidths, finally, is an array that holds the widths of the printout’s columns. The widths of printout’s columns are proportional to the widths of the columns of the ListView control. The printout’s width is the width of the page minus the margins. The available width on the page is split among the columns proportionally to the width of each column on the ListViewcontrol.

The PrintList Method
The PrintList method starts by displaying the Print common dialog box, where the user can select the printer and set the printout’s orientation. Then it sets the printer’s font to the same font as the one used to render the items on the ListView control and the margins by calling the SetMargins subroutine. The margins are hardcoded in the sample application, but you can easily design a dialog box to prompt the user for the margin values. It continues by calculating the width each column should have on the printed page. Because the page’s width (or height, if you’re printing in landscape mode) is different from the window’s width on the screen, you must resize the columns so that they fill the width (or height) of the page. These calculations take place in the CalculateColumnWidthssubroutine.

Finally, the code prints the ListView item’s on the page. This takes place from within a While loop that prints one page at a time, as long as there pages to be printed (variable MorePages). First it prints the column headers by calling the PrintListViewHeaders subroutine and then calls the PrintListView subroutine, which prints as many items as it can fit on the current page. When the end of the page is reached, the code emits the current page, sets the MorePagesvariable to True and exits.

Here’s the PrintList subroutine, which calls the various subroutines to perform the basic printing operations. The actual project contains more code, as well as extensive comments, but this is a fairly substantial outline of the PrintListsubroutine:

Printing the ListView controlPublic Function PrintList(ByVal preview As Boolean) As Boolean    Dim frm As New frmPreview    Dim CD As mscomdlg.CommonDialog    Set CD = frmPreview.CommonDialog1    CD.ShowPrinter    If CD.Orientation = cdlLandscape Then        Printer.Orientation = cdlLandscape    Else        Printer.Orientation = cdlPortrait    End If    If preview Then        Set PRN = frmPreview    Else        Set PRN = Printer        If PRN Is Nothing Then            Exit Function        End If    End If    SetPrinterFont    If PRN Is frmPreview Then        PRN.Show        PRN.Cls    End If    SetMargins    CalculateColumnWidths    MorePages = True    currentRow = 0    While MorePages And currentRow < LV.listitems.Count        If PRN Is frmPreview Then           ShowMargins                 End If        XPos = LeftMargin: YPos = TopMargin        PRN.CurrentX = XPos: PRN.CurrentY = YPos        If PrintListViewHeaders = False Then            PrintList = False            Exit Function        End If        PrintListView    Wend    PrintList = TrueEnd Function

As you can see, most of the procedures used by the LVPrint class are implemented as functions and they return a True/False value. The printing job may fail at several places and the code examines each function's return value upon completion. If False, the job must terminate. If the PrintListViewHeaders function returns False, then the PrintListsubroutine terminates and returns False to indicate that the printout failed.

The PrintListView Procedure
All the action in this example takes place from within the subroutines and the PrintListView subroutine is the core of the application. The subroutine itself is quite lengthy, so only its core code is discussed here. You can open the projectin your Visual Basic editor, examine the code and adjust it for your project needs.

The following code iterates through the rows of the control (the ListItemscollection). For each row, it prints the item's text and then the text of each subitem on the same row. Here are the two basic loops (I've omitted the statements that advance the current location after printing each cell):

Print the ListView control’s itemsPrivate Sub PrintListView()    Dim row As Integer, col As Integer    Dim txtLines() As String    ' Iterate through the ListItems collection    For row = currentRow To LV.listitems.Count - 1        CellHeight = RowHeight        txtLines = Split(BreakItemText(LV.listitems(row + 1).Text, _                                        colWidths(0) - 2), vbCrLf)        PrintSubItems txtLines, 0        XPos = XPos + colWidths(0)        ' Then iterate through the current item's subitems ...        For col = 1 To LV.ColumnHeaders.Count - 1            If col <= LV.listitems(row + 1).listsubitems.Count Then                 txtLines = Split(BreakItemText( _                           LV.listitems(row + 1).listsubitems(col), _                           colWidths(col) - 2), vbCrLf)                PrintSubItems txtLines, col            End If        Next        ' Print a horizontal line between cells        PRN.Line (LeftMargin, YPos + 0)- _                 (LeftMargin + TotalWidth, YPos + 0), GridColor        ' Is there room for another item?        If PRN.CurrentY > TopMargin + PageHeight - 2 * RowHeight Then            PRN.Line (LeftMargin, YMin)-(LeftMargin, YMax), GridColor            DrawVerticalLines            PageNo = PageNo + 1            If PRN Is Printer Then PRN.NewPage            Exit Sub        End If        currentRow = row + 1    Next    PRN.Line (LeftMargin, YMin)-(LeftMargin, YMax), GridColor    DrawVerticalLines    MorePages = False    If PRN Is Printer Then PRN.EndDocEnd Sub

The code is straightforward, except for the statements that break the current cell's text in multiple lines. The BreakItem function accepts as arguments the string to be printed and the width in which it should fit, and breaks it into multiple text lines. The BreakItemText function, along with the GetNextWord helper function, is shown in the next code block. The array of text lines to be printed, along with the current column's index, is passed as argument to the PrintSubItems subroutine, which prints the current cell. Its code is straightforward, because it knows the coordinates of the cell's upper left corner (the statements that manipulate these two coordinates are not shown in this article's listings for brevity). The PrintSubItems subroutine prints the text lines of each cell taking into consideration the alignment of the text on the corresponding column on the ListViewcontrol.

Printing a row's subitemsPrivate Sub PrintSubItems(lines() As String, ByVal col As Integer)    Dim i As Integer    For i = 0 To UBound(lines)        If PRN.CurrentY < TopMargin + PageHeight Then            lines(i) = Trim(lines(i))            ' LEFT ALIGNED COLUMN            If LV.ColumnHeaders(col + 1).Alignment = _                                    lvwColumnLeft Then                PRN.CurrentX = XPos + extraHSpace                PRN.Print lines(i)                ' RIGHT-ALIGNED COLUMN                ElseIf LV.ColumnHeaders(col + 1).Alignment = _                                    lvwColumnRight Then                    PRN.CurrentX = XPos + (colWidths(col) - _                                    PRN.TextWidth(lines(i)) - 1)                    PRN.Print lines(i)                    Else                        ' CENTERED COLUMN                        PRN.CurrentX = XPos + _                               (colWidths(col) - _                               PRN.TextWidth(lines(i)) - 2) / 2 + 1                        PRN.Print lines(i)            End If        Else            PRN.CurrentX = XPos + extraHSpace            PRN.Print "* * *"            Exit For        End If    Next    If CellHeight < PRN.CurrentY - YPos Then        CellHeight = PRN.CurrentY - YPos    End IfEnd Sub

The code assumes that no cell is too tall to cause any problems; in other words, it doesn't break a tall row between two consecutive pages. Each item of the ListItemscollection will be printed on the same page. Should one of the cells become too long, the code detects this condition and skips the part of the item that doesn't fit comfortably on the page (it actually prints three stars in the place of the missing text). One of the improvements you will probably consider for this application is to continue printing tall cells on the following page. To test the application I've created "fake" subitems with very long strings, to see how the code handles unusually tall cells. If a certain column contains long strings, you should give more space to this column. Printing in landscape mode will also minimize the problem of trimming very long items at the bottom of the page.

The Helper Functions
The following code shows the two helper functions: the BreakItemText and GetNextWord functions. The BreakItemText function attempts to fit as many words into the specified width as possible, taking into consideration the current font. If needed, it creates multiple text lines separated by a vbCrLf character. To extract individual words from a string, it calls the GetNextWord function. The GetNextWorduses the space as the only valid word separator. Multiple spaces are not skipped; they're appended to the word they follow.

Private Function BreakItemText(ByVal item As String, _                               ByVal width As Single) As String    If PRN.TextWidth(item) < width Then        BreakItemText = item    Else        Dim iChar As Integer: iChar = 1        Dim newitem As String: newitem = ""        Dim moreWords As Boolean: moreWords = True        Dim nextWord As String        While moreWords            nextWord = GetNextWord(item, iChar)            iChar = iChar + Len(nextWord)            If PRN.TextWidth(newitem & nextWord) < width Then                newitem = newitem & nextWord            Else                newitem = newitem & vbCrLf & nextWord            End If            BreakItemText = newitem            If iChar > Len(item) Then                moreWords = False            End If        Wend    End IfEnd FunctionPrivate Function GetNextWord(ByVal str As String, _                             ByVal pos As Integer)    Dim nextWord As String    While pos <= Len(str) And Mid(str, pos, 1) <> " "        nextWord = nextWord & Mid(str, pos, 1)        pos = pos + 1    Wend    While pos <= Len(str) And Mid(str, pos, 1) = " "        nextWord = nextWord & Mid(str, pos, 1)        pos = pos + 1    Wend    GetNextWord = nextWordEnd Function

There are a few more procedures I'm not showing here, but you will find their implementation in the sample project. The DrawVerticalLines subroutine draws the vertical lines between columns and it's called after printing the last row on the page, because it needs to know the coordinates of the last row. The vertical lines extend from the top horizontal line to the last one that appears below the last row. The horizontal lines are printed from within the PrintSubItems subroutine after each row. The PrintDatePageNo subroutine prints the report's title, date, and page number at the top of the page. This is probably one of the first segments of the project you'll want to change. The PrintListViewHeaders subroutine prints the headers of the columns on each page. Its code is similar to the code of the PrintSubItems subroutine, in the sense that it takes into consideration the alignment of each column. The header titles, however, are not broken into multiple text lines (the code assumes that they will fit on a single line). Finally, the ShowMarginssubroutine draws on the preview form a white rectangle that represents the actual page (the preview form's background color is dark gray and the page area is white).

Open the PRNClass project and check it out. The code is adequately commented and you should be able to adjust it fairly easily. To reuse the LVPrint class, compile it to generate the LVPrint DLL and referenced it from within any project that uses the ListViewcontrol to display tabular data.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: