The Library Project has used features of GDI+ since the moment the first form appeared in the newly created project, but it all came for free through code included in the Framework. Now it's time for you, the programmer, to add your own GDI+ contribution to the application. In this chapter's project code, we'll use GDI+ to enhance the normal display of a control through owner draw features. Plus, we'll finally begin to implement some of the barcode features I tempted you with in earlier chapters.
Load the "Chapter 17 (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 17 (After) Code" instead.
Install the Barcode Font
If you haven't yet obtained a barcode font, now is the time to do it. The features included in this chapter's project code will require you to use such a font. The publisher web site for this book (listed in Appendix A, "Installing the Software") includes suggested resources for obtaining a font at little or no cost for your personal use. You may also purchase a professional barcode font. Make sure the font you obtain is a TrueType font.
Using Owner Draw
In the previous chapter, we added the ItemLookup.vb form with its multiple views of library items. One of those views included the MatchingItems control, a multi-column list box displaying "Author/Name," "Call Number," and "Media Type" columns. Although we stored the column-specific data within each item already, we didn't actually display the individual columns to the user.
The thing about multi-column lists and other limited-space text displays is that some of the text is bound to overrun its "official" area if you let it. For instance, the text in one list column may overlap into another column of text. In such cases, it has become the tradition to chop off the extended content, and replace it with an ellipsis (" . . . "). So we'll need a routine that will determine if a string is too long for its display area, and perform the chopping and ellipsizing as needed. Add the FitTextToWidth method to the General.vb file's module code.
Insert Chapter 17, Snippet Item 1.
Public Function FitTextToWidth(ByVal origText As String, _ ByVal pixelWidth As Integer, _ ByVal canvas As System.Drawing.Graphics, _ ByVal useFont As System.Drawing.Font) As String ' ----- Given a text string, make sure it fits in the ' specified pixel width. Truncate and add an ' ellipsis if needed. Dim newText As String newText = origText If (canvas.MeasureString(newText, useFont).Width() > _ pixelWidth) Then Do While (canvas.MeasureString(newText & "...", _ useFont).Width() > pixelWidth) newText = Left(newText, newText.Length - 1) If (newText = "") Then Exit Do Loop If (newText <> "") Then newText &= "..." End If Return newText End Function
The ItemLookup.vb form has a web-browser-like Back button with a drop-down list of recent entries. The items added to this list may include long book titles and author names. Let's use the new FitTextToWidth method to limit the size of text items in this list. Open the source code for the ItemLookup form and locate the RefreshBackButtons method. About halfway through this routine is this line of code:
whichMenu.Text = scanHistory.HistoryDisplay
Replace this line with the following lines instead.
Insert Chapter 17, Snippet Item 2.
whichMenu.Text = FitTextToWidth(scanHistory.HistoryDisplay, _ Me.Width \ 2, useCanvas, whichMenu.Font)
That will limit any menu item text to half the width of the form, which seems reasonable to me. That useCanvas variable is new, so add a declaration for it at the top of the RefreshBackButtons method.
Insert Chapter 17, Snippet Item 3.
Dim useCanvas As Drawing.Graphics = Me.CreateGraphics()
Also, we need to properly dispose of that created graphics canvas at the very end of the method.
Insert Chapter 17, Snippet Item 4.
Now let's tackle owner draw list items. ListBox controls allow you to use your own custom drawing code for each visible item in the list. You have two options when you are managing the item drawing by yourself: You can keep every item a consistent height, or you can make each list item a different height based on the content for that item. In the MatchingItems list box, we'll use the same height for every list item.
To enable owner draw mode, open the ItemLookup form design editor, select the MatchingItems list box on the form or through the Properties panel, and change its DrawMode property to OwnerDrawFixed.
Each matching list item will include two rows of data: (1) the title of the matching item, in bold; and (2) the three columns of author, call number, and media type data. Add the following code to the form's Load event handler that determines the entire height of each list item, and the position of the second line within each item.
Insert Chapter 17, Snippet Item 5.
' ----- Prepare the form. Dim formGraphics As System.Drawing.Graphics ' ----- Set the default height of items in the matching ' items listbox. formGraphics = Me.CreateGraphics() MatchingItems.ItemHeight = CInt(formGraphics.MeasureString( _ "A" & vbCrLf & "g", MatchingItems.Font).Height()) + 3 SecondItemRow = CInt(formGraphics.MeasureString("Ag", _ MatchingItems.Font).Height()) + 1 formGraphics.Dispose()
I used the text "Ag" to make sure that the height included all of the font's ascenders and descenders (the parts that stick up and stick down from most letters). I think the calculation would include those values even if I used "mm" for the string, but better safe than sorry, I always say. Setting the MatchingItems.ItemHeight property here indicates the size of all items in the list. If we had decided to use variable-height items instead of fixed-height items, we would have handled the control's MeasureItem event. With fixed items, we can ignore that event, and move on to the event that does the actual drawing: DrawItem.
Here is what the code is going to do for each list item: (1) create the necessary brushes and font objects we will use in drawing; (2) draw the text strings on the list item canvas; and (3) clean up. Becasue list items can also be selected or unselected, we'll call some framework-supplied methods to draw the proper background and foreground elements that indicate item selections.
When we draw the multiple columns of text, it's possible that one column of text will be too long, and intrude into the next column area. This was why we wrote the FitTextToWidth function earlier. But it turns out that GDI+ already includes a feature that adds ellipses to text at just the right place when it doesn't fit. It's found in a class called StringFormat, in its Trimming property. Setting this property to EllipsisCharacter and using it when drawing the string will trim the string when appropriate. When we draw the string on the canvas, we will provide a rectangle that tells the string what its limits are. Here is the basic code used to draw one column of truncated text.
Dim ellipsesText As New StringFormat ellipsesText.Trimming = StringTrimming.EllipsisCharacter e.Graphics.DrawString("Some Long Text", e.Font, someBrush, _ New Rectangle(left, top, width, height), ellipsesText)
The code we'll use to draw each list item in the MatchingItems list will use code just like this. Let's add that code now to the MatchingItems.DrawItem event handler.
Insert Chapter 17, Snippet Item 6.
' ----- Draw the matching items on two lines. Dim itemToDraw As MatchingItemData Dim useBrush As System.Drawing.Brush Dim boldFont As System.Drawing.Font Dim ellipsesText As StringFormat ' ----- Draw the background of the item. If (CBool(CInt(e.State) And CInt(DrawItemState.Selected))) _ Then useBrush = SystemBrushes.HighlightText _ Else useBrush = SystemBrushes.WindowText e.DrawBackground() ' ----- The title will use a bold version of the main font. boldFont = New System.Drawing.Font(e.Font, FontStyle.Bold) ' ----- Obtain the item to draw. itemToDraw = CType(MatchingItems.Items(e.Index), _ MatchingItemData) ellipsesText = New StringFormat ellipsesText.Trimming = StringTrimming.EllipsisCharacter ' ----- Draw the text of the item. e.Graphics.DrawString(itemToDraw.Title, boldFont, useBrush, _ New Rectangle(0, e.Bounds.Top, _ ItemColEnd.Left - MatchingItems.Left, _ boldFont.Height), ellipsesText) e.Graphics.DrawString(itemToDraw.Author, e.Font, useBrush, _ New Rectangle(ItemColAuthor.Left, _ e.Bounds.Top + SecondItemRow, _ ItemColCall.Left - ItemColAuthor.Left - 8, _ e.Font.Height), ellipsesText) e.Graphics.DrawString(itemToDraw.CallNumber, e.Font, _ useBrush, New Rectangle(ItemColCall.Left, _ e.Bounds.Top + SecondItemRow, _ ItemColEnd.Left - ItemColType.Left, _ e.Font.Height), ellipsesText) e.Graphics.DrawString(itemToDraw.MediaType, e.Font, _ useBrush, New Rectangle(ItemColType.Left, _ e.Bounds.Top + SecondItemRow, _ ItemColType.Left - ItemColCall.Left - 8, _ e.Font.Height), ellipsesText) ' ----- If the ListBox has focus, draw a focus rectangle. e.DrawFocusRectangle() boldFont.Dispose()
See, it's amazingly easy to draw anything you want in a list box item. In this code, the actual output to the canvas via GDI+ amounted to just the four DrawString statements. Although this library database doesn't support it, we could have included an image of each item in the database, and displayed it in this list box, just to the left of the title. Also, the calls to e.DrawBackground and e.DrawFocusRectangle let the control deal with properly highlighting the right item (although I did have to choose the proper text brush). Figure 17-15 shows the results of our hard labor.
Figure 17-15. A sample book with two lines and three columns
The Library Project includes generic support for barcode labels. I visited a few libraries in my area and compared the barcodes added to both their library items (like books) and their patron ID cards. What I found is that the variety was too great to shoehorn into a single predefined solution. Therefore, the Library application allows an administrator or librarian to design sheets of barcode labels to meet their specific needs. (There are businesses that sell preprinted barcode labels and cards to libraries that don't want to print their own. The application also supports this method, because barcode generation and barcode assignment to items are two distinct steps.)
To support generic barcode design, we will add a set of design classes and two forms to the application.
In a future chapter, we'll add label printing, where labels and pages are joined together in one glorious print job.
Because these three files together include around 2,000 lines of source code, I will show you only key sections of each one. I've already added all three files to your project code, so let's start with BarcodeItemClass.vb. It defines each type of display item that the user will add to a label template in the BarcodeLabel.vb form. Here's the code for the abstract base class, BarcodeItemGeneric.
Imports System.ComponentModel Public MustInherit Class BarcodeItemGeneric <Browsable(False)> Public MustOverride ReadOnly _ Property ItemType() As String Public MustOverride Overrides _ Function ToString() As String End Class
Not much going on here. The class defines two required members: a read-only String property named ItemType, and a requirement that derived classes provide their own implementation for ToString. The other five derived classes in this file enhance the base class to support the distinct types of display elements included on a barcode label. Let's look briefly at one of the classes, BarcodeItemRect. It allows an optionally filled rectangle to appear on a barcode label, and includes private members that track the details of the rectangle.
Public Class BarcodeItemRect ' ----- Includes a basic rectangle element in a ' barcode label. Inherits BarcodeItemGeneric ' ----- Private store of attributes. Private StoredRectLeft As Single Private StoredRectTop As Single Private StoredRectWidth As Single Private StoredRectHeight As Single Private StoredRectColor As Drawing.Color Private StoredFillColor As Drawing.Color Private StoredRectAngle As Short
The rest of the class includes properties that provide the public interface to these private members. Here's the code for the public FillColor property.
<Browsable(True), DescriptionAttribute( _ "Sets the fill color of the rectangle.")> _ Public Property FillColor() As Drawing.Color ' ----- The fill color. Get Return StoredFillColor End Get Set(ByVal Value As Drawing.Color) StoredFillColor = Value End Set End Property
Like most of the other properties, it just sets and retrieves the related private value. Its declaration includes two attributes that will be read by the PropertyGrid control later on. The Browsable property says, "Yes, include this property in the grid," while DescriptionAttribute sets the text that appears in the bottom help area of the PropertyGrid control.
When you've used the Property panel to edit your forms, you've been able to set colors for a color property using a special color selection tool built into the property. Just having a property defined using System.Drawing.Color is enough to enable this same functionality for your own class. How does it work? Just as the FillColor property has attributes recognized by the PropertyGrid control, the System.Drawing.Color class also has such properties, one of which defines a custom property editor class for colors. Its implementation is beyond the scope of this book, but it's cool anyway. If you're interested in doing this for your own classes, you can read an article I wrote about property grid editors a few years ago.
Before we get to the editor forms, I need to let you know about four supporting functions I already added to the General.vb module file.
The BarcodePage form lets the user define a full sheet of labelsnot the labels themselves, but the positions of multiple labels on the same printed page. Figure 17-16 shows the fields on the form with some sample data.
Figure 17-16. The BarcodePage form
Collectively, the fields on the form describe the size of the page and the size of each label that appears on the page. As the user enters in the values, the Page Preview area instantly refreshes with a preview of what the page will look like.
As a code editor derived from BaseCodeForm, you're already familiar with some of the logic in the form; it manages the data found in a single record from the BarcodeSheet table. What's different is the GDI+ code found in the PreviewArea.Paint event handler. Its first main block of code tries to determine how you scale down an 8.5x11 piece of paper to make it appear in a small rectangle that is only 216x272 pixels in size. It's a lot of gory calculations that, when complete, determine the big-to-small-paper ratio, and lead to the drawing of the on-screen piece of paper with a border and a drop shadow.
e.Graphics.FillRectangle(SystemBrushes.ControlDark, _ pageLeft + 1, pageTop + 1, pageWidth + 2, pageHeight + 2) e.Graphics.FillRectangle(SystemBrushes.ControlDark, _ pageLeft + 2, pageTop + 2, pageWidth + 2, pageHeight + 2) e.Graphics.FillRectangle(Brushes.Black, pageLeft - 1, _ pageTop - 1, pageWidth + 2, pageHeight + 2) e.Graphics.FillRectangle(Brushes.White, pageLeft, _ pageTop, pageWidth, pageHeight)
Then, before drawing the preview outlines of each rectangular label, it repositions the grid origin to the upper-left corner of the on-screen piece of paper, and transforms the world scale based on the ratio of a real-world piece of paper and the on-screen image of it.
e.Graphics.TranslateTransform(pageLeft, pageTop) e.Graphics.ScaleTransform(useRatio, useRatio)
There are a few more calculations for the size of each label, followed by a double loop (for both rows and columns of labels) that does the actual printing of the label boundaries (detail calculations omitted for brevity).
For rowScan = 1 To CInt(BCRows.Text) For colScan = 1 To CInt(BCColumns.Text) leftOffset = ... topOffset = ... e.Graphics.DrawRectangle(Pens.Cyan, _ leftOffset, topOffset, _ oneWidthTwips, oneHeightTwips) Next colScan Next rowScan
The BarcodeLabel form is clearly the more interesting and complex of the two barcode editing forms. While the BarcodePage form defines an entire sheet of labels with nothing inside of each label, BarcodeLabel defines what goes inside of each of those labels. Figure 17-17 shows this form with a sample label.
Figure 17-17. The BarcodeLabel form
The BarcodeLabel form does derive from BaseCodeForm, so much of its code deals with the loading and saving of records from the BarcodeLabel and BarcodeLabelItem database tables. Each barcode label is tied to a specific barcode page template (which we just defined through the BarcodePage form), and stores its primary record in the BarcodeLabel table. This table defines the basics of the label, such as its name and measurement system. The text and shape items placed on that label are stored as records in the related BarcodeLabelItem table.
The PrepareFormFields routine loads existing label records from the database, creating instances of classes from the new BarcodeItemClass.vb file, and adds them to the DisplayItems ListBox control. Here's the section of code that loads in a "barcode image" (the actual displayed barcode) from an entry in the BarcodeLabelItems table.
newBarcodeImage = New Library.BarcodeItemBarcodeImage newBarcodeImage.Alignment = CType(CInt(dbInfo!Alignment), _ System.Drawing.ContentAlignment) newBarcodeImage.BarcodeColor = _ System.Drawing.Color.FromArgb(CInt(dbInfo!Color1)) newBarcodeImage.BarcodeSize = CSng(dbInfo!FontSize) newBarcodeImage.Left = CSng(dbInfo!PosLeft) newBarcodeImage.Top = CSng(dbInfo!PosTop) newBarcodeImage.Width = CSng(dbInfo!PosWidth) newBarcodeImage.Height = CSng(dbInfo!PosHeight) newBarcodeImage.RotationAngle = CShort(dbInfo!Rotation) newBarcodeImage.PadDigits = _ CByte(DBGetInteger(dbInfo!PadDigits)) DisplayItems.Items.Add(newBarcodeImage)
The user can add new shapes, text elements, and barcodes to the label by clicking on one of the five "Add Items" buttons that appear just below the DisplayItems control. Each button adds a default record to the label, which can then be modified by the user. As each label element is selected from the DisplayItems, its properties appear in the ItemProperties control, an instance of a PropertyGrid control. Modification of a label element is a matter of changing its properties. Figure 17-18 shows a color property being changed.
Figure 17-18. Modifying a label element property
As with the BarcodePage form, the real fun in the BarcodeLabel form comes through the Paint event of the label preview control, PreviewArea. This 300+ line routine starts out drawing the blank surface of the label with a drop shadow. Then it processes each element in the DisplayItems list, one by one, transforming and drawing each element as its properties indicate. As it passes through the element list, the code applies transforms to the drawing area as needed. To keep things tidy for each element, the state of the surface is saved before changes are made, and restored once changes are complete.
For counter = 0 To DisplayItems.Items.Count - 1 ' ----- Save the current state of the graphics area. holdState = e.Graphics.Save() ...main drawing code goes here, then... ' ----- Restore the original transformed state of ' the graphics surface. e.Graphics.Restore(holdState) Next counter
Each element type's code performs the various size, position, and rotation transformations needed to properly display the element. Let's take a closer look at the code that displays static text elements (code that is also called to display barcode text). After scaling down the world view to the label surface preview area, any user-requested rotation is performed about the upper-left corner of the rectangle that holds the printed text.
e.Graphics.TranslateTransform(X1, Y1) e.Graphics.RotateTransform(textAngle)
Next, a gray dashed line is drawn around the text object to show its selected state.
pixelPen = New System.Drawing.Pen(Color.LightGray, _ 1 / e.Graphics.DpiX) pixelPen.DashStyle = Drawing2D.DashStyle.Dash e.Graphics.DrawRectangle(pixelPen, X1, Y1, X2, Y2) pixelPen.Dispose()
After setting some flags to properly align the text vertically and horizontally within its bounding box, the standard DrawString method thrusts the text onto the display.
e.Graphics.DrawString(textMessage, useFont, _ New System.Drawing.SolidBrush(textColor), _ New Drawing.RectangleF(X1, Y1, X2, Y2), textFormat)
We will somewhat duplicate the label drawing code included in the BarcodeLabel class when we print actual labels in a later chapter.
The only thing left to do is to link up these editors to the main form. Because I've had so much fun with these forms, I'll let you play for a while in the code. Open the code for MainForm, locate the event handler for the AdminLinkBarcodeLabel.LinkClicked event, and add the following code.
Insert Chapter 17, Snippet Item 7.
' ----- Let the user edit the list of barcode labels. If (SecurityProfile( _ LibrarySecurity.ManageBarcodeTemplates) = False) Then MsgBox(NotAuthorizedMessage, MsgBoxStyle.OkOnly Or _ MsgBoxStyle.Exclamation, ProgramTitle) Return End If ' ----- Edit the records. ListEditRecords.ManageRecords(New Library.BarcodeLabel) ListEditRecords = Nothing
Do the same for the AdminLinkBarcodePage.LinkClicked event handler. Its code is almost identical except for the class instance passed to ListEditRecords.
Insert Chapter 17, Snippet Item 8.
' ----- Let the user edit the list of barcode pages. If (SecurityProfile( _ LibrarySecurity.ManageBarcodeTemplates) = False) Then MsgBox(NotAuthorizedMessage, MsgBoxStyle.OkOnly Or _ MsgBoxStyle.Exclamation, ProgramTitle) Return End If ' ----- Edit the records. ListEditRecords.ManageRecords(New Library.BarcodePage) ListEditRecords = Nothing
Fun with Graphics
GDI+ isn't all about serious drawing stuff; you can also have some fun. Let's make a change to the AboutProgram.vb form so that it fades out when the user clicks its Close button. This involves altering the form's Opacity property to slowly increase the transparency of the form. From our code's point of view, there is no GDI+ involved. But it's still involved through the hidden code that responds to the Opacity property.
Open the source code for the AboutProgram.vb file, and add the following code to the end of the AboutProgram.Load event handler.
Insert Chapter 17, Snippet Item 9.
' ----- Prepare the form for later fade-out. Me.Opacity = 0.99
Although this statement isn't really necessary, I found that the form tended to blink a little on some systems when the opacity went from 100% (1.0) to anything else (99%, or 0.99, in this case). This blink was less noticeable when I made the transition during the load process.
In the event handler for the ActClose.Click event, include this code.
Insert Chapter 17, Snippet Item 10.
' ----- Fade the form out. Dim counter As Integer For counter = 90 To 10 Step -20 Me.Opacity = counter / 100 Me.Refresh() Threading.Thread.Sleep(50) Next counter Me.DialogResult = Windows.Forms.DialogResult.Cancel
This code slowly fades out the form over the course of 250 milliseconds, in five distinct steps. So that the form doesn't close abruptly before the cool fade-out, open the form designer, select the ActClose button, and change its DialogResult property to None.
Another thing we never did was to set the primary icon for the application. Although this isn't strictly GDI+, it does involve graphic display, which impacts the user's perception of the program's quality. I've included an icon named Book.ico in the project's file set. Open the project properties, select the Application tab, and use the Icon field to browse for the Book.ico file.
While testing out the icon, I noticed that the splash window appeared (with the default Visual Studio icon) in the Windows task bar. In fact, each opened form appeared in the task bar, right alongside the main form's entry. This is non-standard, and it's all due to the ShowInTaskbar property setting included in each form. I've taken the liberty of going through all the forms (except for MainForm) and setting this property to False. Most of the forms were already set properly, so I altered the dozen or so that were set improperly.
The Library application is really starting to bulk up with features. In fact, by the next chapter, we will have added more than 95 percent of its total code. I can see the excitement on your face. Go ahead, turn the page, and add to your coding enjoyment.