Advanced Visual Basic (Visual Studio 2010) - Project 11

Project 11

There are two parts to this project (Parts A & B). In Part A you will create an application called My Picture Viewer that allows users to browse and display graphic files on their computer or network. In Part B, you will enhance the Part A project by creating and incorporating your own custom User Control.

Part A

Creating the My Picture Viewer application

The My Picture Viewer application will allow the user to browser their drives and folders for picture files.  A ToolStrip will let them choose the type of graphic files to be listed in a files listbox.  When they click on a graphic file it will be displayed in a picturebox on the right side of the form (see the illustration above).

Launch Microsoft Visual Studio 2010.  Drop down the File menu and select New Project...

Be sure the Windows Forms Application template in the New Project dialog is selected in the Templates pane on the right side, then type MyPictureViewer in the Name textbox (as shown below): 

Now click the OK button. 

Save the project by clicking on the Save All button on the toolbar.  This displays the Save Project dialog box, as shown:

Do not change the default Location path.  Be sure to uncheck the Create directory for solution option, as show above, before clicking on the Save button. 

This creates a new folder inside the My Documents\Visual Studio 2010\Projects\  folder named MyPictureViewer:

        My Documents\Visual Studio 2010\Projects\MyPictureViewer

Rename the Form file

With the form file (Form1.vb) selected in the Solution Explorer window (as shown above), the Properties window directly below it displays it's File properties.  Click on the File Name property and type frmMyPictureViewer.vb and press the enter key (don't forget to include the .vb extension, as shown in the illustration below):

Change the Name and Text properties of the Form

To display the properties of the form in the Properties window, click once on the blank Form, which should be displayed on the left side of the screen in the Design window.  Make sure the Name property—which is in parentheses (Name) at the top of the property list so that it's easy to find—is frmMyPictureViewer.  It should have been set to that automatically when we named the form file.  Then scroll the properties windows down—the properties are listed alphabetically—and change the Text property to My Picture Viewer as shown below:

Adding a ToolStrip control to the form

Find the ToolStrip control in the Menus & Toolbars section of the control toolbox and double-click it once to add a ToolStrip to the form:

The new ToolStrip appears in the component tray, like this:

Click on it once to select it, and take a look at how the new empty ToolStrip appears at the top of the form:

The new ToolStrip has one new item place-holder and appears empty the rest of the way across the top of the form.  The default location of a ToolStrip is along the top edge of a form, below the menu.  The default value of it's Anchor property is Top, Left.  The bitmaps for the ToolStripbuttons we are going to add to the ToolStrip are 43 pixels wide by 17 pixels high.  Set the following properties of ToolStrip1 before we start adding ToolStripbuttons:

ToolStrip1
Property Value
ImageScalingSize.Width 43
ImageScalingSize.Height 17

Now click on the ellipses button of the toolstrip's Items collection, as shown in the following illustration:

The Items Collection Editor dialog is displayed:

Adding ToolStripButtons to ToolStrip1

Click on the Select item and add to list below combobox to see all the items you can add to a ToolStrip:

Most ToolStrips contain only buttons.  But all those other item options (shown in the above illustration) are quite impressive.  We are going to be boring and just include buttons and a single separator on our ToolStrip control.  Pick the Button item and click the Add button.  The following illustration should match your screen.  Be sure to click the Alphabetical sort button at the top of the properties pane, as shown below to list the properties alphabetically, so that it matches the illustration below:

 Make sure that ToolStripButton1 is selected, like in the above illustration, so that you can see its properties in the properties pane.  Set the following properties:

ToolStripButton1
Property Value
Name tsbJPG
Text (blank)
ToolTipText List Jpeg Files

Use the following illustration and add 5 more Buttons and a Separator as shown:

Set the their properties as follows:

ToolStripButton2
Property Value
Name tsbBMP
Text (blank)
ToolTipText List Bitmap Files
ToolStripButton3
Property Value
Name tsbGIF
Text (blank)
ToolTipText List Gif Files
ToolStripButton4
Property Value
Name tsbICO
Text (blank)
ToolTipText List Icon Files
ToolStripButton5
Property Value
Name tsbAll
Text (blank)
ToolTipText List All Graphic Files
ToolStripSeparator1
Property Value
Name (default name)
ToolStripButton6
Property Value
Name tsbExit
Text (blank)
ToolTipText Exit My Picture Viewer

Click the OK button to close the Items Collection Editor dialog.  Notice how we didn't set the Image property of any of the ToolStripButtons.  We will take care of that in the next step.  Save the project before continuing. 

Adding an ImageList component to the form

An ImageList control is ideal for storing multiple images within a project.  Once the images are stored inside an ImageList control, you have easy access to them at run-time.  Accessing the images in an ImageList is fast and easy, as compared to reading the images from image files—bitmaps, jpegs, etc.—which is much slower.  You can populate an ImageList dynamically at run-time by filling it with images from image files, or you can populate it at design time; which is what we will do.

Double-click on the ImageList control in the Components section of the control toolbox (as shown below) to add an ImageList control to the Component Tray

It's default name is ImageList1, set these properties of the new ImageList control as follows:

ImageList

Property Value
Name imgListToolStrip
ImageSize.Width 43
ImageSize.Height 17

Adding Images to the imgListToolStrip control

With the imgListToolStrip imagelist control selected in the Component Tray so that it's properties are displayed in the Properties window, click on the ellipses button of it's Images collection property to open the Image Collection Editor, as shown below:

Click on the Add button.  Navigate to the c:\shared\Bitmaps folder and select the JPG-Button1.bmp bitmap file, as shown below, and click the Open button (Note: If you are doing this project off campus, you may download an archive containing these bitmap files by clicking this link):

With the addition of the JPG-Button1.bmp bitmap file, the Image Collection Editor dialog should now look like the following illustration:

Use the following table and add 11 more bitmap files to the images collection—that's 12 bitmap files altogether including the JPG-Button1.bmp bitmap file in position zero.  Be sure to place the bitmaps in the order shown. (Note: You can rearrange the order of the bitmaps by selecting them in the Members list and moving them up and down using the up/down arrow buttons at the upper right corner of the Members list):

Add these Images to imgListToolStrip

Position File Name
0 JPG-Button1.bmp
1 BMP-Button1.bmp
2 GIF-Button1.bmp
3 ICO-Button1.bmp
4 ALL-Button1.bmp
5 EXIT-Button1.bmp
6 JPG-Button2.bmp
7 BMP-Button2.bmp
8 GIF-Button2.bmp
9 ICO-Button2.bmp
10 ALL-Button2.bmp
11 EXIT-Button2.bmp

When you've added all the bitmaps, your Image Collection Editor dialog should look just like this:

Because our ToolStripButtons act like toggles, when the user clicks one we will simulate turning it on and off by changing the image displayed on it.  This list of images in imgListToolStrip represent the buttons in their off (black, top six images) and on (green, bottom 6 images) states.  Note: Once graphic files are added to an ImageList the individual source graphic files are no longer required, since they are actually embedded inside the ImageList control.

Creating Enumerated data types for the On and Off bitmaps

Enumerated data types are used extensively in Visual Studio to improve readability in code.  The Color.<color name> enumerated data type is a prime example.  Take a look at the following code example (don't type this code):

Me.BackColor = Color.Brown
txtBox1.
BackColor = Color.DarkBlue

The Color.Brown and Color.DarkBlue enumerated data types store the number values, in the proper format, that actually represent those colors.  Here's a version of the above code using the actual number values of those colors:

'Set the BackColor of the form to Brown
Me.BackColor = _
    System.
Drawing.ColorTranslator.FromOle(10824234)
'Set the BackColor of txtBox1 to DarkBlue
txtBox1.BackColor = _
    System.
Drawing.ColorTranslator.FromOle(139)

Which version of the code is easier to read?  Thanks to the Color enumerated data type, the first two lines of code are not only self-documenting but far easier to modify later if we want to change to different colors.  Enumerated data types exist for virtually every property/method.  Since all properties/methods require a limited range of values (i.e. BackColor is limited to a specific range of number values).  Here is another example.  The Show method of the MessageBox control returns one of 6 different number values depending upon which button the user clicked to close it, as shown in the following illustration:

The declaration of the intrinsic DialogResult enumerated data type probably looks like this (don't type this code):

Enum DialogResult
    None
= 0

    OK
= 1
    Cancel
= 2

    Abort
= 3
    Retry
= 4
    Ignore
= 5
    Yes
= 6
    No
= 7
End Enum

Each of those return values is really just a number, as shown, DialogResult.OK is 1:

Enumerated data types do two important things: They make code self-documenting.  And they limit a programmer's choices to a list of values that a property or method can accept or return.  This makes for better code, which is easier to maintain and is less error-prone.

We will now create our own enumerated data types to represent the index numbers for the On and Off versions of the bitmaps stored in imgListToolStrip for our ToolStripButtons

Go to the Code View of your frmMyPictureViewer form and type the following code in the Declarations section:

'Create an enumerated data type for the
'    On/Off buttons.  Each member represents
'    the index value of the corresponding On
'    and Off versions of the images stored
'    in the imgListToolStrip imagelist.
Enum TSButton
   
Jpeg_ON = 6
   
Jpeg_OFF = 0
   
Bitmap_ON = 7
   
Bitmap_OFF = 1
   
Gif_ON = 8
   
Gif_OFF = 2
   
Icon_ON = 9
   
Icon_OFF = 3
   
All_ON = 10
   
All_OFF = 4
   
Exit_ON = 11
   
Exit_OFF = 5
End Enum

Setting the Image property of the ToolStripButtons

With the declaration of the TSButton enumerated data type added to the Declarations section, add the following code to the frmMyPictureViewer_Load event procedure to set the starting images of the ToolStripButtons (Note: The Jpeg button will be on by default when the program starts.):

tsbJPG.Image = _
    imgListToolStrip.
Images(TSButton.Jpeg_ON)
tsbBMP.Image = _
    imgListToolStrip.
Images(TSButton.Bitmap_OFF)
tsbGIF.Image = _
    imgListToolStrip.
Images(TSButton.Gif_OFF)
tsbICO.Image = _
    imgListToolStrip.
Images(TSButton.Icon_OFF)
tsbAll.Image = _
    imgListToolStrip.
Images(TSButton.All_OFF)
tsbExit.Image = _
    imgListToolStrip.
Images(TSButton.Exit_OFF)

Without the TSButton enumerated data type we'd have to use the actual index numbers and the above code would look like this (don't type this code):

'Set the image of the JPG button to the On button bitmap
tsbJPG.Image = imgListToolStrip.Images(
6)
'Set the image of the BMP button to the Off button bitmap
tsbBMP.Image = imgListToolStrip.Images(1)
'Set the image of the GIF button to the Off button bitmap
tsbGIF.Image = imgListToolStrip.Images(2)
'Set the image of the ICO button to the Off button bitmap
tsbICO.Image = imgListToolStrip.Images(3)
'Set the image of the EXIT button to the Off button bitmap
tsbExit.Image = imgListToolStrip.Images(4)

I'd rather not need to memorize the numbers that represent the image indexes of the bitmaps in the imgListToolStrip imagelist control.   Using the TSButton enumerated data type not only makes the code self-documenting and easier to modify later, but you can be lazy and significantly reduce the number of comments you need to add to your code.  Important:  Enumerated data types are restricted to Integer values only.  That's right, all the thousands of intrinsic enumerated data types in Visual Studio are just numbers.

Testing the program so far

Save the project now.  Run the program.  Your ToolStrip should look like the following illustration:

Note how the Separator we inserted between the All and Exit buttons adds a small gap between them with a vertical indented line.

Adding a Drives combobox to the form

The user will use a Drives combobox to select the drive on the computer that they want to browse for picture files:

Add a GroupBox and a ComboBox to the form, as shown in the above illustration.  Use the following table to set these properties:

GroupBox
Property Value
Text Drives
ComboBox
Property Value
Name cboDrives

Populating the Drives combobox

We will populate the cboDrives combobox automatically when the program starts.  So we need to add some more code to the frmMyPictureViewer_Load event procedure.  In project 8 we created a Drives combobox to test the pDisk class of the Computer component we created.  In fact, the code we need to populate this Drives combobox is identical to the code we used in project 8.  So the following code should look familiar.  Add this code to the frmMyPictureViewer_Load event procedure:

'Copy the contents of the string array returned
'    by the GetLogicalDrives method into an
'    sDrives string array.
Dim sDrives As String() =  _
        System.
Environment.GetLogicalDrives()
'Clear the contents of the combobox before filling it
cboDrives.Items.Clear()
'Create an sDrive variable to grab each element
'    of the array with a For-Each loop, and add
'    it to the combobox.
Dim sDrive As String
For Each
sDrive In sDrives
    cboDrives.Items.Add(sDrive)
Next

Let's make drive C: the default drive.  To do this we need to set the SelectedIndex property of cboDrives to the index value of the C:\ entry in the combobox.  We cannot assume that C:\ will always be the second entry in the combobox (after A:\), so we need to examine all the drive entries one-by-one, searching for C:\.  Add the following code—after the code above—to the frmMyPictureViewer_Load event procedure, to make drive C:\—or at least the first fixed disk—the default drive in the cboDrives combobox:

'i is an index counter.  Remember that number
'    variables are automatically initialized to
'    zero when they are created.
Dim i As Integer
'Loop through the entries in the Items
'    collection of cboDrives.
For Each sDrive In cboDrives.Items
     'Test for the C:\ entry.
    If sDrive.ToString.ToUpper.Equals("C:\") Then    
          'Setting SelectedIndex raises the
          '    SelectedIndexChanged event procedure. 
          '    Which is where we will add the code to
          '    populate the TreeView control with
          '    Folder Info.
        cboDrives.SelectedIndex = i
    End If
     'Increment the index counter.
    i  += 1
Next
'If C:\ is not found, set SelectedIndex to the
'    second item in the list (the second item is
'    unlikely to be a floppy drive).
If cboDrives.SelectedIndex =
-1 Then
    Try
          'If the attempt to assign 1 to SelectedIndex
          '    fails, display a MessageBox.
        cboDrives.SelectedIndex = 1
   
Catch
        MessageBox.
Show(
"No fixed disks found!",  _
            "Drive Error!", MessageBoxButtons.OK,  _
            MessageBoxIcon.Exclamation)
    End Try

End If

Testing the program so far

Save the project now.  Run the program.  Is drive C:\ set as the default entry in the cboDrives combobox?  Be sure to take a look at the other entries in the cboDrives combobox.  Are all the drives on the computer listed?

Using a TreeView control to create a DirListBox

In Visual Basic 6 there was a DirListBox control.  All you had to do was drop it on your form and set a couple of it's properties and you had a nicely displayed directory hierarchy of the current drive.  The old DirListBox control is gone, so in Visual Studio 2010 we need to create our own.  We will use a TreeView control for this purpose.  The following illustration shows how adding Nodes—that represent the folders on a drive—to a TreeView control can create a familiar looking folder hierarchy display:

Adding a TreeView control to the form

The images used to represent the Nodes of a TreeView come from an ImageList control.  We will add another ImageList control to the component tray that will be for the exclusive use of the TreeView control.  Add a new ImageList control to the component tray now.  Use the following table to set these properties:

ImageList
Property Value
Name imgListFolders
ImageSize.Width 16
ImageSize.Height 16

Adding 2 folder Images to the imgListFolders control

We need just two images for the Nodes of our TreeView control: One of a closed folder and one of an open folder.  With the imgListFolders imagelist control selected in the Component Tray, so that it's properties are displayed in the Properties window, click on the ellipses button of it's Images collection property.  This opens the Image Collection Editor dialog.  Use the table below and add the following bitmap files at the positions shown.  Use the Add button to add each image.  Look inside the c:\Shared\Bitmaps folder for the bitmap files (Note: If you are doing this project off campus, you may download an archive containing these bitmap files by clicking this link):

Add these Images to imgListFolders

Position File Name
0 closedfolder.bmp
1 openfolder.bmp

When you are finished, your Image Collection Editor dialog should look like this:

Click the OK button to close the dialog. 

Add a GroupBox and a TreeView control where show—in the illustration prior the one above.  Set their properties like this:

GroupBox
Property Value
Text Folders
TreeView (inside the GroupBox)
Property Value
Name tvFolders
ImageList imgListFolders
ImageIndex 0
SelectedImageIndex 1

The SelectedImageIndex property is set to the index of the opened folder image in imgListFolders.  The ImageIndex property is set to the index of the closed folder image in imgListFolders.  These two properties determine which image from imgListFolders will be used when a Node is selected and not selected respectively.

Populating the tvFolders TreeView control

We want to populate the tvFolders TreeView control with a hierarchy of all the folders (also called Directories) on the selected drive.  Each folder will be added as a new Node to the TreeView.  To create a true hierarchy, we will also add Nodes inside other Nodes when folders are found inside other folders (called Sub-Folders or Sub-Directories).  Let's begin by adding the following code to the cboDrives_SelectedIndexChanged event procedure:

'If this drive contains hundreds of folders, this
'    may take some time, so make the mouse
'    pointer an hourglass.
Windows.Forms.Cursor.Current = _
       
Cursors.WaitCursor
'Clear the TreeView control before populating it
'    with nodes of the drive's folder hierarchy.
tvFolders.Nodes.Clear()
'Add the selected drive (i.e. C:\) as the first
'    (root) Node.
tvFolders.Nodes.Add(cboDrives.Text)
'Call the AddDirectories procedure to populate
'    the TreeView control, passing it the Root
'    node of the hierarchy.
AddDirectories(tvFolders.Nodes(0))
'Restore the default mouse pointer.
Windows.Forms.Cursor.Current = _
        Cursors.
Default

The above code changes the mouse pointer to an hourglass during the process of populating tvFolders, because on a large drive this may take awhile.  We then clear any existing nodes from tvFolders and add the Root of the selected drive as the first Node.  After adding the above code, the AddDirectories procedure call should have a blue squiggly underline, since we have not created it yet.  Note: The cboDrives_SelectedIndexChanged event procedure will be raised once at program startup because we explicitly set the SelectedIndex property of the cboDrives combobox to the index value of the drive C:\ entry—in the frmMyPictureViewer_Load event procedure. 

Creating an AddDirectories procedure

Our call to the AddDirectories procedure above is passed a node from the tvFolders TreeView control.  So our procedure declaration for AddDirectories must include a TreeNode variable (called Node) as the argument that's passed to it.  Create the AddDirectories procedure by typing the following line of code on a blank line above the End Class statement at the bottom of the code window:

Private Sub AddDirectories(ByVal Node As TreeNode)

Press enter at the end of the line to automatically add the End Sub statement that terminates it.  Now your cursor should be flashing between the AddDirectories declaration and the End Sub statement, which is where you need to be to add the code that follows. 

We will construct the node hierarchy for tvFolders by finding the folders inside Node, and adding each folder as a new node.  How do we list the folders on a drive?  This is were a DirectoryInfo object from the System.IO class library comes in handy.

Constructing DirectoryInfo objects to list folders on a drive

We begin by creating a DirectoryInfo object (named Dir), for the folder represented by Node.  Type this code inside the AddDirectories procedure:

'Construct a DirectoryInfo object of Node.FullPath
Dim Dir As New _
    System.
IO.DirectoryInfo(Node.FullPath)

The DirectoryInfo constructor requires a string argument that is the explicit path to a folder on the drive.  TreeNode objects have a FullPath method that returns a string containing the complete path to the location of the folder the Node represents.

Once we've created a DirectoryInfo object for Node—named Dir in this case—we have access to the many properties and methods that a DirectoryInfo object provides, some of which are shown in the following illustration:

We will use the GetDirectories method of the DirectoryInfo object (Dir) to get an array of DirectoryInfo objects for all the folders located at Node.FullPath.  The following line of code constructs an array (named Folders) of DirectoryInfo objects of all the directories at Node.FullPath.  Add this code to the AddDirectories procedure:

'Construct and reference a DirectoryInfo object
'    array of all the folders inside Node.FullPath.
Dim Folders As System.IO.DirectoryInfo() =  _
        Dir.
GetDirectories()

We can then use a For-Next loop, with the Length property—which is equal to the number of elements in the array—of the Folders DirectoryInfo array as the limit, to add these folders as new nodes inside Node.  Add the following code to the AddDirectories procedure:

Dim i As Integer
For
i = 0 to Folders.Length - 1
     'Add a new node to Node for each folder found.
    Node.Nodes.Add(Folders(i).Name)
Next
i

Here's an illustration of the code you've added to the AddDirectories procedure so far.  Make sure your code is the same:

Testing the program so far

Save the project now.  Run the program.  Your tvFolders TreeView control should look like this:

Click on the little + box in front of the C:\ folder and it should expand like this (Your folders will be different):

The top C:\ folder in the illustration above is the root node.  We passed this node to the AddDirectories procedure which filled it with new nodes for each of the folders on the root (\) of drive C:.  But what about all the sub-folders that are inside the other folders?  They have not been included in the hierarchy or you would see + expand boxes in front of all the folders.  Nodes for those folders are not included until we add them as well. 

We need to call the AddDirectories procedure for each node in C:\ to add nodes for the sub-folders that they contain to tvFolders.  And then we need to call the AddDirectories procedure for those nodes as well to enumerate the sub-folders they contain, and so on and so on, continuing this process until we've included the lowest sub-folders in the hierarchy.

Using Recursion to enumerate ALL the folders on a drive

When a procedure calls itself, it is referred to as a Recursive Procedure.  This is usually a very bad thing to do.  The area in memory where the procedures of your programs are executed (called the Stack space) is limited in size and is quickly depleted when a procedure calls itself.  The result of an accidental recursive procedure call is usually the termination of your program with a Stack Overflow error message box like this:

In the case of our AddDirectories procedure, we will be creating a Limited Recursive Procedure because there is a limited depth to the hierarchy of folders on a drive.  So our AddDirectories procedure will only need to call itself until the maximum depth of the folder hierarchy is reached.  Which should be well before we run out of Stack space (keep your fingers crossed).  There are issues to watch out for when calling a procedure recursively.  Error trapping becomes more important than ever when you are recursively calling a procedure several hundred, or even thousand, times in a row.  One potential problem with our AddDirectories procedure can occur if we call the GetDirectories method on a folder that we don't have the security privileges to access—this can even happen when you are logged on as Administrator.  In the event that this happens, we want our program to just skip that folder and move on to the next one.  So we need to Try-Catch all the code in the AddDirectories procedure so that execution will continue normally after an access error occurs.  Add the following Try-Catch error trapping code to the AddDirectories procedure (large italicized font):

Try
     'Construct a DirectoryInfo object of Node.FullPath
    Dim Dir As New System.IO.DirectoryInfo(Node.FullPath)
     'Construct and reference a DirectoryInfo object array
     '    of all the folders inside Node.FullPath.
   
 Dim Folders As System.IO.DirectoryInfo() =  _
            Dir.
GetDirectories()
    Dim i As Integer
    For
i = 0 to Folders.Length - 1
           'Add a new node to Node for each folder found.
        Node.Nodes.Add(Folders(i).Name)
    Next
i
Catch
     'This error trap prevents a crash when
     '    attempting to access restricted folders.
End Try

Now insert the following line of code where shown (large italicized font) to make the AddDirectories procedure Recursive:

Try
     'Construct a DirectoryInfo object of Node.FullPath
    Dim Dir As New System.IO.DirectoryInfo(Node.FullPath)
     'Construct a DirectoryInfo object array of all the
     '    folders inside Node.FullPath.
   
 Dim Folders As New System.IO.DirectoryInfo() =  _
            Dir.
GetDirectories()
    Dim i As Integer
    For
i = 0 to Folders.Length - 1
            'Add a new node to Node for each folder found.
         Node.Nodes.Add(Folders(i).Name)
            'Recursively call AddDirectories to enumerate all the
            '    folders contained inside the newly added Node.
      AddDirectories(Node.Nodes(i))
    Next i
Catch
     'This error trap prevents a crash when
     '    attempting to access restricted folders.
End Try

All we had to do to make the AddDirectories procedure Recursive is add a call to itself and pass it the new node that was just added to the current NodeNote: The AddDirectories procedure is now Recursive because we are calling the AddDirectories procedure from inside itself.  Which means that before the original call to AddDirectories terminates it may call itself again and again, and again, and again—on a drive with a very deep folder hierarchy, there will be several instances of the AddDirectories procedure running in memory simultaneously!  Let's try to visualize how this will work on a drive that has the following very simple folder hierarchy that is five levels deep:

  1. We begin by passing the root node (\), as Node, to the first call to AddDirectories—from the cboDrives_SelectedIndexChanged event procedure—hereafter called AddDirectories(\).
  1. The Windows folder is discovered and added as a new node to Node(\).
  2. We recursively call AddDirectories and pass it the Windows node—execution of AddDirectories(\) is suspended while the call to AddDirectories(Windows) is being processed.
  1. Within AddDirectories(Windows) the System folder is discovered and added as a new node to Node(Windows).
  2. We recursively call AddDirectories and pass it the System node—execution of AddDirectories(Windows) is suspended while the call to AddDirectories(System) is being processed.
  1. Within AddDirectories(System) the Docs folder is discovered and added as a new node to Node(System).
  2. We recursively call AddDirectories and pass it the Docs node—execution of AddDirectories(System) is suspended while the call to AddDirectories(Docs) is being processed.
  1. Within AddDirectories(Docs) no folders are discovered, so the AddDirectories(Docs) procedure exits and execution of the AddDirectories(System) procedure resumes.
  1. Within AddDirectories(System) the Data folder is discovered and added as a new node to Node(System).
  2. We recursively call AddDirectories and pass it the Data node—execution of AddDirectories(System) is suspended while the call to AddDirectories(Data) is being processed.
  1. Within AddDirectories(Data) the Status folder is discovered and added as a new node to Node(Data).
  2. We recursively call AddDirectories and pass it the Status node—execution of AddDirectories(Data) is suspended while the call to AddDirectories(Status) is being processed.  Note: We are now at the deepest level in the folder hierarchy, so five is the maximum instances of the AddDirectories procedure that will be running simultaneously.
  1. Within AddDirectories(Status) no folders are discovered, so the AddDirectories(Status) procedure exits and execution of the AddDirectories(Data) procedure resumes.
  1. Within AddDirectories(Data) the Pending folder is discovered and added as a new node to Node(Data).
  2. We recursively call AddDirectories and pass it the Pending node—execution of AddDirectories(Data) is suspended while the call to AddDirectories(Pending) is being processed.  Note: Here we are again, at the deepest level in the folder hierarchy, with five instances of the AddDirectories procedure running simultaneously.
  1. Within AddDirectories(Pending) no folders are discovered, so the AddDirectories(Pending) procedure exits and execution of the AddDirectories(Data) procedure resumes.
  1. Within AddDirectories(Data) no other folders are discovered, so the AddDirectories(Data) procedure exits and execution of the AddDirectories(System) procedure resumes.
  1. Within AddDirectories(System) the Scripts folder is discovered and added as a new node to Node(System).
  2. We recursively call AddDirectories and pass it the Scripts node—execution of AddDirectories(System) is suspended while the call to AddDirectories(Scripts) is being processed.
  1. Within AddDirectories(Scripts) no folders are discovered, so the AddDirectories(Scripts) procedure exits and execution of the AddDirectories(System) procedure resumes.
  1. Within AddDirectories(System) no other folders are discovered, so the AddDirectories(System) procedure finally exits and execution of the AddDirectories(Windows) procedure resumes.
  1. Within AddDirectories(Windows) no other folders are discovered, so the AddDirectories(Windows) procedure exits and execution of the AddDirectories(\) procedure resumes.
  1. Within AddDirectories(\) the Temp folder is added as a new node to Node(\).
  2. We recursively call AddDirectories and pass it the Temp node—execution of AddDirectories(\) is suspended while the call to AddDirectories(Temp) is being processed.
  1. Within AddDirectories(Temp) no folders are discovered, so the AddDirectories(Temp) procedure exits and execution of the AddDirectories(\) procedure resumes.
  1. Within AddDirectories(\) no other folders are discovered, so the AddDirectories(\) procedure finally exits.

As you can see, by the time we reach step 12 above, there are five instances of the AddDirectories procedure running in memory. 

The following table compresses and graphically illustrates the above 26 steps.  The running instances of the AddDirectories procedure are in the green boxes (Italicized Bold Font), the suspended instances are in the red boxes (Normal Font):

Folder Being Processed Folder Folder Folder Folder Folder
\ \        
Windows \ Windows      
System \ Windows System    
Docs \ Windows System Docs  
System \ Windows System    
Data \ Windows System Data  
Pending \ Windows System Data Pending
Data \ Windows System Data  
Status \ Windows System Data Status
Data \ Windows System Data  
System \ Windows System    
Scripts \ Windows System Scripts  
System \ Windows System    
Windows \ Windows      
Temp \ Temp      
\ \        

 

 

 

 

 

 

 

 

 

 


The depth of the folder hierarchy determines the maximum number of instances that the recursively called AddDirectories procedure will be running simultaneously.  The maximum folder hierarchy depth is restricted to the sum of the number of characters in the folder's names, including the number of characters in the longest file name in the deepest folder in the hierarchy—which cannot exceed 255 total characters.  This puts a built-in restriction on the depth of a folder hierarchy with reasonable folder names (like in our above example).  But what about a folder hierarchy created by someone who is insane, like this:

    c:\a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z

The above folder hierarchy would require AddDirectories to recurse 26 times, with 26 instances running in memory at the same time—sounds like a stack overflow error could happen.  That could be a problem!  Fortunately, within Windows XP there is also sub-folder depth limit which prevents the above folder hierarchy from being created even though it complies with the 255 character length limit.  I believe that folder depth limit is about 8 folders deep, but I have never tested it myself.  Knowing that ahead of time would have made the above 26 step analysis unnecessary, since having up to a maximum of 8 instances of AddDirectories running at the same time would likely never cause a stack overflow.

Testing the program so far

Save the project now.  Run the program—it will take longer to load this time, because the entire folder hierarchy of drive c: is being enumerated when it starts.  Expand the root folder.  Your tvFolders TreeView control should look like this (your folders will be different):

Notice the + expand boxes in front of many of the sub-folders.  Click on them.  Does the folder hierarchy go deeper and deeper as you expand other folders?

Adding a Splash Screen to the project

With the addition of the recursive AddDirectories procedure and the folder enumeration of drive C: that occurs when we start the program now, there is a significant delay before the My Picture Viewer form is displayed whenever we launch the program.  A long enough delay, in fact, that on some computers with a huge drive c: the user might assume your program has crashed and frozen at launch.  This might be a good time to add a Splash Screen to this project.  So that when the user launches the program they see the Splash Screen right away and don't assume that something is wrong because of the long delay before they see the main form.

Drop down the Project menu and choose Add New Item... as shown below:

Use the following illustration and selected the Splash Screen template on the Add New Item dialog.   Change the name to SplashScreenMyPictureViewer.vb, as shown, and click the Add button:

Your solution explorer window should now include the new SplashScreenMyPictureViewer.vb form, as shown:

Drop down the Project menu and select the MyPictureViewer Properties item to display the project properties:

Set the Splash Screen item, as shown above, to SplashScreenMyPictureViewer.  Now save the project. 

Configuring the Splash Screen

Take a look at the design view of your splash screen:

Add a Label to the splash screen, like in the following illustration, to let the user know what is happening when they launch the program:

I leave it to you to customize the splash screen however you like.  Save and run the program now.  Is the splash screen displayed while the folders on drive c: is being enumerated?

Adding a File Listbox

Add a GroupBox and a ListBox to the form, as shown in the above illustration on the right.  Use the following table to set these properties:

GroupBox
Property Value
Text Files
ListBox
Property Value
Name lstFiles

Populating the Files listbox

The Files listbox needs to list the graphic files of the type—selected on the ToolStrip—that are contained inside the selected folder in the TreeView control.  In the above illustration, only JPG files are listed because the JPG button on ToolStrip1 is selected.

TreeView controls have a BeforeSelect event procedure.  They also have an AfterSelect event procedure, which we will not use.  The BeforeSelect event procedure is raised when the user selects a node in the TreeView BeforeSelect is raised before the graphic for the node is updated to the image represented by the SelectedImageIndex property—That would be the Open Folder image.  This is the ideal place to put the code to enumerate any graphic files the selected folder contains.

Enumerating the Files inside a Folder

In addition to the GetDirectories method, which returns an array of DirectoryInfo objects, a DirectoryInfo object also has a GetFiles method that returns an array of System.IO.FileInfo objects, as shown in the following illustration:

We can use this GetFiles method to enumerate the files inside a folder.  The code to achieve this goal is below.  Add the following code to the tvFolders_BeforeSelect event procedure:

'Construct a DirectoryInfo object of
'    the selected Node.
Dim Dir As New _
    System.
IO.DirectoryInfo(e.Node.FullPath)
'Construct a FileInfo object array of all the
'    files inside e.Node.FullPath that match
'    FilePattern.
Dim Files As System.IO.FileInfo() =  _
        Dir.
GetFiles(FilePattern)

Note: FilePattern should have a blue squiggly underline, because we haven't declared it yet.  Notice that the e parameter, that is passed to the tvFolders_BeforeSelect event procedure by the operating system, includes the Node that was selected.  So we can extract the FullPath to the folder the Node represents to create our DirectoryInfo and FileInfo objects.  Then we can enumerate the files located there. 

The GetFiles method, in the code above, takes a FilePattern parameter in this format: <filename>.<extension>.  For example *.jpg for JPG files, or *.bmp for BMP files.  The asterisk (*) is a wildcard character that represents any file name.  The specification of a file extension—which determines the type of file—limits the enumeration of files to that type only.  After executing the above code, which creates our Files array of FileInfo objects, we can add the names of the files that were found to the lstFiles listbox with a For-Each loop like this.  Add the following code to the tvFolders_BeforeSelect event procedure:

'Create a FileInfo object (File) for the
'    For-Each loop and clear the lstFiles
'    listbox before filling it.
Dim File As System.IO.FileInfo
lstFiles.
Items.Clear()
For Each
File In Files
     'Add the file name to the lstFiles listbox
    lstFiles.Items.Add(File.Name)
Next

Before this code will work, we need to create a FilePattern variable.  Add this dimension statement for a FilePattern string variable to the Declarations section:

Dim FilePattern As String = "*.jpg"

The value of FilePattern should be set when the user selects a graphic file type button on the ToolStrip—we'll work on that code in a moment.  In our dimension statement above, we've set the default value of FilePattern to *.jpg, because the JPG files button on the ToolStrip is selected by default when the program starts.

Testing the program so far

With FilePattern set to the default value *.jpg, we can test the program.  Save the project now.  Run the program.  Use the illustration above and select the c:\Shared\JPG Images folder on drive C: in the TreeView control—or any folder that you know contains some JPG files.  You should see the JPG files listed in the lstFiles listbox. 

Setting the FilePattern in the ToolStrip1_ItemClicked event procedure

When the user selects a different graphic file type button on the ToolStrip, we need to change the value of the FilePattern variable to  the pattern that matches that file type, i.e. *.bmp, *.ico, etc.  We are also changing the graphic on the selected button from black to green to indicate it is selected—recall that we added two sets of images to the imgListToolStrip imagelist control with identical button images that are black and green.  So where do we put the code to do all this?  The ItemClicked event procedure of the ToolStrip is raised when a user clicks on a ToolStrip button.  So let's do it there.  Add the following code to the ToolStrip1_ItemClicked event procedure:

'Turn Off the previously selected button.  Since we don't
'    know which button it was, turn off all the buttons.
tsbJPG.Image = _
    imgListToolStrip.
Images(TSButton.Jpeg_OFF)
tsbBMP.Image = _
    imgListToolStrip.
Images(TSButton.Bitmap_OFF)
tsbGIF.Image = _
    imgListToolStrip.
Images(TSButton.Gif_OFF)
tsbICO.Image = _
    imgListToolStrip.
Images(TSButton.Icon_OFF)
tsbAll.Image = _
    imgListToolStrip.
Images(TSButton.All_OFF)
tsbExit.Image = _
    imgListToolStrip.
Images(TSButton.Exit_OFF)

We can use the ClickedItem.Name member of e within the ItemClicked event procedure to determine which ToolStripButton was clicked.  Add the following Select Case structure to the ToolStrip1_ItemClicked event procedure, below the code above:

'Turn On the button the user selected and
'    set the FilePattern variable.
Select Case e.ClickedItem.Name
Case tsbJPG.Name
        'They clicked the Jpeg button
    tsbJPG.Image = _
        imgListToolStrip.
Images(TSButton.Jpeg_ON)
    FilePattern
= "*.jpg"
Case tsbBMP.Name   
 'They clicked the Bitmap button
    tsbBMP.Image = _
        imgListToolStrip.
Images(TSButton.Bitmap_ON)
    FilePattern
= "*.bmp"
Case tsbGIF.Name      
'They clicked the Gif button
    tsbGIF.Image = _
        imgListToolStrip.
Images(TSButton.Gif_ON)
    FilePattern
= "*.gif"
Case tsbICO.Name
        'They clicked the Icon button
    tsbICO.Image = _
        imgListToolStrip.
Images(TSButton.Icon_ON)
    FilePattern
= "*.ico"
Case tsbAll.Name   
     'They clicked the All button
    tsbAll.Image = _
        imgListToolStrip.
Images(TSButton.All_ON)
    FilePattern
= "*.*"
Case tsbExit.Name     
'They clicked the Exit button
    tsbExit.Image = _
        imgListToolStrip.
Images(TSButton.Exit_ON)

    Me.
Close()
End Select
'More code to go here soon

The code above is incomplete.  After assigning a new value to FilePattern we need to update the list of files in the lstFiles listbox to show those files that match the new file pattern.  A call to the tvFolders_BeforeSelect event procedure would do the job.  But when we call an event procedure where the e parameter is used in the code there, we must be sure to provide the correct value for e in our call.  The e parameter of the BeforeSelect event procedure of a TreeView control is a TreeViewCancelEventArgs data type.  So we need to construct our own.  Replace the  'More code to go here soon comment in the above code, with the following line of code that creates a new TreeViewCancelEventArgs object:

'Create an e parameter so we can manually
'    call the BeforeSelect event procedure of
'    the tvFolders TreeView control.
Dim eParam As New  _
       
TreeViewCancelEventArgs( _
           
tvFolders.SelectedNode,  False, _
            TreeViewAction.
Expand)

We need to provide three parameters to the TreeViewCancelEventArgs constructor declaration.  The first is a TreeView node (tvFolders.SelectedNode).  The second parameter (False) is a boolean that indicates whether the event is being canceled or not.  And the last parameter (TreeViewAction.Expand) is the TreeView action.  The important parameter is the first one, which is the node that represents the folder with the files we want listed in the Files listbox.   Now that we've constructed a TreeViewCancelEventArgs object (eParam), we can manually call the tvFolders_BeforeSelect event procedure.  Add this line of code below the line above:

'Call the BeforeSelect event procedure of tvFolders
tvFolders_BeforeSelect(sender, eParam)

For the sender parameter (which we are not using in the tvFolders_BeforeSelect event procedure), we are passing along the sender value of the ToolStrip1_ItemClicked event procedure.  Here is a summary of the code in the  ToolStrip1_ItemClicked event procedure.  Make sure your code matches this before going on:

'Turn Off the previously selected button.  Since we don't
'    know which button it was, turn off all the buttons.
tsbJPG.Image = _
    imgListToolStrip.
Images(TSButton.Jpeg_OFF)
tsbBMP.Image = _
    imgListToolStrip.
Images(TSButton.Bitmap_OFF)
tsbGIF.Image = _
    imgListToolStrip.
Images(TSButton.Gif_OFF)
tsbICO.Image = _
    imgListToolStrip.
Images(TSButton.Icon_OFF)
tsbAll.Image = _
    imgListToolStrip.
Images(TSButton.All_OFF)
tsbExit.Image = _
    imgListToolStrip.
Images(TSButton.Exit_OFF)

'Turn On the button the user selected and
'    set the FilePattern variable.
Select Case e.ClickedItem.Name
    Case tsbJPG.Name
          'They clicked the Jpeg button
        tsbJPG.
Image = _
            imgListToolStrip.
Images(TSButton.Jpeg_ON)
        FilePattern
= "*.jpg"
    Case tsbBMP.Name     
'They clicked the Bitmap button
        tsbBMP.Image = _
            imgListToolStrip.
Images(TSButton.Bitmap_ON)
        FilePattern
= "*.bmp"
    Case tsbGIF.Name      
 'They clicked the Gif button
        tsbGIF.Image = _
            imgListToolStrip.
Images(TSButton.Gif_ON)
        FilePattern
= "*.gif"
    Case tsbICO.Name
       'They clicked the Icon button
        tsbICO.Image = _
            imgListToolStrip.
Images(TSButton.Icon_ON)
        FilePattern
= "*.ico"
    Case tsbAll.Name   
      'They clicked the All button
        tsbAll.Image = _
            imgListToolStrip.
Images(TSButton.All_ON)
        FilePattern
= "*.*"
    Case tsbExit.Name     
 'They clicked the Exit button
        tsbExit.Image = _
            imgListToolStrip.
Images(TSButton.Exit_ON)
        Me.
Close()
End Select
'Create an e parameter so we can manually
'    call the BeforeSelect event procedure of
'    the tvFolders TreeView control.
Dim eParam As New  _
       
TreeViewCancelEventArgs( _
         
tvFolders.SelectedNode,  False, _
          TreeViewAction.
Expand)

'Call the BeforeSelect event procedure of tvFolders
tvFolders_BeforeSelect(sender, eParam)

Testing the program so far

Save the project now.  Run the program.  Navigate to the \Shared\Bitmaps folder on drive C:—or any folder that you know contains BMP, GIF, ICO, or JPG graphic files.  There are no JPG files at this location, so your Files listbox should be empty.  Now click on the BMP button on the ToolStrip.  You should see enough BMP files to completely fill the Files listbox.  This folder also contains an ICO (icon) file or two as well, but it should not be listed with the BMP button selected.  Click on the ICO button on the ToolStrip.  Are the ICO files listed now?  As a final test, click on the ALL button on the ToolStrip.  Now the BMP files and the ICO file should be listed in the File listbox—the files are sorted alphabetically. 

Adding a PictureBox to the form

Use the first illustration, at the beginning of this project, and add a PictureBox control on the right side of the form, where shown.  Set it's properties like this:

PictureBox
Property Value
Name PictureBox1
BorderStyle Fixed3D
SizeMode CenterImage

Displaying the selected Picture in the Files listbox

When the user selects a graphic file in the Files listbox—raising its SelectedIndexChanged event procedure—we want to display the image it contains in the PictureBox.  To display the picture in the selected file, we will use the FromFile method of the intrinsic Image object to set the Image property of the picturebox.  The Image.FromFile method requires a string parameter that is the full path and filename of the image file.  Let's create a string variable with that information.  Add the following code to the lstFiles_SelectedIndexChanged event procedure:

Dim sImageFile As String = _
       
tvFolders.SelectedNode.FullPath & _
        "
\" &  lstFiles.SelectedItem

If the Jeffrey.jpg file, located inside the c:\Shared\JPG Images folder, was selected in the lstFiles listbox—as shown in the first illustration at the beginning of the project—the above line of code would assign the following value to the newly dimensioned sImageFile string variable:

c:\Shared\JPG Images\Jeffrey.jpg

Now add the following line of code to the lstFiles_SelectedIndexChanged event procedure—below the previous line of code—to display the selected image in PictureBox1:

PictureBox1.Image = _
       
Image.FromFile(sImageFile)

Testing the program so far

Save the project now.  Run the program.  Navigate to the \Shared\JPG Images folder on drive C: and select the Jeffrey.jpg file.  Is the image of Jeffery displayed in the picturebox?   Try viewing other graphic files. 

Important: As a final test, click on the ALL button on the ToolStrip—which lists all files, even those that are not graphic files—and select a non-graphic file.  What happens?  The most likely scenario involves the following message box, which will appear as your program crashes:

Creating a ValidGraphicFile function

If the user has the ALL button selected on the ToolStrip—which displays even non-graphic files in the Files listbox—and they select a non-graphic file in the Files listbox, your program will crash hard.  The FromFile method of the intrinsic Image object gets really upset if you pass it a non-graphic file.  There are two ways we can prevent this problem: 

  1. List only JPG, BMP, GIF, and ICO files if the ALL button is selected.  This would require some modification of the code in the tvFolders_BeforeSelect event procedure. 
  2. Ignore non-graphic files if the user selects them in the Files listbox.  This would require some modification of the code in the lstFiles_SelectedIndexChanged event procedure. 

Either way, we need to devise a method for determining whether a file is a valid graphic file or not.

Let's create a ValidGraphicFile function that will return True or False if the file name we pass to it does or does not qualify as a valid graphic file.  We can then use this ValidGraphicFile function to implement either of the two options above.  Make a blank line above the End Class statement at the bottom of the code window and type the following function declaration and press enter:

Private Function ValidGraphicFile( _
        ByVal
sFileName As String) As Boolean

We can examine a file's extension to determine if it is a valid graphic file.  Add the following Select Case statement to the ValidGraphicFile function:

'Examine the file extension of sFileName to
'    determine if it is a valid graphic file or not.
Select Case sFileName.ToLower.Substring( _
                               
sFileName.Length - 3)
    Case
"jpg", "bmp", "gif", "ico"
        Return
True
    Case Else
        Return
False
End Select

The last three characters of sFileName is the file extension, i.e. Jeffrey.jpg.  In the code above, we are using the SubString method to extract a lowercase (ToLower) version of the file extension of sFileName so that we can compare it to the extensions of the four graphic file types our program supports.  If it matches one of those types, we return True.  If not, we return False.

Use the ValidGraphicFile function like this:

If ValidGraphicFile(<FileName>) Then

Now modify the code in either the tvFolders_BeforeSelect or lstFiles_SelectedIndexChanged event procedures to implement either of the two options I outlined above to prevent your program from crashing if the user tries to view a non-graphic file.

Testing the program so far

When you are finished, save the project and test it thoroughly before going on to Part B

Part B

Creating a Custom PictureBoxPlus Control

To enhance our MyPictureViewer program, we will create a new custom control.   We will call it PictureBoxPlus and it will appear as an icon in the Control Toolbox, like all other controls.  Not only will we be able to use it in the MyPictureViewer program, but it will be available to any new projects we create as well:

 

Close the MyPictureViewer project and create a Windows Control Library project

Select Close Project on the File drop-down menu to close the MyPictureViewer project.  Now drop-down the file menu again and choose New Project.

Select the Windows Forms Control Library template (Note: the illustration above is from Visual Studio 2010) and type PictureBoxPlus in the name textbox, as shown above.  Then click the OK button.  This creates a new Windows Control Library project with a single UserControl module file.  Select the UserControl.vb module file in the solution explorer window so that it's properties are displayed in the property window, as shown:

Change the name property to PictureBoxPlus.vb.  Don't forget to add the .vb extension:

Click on the Save All button on the standard toolbar to save the project:

Do not change the default Location path.  Be sure to uncheck the Create directory for solution option, as show above, before clicking on the Save button. 

This creates a new folder inside the My Documents\Visual Studio 2010\Projects\  folder named PictureBoxPlus:

        My Documents\Visual Studio 2010\Projects\PictureBoxPlus

Take a look at the new PictureBoxPlus user control in the design window:

The User Control form a Container for our custom control

The term Container Control refers to a control that can contain other controls. The Form is the main Container control for most Standard Windows applications.  Some other controls are also Container controls: TabControls, Panels, and GroupBoxes for example. No single control (except for the Form itself) can exist alone. Even if you wanted to create a simple program that uses only one control (i.e. a single PictureBox), you would still need to place that PictureBox control on to a Form. The same holds true for creating a custom User Control.  Unlike a standard Form, the form that comes with a new User Control has no border.  We will use this form as a foundation, kind of a blank canvas, and build a new custom control by adding intrinsic controls as building blocks—Note: Intrinsic controls are the pre-existing controls that come with Visual Basic 2005.  Intrinsic controls are also called Constituent controls once we add them to our custom user control.

Options for creating a custom User Control

There are three options for creating a custom control in Visual Studio 2010:

  1. Enhance a single intrinsic Control, . i.e. Take a Textbox control and add additional Properties, Methods, and Event Procedures to it. 
  2. Create a control comprised of multiple intrinsic controls. With the same options to add Properties, Methods, and Event procedures as in method 1.
  3. Create a control from scratch. Creating a control from scratch is much harder than methods 1 or 2 and usually not necessary.  If you look hard enough, it is likely you can find an intrinsic control to base your new control on, and just do step 1.

We are going to use method number 2 to create a custom PictureBoxPlus control built from 1 PictureBox and 2 Label controls. When a programmer displays an image in the PictureBoxPlus control they will also have the option to display the image file's name and size in the built-in labels above the image, as in the following illustration:

Adding Intrinsic controls to build a custom User Control

Use the illustration above—do not resize the form.  Add two Labels and one PictureBox control where shown.  Use the following table to set their properties:

Label
Property Value
Name lblName
AutoSize False
BackColor White
BorderStyle Fixed3D
Text Name:
TextAlign MiddleLeft
Label
Property Value
Name lblSize
AutoSize False
BackColor White
BorderStyle Fixed3D
Text Size:
TextAlign MiddleLeft
PictureBox
Property Value
Name PictureBox1
BorderStyle Fixed3D
SizeMode CenterImage

By default, the Size.Width and Size.Height properties of the User Control form are 150 by 150 pixels.  This is the minimum size that we want our PictureBoxPlus control to be when a programmer places and resizes it on a form at design time. Of course you want the programmer to be able to resize the control to their liking, however you’re not going to let them make it any smaller than this or it wouldn’t be useable. To that end, we need to add some code to the PictureBoxPlus_Resize event procedure, that will do two things:

  1. Prevent the PictureBoxPlus control from being resized smaller than 150 by 150 pixels.
  2. Stretch the Labels and PictureBox to fit when the programmer resizes the PictureBoxPlus control larger.

The programmer who uses your control will not have access to this Resize event procedure.  It is internal to our User Control, and available only to us; the User Control creator.  With PictureBoxPlus.vb selected in solution explorer window, click on the View Code button at the top of the solution explorer window (as shown below) to display the Code view of the User Control:

In the Declarations section at the top of the code window, add the following constant declarations:

'Constants for the Resize event procedure
Private Const MIN_HEIGHT = 150
Private Const
MIN_WIDTH
= 150
'2 Pixels (Also used for the TOP Margin)
Private Const LEFT_MARGIN = 2
'4 Pixels (Includes LEFT_MARGIN)
Private Const RIGHT_MARGIN = 4
'6 Pixels (Includes spacing between controls)
Private Const BOTTOM_MARGIN = 6

Drop down the Class list and choose (PictureBoxPlus Events).  Then select the Resize event procedure from the Methods drop down list to get to the PictureBoxPlus_Resize event procedure.  Now add the following code to the PictureBoxPlus_Resize event procedure:

'No shorter than MIN_HEIGHT (150)
If Me.Height < MIN_HEIGHT Then
   
Me.Height = MIN_HEIGHT

End If
'No narrower than MIN_WIDTH (150)
If Me.Width < MIN_WIDTH Then
   
Me.Width = MIN_WIDTH

End If
'Adjust the positions and sizes of the Labels to fit.
lblName.Left = LEFT_MARGIN
lblName.Top = LEFT_MARGIN
lblName.Width = _
   
Me.Width - RIGHT_MARGIN
lblSize.Left = LEFT_MARGIN
lblSize.Top = _
   
lblName.Height + LEFT_MARGIN
lblSize.Width = _
   
Me.Width - RIGHT_MARGIN

'Adjust the position and size of the PictureBox to fit.
PictureBox1.Left = LEFT_MARGIN
PictureBox1.Top = lblName.Height + _
       
lblSize.Height + RIGHT_MARGIN
PictureBox1.Width = Me.Width - _
       
RIGHT_MARGIN
PictureBox1.Height = Me.Height - _
       
lblName.Height - lblSize.Height - _
       
BOTTOM_MARGIN

Adding a Standard project to the Solution so we can test the PictureBoxPlus control

Before doing anything else, save the project.  Do not close the PictureBoxPlus project.  We will now create a solution by adding a Windows Application project so that we can test the PictureBoxPlus control as we continue to create it.

Pull down the File menu and under the Add fly-out menu, choose New Project:

Select the Windows Application template and name of the project TestPictureBoxPlus (as shown in the above illustration), and click the OK button.  Your solution explorer window should now look like this:

Do not change any of the properties of the form (Form1.vb) or the project itself.  Note: This TestPictureBoxPlus project is for testing purposes only, we will be deleting it when we are finished creating our PictureBoxPlus control.

Testing the PictureBoxPlus control so far

Before we can add a PictureBoxPlus control to Form1 of the TestPictureBoxPlus project, we need to Build the control in its current state.  Right-Click on the PictureBoxPlus project in the solution explorer window and select Build, as shown below:

If there were compile errors, fix them now and try again until you get a successful build before going on.

Double-click on Form1.vb file in the solution explorer window to open the design view of Form1.  Because the PictureBoxPlus control project is part of this solution it is automatically added to the control toolbox under a special PictureBoxPlus Components section at the top of the control toolbox, like this:

Click on the PictureBoxPlus control in the toolbox and draw one on Form1, as shown below.  It's default name will be PictureBoxPlus1:

Now grab the sizer-handle—the little white squares on the borders of the control when it is selected—in the lower right corner of the PictureBoxPlus1 control and stretch it larger.  Does the code we added to the PictureBoxPlus_Resize event procedure above work?  Are the Labels and PictureBox—the constituent controls of our PictureBoxPlus control—resized to fit?  Take a look at the Size property of the PictureBoxPlus1 control in the properties window, and note the numbers there.  Now try to stretch the PictureBoxPlus1 control smaller.  Can you make it smaller than 150, 150 in size? 

Take a look at the other properties of the PictureBoxPlus1 control.  What you're seeing are the inherited properties of the base UserControl class that our PictureBoxPlus control is built on.  Take a close look through the list or properties.  Do you see an Image property?  No.  How can our PictureBoxPlus control display a picture without an Image property?!

Adding an Image property to the PictureBoxPlus control

As a replacement for a common PictureBox control, PictureBoxPlus would be useless if there was no way to display a picture in it.   So the first property we will add to our PictureBoxPlus control is an Image property.  Recall from project 7 that the basic format of a property declaration looks like this:

Property <Name> As <Some Data Type>
     'The Get section returns the
     '    value of the property.
    Get
        Return <Some Value>
    End Get
     'The Set section applies a
     '    value to the property.
    Set(ByVal Value As <Some Data Type>)
       
<Something> = Value
    End Set
End Property  

But unlike the properties we created in project 7, which we made ReadOnly by not including the Set section, the Image property for our PictureBoxPlus control will have both the Get and Set sections.  We want the Image property of our PictureBoxPlus control to work exactly like the Image property of a normal PictureBox.  The programmer will be able to assign an image to the PictureBoxPlus control—via the Set section, and copy the image from it—via the Get section.  For example, they should be able to use code like this (don't type this code):

'Assign an Image to the PictureBoxPlus control
PictureBoxPlus1.Image =  _
        Image.
FromFile( _
        "c
:\shared\JPG Images\Jeffrey.jpg")
  

'Copy the Image from the PictureBoxPlus
'    control to another picturebox.
picMyPic.Image = PictureBoxPlus1.Image  

In the example code above, the first line will work if our Image property contains a Set section.  The second line above will work if our Image property contains a Get section.

Return to the code view window of your PictureBoxPlus control and make a blank line above the End Class statement.  Put your cursor at the beginning of the blank line and type the following line of code and press the enter key:

Public Property Image() As Image

After pressing enter, the following property declaration structure should automatically be created:

Public Property Image() As Image
    Get

    End Get
    Set(ByVal Value As Image
)

    End Set
End Property

Now all we need to do is fill in the blanks.  Between the Get and End Get statements type the following line of code:

'Return the image stored in the
'    Image property of PictureBox1.
Return PictureBox1.Image

Type the following line of code between the Set and End Set statements:

'Set the Image property of
'    PictureBox1 to Value.
PictureBox1.Image = Value

Your code for the finished Image property declaration should look like this:

Public Property Image() As Image
    Get
           'Return the image stored in the
           '    Image property of PictureBox1.
        Return PictureBox1.Image
    End Get
    Set(ByVal Value As Image)
           'Set the Image property of
           '    PictureBox1 to Value.
        PictureBox1.Image = Value
    End Set

End Property

Testing the PictureBoxPlus control so far

Before we can test the new Image property of PictureBoxPlus on Form1 of the TestPictureBoxPlus project, we need to Build the control again.  Save the project and right-click on the PictureBoxPlus project in the solution explorer window and select Build.  Now bring up the design view for Form1 of the TestPictureBoxPlus project.  The PictureBoxPlus1 control you added before should still be sitting in the center of the form.  Click once on the PictureBoxPlus1 control to select it so you can see it's properties in the properties window.  Does it have an Image property now?  This is the Image property that you just added!  Click on the ellipses button of the Image property of PictureBoxPlus1:

Select Local resource and click the Import button, as shown above.  Select and open the Jeffrey.jpg file located in the c:\Shared\JPG Files folder.  Then click the OK button of the Select Resource dialog to close it.  PictureBoxPlus1 should now look like this:

Adding a Description and Category to the Image property

When you click on a property in the properties window, a short description appears at the bottom of the window, as shown below:

Note: If you can't see this property description section at the bottom of your properties window, you may need to grab the double-line border near the bottom of the properties window and stretch it up to make the description section bigger.  Click on the new Image property of the PictureBoxPlus1 control in the properties window.  Since we have not created a description for it, the description section is blank.  Also, we need to specify a category for the Image property, so that a programmer who likes to sort their properties by Category will be able to find it. 

Return to the code view window of your PictureBoxPlus control and add the following Description and Category attributes to the beginning of the Image property declaration (type this on the blank line immediately above the Public Property Image line):

<System.ComponentModel.Description( _
    "Sets or Gets the Image displayed in the control"
),  _
  
System.ComponentModel.Category("Appearance")_

Your Image property declaration should look like his now:

Don't forget to include the space and underline character at the end of the last line above, because this code is being inserted as the first part of the Image property declaration.  The Description attribute lets us specify the descriptive string that will appear in the description section at the bottom of the properties window when the Image property is selected.  The Category attribute lets us specify the name of the category in the properties window that the Image property will appear in if the programmer sorts their properties window by category.

Testing the PictureBoxPlus control so far

Save the project and right-click on the PictureBoxPlus project in the solution explorer window and select Build.  Now bring up the design view for Form1 of the TestPictureBoxPlus project.  Be sure you're looking at the properties of the PictureBoxPlus1 control in the properties window and click on the Image property.  Does the description we added appear in the description section?  Change the display option of the properties window to Categorized view by clicking on the Categorized button at the top left of the properties windows.  Does the Image property appear under the Appearance section?

Getting the Name and Size labels to work

We want to display the name of the image file and its size in the Name and Size labels.  To do this, our PictureBoxPlus control needs to know two things:

  1. The name of the image file
  2. The Path to where it is located—so that it's size can be determined. 

Adding a FilePathName property to the PictureBoxPlus control

A simple way to get this information is through a new property that we will call, FilePathName.  When the programmer dynamically sets the Image property of a PictureBoxPlus control in code, they will also have the option to set the FilePathName property, like this (do not type this code):

'Assign an Image to the PictureBoxPlus control
PictureBoxPlus1.Image =  _
        Image.
FromFile( _
        "c
:\shared\JPG Images\Jeffrey.jpg")

'Assign the path and filename to the
'    FilePathName property.
PictureBoxPlus1.FilePathName =  _
   
"c:\shared\JPG Images\Jeffrey.jpg"

Instead of storing the value assigned to the FilePathName property in the property of one of our constituent controls—i. e. the Image property value is actually stored in the PictureBox1.Image property.  We will create private variable to store the value of FilePathName

Return to the code view window of your PictureBoxPlus control and add the following dimension statement to the Declarations section:

'Private variable to store the value
'    of the FilePathName property.
Private sFilePathName As String

This creates a private variable (visible inside the PictureBoxPlus control, but not visible to programmers using the control) to store the FilePathName property value. 

Now make a blank line above the End Class statement at the bottom of the code window.  Put your cursor at the beginning of the blank line and type the following line of code and press the enter key:

Public Property FilePathName() As String

After pressing enter, the following property declaration structure should automatically be created:

Public Property FilePathName() As String
   
Get

    End Get
    Set(ByVal Value As String
)

    End Set
End Property

Like before, all we need to do is fill in the blanks.  Between the Get and End Get statements type the following line of code:

'Return the value stored
'    in sFilePathName.
Return sFilePathName

Type the following code between the Set and End Set statements:

'Make sure the file specified in Value exists
If System.IO.File.Exists(Value) Then
    'Set sFilePathName to Value
    sFilePathName = Value

Now that we have the file name and path information—copied from the Value parameter into the private sFilePathName variable—we can set the text properties of the Name and Size labels.  Continue adding the following code after the line above inside the Set section:

    'Extract just the file name from sFilePathName
    '    and store it in a new sFileName variable.
   
Dim sFileName As String =  _
      
sFilePathName.Substring( _
       sFilePathName.
LastIndexOf("\") + 1)
    'Display the file name in the Name label
    lblName.Text = "Name:" & sFileName
    'To get the size of sFileName, create a
   
'    DirectoryInfo object (Dir) for the
    '    folder where sFileName is located.
   
Dim Dir As New  _
       System.IO.DirectoryInfo( _
           sFilePathName.
Substring(0, _
          
sFilePathName.LastIndexOf("\")))
    'Use the GetFiles method and set a reference
    '    to the FileInfo object for sFileName.
   Dim File As System.IO.FileInfo() = _
           
Dir.GetFiles(sFileName)
    'Display the file size in the Size label.  The
    '    Length  property of the FileInfo object
    '    is the file size in bytes.
   lblSize.Text = "Size:" &  _
        Format(
File(0).Length, _
        "#0.##"
) & " bytes"
Else
    'If no filename was specified, clear
    '    the labels and sFilePathName.
   lblName.Text = "Name:"
   lblSize.Text = "Size:"
  
sFilePathName = ""
End If

To help explain the code above, let's assume that sFilePathName contains this string:

        "c:\shared\JPG Images\Jeffrey.jpg"

To extract just the file name from sFilePathName we use it's SubString and LastIndexOf methods.  The LastIndexOf method returns the location in sFilePathName of the last occurrence of a search string ("\"), which in the example above would be the location of the last backslash before the file name Jeffrey.jpg.   By adding 1 to this number and passing it to the SubString method, we extract just the filename (i.e. c:\shared\JPG Images\Jeffrey.jpg) from the end of sFilePathName

To determine the size of the file, we created a DirectoryInfo object for the folder that contains sFileName, and then used the DirectoryInfo object's GetFiles method—with sFileName as the file pattern—to return a FileInfo object for the file.  The Length property of a FileInfo object is the size of sFileName in bytes.

Now add the following Description and Category attributes to the beginning of the FilePathName property declaration:

<System.ComponentModel.Description( _
    "Path and Filename of the displayed image file"
),  _
  
System.ComponentModel.Category("Misc")_

Testing the PictureBoxPlus control so far

Save the project and right-click on the PictureBoxPlus project in the solution explorer window and select Build.  Now bring up the design view for Form1 of the TestPictureBoxPlus project.  Be sure you're looking at the properties of the PictureBoxPlus1 control in the properties window and type:

    c:\shared\jpg images\Jeffrey.jpg

into the new FilePathName property—check your spacing and spelling—and press enter, as shown below:

Do the Name and Size labels of the PictureBoxPlus control show the correct information, as in the following illustration:

Adding more Properties to the PictureBoxPlus control

The properties of constituent controls, i.e. lblName and lblSize, cannot be modified directly by a programmer.  But we can expose select properties of constituent controls by creating custom properties—like we exposed the Image property of Picturebox1.  To allow a programmer using our PictureBoxPlus control to change the ForeColor and BackColor properties of the Name and Size labels, we will create four new properties:

  1. NameForeColor -- gives the programmer access to the ForeColor property of lblName.
  2. NameBackColor -- gives the programmer access to the BackColor property of lblName.
  3. SizeForeColor -- gives the programmer access to the ForeColor property of lblSize.
  4. SizeBackColor -- gives the programmer access to the BackColor property of lblSize.

Let's create the NameForeColor property together, then you can create the remaining three properties yourself.  We won't need to create private variables—like we did for the FilePathName property—to store the values of these properties because we are going to set the ForeColor and BackColor properties of lblName and lblSize directly.

  1. Make a blank line above the End Class statement at the bottom of the code window.  Put your cursor at the beginning of the blank line and type the following line of code and press the enter key:

Public Property NameForeColor() As _
        System.
Drawing.Color

  1. Between the Get and End Get statements type the following line of code:

'Return the ForeColor property
'    value of lblName.
Return lblName.ForeColor

  1. Between the Set and End Set statements type the following line of code:

'Set the ForeColor property
'    value of lblName.
lblName.ForeColor = Value

  1. Insert the following Description and Category attributes at the beginning of the NameForeColor property declaration:

<System.ComponentModel.Description( _
   
"Determines the ForeColor of the Name label"),  _
  
System.ComponentModel.Category("Appearance")_

Now follow the four steps above and add the NameBackColor, SizeForeColor, and SizeBackColor properties on your own.  When you are finished, save the project.  Build it, and see if these new properties work.  Note: Before you can test these new properties be sure to build PictureBoxPlus first.  Go to the TestPictureBoxPlus project,  and select the PictureBoxPlus1 control on Form1 so you can see it's properties in the properties window.  Then try setting the new NameForeColor, NameBackColor, SizeForeColor, and SizeBackColor properties. 

Adding a SizeMode property to the PictureBoxPlus control

The SizeMode property of a normal picturebox control allows the programmer to determine how images are displayed in the picturebox.  The SizeMode can be set to one of five PictureBoxSizeMode values, which are:

  1. Normal
  2. StretchImage
  3. AutoSize
  4. CenterImage
  5. Zoom

Our PictureBoxPlus control cannot completely replace a normal picturebox control unless we allow the programmer to change the SizeMode property of the constituent PictureBox1 control.  But like the Name and Size labels, the properties of PictureBox1 are not visible to the programmer.  So we will create our own SizeMode property to expose the SizeMode property of PictureBox1.  The declaration for the new SizeMode property will look like this:

Public Property SizeMode() As PictureBoxSizeMode

Try to create the SizeMode property on your own.  Hint: Begin by typing the above property declaration on a blank line above the End Class statement at the bottom of the code window.  The SizeMode property should let the programmer change the SizeMode property of the constituent PictureBox1 control.

Adding an Event Procedure to the PictureBoxPlus control

Adding event procedures to a custom User Control is easy.  We will now create an ImageChanged event procedure.  The ImageChanged event procedure will be raised whenever the programmer sets the Image property.  The ImageChanged event procedure will be the ideal place for the programmer to set the FilePathName property.  So that whenever the image changes the Name and Size labels are updated.

Every event procedure you add to a custom user control requires a Public Event declaration in the Declarations section.  Return to the code view window of your PictureBoxPlus control and add the following Public Event declaration to the Declarations section:

Public Event ImageChanged()

The single line of code above creates an ImageChanged event procedure.   Now all we need to do is add the code that will raise it.  Since we want the ImageChanged event procedure to be raised whenever the Image property is changed, add the following line of code as the last line of code in the Set section of the of the Image property (above the End Set line):

'Raise the ImageChanged event procedure
RaiseEvent ImageChanged()

Your code for the Image property declaration should now look like this (with the new line of code italicized in larger font):

Public Property Image() As Image
    Get
           'Return the image stored in the Image property of PictureBox1
        Return PictureBox1.Image
    End Get
    Set(ByVal Value As Image)
           'Set the Image property of PictureBox1 to Value
        PictureBox1.Image = Value
           'Raise the ImageChanged event procedure
       RaiseEvent ImageChanged()
    End Set
End Property

Save the PictureBoxPlus project and Build it.  Go to the code view window of Form1 in the TestPictureBoxPlus project.  Select the PictureBoxPlus1 control on the class drop-down list, then pull down the method list.  You should see your new ImageChanged event procedure, like in the following illustration:

Using the PictureBoxPlus control in the MyPictureViewer project

Drop down the File menu and choose Close Solution to close the PictureBoxPlus and TestPictureBoxPlus projects.  Drop down the File menu again and choose Open Project...  Find your MyPictureViewer project and open it.  

Adding the PictureBoxPlus control to the Control toolbox

Open the design view for your frmMyPictureViewer.vb file.   Take a look at the items in the Control Toolbox.  What happened to our PictureBoxPlus control?  The PictureBoxPlus control appeared in the toolbox automatically when the solution contained the PictureBoxPlus control project.  But we need to add the PictureBoxPlus control to the Components section of the Control Toolbox manually so that other projects can have access to it.  Right-click anywhere in the Components section and select the Choose Items... option on the context menu, as shown:

When the Choose Toolbox Items dialog appears, be sure the .NET Framework Components tab is selected.  This is a list of all of the .NET Components that are included with Visual Studio 2010.  Click on the Browse button (as show below) so that we can add our PictureBoxPlus.dll control library to the list:

Click on the My Projects icon on the left side of the Open dialog, as shown below:

Then navigate to the PictureBoxPlus\bin\Release folder, select the newly built PictureBoxPlus.dll file and click the Open button.

You should now see a new Computer entry in the .NET Framework Components list, as shown below:

With the new PictureBoxPlus item check marked, click the OK button to add your PictureBoxPlus control to the Components section in the Control Toolbox, as shown below:

Replacing the regular PictureBox control with a PictureBoxPlus control

Select the original PictureBox1 control on the MyPictureViewer form and delete it.  Click on the PictureBoxPlus control item in the control Toolbox and draw a PictureBoxPlus control in the same location as the original picturebox control.  The MyPictureViewer form should now look like this:

Find the places in the code of your MyPictureViewer project where you referred to the original PictureBox1 picturebox.  Change that code so that it uses the new PictureBoxPlus1 control instead.  Note: They are easy to spot, just look for the squiggly blue underlines.

Testing the MyPictureViewer project so far

Save the project.  Now run it.  Does the PictureBoxPlus control work like the PictureBox control did before?  Note: The Name and Size labels of PictureBoxPlus1 should remain blank because we haven't set the FilePathName property yet.

Using the FilePathName property to update the Name and Size labels of the PictureBoxPlus control

To get the Name and Size labels of the PictureBoxPlus control to work, we need to set it's FilePathName property.  Add the following line of code below the line that sets the Image property of the PictureBoxPlus1 in the lstFiles_SelectedIndexChanged event procedure:

'Display the Name and Size of the Image file
PictureBoxPlus1.FilePathName = sImageFile

The final version of the code in the lstFiles_SelectedIndexChanged event procedure should look like this  (with the new line of code italicized in larger font):

'Be sure the user selected a valid graphic file
If ValidGraphicFile(lstFiles.SelectedItem) Then
    Dim
sImageFile As String = _
       
tvFolders.SelectedNode.FullPath & "\" & _
       
lstFiles.SelectedItem
   
'Display the Image file
    PictureBoxPlus1.Image = Image.FromFile(sImageFile)
   
'Display the Name and Size of the Image file
   PictureBoxPlus1.FilePathName = sImageFile
Else
   
PictureBoxPlus1.Image = Nothing
End If

Testing the MyPictureViewer project

Save the project.  Now run it.  Test it.  Are the Name and Size labels of the PictureBoxPlus control displaying the name and size of the file?

Required Enhancements


To copy a Project folder from your Projects folder on the Hard Drive to a floppy diskette or pen-drive follow these steps:

  1. Exit Visual Studio 2010 and insert the floppy diskette or pen-drive, that you want to copy the MyPictureViewer and PictureBoxPlus folders to:
  2. Select the My Documents item on the Start Menu to open the My Documents folder.
  3. In the My Documents folder, double-click the Visual Studio 2010 folder to open it.
  4. Double-click on your Projects folder to open it.
  5. Right-click on the MyPictureViewer and PictureBoxPlus folders and selected: 31/2" Floppy A: or your pen-drive on the Send To fly-out menu.