As advertised, this chapter's project focuses on the printing of check-out and fine-payment receipts. But we'll also add all of the code that lets patrons and librarians check in and check out books and other library items.

Project Access

Load the "Chapter 19 (Before) Code" project, either through the New Project templates, or by accessing the project directly from the installation directory. To see the code in its final form, load "Chapter 19 (After) Code" instead.

Supporting Raw Printing

In the interest of frank and honest discussion, I must tell you that I didn't come up with the basic code for raw printing in this section. Oh, some of the code is mine, both stylistically and imaginatively. But I didn't figure out all of the links between the application and the winspool.drv file. That code originally came from Microsoft Knowledge Base article number 322090, which describes raw printing support from .NET applications. It uses a feature of .NET known as "interop" that allows .NET code to "interoperate" with older unmanaged COM-based components and applications.

Boy, am I glad that I got that off my chest. I mean, if anyone thought I was the one who came up with the code you are about to see, there would be angry mobs storming my house nightly, and general turmoil in the streets. The code, contained in the RawPrinterHelper class, is just plain ugly. Well, there's no sense postponing it any longer. Create a new class named RawPrinterHelper.vb, and use the following code for its definition.

Insert Snippet

Insert Chapter 19, Snippet Item 1.

Imports System.Runtime.InteropServices Public Class RawPrinterHelper    ' ----- The code in this class is based on Microsoft    '       knowledge base article number 322090.    '       Web:    ' ----- Structure and API declarations.    <StructLayout(LayoutKind.Sequential, _    CharSet:=CharSet.Unicode)> _    Private Structure DOCINFOW       <MarshalAs(UnmanagedType.LPWStr)> _          Public pDocName As String       <MarshalAs(UnmanagedType.LPWStr)> _          Public pOutputFile As String       <MarshalAs(UnmanagedType.LPWStr)> _          Public pDataType As String    End Structure    <DllImport("winspool.Drv", EntryPoint:="OpenPrinterW", _       SetLastError:=True, CharSet:=CharSet.Unicode, _       ExactSpelling:=True, _       CallingConvention:=CallingConvention.StdCall)> _    Private Shared Function OpenPrinter(ByVal src As String, _       ByRef hPrinter As IntPtr, ByVal pd As Long) As Boolean    End Function    <DllImport("winspool.Drv", EntryPoint:="ClosePrinter", _       SetLastError:=True, CharSet:=CharSet.Unicode, _       ExactSpelling:=True, _       CallingConvention:=CallingConvention.StdCall)> _    Private Shared Function ClosePrinter( _       ByVal hPrinter As IntPtr) As Boolean    End Function    <DllImport("winspool.Drv", EntryPoint:="StartDocPrinterW", _       SetLastError:=True, CharSet:=CharSet.Unicode, _       ExactSpelling:=True, _       CallingConvention:=CallingConvention.StdCall)> _    Private Shared Function StartDocPrinter( _       ByVal hPrinter As IntPtr, ByVal level As Int32, _       ByRef pDI As DOCINFOW) As Boolean    End Function    <DllImport("winspool.Drv", EntryPoint:="EndDocPrinter", _       SetLastError:=True, CharSet:=CharSet.Unicode, _       ExactSpelling:=True, _       CallingConvention:=CallingConvention.StdCall)> _    Private Shared Function EndDocPrinter( _       ByVal hPrinter As IntPtr) As Boolean    End Function    <DllImport("winspool.Drv", EntryPoint:="StartPagePrinter", _       SetLastError:=True, CharSet:=CharSet.Unicode, _       ExactSpelling:=True, _       CallingConvention:=CallingConvention.StdCall)> _    Private Shared Function StartPagePrinter( _       ByVal hPrinter As IntPtr) As Boolean    End Function    <DllImport("winspool.Drv", EntryPoint:="EndPagePrinter", _       SetLastError:=True, CharSet:=CharSet.Unicode, _       ExactSpelling:=True, _       CallingConvention:=CallingConvention.StdCall)> _    Private Shared Function EndPagePrinter( _       ByVal hPrinter As IntPtr) As Boolean    End Function    <DllImport("winspool.Drv", EntryPoint:="WritePrinter", _       SetLastError:=True, CharSet:=CharSet.Unicode, _       ExactSpelling:=True, _       CallingConvention:=CallingConvention.StdCall)> _    Private Shared Function WritePrinter( _       ByVal hPrinter As IntPtr, ByVal pBytes As IntPtr, _       ByVal dwCount As Int32, ByRef dwWritten As Int32) _       As Boolean    End Function    Public Shared Function SendStringToPrinter( _          ByVal targetPrinter As String, _          ByVal stringContent As String, _          ByVal documentTitle As String) As Boolean       ' ----- Send an array of bytes to a printer queue.       '       Return True on success.       Dim printerHandle As IntPtr       Dim errorCode As Int32       Dim docDetail As DOCINFOW = Nothing       Dim bytesWritten As Int32       Dim printSuccess As Boolean       Dim contentBytes As IntPtr       Dim contentSize As Int32       On Error Resume Next       ' ----- Set up the identity of this document.       With docDetail          .pDocName = documentTitle          .pDataType = "RAW"       End With       ' ----- Convert the string to ANSI text.       contentSize = stringContent.Length()       contentBytes = Marshal.StringToCoTaskMemAnsi( _          stringContent)       ' ----- Open the printer and print the document.       printSuccess = False       If OpenPrinter(targetPrinter, printerHandle, 0) Then          If StartDocPrinter(printerHandle, 1, docDetail) Then             If StartPagePrinter(printerHandle) Then                ' ----- Send the content to the printer.                printSuccess = WritePrinter(printerHandle, _                   contentBytes, contentSize, bytesWritten)                EndPagePrinter(printerHandle)             End If             EndDocPrinter(printerHandle)          End If          ClosePrinter(printerHandle)       End If       ' ----- GetLastError may provide information on the       '       last error. For now, just ignore it.       If (printSuccess = False) Then errorCode = _          Marshal.GetLastWin32Error()       ' ----- Free up unused memory.       Marshal.FreeCoTaskMem(contentBytes)       ' ----- Complete.       Return printSuccess    End Function End Class 

Although ugly, the code is relatively clear-cut. The SendStringToPrinter method prepares a string for printing by forcing it to a standard ANSI format. It then uses the functions in the winspool.drv library to open a new print job, and send the prepared content to it. There's a whole lot of "marshalling" going on in the code through members of the Marshall class. Because winspool.drv is an unmanaged library, all data calls must be shuttled indirectly between the managed Library application and the unmanaged winspool.drv library.

Printing Tickets

Now that we have a convenient class that will send any raw content to any specific printer, let's add some code to use it. First, we need to add a helper class for a portion of the ticket printing. Create a new class file named CheckedOutItem.vb, and replace its empty class template with the following code.

Insert Snippet

Insert Chapter 19, Snippet Item 2.

Public Class CheckedOutItem    ' ----- Used to store the details of each checked out    '       on the main form, although it also supports    '       receipt printing.    Public ItemTitle As String    Public CopyNumber As Integer    Public Barcode As String    Public DueDate As Date End Class 

We'll use this class to convey the details to be printed on the receipt when checking out items. Speaking of ticket printing, let's add the class that does the actual printing. Create a new module file (not a class) named TicketPrinting.vb. Replace its empty module definition with the snippet code.

Insert Snippet

Insert Chapter 19, Snippet Item 3.

The code includes three methods that drive printing: PrintCheckoutTicket, PrintBalanceTicket, and PrintPaymentTicket. These methods are called from other parts of the application when it's time to present a printed ticket to the user. The TicketPrinting module also includes a few other methods that support these three primary methods. Because these three methods are somewhat similar in structure, let's just look at PrintCheckoutTicket.

Public Sub PrintCheckoutTicket(ByVal patronID As Integer, _       ByVal checkedOutItems As ListBox)    ' ----- Print out a ticket of what the patron checked    '       out. The supplied ListBox control contains    '       objects of type CheckedOutItem.    Dim ticketWidth As Integer    Dim ticketText As System.Text.StringBuilder    Dim counter As Integer    Dim patronFines As Decimal    Dim itemDetail As CheckedOutItem    On Error GoTo ErrorHandler    ' ----- Ignore if there is nothing to print.    If (patronID = -1) Or (checkedOutItems.Items.Count = 0) _       Then Return    ' ----- Get the width of the ticket.    ticketWidth = My.Settings.ReceiptWidth    If (ticketWidth <= 0) Then ticketWidth = 40    ' ----- Build the heading.    ticketText = GetTicketHeader(patronID, ticketWidth)    If (ticketText Is Nothing) Then Return    ' ----- Process each checked-out item.    For counter = 0 To checkedOutItems.Items.Count - 1       ' ----- Extract the detail from the list.       itemDetail = CType(checkedOutItems.Items(counter), _          CheckedOutItem)       ' ----- Add the item name.       ticketText.AppendLine(Left(itemDetail.ItemTitle, _          ticketWidth))       ' ----- Add the barcode number and due date.       ticketText.AppendLine(LeftAndRightText( _          itemDetail.Barcode, "Due: " & _          Format(itemDetail.DueDate, "MMM d, yyyy"), _          ticketWidth))       ticketText.AppendLine()    Next counter    ' ----- If there are fines due, print them here.    patronFines = CalculatePatronFines(patronID)    If (patronFines > 0@) Then       ticketText.AppendLine("Fines Due: " & _          Format(patronFines, "$#,##0.00"))       ticketText.AppendLine()    End If    ' ----- Add the bottom display text.    ticketText.Append(GetTicketFooter(ticketWidth))    ' ----- Send the ticket to the printer.    RawPrinterHelper.SendStringToPrinter( _       My.Settings.ReceiptPrinter, _       ticketText.ToString(), "Checkout Receipt")    Return ErrorHandler:    GeneralError("TicketPrinting.PrintCheckoutTicket", _       Err.GetException())    Return End Sub 

The code builds a string (actually a StringBuilder) of display content, adding details about each checked-out item to a string buffer. Then it calls SendStringToPrinter to send the content to the configured receipt printer (My.Settings.ReceiptPrinter).

We'll add the code that calls PrintCheckoutTicket later. Right now, let's add code that calls the two other methods. When the Payment Record form closes, we want to automatically print a receipt of all payments made while the form was open. Add the following code to the PatronRecord.ActClose_Click event handler, just before the code already found in that handler.

Insert Snippet

Insert Chapter 19, Snippet Item 4.

' ----- Print out a ticket if needed. If (SessionPayments.Count > 0) Then _    PrintPaymentTicket(ActivePatronID, SessionPayments) SessionPayments.Clear() SessionPayments = Nothing 

Then, add some code to the ActBalanceTicket_Click event handler, also in the PatronRecord class, that prints a balance ticket when the user requests it.

Insert Snippet

Insert Chapter 19, Snippet Item 5.

' ----- Print a ticket of all balances. PrintBalanceTicket(ActivePatronID, Fines) 

Printing Barcodes

The Library Project prints three types of barcodes: (1) item barcodes that you can stick on books, CDs, and anything else that can be checked-out or managed by the system; (2) patron barcodes that can be made into patron identification cards; and (3) miscellaneous barcodes that a library can use for any other purpose. All three barcode types are printed through the new BarcodePrint form. Figure 19-9 shows the controls included on this form.

Figure 19-9. One form, three barcode types, many happy labels

I've already added this form to the project, including its code. Here's the code for the Preview button, which should look familiar after I beat its concepts into you throughout this chapter.

Private Sub ActPreview_Click(ByVal sender As System.Object, _       ByVal e As System.EventArgs) Handles ActPreview.Click    ' ----- The user wants to preview the labels.    On Error Resume Next    ' ----- Make sure the user supplied valid data.    If (VerifyFields() = False) Then Return    ' ----- Load in all of the page-specific details to be    '       used in printing.    If (LoadPageDetails() = False) Then Return    ' ----- Create the preview dialog.    Me.Cursor = Windows.Forms.Cursors.WaitCursor    PageSoFar = 0    PreviewMode = True    BarcodeDoc = New System.Drawing.Printing.PrintDocument    ' ----- Display the preview.    BarcodePreview.Document = BarcodeDoc    BarcodePreview.ShowDialog()    BarcodeDoc = Nothing    Me.Cursor = Windows.Forms.Cursors.Default End Sub 

The Print button's code is almost exactly the same, but it uses a PrintDialog instance instead of PrintPreviewDialog. It also keeps track of the final printed barcode number so that it can help avoid overlaps the next time they are printed.

The BarcodeDoc_PrintPage event handler does the actual barcode printing. Its code combines the BarcodeLabel.PreviewArea_Paint and BarcodePage.PreviewArea_Paint event handlers into one glorious printing machine.

To enable use of the barcode printing form, add the following statements to the ActReportsBarcode_Click event handler in the MainForm class.

Insert Snippet

Insert Chapter 19, Snippet Item 6.

' ----- Make sure the user is allowed to do this. If (SecurityProfile(LibrarySecurity. _       ManageBarcodeTemplates) = False) Then    MsgBox(NotAuthorizedMessage, MsgBoxStyle.OkOnly Or _       MsgBoxStyle.Exclamation, ProgramTitle)    Return End If ' ----- Show the barcode label printing form. Call (New BarcodePrint).ShowDialog() 

Renewal of Checked-Out Patron Items

For a library patron, the only thing more important than checking out and in items is being able to read those items. The Library Project won't help anyone with that, but it will do that check-in, check-out transaction thing through the code we add in this chapter. Let's start by adding the renewal code for currently checked-out items. The Renew button on the Patron Record form initiates the process. Add the code to the PatronRecord.ActRenewItemsOut_Click event handler that does the renewal.

Insert Snippet

Insert Chapter 19, Snippet Item 7.

The code does some calculations to determine the new due date (avoiding holidays), and then updates the database in a transaction.

TransactionBegin() ' ----- Update the record. sqlText = "UPDATE PatronCopy SET DueDate = " & _    DBDate(dueDate) & ", Renewal = " & renewsSoFar & _    " WHERE ID = " & itemDetail.DetailID ExecuteSQL(sqlText) ' ----- Update the patron record. sqlText = "UPDATE Patron SET LastActivity = Now " & _    "WHERE ID = " & ActivePatronID ExecuteSQL(sqlText) TransactionCommit() 

Support for Check-In and Check-Out

If a library adds barcode labels to all of its items, then check-in and check-out will be via a barcode reader. But a very small library using the program may not have the staff time available to barcode everything on the shelves. Therefore, the Library Project needs to support check-in and check-out by title. During check-out or check-in, the user enters either a barcode or a title (partial or complete). Non-numeric entries are assumed to be titles, and initiate a title search. The new CheckLookup.vb form, pictured in Figure 19-10, displays all matches for the entered title.

Figure 19-10. A title matching form for both check-in and check-out

Although the fields on the form initially indicate that they are for check-out only, the form does double duty, altering its visage for check-in purposes. Additionally, check-in listings are limited to only those items already checked out.

I've already added this form to the project, along with its source code. Most of the code queries the database for matching library items and displays the results using an owner draw list box. It is a subset of the code found in the ItemLookup.vb form. The only real difference between check-in and check-out occurs in the PerformLookup method. This code block starts to build the main item selection SQL command, and then ends it with these statements.

If (asCheckIn) Then sqlText &= " AND IC.ID IN" _    Else sqlText &= " AND IC.ID NOT IN" sqlText &= " (SELECT ItemCopy FROM PatronCopy " & _    "WHERE Returned = 0)" 

So the difference is "IN" versus "NOT IN."

The CheckItemByTitle function is the main interface to the form's logic.

Public Function CheckItemByTitle(ByVal CheckIn As Boolean, _    ByVal searchText As String) As Integer 

You pass this function the user-supplied title (searchText) and a flag indicating check-in or check-out, and it returns the ItemCopy.ID database field for the selected library item.

All of the remaining changes in this chapter occur in the MainForm class, so let's go there now. The UpdateDisplayForUser method adjusts the main form's features when an administrator logs in or out. One feature we didn't take into account before is the administrator-defined ability for patrons to check out their own items without librarian assistance. To support that feature, we need to change some of the code in the UpdateDisplayForUser method. About ten lines into the code, in the conditional section that sets up the display for patrons, you'll find these four lines.

LabelTasks.Visible = False LineTasks.Visible = False PicCheckOut.Visible = False ActCheckOut.Visible = False 

Replace these four lines with the following code.

Insert Snippet

Insert Chapter 19, Snippet Item 8.

' ----- See if patrons can check out items by themselves. Dim userCanCheckOut As Boolean = _    CBool(Val(GetSystemValue("PatronCheckOut")))    LabelTasks.Visible = userCanCheckOut    LineTasks.Visible = userCanCheckOut    PicCheckOut.Visible = userCanCheckOut    ActCheckOut.Visible = userCanCheckOut 

We also need to add similar security-related code to the TaskCheckOut method. Here are the first few lines of code from that method.

' ----- Update the display. AllPanelsInvisible() If (SecurityProfile(LibrarySecurity.CheckOutItems)) Then _    PanelCheckOut.Visible = True 

Replace these lines with the following code.

Insert Snippet

Insert Chapter 19, Snippet Item 9.

' ----- Check Out mode. Dim userCanCheckOut As Boolean ' ----- See if patrons can check out items by themselves. userCanCheckOut = CBool(Val(GetSystemValue("PatronCheckOut"))) ' ----- Update the display. AllPanelsInvisible() If (userCanCheckOut Or _    SecurityProfile(LibrarySecurity.CheckOutItems)) Then _    PanelCheckOut.Visible = True 

The actual check-out of items occurs on the main form itself. First, a patron is identified, and then the items to check out get processed. Let's add a class-level variable to MainForm that keeps track of the patron. And as long as we're adding definitions, we'll also add two constants that refer to images stored in the MainForm.StatusImages control. These constants will be used in some check-in-related code added a little later. Add the following code to the start of the class definition.

Insert Snippet

Insert Chapter 19, Snippet Item 10.

Private ActiveCheckOutPatron As Integer = -1 Private Const StatusImageBad As Integer = 0 Private Const StatusImageGood As Integer = 1 

When the user identifies the patron to use for check-out, and then starts checking items out, the last step is a click of the Finish button, indicating the end of the check-out process for that patron. (Skip ahead to Figure 19-11 if you want to see the Finish button now.) However, there is nothing to stop the user from jumping to another part of the program, or from exiting the program completely, without first pushing the Finish button. We must anticipate this rude behavior so typical of software users. To ensure that check-out completes properly, we will add some code to three places in MainForm that should catch any such discourteous actions by the user. Add the following code to the start of these three methods: (1) the MainForm_FormClosing event handler; (2) the ShowLoginForm method; and (3) the AllPanelsInvisible method.

Figure 19-11. The check-out panel on the main form

Insert Snippet

Insert Chapter 19, Snippet Item 11 three times.

' ----- Finish the in-process check-out if needed. If (ActiveCheckOutPatron <> -1) Then _    ActFinishCheckOut.PerformClick() 

Checking Out Items

All of the check-out code (except for the code in the CheckLookup.vb form) appears in the main form's class. Check-out is one of the eight main display panels accessed through this form (see Figure 19-11).

Here's the process for checking out items from the check-out panel.


The user clicks the Patron button and identifies the patron who will check out items.


The user enters the title or barcode for each item to check out, and clicks the Check Out button for each one.


The user clicks the Finish button when check-out is complete.

Let's add the code for each of these three buttons. First, add code to the ActCheckOutPatron_Click event handler.

Insert Snippet

Insert Chapter 19, Snippet Item 12.

This code prompts the user for patron selection, and displays the remaining fields if successful. Here's the part of the code that does the prompting.

' ----- Get the ID of the patron. patronID = (New PatronAccess).SelectPatron() If (patronID = -1) Then Return ' ----- Get the patron name. sqlText = "SELECT FirstName + ' ' + LastName FROM Patron " & _    "WHERE ID = " & patronID patronName = CStr(ExecuteSQLReturn(sqlText)) ' ----- Is this patron active? sqlText = "SELECT Active FROM Patron WHERE ID = " & patronID If (CBool(ExecuteSQLReturn(sqlText)) = False) Then   MsgBox("Patron '" & patronName & _      "' is marked as inactive.", MsgBoxStyle.OkOnly Or _      MsgBoxStyle.Exclamation, ProgramTitle)   Return End If 

Add code to the ActDoCheckOut_Click event handler, which processes each item through the Check Out button.

Insert Snippet

Insert Chapter 19, Snippet Item 13.

As I mentioned before, this code differentiates between numeric entry (barcodes) and other entries (titles).

If (IsNumeric(Trim(CheckOutBarcode.Text))) Then    ' ----- Probably a barcode supplied. Get the related ID.    sqlText = "SELECT ID FROM ItemCopy WHERE Barcode = " & _       DBText(Trim(CheckOutBarcode.Text))    copyID = DBGetInteger(ExecuteSQLReturn(sqlText))    If (copyID = 0) Then       ' ----- Invalid barcode.       MsgBox("Barcode not found.", MsgBoxStyle.OkOnly Or _          MsgBoxStyle.Exclamation, ProgramTitle)       CheckOutBarcode.Focus()       CheckOutBarcode.SelectAll()       Return    End If Else    ' ----- Look up by title.    copyID = (New CheckLookup).CheckItemByTitle(False, _       Trim(CheckOutBarcode.Text))    If (copyID = -1) Then Return End If 

Eventually, after verifying that the item is available for patron use, the code checks out the item by updating the relevant records in the database.

TransactionBegin() ' ----- Update patron copy record. sqlText = "INSERT INTO PatronCopy (Patron, ItemCopy, " & _    "CheckOut, Renewal, DueDate, Returned, Missing, " & _    "Fine, Paid) VALUES (" & ActiveCheckOutPatron & ", " & _    copyID & ", " & DBDate(Today) & ", 0, " & _    DBDate(untilDate) & ", 0, 0, 0, 0)" ExecuteSQL(sqlText) ' ----- Update the patron record. sqlText = "UPDATE Patron SET LastActivity = GETDATE() " & _    "WHERE ID = " & ActiveCheckOutPatron ExecuteSQL(sqlText) TransactionCommit() 

The last of the three buttons is the Finish button. Add code to the ActFinishCheckOut_Click event handler.

Insert Snippet

Insert Chapter 19, Snippet Item 14.

This code simply resets the display fields in preparation for the next patron check-out.

The list box on the check-out panel needs to display two columns of data: (1) the due date; and (2) details of the item such as title and barcode. These values were added to the list using the CheckedOutItem class we added a little earlier in the chapter. Add code to the CheckedOutItems_DrawItem event handler.

Insert Snippet

Insert Chapter 19, Snippet Item 15.

Checking In Items

Checking in items is much simpler, because we don't need to first identify the patron. The barcode or title of the check-in item is sufficient to complete all processing. Figure 19-12 shows the check-in panel.

Figure 19-12. The check-in panel on the main form

This panel includes a date indicating when the item will be checked in. Normally, that's today, but if library items are turned in through a nighttime repository after business hours, the librarian might want to adjust the date to "Yesterday," just in case any of these items were turned in before midnight. Let's add some code so that the panel indicates "Today" or "Yesterday" or some other day when a date changes. Add the following code to the CheckedInDate_ValueChanged event handler.

Insert Snippet

Insert Chapter 19, Snippet Item 16.

' ----- Adjust the day in the display. Select Case DateDiff(DateInterval.Day, _       CheckInDate.Value, Today)    Case 0 ' ----- Today       CheckInDay.Text = "Today"       CheckInDay.BackColor = SystemColors.Control       CheckInDay.ForeColor = SystemColors.ControlText    Case 1 ' ----- Yesterday       CheckInDay.Text = "Yesterday"       CheckInDay.BackColor = Color.Red       CheckInDay.ForeColor = Color.White    Case Else ' ----- X days ago       CheckInDay.Text = DateDiff(DateInterval.Day, _       CheckInDate.Value, Today) & " days ago"       CheckInDay.BackColor = Color.Red       CheckInDay.ForeColor = Color.White End Select 

The actual check-in occurs when the user enters a barcode or title in the text field, and clicks the Check In button. Add code to the ActDoCheckIn_Click event handler.

Insert Snippet

Insert Chapter 19, Snippet Item 17.

After doing some lookups and confirmation checks, the code checks in the item through database updates.

' ----- Do the check-in in a transaction. TransactionBegin() ' ----- Update patron copy record. sqlText = "UPDATE PatronCopy SET CheckIn = " & _    DBDate(CheckInDate.Value) & _    ", Returned = 1 WHERE ID = " & patronCopyID ExecuteSQL(sqlText) ' ----- Update the patron record. sqlText = "UPDATE Patron SET LastActivity = " & _    "GETDATE() WHERE ID = " & patronID ExecuteSQL(sqlText) TransactionCommit() 

That's it for the check-in and check-out procedures, and all ticket printing. It's pretty good code, but not yet perfect. What we haven't yet added is code to properly process fines on items before they are checked in, or as they are adjusted in other ways. We will postpone this logic until Chapter 21, "Licensing Your Application." Until then, let's look at another application printing feature: reports.

Start-to-Finish Visual Basic 2005. Learn Visual Basic 2005 as You Design and Develop a Complete Application
Start-to-Finish Visual Basic 2005: Learn Visual Basic 2005 as You Design and Develop a Complete Application
ISBN: 0321398009
EAN: 2147483647
Year: 2006
Pages: 247
Authors: Tim Patrick © 2008-2017.
If you may any questions please contact us: