SQLServerCentral Article

Printing in .NET



A quick summary for those who just want the code and don't want to read the history... I have searched all over the web for good printing code and was unable to find it. I have created a printing class which allows text to be formatted at the character level. It has taken me about a week to get what I have working correctly and I thought it would be nice to save others the time.

The Long Version

Now for those who would like the longer version of the story... I was recently tasked with created a web application which would allow printing to be initiated without the irritation of having to respond to successive prompts. I chose ASP.NET for this project, not only because it is replacing traditional ASP but also because I had had experience with printing in prior versions of MS VB, which is one of the languages that ASP.NET supports. So I started browsing the Internet looking for a printing solution that would fit my needs, unfortunately I didn't find one. While I did find multiple solutions which allowed me to print a large block of text, I couldn't find anything that allowed me to control the formatting at the character level as I needed.

Finally I found a tutorial on how to format each paragraph by placing them separately on the page before it was sent to the printer. Combining this latest find with some of the other tutorials I went to work to make a class which would let me bold a single character if I chose. From the start I realized that this class should be as separate from the rest of the program as possible so I could use it in future projects with similar requirements, this resulted in the creation of variables which could be changed through properties of the class object; it also prompted me to use formatting tags in the text to be printed. I choose the HTML style of tags because I work a lot with them and I figured they would be easier for me and others to remember and use than proprietary tags. Also HTML tags seemed uniquely suited for my purpose because people rarely use them when creating an everyday document which means that my printing class wouldn't pick up a tag where one wasn't meant to be. I do suggest, however, that when utilizing this class you check for the <'s and >'s signifying HTML tags and flag them so the user knows they could be a source of problems.

Through this process I learned a lot, here are some of what I consider to be the more important or obscure. First, when using the OnPrintPage sub of the PrintDocument class, I had trouble understanding how multiple pages were printed since I couldn't find a sendpagetoprinter command. I learned that the PrintDocument class essentially puts the OnPrintPage sub into a while loop as long as e.HasMorePages=True, this means that as soon as your code is finished executing and the sub is done, then the sub will start back over again if the HasMorePages value is true. There is no magical sustaining of where in the print job I am or what my variable values are which means that I have to be sure that my variables scope is set correctly. The other big problem I was having was figuring out the coordinates to start each character at considering they will be different if the char is bolded or not, this was resolved when one of the tutorials I looked up showed me how to use the MeasureString function from the Graphics object.

My final challenge came when I needed to make sure that words were not chopped off at the end of a line. To overcome this issue I ended up putting a kind of memory in my program which allowed it to collect one words worth of characters before printing them out. To make sure that this word still got printed I implemented a rollback feature so the code would move to the next line or page and start printing at the position it was rolled back to.

Code and Description

My code is easily modified and shouldn’t be too hard to understand. It is well commented. It recognizes the HTML bold tag and a special tag which I created to take the place of what tables do in HTML. Paragraphs are separated using vbCrLf and lines will wrap correctly without chopping off a word. The checkBold function is used to parse the incoming text and format the characters correctly; this is the function where you could add support for Italicizing and other features.

Here is the block of code to instantiate and use the class:

        ' Create object, passing in text
        Dim MyPrintObject As New TextPrint("<B>this will be bold</B>" + vbCrLf + "<ST=400>this will start at 400 pixels")
        ' Set font, defaults to times new roman, 12 if omitted
        MyPrintObject.Font = New Font("Tahoma", 8)
        ' Issue print command

Here is the actual printing class which does the work:

Public Class TextPrint
    ' Inherits all the functionality of a PrintDocument
    Inherits Printing.PrintDocument
    ' Private variables to hold default font and text
    Private fntPrintFont As Font
    Private strText As String
    Dim MySplitLine As String()
    Dim varStart As Integer = 0
    Dim varChar As Integer = 0

    ' New constructor
    Public Sub New(ByVal Text As String)
        ' Sets the file stream
        varStart = 0
        strText = Text
        MySplitLine = strText.Split(vbCrLf)
    End Sub
    Public Property Text() As String
            Return strText
        End Get
        Set(ByVal Value As String)
            strText = Value
            MySplitLine = strText.Split(vbCrLf)
        End Set
    End Property
    Protected Overrides Sub OnBeginPrint(ByVal ev _
                                As Printing.PrintEventArgs)
        ' Run base code
        ' Sets the default font
        If fntPrintFont Is Nothing Then
            fntPrintFont = New Font("Times New Roman", 12, FontStyle.Regular, GraphicsUnit.Point)
        End If
    End Sub
    Public Property Font() As Font
        ' Allows the user to override the default font
            Return fntPrintFont
        End Get
        Set(ByVal Value As Font)
            fntPrintFont = Value
        End Set
    End Property
    Protected Overrides Sub OnPrintPage(ByVal e As Printing.PrintPageEventArgs)
        ' Provides the print logic for our document
        ' Run base code
        ' Draw the margins (for debugging).
        'e.Graphics.DrawRectangle(Pens.Red, e.MarginBounds)
        Dim the_font As Font = fntPrintFont
        Dim string_format As New StringFormat
        ' Draw the text left justified,
        ' wrap at words, and don't draw partial lines.
        string_format.Alignment = StringAlignment.Near
        string_format.FormatFlags = StringFormatFlags.LineLimit
        string_format.Trimming = StringTrimming.Word
        ' Draw some text.
        Dim ymin As Integer = e.MarginBounds.Top
        Dim layout_rect As RectangleF
        Dim text_size As SizeF
        Dim characters_fitted As Integer
        Dim lines_filled As Integer
        Static i As Integer
        For i = varStart To MySplitLine.GetUpperBound(0)
            ' get ready for the 1 char printing
            Dim smallArray As String(,)
            Dim xmin As Integer = e.MarginBounds.Left
            Dim varWord As RectangleF()
            ReDim varWord(1)
            Dim wordCountForLine As Integer = 0
            ' make sure a space prints if a two vbcrlf's are in a row
            If Trim(Len(MySplitLine(i))) = 1 Then
                ReDim smallArray(3, 1)
                smallArray(0, 0) = ""
                smallArray(1, 0) = FontStyle.Regular
                smallArray(2, 0) = -1
                ymin += CInt(the_font.Height)
                '****************************Special print 1 char at a time for formatting*********************************
                smallArray = checkBold(Trim(MySplitLine(i).ToString), fntPrintFont)
                '******************************END print 1 char at a time for formatting***********************************
            End If
            Dim x As Integer
            For x = varChar To smallArray.GetUpperBound(1) - 1
                'remove blanks so ascii works
                If smallArray(0, x).Length = 0 Then smallArray(0, x) = Chr(0)
                ' Get the font for measurement.
                the_font = New Font(fntPrintFont.Name, fntPrintFont.Size, CInt(smallArray(1, x)), fntPrintFont.Unit)
                ' Set the text start location if desired
                If CInt(smallArray(2, x)) > -1 Then xmin = CInt(smallArray(2, x))
                ' Get the area available for this text.
                layout_rect = New RectangleF(xmin, ymin, e.MarginBounds.Right - xmin, the_font.Height)
                ' If the layout rectangle's height < 1, make it 1.
                If layout_rect.Height < 1 Then layout_rect.Height = 1
                ' See how big the text will be and
                ' how many characters will fit.
                text_size = e.Graphics.MeasureString(smallArray(0, x).ToString, the_font, _
                    New SizeF(layout_rect.Width, layout_rect.Height), _
                    string_format, characters_fitted, lines_filled)
                ' See if any characters will fit.
                If characters_fitted > 0 Then
                    ' start accumulating the print location
                    varWord(varWord.GetUpperBound(0) - 1) = layout_rect
                    ' ************Draw the word when finished.************
                    If Asc(smallArray(0, x).Chars(0)) = 32 Or x = smallArray.GetUpperBound(1) - 1 Then
                        Dim z As Integer
                        For z = x - (varWord.GetUpperBound(0) - 1) To x
                            ' Get the font for measurement.
                            the_font = New Font(fntPrintFont.Name, fntPrintFont.Size, CInt(smallArray(1, z)), fntPrintFont.Unit)
                            ' actually print the character on the page.
                            e.Graphics.DrawString(smallArray(0, z), _
                                the_font, Brushes.Black, _
                                varWord((z - x) + (varWord.GetUpperBound(0) - 1)), string_format)
                        xmin += 4
                        ReDim varWord(0)
                        wordCountForLine += 1
                    End If
                    '' Draw a rectangle around the text (for debugging).
                    'e.Graphics.DrawRectangle(Pens.Green, _
                    '    layout_rect.Left, _
                    '    layout_rect.Top, _
                    '    text_size.Width, _
                    '    text_size.Height)
                    ' Increase the location where we can start.
                    xmin += CInt(text_size.Width) - 4
                    ReDim Preserve varWord(varWord.GetUpperBound(0) + 1)
                ElseIf Asc(smallArray(0, x).Chars(0)) < 30 Then ' make sure to dispose of odd char's in the array
                Else ' See if some of the paragraph didn't fit
                    ' ********Draw the word if longer than one line.**********
                    If wordCountForLine = 0 Then
                        varWord(varWord.GetUpperBound(0) - 1) = layout_rect
                        Dim z As Integer
                        For z = x - (varWord.GetUpperBound(0) - 1) To x
                            e.Graphics.DrawString(smallArray(0, z), _
                                the_font, Brushes.Black, _
                                varWord((z - x) + (varWord.GetUpperBound(0) - 1)), string_format)
                        ReDim varWord(0)
                    End If
                    '*******reset the variables*********
                    wordCountForLine = 0
                    x -= varWord.GetUpperBound(0)
                    ReDim varWord(1)
                    xmin = e.MarginBounds.Left
                    ymin += CInt(the_font.Height) ' move to the next line
                    'see if there are more lines available
                    If (e.MarginBounds.Bottom - ymin) < the_font.Height Then
                        Exit For ' exit for loop so page can print
                    End If
                End If
            ymin += CInt(the_font.Height) ' move to the next line
            If (e.MarginBounds.Bottom - ymin) < the_font.Height Then
                varChar = x 'save character location
                varStart = i 'save line location
                e.HasMorePages = True 'after printing page, run sub again
                Exit For ' exit for loop so page can print
                varChar = 0
                e.HasMorePages = False
            End If
    End Sub
    Private Function checkBold(ByVal varString As String, ByVal startFont As Font) As String(,)
        Dim aryString As String(,)
        ReDim aryString(3, 1)
        Dim printStyle As FontStyle = FontStyle.Regular
        Dim varStartPlace As Integer = -1
        aryString(0, 0) = "" 'initialize the array to avoid errors
        aryString(1, 0) = printStyle
        aryString(2, 0) = varStartPlace
        Dim varPlace As Integer = 0
        For varPlace = 1 To varString.Length
            If Mid(varString, varPlace, 3) = "<B>" Then
                printStyle = FontStyle.Bold
                varPlace += 2
            ElseIf Mid(varString, varPlace, 4) = "</B>" Then
                printStyle = FontStyle.Regular
                varPlace += 3
            ElseIf Mid(varString, varPlace, 4) = "<ST=" Then
                varStartPlace = CInt(Mid(varString, varPlace + 4, InStr(varPlace + 4, varString, ">") - (varPlace + 4)))
                varPlace += 4 + varStartPlace.ToString.Length
                ReDim Preserve aryString(3, aryString.GetUpperBound(1) + 1)
                aryString(0, aryString.GetUpperBound(1) - 1) = Mid(varString, varPlace, 1)
                aryString(1, aryString.GetUpperBound(1) - 1) = printStyle
                aryString(2, aryString.GetUpperBound(1) - 1) = varStartPlace
                varStartPlace = -1
            End If
        checkBold = aryString
    End Function
End Class


Obviously there are many enhancements which can be done to this code, however, it should be a great start for most programmers.

For those who are interested in my research and the tutorials I found that helped me with this project, here are the two articles I used:





5 (1)

You rated this post out of 5. Change rating




5 (1)

You rated this post out of 5. Change rating