Printing text with word wrap

Reverend Jim 2 Tallied Votes 3K Views Share

One of the things I have been steadfastly avoiding is writing code to print stuff. Most of what I want to print is from withing apps like Outlook or Word that already provide that functionality. But I finally ended up having to bite the bullet. What I ended up with was not pretty but it is (except for one flaw noted in the comments) adequate. I present it here for comments. Perhaps it will be of use to someone else.

In a nutshell, printing requires that you "draw" each item on the page, be it text or graphics. As such, you can't just stream text to the page. You must calculate the position of each string. When you print a document, the PrintPage event gets fired until there are no more pages to print. As long as you set e.HasMorePages to True before you exit the PrintPage handler the PrintPage event will continue to fire.

As I quickly learned by starting with a Microsoft example, if you try to print a line that is too long to fit on the page, it will just get truncated. You must provide the word wrap functionality by determining what parts of the line will fit on the page and what will not. The sample code does this by tokenizing each line of text and rendering each token separately. If a token will not fit in the remaining line then it is saved for the next line.

This is my first attempt at printing so I expect that there are improvements to be had. All comments and suggestions are welcome.

pd is a PrintDocument control. The only other control on the form is a button.

Imports System.IO

Public Class Form1

    Private pf As New Font("Arial", 10)     'this font will be used for the print
    Private sr As StreamReader

    Private Sub btnPrint_Click(sender As System.Object, e As System.EventArgs) Handles btnPrint.Click

        Try
            sr = New StreamReader("D:\My Documents\History of the Amiga.txt")
            Try
                pd.Print()
            Finally
                sr.Close()
            End Try
        Catch ex As Exception
            MessageBox.Show(ex.Message)
        End Try

    End Sub

    'The PrintPage event is raised for each page to be printed. This Sub will handle
    'word wrap. It does this by parsing each line into blank delimited tokens and   
    'calculating the pront position of each token. If the current token will not fit
    'on the current line then a new line is started. Note that this sub will not    
    'handle a line with no blanks that is wider than the page printable width.      

    Private Sub pd_PrintPage_1(sender As System.Object, e As System.Drawing.Printing.PrintPageEventArgs) Handles pd.PrintPage

        Dim linesPerPage As Integer     'number of lines that will fit on a page    
        Dim yPos As Single = 0          'y position of the next line to print       
        Dim lineNum As Integer = 0      'the current line number                    
        Dim line As String = Nothing    'next line from text file                   

        'these must be declared as Static to preserve their values between calls    

        Static xPos As Single = 0       'x position of the next token to print      
        Static tokens() As String       'current line tokenized by blanks           
        Static tokidx As Single = -1    'index of next token to print               

        ' Calculate the number of lines per page.

        linesPerPage = e.MarginBounds.Height / pf.GetHeight(e.Graphics)

        ' Print each line of the file. 

        While lineNum < linesPerPage

            'if we are not continuing with unused tokens from the previous line     
            'then read a new line and parse it                                      

            If tokidx = -1 Then
                line = sr.ReadLine()                    'read a new line            
                If line Is Nothing Then Exit While
                xPos = e.MarginBounds.Left              'reset to left margin       
                tokens = line.Split()                   'parse the new line         
                tokidx = 0                              'reset token index          
            End If

            yPos = e.MarginBounds.Top + lineNum * pf.GetHeight(e.Graphics)

            'print all tokens

            While tokidx < tokens.Length

                'get the next token and calculate the required width

                Dim token As String = tokens(tokidx) & " "
                Dim w As SizeF = e.Graphics.MeasureString(token, pf)

                'if the token will fit on the current line then render it otherwise 
                'reset xpos to the left margin and start a new line                 

                If w.Width + xPos <= e.MarginBounds.Right Then
                    e.Graphics.DrawString(token, pf, Brushes.Black, New Point(xPos, yPos))
                    xPos += w.Width
                    tokidx += 1
                Else
                    xPos = e.MarginBounds.Left
                    Exit While
                End If

            End While

            'if all tokens have been processed then reset the index to -1 to force  
            'a read of a new line                                                   

            If tokidx = tokens.Length Then tokidx = -1
            lineNum += 1

        End While

        ' If more lines exist, print another page.

        e.HasMorePages = (line IsNot Nothing)

    End Sub

End Class
april88t 0 Newbie Poster

is this really working??

Reverend Jim 4,968 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

I tried it before I posted it and it was working then. It's not elegant but it does work and if anyone has improvements/suggestions I'd love to hear them. I just noticed one improvement - reset the values of the static variables after the last page is printed so that the next print starts properly.

kplcjl 17 Junior Poster

When I read the title, I had already figured out how to wrap when the whole word wouldn't fit on a line.
I hadn't figured out how to find the split points on a word so it puts part of the word on the fir-
st line and finishes the word on the next line. Shoot.

tinstaafl 1,176 Posting Maven

Was doing some digging on a different matter and came across this sample code from MSDN. As was typical, I had to tweak things, namely adding the button to the controls collection, and adding handlers for the button click, and the printdocument printpage. Then I created a text file with a multiline run on string, and the code printed it with proper word wrapping.

The printpage handler code is here:

    Private Sub printDocument1_PrintPage(ByVal sender As Object, _
        ByVal e As PrintPageEventArgs)

        Dim charactersOnPage As Integer = 0
        Dim linesPerPage As Integer = 0

        ' Sets the value of charactersOnPage to the number of characters  
        ' of stringToPrint that will fit within the bounds of the page.
        e.Graphics.MeasureString(stringToPrint, Me.Font, e.MarginBounds.Size, _
            StringFormat.GenericTypographic, charactersOnPage, linesPerPage)

        ' Draws the string within the bounds of the page
        e.Graphics.DrawString(stringToPrint, Me.Font, Brushes.Black, _
            e.MarginBounds, StringFormat.GenericTypographic)

        ' Remove the portion of the string that has been printed.
        stringToPrint = stringToPrint.Substring(charactersOnPage)

        ' Check to see if more pages are to be printed.
        e.HasMorePages = stringToPrint.Length > 0

    End Sub
Reverend Jim 4,968 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

It's easy enough to calculate how many lines of text will fit on a page but when using a proportional font, there is no way of determining how many characters will fit on a line because the number will vary depending on the actual characters. But I will still play with the code you posted. Thanks.

tinstaafl 1,176 Posting Maven

Your Welcome. A thought, since Me.Font is part of the parameters for DrawString, perhapse adjusting the size of the font will change how many characters fit on a line.

Reverend Jim 4,968 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster

Not if the font is proportional. Look at the attached image. Both lines contain 25 characters.

prop

tinstaafl 1,176 Posting Maven

When I look at the printout, that did the word wrap properly, it seems to be in proportional font, the 'i' uses less space than the 'w'. When I query the Me.Font.Name, it returns Microsoft Sans Serif. When I change the size of the font the printout is adjusted accordingly.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.