Advanced Visual Basic .NET - Project 11

The TreeView control, using Recursion, and Creating a custom User Control

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 toolbar will let them choose the type of graphic files to be listed.  When they select a file in file listbox, it will be displayed in a picturebox (see the illustration above).

Removing your old project and creating a new one

Run Visual Basic .NET and open your Solution (<your name>.sln).

Right-click on the DatabaseSnooper project in the Solution Explorer window and select Remove from the context menu. Do not save this project if prompted to do so.  Now Right-click on the SetupDatabaseSnooper project in the Solution Explorer window and select Remove from the context menu.  Your solution should now be empty.

Drop down the File menu and select New Project under the Add Project menu item.  When the Add New Project dialog appears, be sure that the Visual Basic Projects folder is open in the Project Types pane, and that the Windows Application template is selected in the Templates pane.  Type MyPictureViewer in the Name textbox.  Then click the OK button.  This creates a new folder inside the \Visual Studio Projects\<Your Name> folder named MyPictureViewer:

        ...My Documents\Visual Studio Projects\<Your Name>\MyPictureViewer.

Note: When class is over, be sure to follow the instructions at the end of this project that tell you how to copy your project to your floppy diskette so you can take it home with you.

Rename the Form file and change it's Name and Text properties

With the form file (Form1.vb) selected in the Solution Explorer window, so that it's File properties are displayed in the Properties window, change the File Name property to frmMyPictureViewer.vb (don't forget to include the .vb extension).

Now click on the form in the Designer window to display it's properties:

  • Change the Text property to My Picture Viewer
  • Change the Name property to frmMyPictureViewer

Setting the Startup Object

Right-click on the MyPictureViewer project in your Solution Explorer window, click on the Properties item at the bottom of the context-menu.  In the MyPictureViewer Property Pages dialog drop down the Startup object list and choose frmMyPictureViewer and click the OK button.

Because we changed the Solutions Configuration option to Release in Project 10,  it may have kept this setting.  Be sure to drop down the Solutions Configuration listbox, on the middle of the standard toolbar, and change it back to Debug so that debugging information is included when we compile our project.  Without the inclusion of debugging information, we cannot set break points, and code that generates critical errors will not be highlighted when the program crashes.

Adding a Toolbar control to the form

Find the Toolbar control in the control toolbox and double-click it once to add a Toolbar to the form:

The new Toolbar has no buttons and appears as a blank bar across the top of the form.  This is because the default value of it's Anchor property is Top, Left.  This is the normal location of toolbars so we will leave this unchanged.  It's default name is Toolbar1, we don't need to change that either.  Toolbar buttons usually contain pictures.  The pictures on the buttons of a toolbar must be stored inside an ImageList control.  Every toolbar is usually associated with it's own ImageList control.  So before we can start adding buttons to our Toolbar, we need to add an ImageList control to the form.

Adding an ImageList control 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 icon on the control toolbox (as shown below) to add an ImageList control to the Component Tray

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

ImageList

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

Adding Images to the imgListToolbar control

With imgListToolbar 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 property, in the properties window, as shown below:

This opens the Image Collection Editor dialog.  Use the table below and add the following bitmap files to imgListToolbar, at the positions shown.  Use the Add button to add each image.  Look inside the \Shared\Bitmaps folder on drive C: for the bitmap files (Note: In some labs, the \Shared\Bitmaps folder may be on drive D:  You may also download an archive containing these bitmap files by clicking this link):

Add these Images to imgListToolbar

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

If you place a bitmap in the wrong position, just select it in the list and use the up/down arrow buttons to move it up and down in the list.  The Separator.bmp file is inserted twice at positions 5 and 12.  This image is blank because it is just a placeholder for a separator (a small gap) that we will add to the toolbar between the image-type buttons and the exit button--see the first illustration.  When the user clicks on a button of our Toolbar, we will turn it on by changing it's image.  This list of images represent the buttons in their off (black) and on (green) states.  When you are finished, your Image Collection Editor dialog should look just like this:

Once these bitmaps are added to imgListToolbar, the individual files are no longer required, since the bitmaps are actually embedded inside the imgListToolbar control.

Adding Buttons to Toolbar1

With ToolBar1 selected at the top of the form so that it's properties are displayed in the Properties window, set it's ImageList property to imgListToolbar, as shown below.  Note: Any ImageList controls your project contains are listed in the drop down list:

Now click on the ellipses button of the Buttons property of Toolbar1, in the properties window, as shown below:

This opens the ToolBarButton Collection Editor dialog.  The buttons we are adding will be listed in the Member pane on the left side of the dialog.  The properties of a selected button in the Members pane will be displayed in Properties pane on the right side.

Let's add the first button together.  Follow these four steps:

  1. Click on the Add button.   A new button (ToolBarButton1) appears in the Members pane, as shown below:

  1. Change it's Name property to tbJpg.
  2. Use the drop down arrow of the button's ImageIndex property and select image number 7 (this JPG button will be on by default), as shown below:

Recall that we set the ImageList property of Toolbar1 to the imgListToolbar imagelist control, so it's images are listed here for us to choose from.

  1. Now set the Tag property to *.jpg.

Using the four steps above as a guide, add six more buttons to Toolbar1, and set their properties like this.  Note: You need to set the Style property of the tbSeparator button to Separator.  Leave the Style property of the other buttons at the default value of PushButton:

Add 6 more buttons to Toolbox1
ToolBarButton2
Property Value
Name tbBmp
ImageIndex 1
Tag *.bmp
ToolBarButton3
Property Value
Name tbGif
ImageIndex 2
Tag *.gif
ToolBarButton4
Property Value
Name tbIco
ImageIndex 3
Tag *.ico
ToolBarButton5
Property Value
Name tbAll
ImageIndex 4
Tag *.*
ToolBarButton6
Property Value
Name tbSeparator
Style Separator
ImageIndex 5
ToolBarButton7
Property Value
Name tbExit
ImageIndex 6
Tag exit

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

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

Note that button number 5 (tbSeparator--which is actually the 6th button because the first button is button 0), has it's Style property set to Separator.  This will add a small gap between the main toolbar buttons and the tbExit button.  Click the OK button.  Toolbar1 on your form should now look like this (note the gap between the All and Exit buttons):

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 their 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 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 were 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?  Use it's down-arrow and 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 listing of the current drive.  The old DirListBox control is gone, so in VB.NET 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 exclusive use of the TreeView control.  Use the illustration above and add a GroupBox and a TreeView control to the form.  Now also add a new ImageList control to the component tray.  Use the following table to set the properties of the GroupBox and ImageList as follows--Note: we will set the TreeView properties later:

GroupBox
Property Value
Text Folders
ImageList
Property Value
Name imgListFolders
ImageSize.Width 16
ImageSize.Height 16

Adding 2 folder Images to the imgListFolders control

As you can see in the illustration above, 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 property.  This opens the Image Collection Editor dialog.  Use the table below and add the following bitmap files to imgListFolders, at the positions shown.  Use the Add button to add each image.  Look inside the \Shared\Bitmaps folder on drive C: for the bitmap files (Note: In some labs, the \Shared\Bitmaps folder may be on drive D:):

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.  With the TreeView control selected on the form so that it's properties are displayed in the Properties window, use the following table and set it's properties like this:

TreeView
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 the imgListFolders will be used when a Node is selected and not selected respectively.

Populating the tvFolders TreeView control

When our program starts and we explicitly set the SelectedIndex property of the cboDrives combobox to the index value of the drive C:\ entry in the combobox--in the frmMyPictureViewer_Load event procedure--it has the side-effect of raising the SelectedIndexChanged event procedure of cboDrives.  And whenever the user selects a different drive in the cboDrives combobox, it also raises the SelectedIndexChanged event procedure of cboDrives.  So the cboDrives_SelectedIndexChanged event procedure is the best place to put the code that populates 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.
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 mousepointer.
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 tvFolders and add the Root of the selected drive as the first Node.  The AddDirectories procedure call should have a blue squiggly underline, since we have not created it yet.

Creating an AddDirectories procedure

Since our call to the AddDirectories procedure above is passed a node from the tvFolders TreeView control, 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 the drive

We begin by creating a DirectoryInfo object (named Dir), for the folder represented by Node.  Type this first line of 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 an 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 explicit 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 a summary of the code you've added to the AddDirectories procedure so far.  Make sure your code is the same:

'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

Testing the program so far

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

Click on the + expand box in front of the C:\ folder and it should expand like this (Your folders may 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 sudden 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 the AddDirectories procedure to make it 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 the AddDirectories procedure 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 may be many calls to AddDirectories 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: 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(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 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 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 depth of the folder hierarchy determines the maximum number of instances that AddDirectories 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 longest file name in the deepest folder in the hierarchy--which cannot exceed 255 characters. 

The following table compresses and graphically illustrates the above 26 steps.  The running instances of the AddDirectories procedure are in the green boxes (Italicized 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      
\ \        

 

 

 

 

 

 

 

 

 

 

 

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 the drive is being enumerated.  Expand the root folder.  Your tvFolders TreeView control should look like this (your folders may be different):

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

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 their 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 Toolbar--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 Toolbar1 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--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, looks like this.  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 it represents to create our DirectoryInfo object. 

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 toolbar--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 toolbar 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 \Shared\JPG Images folder on drive C: in the TreeView control--or any folder that you know contains JPG files.  You should see the same files in the Files listbox as those included in the above illustration. 

Setting the FilePattern in the Toolbar1_ButtonClick event procedure

When the user selects a different graphic file type button on the toolbar, 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 imgListToolbar imagelist control with identical button images that are black and green.  So where do we put the code to do all this?  The ButtonClick event procedure of the toolbar is raised when a user clicks on a toolbar button.  So let's do it there.  Add the following code to the Toolbar1_ButtonClick event procedure:

Dim i As Integer
'Turn Off the previously selected button.  Since we don't
'    know which button it was, turn off all the buttons.
For i = 0 To ToolBar1.Buttons.Count - 1
   
ToolBar1.Buttons.Item(i).ImageIndex = i
Next
i

By setting the the ImageIndex property of each of the buttons to the index values 0 through 6 we turn them all off.  Take a look at the first 7 images (0 through 6) in the imgListToolbar imagelist control, in the illustration below: 

Images 0 through 6 are the button graphics for the buttons when they are not selected, or off--the black buttons.  Now you can see why we needed to add a blank image as a placeholder for the separator in the toolbar, so that the index value of the Exit button would correspond to the position of it's image in imgListToolbar.  Using the illustration above, what number value do we need to add to the ImageIndex property of a button to display it's selected button graphic?  Take a look at the illustration above.  Button 0 is the black JPG button.  What do we need to add to 0 to specify the index value of the green JPG button?  By adding 7 to a button's ImageIndex property we can select it--display the green button.  Add the following line of code to the Toolbar1_ButtonClick event procedure, below the code above:

'Turn On the selected button by adding 7 to it's
'    ImageIndex value.
e.Button.ImageIndex += 7

The e parameter, that is passed to the Toolbar1_ButtonClick event procedure by the operating system, includes the Button on the toolbar that was selected.  When we created the buttons for Toolbar1 we set the Tag property of each button to the value that the FilePattern variable needs to be set to when that button is selected.  So we can do this to set the FilePattern variable (don't type this line yet):

FilePattern = e.Button.Tag

But don't forget about the Exit button!  Add the following code to the Toolbar1_ButtonClick event procedure:

'Make sure the Exit button was not selected
If e.Button.Tag <> "exit" Then
     'Use the file patterns that we added to the Tag
     '    properties of the buttons to set FilePattern.
    FilePattern = e.Button.Tag
     'Update the list of files in the lstFiles listbox to those
     '    that match the new FilePattern.
    
     'More code to go here soon

Else
     'The user selected the Exit button.  Quit the program.
    Me.Close()
End If

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 those files in the selected folder 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 object.  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 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 Toolbar1_ButtonClick event procedure.  Here is a summary of the code in the  Toolbar1_ButtonClick event procedure.  Make sure your code matches this before going on:

Dim i As Integer
'Turn Off the previously selected button.  Since we don't
'    know which button it was, turn off all the buttons.
For i = 0 To ToolBar1.Buttons.Count - 1
   
ToolBar1.Buttons.Item(i).ImageIndex = i
Next
i
'Turn On the selected button by adding 7 to the
'    ImageIndex value.
e.Button.ImageIndex += 7
'Exit if the Exit button was selected
If e.Button.Tag <> "exit" Then
     'Use the file patterns that we added to the Tag
     '    properties of the buttons to set FilePattern.
    FilePattern = e.Button.Tag
     '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)
     'Update the list of files in the lstFiles listbox to those
     '    that match the new FilePattern.
     'Call the BeforeSelect event procedure of tvFolders
    tvFolders_BeforeSelect(sender, eParam)
Else
     'The user selected the Exit button.  Quit the program.
    Me.Close()
End If

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 toolbar.  You should see enough BMP files to completely fill the Files listbox.  This folder also contains a single ICO (icon) file, but it should not be listed with the BMP button selected.  Click on the ICO button on the toolbar.  Is the ICO file listed now?  As a final test, click on the ALL button on the toolbar.  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 it's 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 set to 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 could assign the following value to the newly dimensioned sImageFile string variable:

c:\Shared\JPG Images\Jeffrey.jpg

Now add the following line of code--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 toolbar--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 toolbar--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

To determine if a file is a valid graphic file will can examine it's extension.  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:

 

Removing your MyPictureViewer project and creating a Windows Control Library project

Right-click on the MyPictureViewer project in the Solution Explorer window and select Remove from the context menu.  Click the OK button when warned that MyPictureViewer will be removed from the Solution.  Drop down the File menu and select New Project under the Add Project menu item.  When the Add New Project dialog appears, be sure that the Visual Basic Projects folder is open in the Project Types pane, and that the Windows Application template is selected in the Templates pane.  Type PictureBoxPlus in the Name textbox.  Then click the OK button. 

Converting a Standard project into a Windows Control Library project

Right-click on the PictureBoxPlus project in the solution explorer window and chose Add User Control from the Add fly-out menu, as shown below:

With the User Control template selected in the Templates pane of the Add New Item dialog, type PictureBoxPlus.vb in the Name textbox and click the Open button, as shown below:

Remove the Form1.vb form file from the project by right-clicking on it in the solution explorer window and choosing Delete from the context menu--do not save this file if prompted to do so.  When are are done, your Solution Explorer window should look like this:

Important: Because we are using the Standard edition of Visual Basic .NET, we did not have the option to create a new Windows Control Library project--the templates to do this are only included in the Professional edition.  But it is easy to convert a Standard project into a Windows Control Library project.  We have already taken the necessary first steps, by creating a standard project, removing the Form1.vb file from it, and adding a new User Control (PictureBoxPlus.vb) to it.  Now for the final step:

Exit VB.NET and save your project if prompted to do so. 

Modifying the Project file with Notepad

Important: Be sure that you have completely exited from VB.NET before doing this step!  Open the My Documents folder, then open the Visual Studio Projects folder.  Locate your solution folder (your name) and open it.  Inside you should find your PictureBoxPlus project folder.  Open it to see your PictureBoxPlus project files, as shown below:

Important: You must make sure that you can see the file extensions of your files.  By default, the file extensions of known file types are hidden.  To turn this option off--so that you can see all file extensions--do the following:

  1. Drop down the Tools menu (shown above) and select Folder Options.
  2. Click on the View tab of of the Folder Options dialog.
  3. Remove the checkmark from the Hide extensions of known file types item and click the OK button.

Your main project file should be named, PictureBoxPlus.vbproj.   Right-click on the PictureBoxPlus.vbproj file and select the Open With... item on the context menu (as shown above).  The Open With dialog should appear.   Important: Make sure that the Always use the selected program to open this kind of file item below the listbox is NOT check marked.  Uncheck it if it is checked.  Select the Notepad item on the list.  Now click the OK button.

This should remind you of what we did in project 7 when we converted a standard project into a code library project.  Once PictureBoxPlus.vbproj is loaded into Notepad, you can see that a project file is just a plain text file.  In order to change the project so that it produces a Windows Control Library file (a DLL file) instead of a stand-alone EXE file, modify the circled lines in the illustration below to this:

    OutputType = "Library"

And this:

    StartupObject = ""

Check your spelling!  The default value "WinExe" for the OutputType must be changed to "Library".  And the value for StartupObject must be changed to an empty string "".  Once your OutputType and StartupObject lines match the circled lines in the illustration above, choose Save on Notepad's File drop down menu to save the modified project file.  Now exit from Notepad.

Run Visual Basis .NET and open your Solution (<your name>.sln), which should still contain the PictureBoxPlus User Control project.

Double-click on the PictureBoxPlus.vb file in the solution explorer window to open the Design view.  The user control form should look like this:

Make sure the user control form is select like in the above illustration--so that it's properties are displayed in the properties window--and change it's Name property to PictureBoxPlus.

The User Control - 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 Visual Basic applications (Some other controls are also Container controls: Frames, Panels, and PictureBoxes 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, 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 .NET.  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 Basic .NET:

  1. Enhance a single intrinsic Control, . i.e. Take a Textbox control and add additional Properties, Methods, or 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.

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
Text Name:
TextAlign MIddleLeft
Label
Property Value
Name lblSize
Text Size:
TextAlign MIddleLeft
PictureBox
Property Value
Name (Default Name)
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. Resize 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.  We will now add a separate Windows Application project to the solution and use it to test the PictureBoxPlus control as we continue to create it.

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

Make the name of the project TestPictureBoxPlus (as shown in the above illustration), and click the OK button.  When you are finished, you solution explorer window should look like this:

Do now 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 My User Controls tab.  Note: Click on the My User Controls tab at the top of the control toolbox to see your PictureBoxPlus control.  If the My User Controls tab is not visible, be sure to go to Design View of Form1 and it should appear:

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 need to 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 Value>
     '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 Value>)
       
<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 our 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 and select an image file (i.e. Try the Jeffrey.jpg file located in the c:\Shared\JPG Files folder).  Does PictureBoxPlus display the image?

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 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 the 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")_

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 Categorize 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 goes to set the Image property of a PictureBoxPlus control, they will also need to set the FilePathName property, like this:

'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"

We won't be able to store the value assigned to the new FilePathName property in the property of one of our constituent controls--i. e. the Image property value is stored in the PictureBox1.Image property.  So we will need to 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 (not visible outside the project) 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 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 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.  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: To test the new properties go to the TestPictureBoxPlus project, make sure the PictureBoxPlus1 control is selected, 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 four PictureBoxSizeMode values, which are:

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

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 need to create our own SizeMode property.  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 is supposed to 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 finished Image property declaration should look like this (with the new line of code above italicized and large 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

Using the PictureBoxPlus control in the MyPictureViewer project

Save the project and right-click on the PictureBoxPlus project in the solution explorer window and select Build--this creates the final version of the PictureBoxPlus.dll control library file. 

Right-click on the TestPictureBoxPlus project in the Solution Explorer window and select Remove from the context menu. Do not save this project if prompted to do so.  Now Right-click on the PictureBoxPlus project in the Solution Explorer window and select Remove from the context menu.  Your solution should now be empty.

Drop down the File menu and select Existing Project under the Add Project menu item.  Find your MyPictureViewer project and add it to the solution.  

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 Windows Forms section of the 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 and the design view for the user control is opened.

To add the PictureBoxPlus control to the control Toolbox, right-click on the toolbox and select the Add/Remove Items... option from the context menu, as shown below:

Click on the .Net Framework Components tab of the Customize Toolbox dialog.  Then click the Browse button.  Navigate to the Bin folder inside your PictureBoxPlus project folder and select the PictureBoxPlus.dll control library file as shown below:

With the PictureBoxPlus.dll file selected, click the Open button to add the PictureBoxPlus control to the .NET Framework Components list, as shown below:

Click the OK button to add the PictureBoxPlus control to the control Toolbox (at the bottom of the Windows Forms section).

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 icon in the control Toolbox (at the bottom of the list), and draw PictureBoxPlus control in the same location as the original picturebox control.  The MyPictureViewer form should now look like this:

There are three or four 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 above italicized and 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 Solution on the Hard Drive to a floppy diskette, follow these steps:

  1. Exit Visual Basic .NET and insert the floppy diskette, that you want to copy the Project folder to, into drive A:
  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 Projects folder to open it.
  4. Double-click on your Solution folder to open it (it should have your name).
  5. Open the Project folder that you want to copy, by double-clicking on it.

Deleting the Obj and Bin folders from inside the Project folder before copying it.

  1. Inside the Project folder, delete the Obj and Bin folders--these folders are created automatically when you open a project.  You do not need to copy them, or their contents, to your floppy diskette.
  2. Hit the Backspace key once--or click the Back button on the toolbar.  This moves you from inside the Project folder to back inside your Solution folder.
  3. Right-click on the Project folder and selected: 3 1/2" Floppy A: on the Send To fly-out menu.  This copies the Project folder to your floppy diskette.