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.