DevCity.NET -
A Simple Photo Browser
Chris Mills
Chris graduated from the Robert Gordon University, Aberdeen UK in 2002 with an MEng in Electronic and Computer Engineering. Chris now works for an engineering company based in Aberdeen and develops condition monitoring and data logging solutions (both hardware and software development), he also develops tools for data analysis, visualisation and data mining.  
by Chris Mills
Published on 4/14/2006

In this article my aim is to guide a relative Visual Basic .NET "newbie" through the process of creating a simple Photo Browser application.  We will gradually build up the application, examining a number of the techniques available to you in Visual Basic .NET including creating a custom thumbnail control and creating instances of it at run-time. 

This article is written using Visual Studio .NET 2003 and .NET framework Version 1.1.

Creating a Thumbnail Control

First we'll look at creating a thumbnail custom control for use in our application.  Of course this control will also be usable in any other application.

Why Create a Custom Control?

You may be wondering "why create a custom control?"  Well, whilst the range of controls available in the .NET framework is extensive, at present it does not include a thumbnail control.  Using one of the existing controls, like a PictureBox, would work but would also be be less than ideal - for our Photo Browser I would like to display both the thumbnail and the name of the file it represents.  We could use both a PictureBox and Label to do this, but it could make tasks like re-arranging the thumbnails unnecessarily complicated as we'll have two controls to move instead of one.  Furthermore a custom control can take on all the work involved in generating the thumbnail, displaying the filename and redrawing the thumbnail when resized.

Creating a User Control

Creating a custom control may seem daunting, but it is pretty straightforward if you use the built in UserControl as a base.  You create a user control in much the same way that you design a form; you can add any of controls in your toolbox onto your control, arrange them and set their properties. 

Start by adding a new user control called "Thumbnail" to a Windows Forms project (File->Add New Item… then select "User Control").  You will be presented with your new blank control - which looks a lot like a borderless form.  Start by setting the new controls size to 120x120, which will be the default size of our thumbnail control. 

Laying Out Our Control

We'll use the Dock property of control in the .NET framework to ensure that the controls we add always fill our thumbnail control and resize automatically.  First add a Panel to your control and set its Dock property to "Fill" and its border style to "Fixed3D".  Next add a label to the panel, set its name to "lblFilename", Dock property to "Bottom" and TextAlign property to "MiddleCenter".  Finally add a PictureBox to your panel, set its name to "picThumbnail", Dock property to "Fill" and SizeMode property to "CenterImage" (don't worry that you can't see the border of the PictureBox, this is intentional).  Your thumbnail control should now look something like this:

Drawing the Thumbnails

The DrawThumbnail function will generate and display the thumbnail image, so that when you use the control all you have to do is specify the filename of the picture to view.  First we'll declare a private variable to store the filename of the thumbnail.  Add the following declaration at class level:

   Private strFileName As String = ""

Now we'll create a function called DrawThumbnail which will generate and display a thumbnail from the file whose name is in the variable "strFileName".  Add the following function to your code:

    Private Sub DrawThumbNail()

        Dim dAspectRatio As Double
        Dim bm As Bitmap
        Dim iTNHeight As Integer, iTNWidth As Integer

            ' Display the filename on the label
            lblFileName.Text = IO.Path.GetFileName(strFileName)

            ' Check that the variable strFileName contains something
            If strFileName <> "" Then

                ' Load the photo into a bitmap
                bm = New Bitmap(strFileName)

                ' Calculate the aspect ration of the image
                dAspectRatio = bm.Width / bm.Height

                ' Calculate the width and height of the thumbnail to be created
                If dAspectRatio > 1 Then
                    ' If the image is wider than it is tall, set it's width to be the width of the
                    ' picturebox and calculate it's height using the aspect ratio
                    iTNWidth = picThumbnail.Width
                    iTNHeight = CInt(iTNWidth / dAspectRatio)
                    ' If the image is taller than it is wide, set it's height to be the height of the
                    ' picturebox and calculate it's width using the aspect ratio
                    iTNHeight = picThumbnail.Height
                    iTNWidth = CInt(iTNHeight * dAspectRatio)
                End If

                ' Check we have valid heights and widths
                If iTNHeight > 0 And iTNWidth > 0 Then
                    ' Generate the thumbnail
                    picThumbnail.Image = bm.GetThumbnailImage(iTNWidth, iTNHeight, Nothing, IntPtr.Zero)
                End If

                ' Dispose of the bitmap
                ' Tf strFileName is empty, clear the picturebox
                picThumbnail.Image = Nothing
            End If

        Catch ex As Exception
            MsgBox("An exception occured when trying to render the thumbnail for the image '" & _
              strFileName & "' with the message:" & vbCrLf & vbCrLf & ex.ToString, _
        End Try

    End Sub

This function is declared as Private because we don't want it to be available to users of the control; we just use it inside the control to generate and draw the thumbnail image as required.  The DrawThumbnail function first assigns the Label's Text property to "strFileName" and loads the whole picture into an off-screen bitmap.  The function calculates the aspect ratio (ratio of width to height) of the image and then calculates the best size of the thumbnail to fill the PictureBox preserving the aspect ratio of the original image.  Our thumbnail will always appear in the centre of the PictureBox because the PictureBox's SizeMode is set to "CenterImage".  Assuming the function manages to calculate a valid size for the thumbnail it then generates the thumbnail image using the GetThumbnailImage method of the Bitmap holding the complete image we created earlier.  This thumbnail image is assigned to the PictureBox's Image property and finally the bitmap is disposed of, clearing the whole picture from memory leaving only the much smaller thumbnail.  If "strFileName" contains an empty string the picture box will be cleared.


Creating a Thumbnail Control - Properties and Events
Properties and Events

To be able to use our thumbnail control effectively we need to add properties and events.  Fortunately, properties like the control's size are all managed by the UserControl base class.  We only need to add any extra ones we want.  We'll add one property to our control to allow the user to get or set the filename of the thumbnail to be displayed.  This is shown below:

     Public Property FileName() As String
            Return strFileName
        End Get
        Set(ByVal Value As String)
            strFileName = Value
        End Set
    End Property

This property will return the value of the string "strFileName" which is the private variable used to store the filename.  If a new filename is assigned to the property it will assign it to the variable strFileName then redraw and display the thumbnail, using the DrawThumbnail function.

Next we'll add a resize event handler so that our thumbnail will redraw its image to fit the available space after the control is resized.  Most of the work involved in resizing our thumbnail control will be handled automatically because we assigned Dock properties to all the constituent controls.  However when the control is resized, it would be a good idea to regenerate the thumbnail to ensure it still fills the PictureBox nicely.  You can add an event handler to a custom control in the same way as you would a form; I set up my resize event handler as shown below:

    Private Sub Thumbnail_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Resize
    End Sub

For our control to be useful, it must be able to respond to actions like being clicked on.  The UserControl base class has an extensive set of built in events.  However these events are only raised if they happen to the UserControl and not any controls which happen to be added to it.  As our control is entirely filled with controls, some of these built in events, like the "OnClick" event, will never fire.  To get around this we're going to add our own events to the control.  I added three which I named "ThumbnailMouseDown", "ThumbnailClick" and "ThumbNailDoubleClick".  For ease of use these events have the same parameters as the events raised by the built in .NET controls.  To define these events add the following declarations at class level to your code.

    Public Event ThumbnailMouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs)
    Public Event ThumbnailDoubleClick(ByVal sender As Object, ByVal e As System.EventArgs)
    Public Event ThumbnailClick(ByVal sender As Object, ByVal e As System.EventArgs)

To raise the events we have just defined we'll add event handlers to our control to respond to click, double click and mouse down events on either the Label or PictureBox.  These event handlers will in turn raise one the events we have just defined.  One of the nice features of Visual Basic.NET is that it allows one event handler to handle the events raised by more than one control.  We can use this to create one event handler that will handle the events raised by both the Label and PictureBox.  The Click and double click event handlers are added as shown below.

Private Sub Control_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles lblFileName.Click, picThumbnail.Click
        RaiseEvent ThumbnailClick(Me, e)
    End Sub

    Private Sub Control_DoubleClick(ByVal sender As Object, ByVal e As System.EventArgs) Handles lblFileName.DoubleClick, picThumbnail.DoubleClick
        RaiseEvent ThumbnailDoubleClick(Me, e)
    End Sub

We pass "Me" - which represents the instance of the control that raised the event - to the event handler functions as the sender so that the user of the control can identify the control which raised the event.  We are also passing System.EventArgs generated by the constituent controls; this may seem unnecessary but by making our event have the same parameters as all the standard .NET controls it will allow the user of the control to use the one event handler to handle events from our thumbnail control and any other standard .NET control.

The MouseDown event is a little trickier.  MouseDown events are commonly used to get the position over a control where the mouse was clicked, typically so a popup menu can be displayed.  For our control's MouseDown event to behave as expected the event handler must be passed the coordinates of the mouse relative to the entire thumbnail control, not the constituent controls (i.e. the PictureBox or Label).  We get around this problem by adding the position of the control on which the MouseDown event occurred to the coordinates received by the event handler before raising the ThumbnailMouseDown event, as shown below:

    Private Sub Control_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles lblFileName.MouseDown, Panel1.MouseDown
        Dim eNew As System.Windows.Forms.MouseEventArgs
        ' Convert the sender to a Control so we can access its position
        Dim c As Control = DirectCast(sender, Control)
        'Create the new MouseEventArgs
        eNew = New System.Windows.Forms.MouseEventArgs(e.Button, e.Clicks, e.X + c.Top, e.Y + c.Left, e.Delta)
        RaiseEvent ThumbnailMouseDown(Me, eNew)
    End Sub

Overloading the Constructor

Finally we'll overload the constructor of our control so we can pass in the filename of the thumbnail to be displayed.  An object's constructor is the code that is run when an object is created and are used to initialise the object.  We will overload the constructor of our control so that we can specify the filename for our thumbnail to draw when it is created - this will save coding when we start using our control later.  Add the following function:

    Public Sub New(ByVal strFN As String)
        'This call is required by the Windows Form Designer.
        'Initialize our thumbnail control
        strFileName = strFN ' assign the filename for the thumbnail
        DrawThumbnail() ' draw it
    End Sub

The "MyBase.New" and InitializeComponent() calls are required because we are using the UserControl base class, then we simply assign "strFileName" to the filename passed in and draw the thumbnail.  That is the control ready for use! 

Using the Completed Thumbnail Control

You can use this control in any of your projects by adding the Thumbnail control source code to it.  Alternatively you can create a UserControl project that contains the finished thumbnail control and compile it to a DLL.  You may then either add the compiled user control to any of your projects as a reference or add it to your toolbox and use it in the Visual Studio IDE.


The User Interface

We'll begin with laying out the basic user interface for our image browser application. Add a Panel to your applications form and name it "pnlThumbnails", we'll use this panel to contain our thumbnails.  Set the Panels BorderStyle property to "Fixed3D", its AutoScroll property to "True" and its Dock property to "Left" - this will ensure that this panel always fills left side of the Form.  Stretch this panel so it occupies around half of the form.  Now add a Splitter control to your Form; to do this select a Splitter control from the Toolbox then click on the area to the right of the panel we just created.  You will see a selected area to the right of the Panel we just created stretching the full height of the Form - this is the Splitter.  Next add another Panel to your from, to the right of the Splitter and name it "pnlPhoto".  Set this Panels BorderStyle to "Fixed3D", its AutoScroll property to "True" and its Dock property to "Fill" - the panel should expand to fill the remaining area of your Form.  Finally add a PictureBox to the Panel you just created.  Name it "picPhoto" and set its Location property to "0,0" and its SizeMode to "AutoSize".  We will use this PictureBox to display the selected photo.

To complete our User Interface add a MainMenu to your Form.  Add a File menu with two options "Browse…" and "Exit" and name them "mnuFileBrowse" and "mnuFileExit".  To assign a key to activate a menu add a "&" character to the menu in front of the character you wish to use.  For example if you created a menu with the Text "E&xit" would appear "Exit" on your form - the underline on the "x" indicates that you can use the "x" key to select this menu option if the menu is active.

By using a Splitter and the Dock Properties on the two Panels we have created a user interface that will allow the area of the two panels to be resized at run-time.  The AutoScroll feature of the Panels will mean that if more thumbnails are added than will fit on the Panel that it will automatically add scrollbars to allow the user to view all the thumbnails.  Using the AutoSize SizeMode of the PictureBox will mean that it will resize to fit any image added to it.  If an image is displayed on the PictureBox that is too large to fit on the Panel, Scrollbars will be automatically added to allow the user to scroll around the complete image.  Your application should now look like this:

It should be noted that by using appropriate properties of the standard .NET Framwork controls we have created a user interface that can be resized at run time without using any code - it is possible to achieve this for many .NET applications!


Displaying Thumbnails

There are a number of ways in which the thumbnail control we have created could be used.  For our Photo Browser Application, we want to be able view thumbnails for all the photos in a specified location; this means we will have to create -instances of our thumbnail control, add event handlers and clear the thumbnails we have created when required - all at runtime.

Adding controls to an application at run-time is pretty straightforward using Visual Basic .NET and can be a very useful technique.  All controls in the .NET Framework, including Forms, are derived from the "Control" base class.  The Control class contains a "Controls" collection, this collection contains all controls to be displayerd on the control.  If you wish to add a control to your application at runtime you create a new instance of the required control and add it to the Controls collection of the relevent control - the process is not unlike populating a TreeView or ListView at runtime except you have to specify the position of your new control manually.

Arranging Thumbnails on a Panel

Before we write any functions add the necessary Imports statements above your form code to give us easy access to the functions we will be using.  Add the following at the top of your forms code:

Imports System.IO
Imports System.Drawing

We need access to the file functions and classes in the System.IO namespace and the classes used to define location are in the "System.Drawing".

First, add set of constants to define the size and spacing of the thumbnails when displayed on the controls.  By default all control sizes and locations in the .NET framework are defined in pixels, so we will also use pixels to define the layout of our thumbnails.  Add the following at class level to your forms code.

    ' Constants to define the size and spacing of our thumbnails
    Const iTNWidth As Integer = 120
    Const iTNHeight As Integer = 120
    Const iHPadding As Integer = 5
    Const iVPadding As Integer = 5

The constants iTHWidth and iTNHeight will be used to define the size of the thumbnails, and iHPadding and iVPadding are used to define the spacing between our thumbnails.  Using constants means these values cannot be changed once the application is running and they also make it is easy change how our thumbnails are arranged on the form by simply changing the values in these constants.

The first function we'll add is to set the position of a thumbnail with in a specified scrollable control (a form or panel).  We'll pass into the function a reference to the Thumbnail control to be positioned, a reference to the control it is to be positioned on and the number of thumbnails already positioned - we'll use this to calculate the position of the specified thumbnail.  By creating our function in this way it will make it easier to reuse this function in any future applications.  Add the following function:

    Private Sub SetThumbnailPosition(ByRef tnThumbnail As Thumbnail, ByRef cntlThumbnailContainer As ScrollableControl, ByVal iThumbNailNo As Integer)

        Dim iColumns As Integer, iRow As Integer, iCol As Integer
        Dim iLeft As Integer, iTop As Integer

        ' Calculate the number of thumbnails we can fit across the control
        iColumns = (cntlThumbnailContainer.Width - 30) \ (iTNWidth + iHPadding)
        ' If the number if less then 1, set it to 1 so the thumbnails will always be displayed
        If iColumns < 1 Then iColumns = 1

        ' Calculate the position of our thumbnail in our control in terms of the
        ' row and column of grid of displayed thumbnails
        iRow = (iThumbNailNo \ iColumns) + 1
        iCol = (iThumbNailNo Mod iColumns) + 1

        ' Now calculate the position in Pixels.  We use the defined constants to calculate the
        ' position of the thumbnail and add the AutoScrollPosition of the control we are
        ' going to be arranging it on - this is necessary to ensure that the thumbnails are
        ' displayed in the correct location if the control is not currently scrolled to the top
        ' of its available area.
        iLeft = (iCol * iHPadding) + ((iCol - 1) * iTNWidth)
        iLeft += cntlThumbnailContainer.AutoScrollPosition.X()
        iTop = (iRow * iVPadding) + ((iRow - 1) * iTNWidth)
        iTop += cntlThumbnailContainer.AutoScrollPosition.Y()

        ' Set the thumbnails position
        tnThumbnail.Left = iLeft
        tnThumbnail.Top = iTop

    End Sub

We will use this function in our application to set the position of any new thumbnail control created and later to rearrange the thumbnails displayed on a panel if it is resized. 

Next add the function which we will later use to handle any Thumbnails "ThumbnailDoubleClick" event.  Add the following to your code:

    Private Sub OnThumbnailDoubleClick(ByVal sender As System.Object, ByVal e As System.EventArgs)
        Dim tnThumbnail As Thumbnail
        If sender.GetType Is GetType(Thumbnail) Then
            tnThumbnail = DirectCast(sender, Thumbnail)
            picPhoto.Image = Bitmap.FromFile(tnThumbnail.FileName)
        End If
    End Sub

This function will first check if the sender is a Thumbnail control, and if it is load the file specified in the Thumbnails FileName property into the PictureBox on our form.  We will use this to display the image represented by any thumbnail that is double clicked on in our PictureBox.


Creating Thumbnails at Run-Time

We'll now add a function to generate a new thumbnail, set its size, add it to a specified Scrollable Control and set up an event handler for its "ThumbnailDoubleClick".  The function is shown below:

    Public Function AddThumbnail(ByRef cntlThumbnailContainer As ScrollableControl, ByVal strFileName As String) As Thumbnail

        Dim tnNewThumbnail As Thumbnail

        ' Create a new thumbnail
        tnNewThumbnail = New Thumbnail(strFileName)
        ' Assign its height, width and make sure it is visible
        With tnNewThumbnail
            If .Width <> iTNWidth Then .Width = iTNWidth
            If .Height <> iTNHeight Then .Height = iTNHeight
            .Visible = True
        End With
        ' Add the thumbnail to the specified control
        ' Set up the event handler
        AddHandler tnNewThumbnail.ThumbnailDoubleClick, AddressOf OnThumbnailDoubleClick

        ' Return the thumbnail we just created
        Return tnNewThumbnail

    End Function

The next function used for the displaying of our thumbnails will be used to clear all thumbnails on the specified scrollable control.  Add the following function to your code:

    Public Sub ClearThumbnails(ByRef cntlThumbnailContainer As ScrollableControl)

        Dim i As Integer = 0
        Dim c As Control

        While i < cntlThumbnailContainer.Controls.Count
            c = cntlThumbnailContainer.Controls(i)
            If c.GetType Is GetType(Thumbnail) Then
                cntlThumbnailContainer.Controls.Remove(c) ' Remove the current thumbnail
                c.Dispose() ' Dispose of it
                i += 1
            End If
        End While

    End Sub

The loop used may seem complicated but is necessary as we are removing controls from the specified controls collection the more conventional For Each loop structure would not work as the number of controls in the collection could change after each loop iteration.

Generating Thumbnails for all Images in a Folder

Now we have all the functions to generate and display individual thumbnails we'll add a function to display thumbnails for all JPEGs in a folder, specified by the user using a FolderBrowserDialog, to the specified scrollable control.  Add this function to your forms code:

    Private Sub GenerateThumbnailsForFolder(ByRef cntlThumbnailContainer As ScrollableControl)

        Dim dlg As FolderBrowserDialog
        Dim iThumbnailCount As Integer = 0
        Dim tnNewThumbnail As Thumbnail

        ' Create a new instance of the folder browser dialog
        dlg = New FolderBrowserDialog

        ' Show the dialog and check that the user pressed OK to close it - not cancel
        If dlg.ShowDialog = DialogResult.OK Then

            Me.Cursor = Cursors.WaitCursor ' Set the Forms cursor to a hour glass

            ' Clear all thumbnails currently on the control

            ' Get all the JPEG files in the specified folder and loop through them
            For Each objFile As FileInfo In New DirectoryInfo(dlg.SelectedPath).GetFiles("*.jpg")
                ' Create a new thumbnail to display the current file
                tnNewThumbnail = AddThumbnail(cntlThumbnailContainer, objFile.FullName)
                ' Arrange the thumbnail
                SetThumbnailPosition(tnNewThumbnail, cntlThumbnailContainer, iThumbnailCount)
                iThumbnailCount += 1  ' Increment the count of the thumbnails on the control

            ' Set the title of the application to reflect the folder name being browsed
            Me.Text = Application.ProductName & " - " & dlg.SelectedPath
            Me.Cursor = Cursors.Arrow ' Return the cursor to an arrow
        End If
        ' Dispose of the dialog

    End Sub

This function uses the DirectoryInfo class to get all the JPEG images in the selected folder.  It then uses the functions we created to display and arrange the thumbnails on the specified control.  While the thumbnails are being generated we set the forms cursor to be an hour glass to indicate to the user that the application is busy, the cursor is restored to an arrow when the operation is complete.

We are now ready to add event handlers to the "Browse…" menu on our form.  It would have been possible to add the code in the "GenerateThumbnailsForFolder" function to this event handler; however this is not good practice.  In modern Windows application there is often more than one way of performing the same task, for example a main menu item, context menu item and toolbar button may all perform the same, or very similar tasks - to prevent adding the same large block of code several times in an application we create a function to do all the hard work and simply call this function from the required event handler.  Add the following to your code:

    Private Sub mnuFileOpen_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles mnuFileOpen.Click
        GenerateThumbnailsForFolder(DirectCast(pnlThumbnails, ScrollableControl))
    End Sub

We simply call the function "GenerateThumbnailsForFolder" passing in the control the thumbnails are to be displayed on.  We have to cast the Panel to a ScrollableControl, which may seem unnecessary, but by passing in a reference to a ScrollableControl we can pass in a reference any control that is inherited from the ScrollableControl base class (either a Panel or Form - as before, this may make this code a little more useful in future applications.

Finally we'll add an event handler to allow the user to close the application from the menu we created earlier.  The code is shown below:

    Private Sub mnuFileExit_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles mnuFileExit.Click
    End Sub

Whilst the application is far from finished, it is how ready for to view its first images!



Now we have created the basic application we will expand it to allow the user to both rotate and flip images as well as zoom in and out on the image displayed.  We'll also look at re-arranging thumbnails on the panel so they fill it after it is resized.


Currently our application only displays images at 100% size.  Modern digital cameras are capable if taking photos at resolutions several times that of the a PCs monitor.  This means that some users may only be able to see less than a quarter of their image on the screen at any time.  In order to view the complete image we need to be able to zoom in or out to resize the image to fit the screen.

We'll start by declaring the variables that will hold the current zoom factor and the amount it will be increased or decreased. Add the following declarations at class level.

    ' Sets the initial zoom factor to 100%
    Dim dZoomFactor As Double = 1
    ' Specifies that the zoom factor will be changed in increments of 15%
    Const dZoomIncrements As Double = 0.15

Currently we are using the SizeMode property of the PictureBox "picPhoto" set to "AutoSize" so that the application automatically displays the selected photo at 100% size.  For our zooming functionality we need to change the size mode of the PictureBox "picPhoto" to "StretchImage".  When a PictureBox has its SizeMode property set to "StretchImage" it automatically stretches the image to fit its current size.  To zoom in our out on out image we are going to alter the size of the PictureBox and it will stretch the image to fit.  As an example, to display an image at 200% size we would set the dimensions of the PictureBox to twice the size of the photo it contains.

Now we're going to add a function to redraw the photo after a zoom operation.  Basically this function calculates and sets the new size of the PictureBox using the zoom factor variables we declared earlier.  To make our application to be easy to use we will keep area zoomed in at the same location on the screen after the zoom operation is complete - this is where things start to get a little more complicated… 

The function I have written for this task has two arguments, the X and Y coordinates of the position on the panel that will remain in the same place on the screen after the zooming operation.  This function will allow us to add mouse wheel zooming to our application so that the user can hold the mouse over a specific location on a photo and zoom in on that location.  Add the following function to your application's code:

    Private Sub ReDrawPhoto(ByVal xPos As Integer, ByVal yPos As Integer)

        Dim dPicStaticXPos, dPicStaticYPos As Double
        Dim dPanelStaticXPos, dPanelStaticYPos As Double
        Dim iNewXPos, iNewYPos As Integer

        If Not picPhoto.Image Is Nothing Then
            ' Get the position on the picture in pixels of
            ' the cordinates specified in the function, we will use this to
            ' ensure these cordinates stay in the same position each time we redraw the photo
            dPicStaticXPos = xPos / (picPhoto.Width / picPhoto.Image.Width)
            dPicStaticYPos = yPos / (picPhoto.Height / picPhoto.Image.Height)
            dPanelStaticXPos = xPos + pnlPhoto.AutoScrollPosition.X
            dPanelStaticYPos = yPos + pnlPhoto.AutoScrollPosition.Y

            ' Set the new image height and width using the zoom factor
            picPhoto.Height = CInt(picPhoto.Image.Height * dZoomFactor)
            picPhoto.Width = CInt(picPhoto.Image.Width * dZoomFactor)

            ' Set the AutoScrollPosition so that the point clicked on is in the same position on the screen after zooming
            iNewXPos = CInt((dPicStaticXPos * dZoomFactor) - dPanelStaticXPos)
            iNewYPos = CInt((dPicStaticYPos * dZoomFactor) - dPanelStaticYPos)
            pnlPhoto.AutoScrollPosition = New Drawing.Point(iNewXPos, iNewYPos)
        End If

    End Sub

Now we'll add a function to get the image to fit the available space on the screen.  This function calculates the maximum size of the image that can fit on the screen and adjusts the zoom factor accordingly.  Add the function below to your code:

    Private Sub FitPhotoToPanel()

        ' Assumes that the Panel is approximately square
        ' Calculates the maximum zoom factor that can be used and still
        ' fit the image on the panel used to contain it
        If picPhoto.Image.Height > picPhoto.Image.Width Then
            dZoomFactor = (pnlPhoto.Height - 5) / picPhoto.Image.Height
            dZoomFactor = (pnlPhoto.Width - 5) / picPhoto.Image.Width
        End If

    End Sub

We will now modify the event handler that responds to a double click on a thumbnail so that the photo displayed will be resized to fit the screen.  Alter the event handler so it looks like this:

    Private Sub OnThumbnailDoubleClick(ByVal sender As System.Object, ByVal e As System.EventArgs)
        Dim tnThumbnail As Thumbnail
        If sender.GetType Is GetType(Thumbnail) Then
            tnThumbnail = DirectCast(sender, Thumbnail)
            picPhoto.Image = Bitmap.FromFile(tnThumbnail.FileName)
            ReDrawPhoto(0, 0)
        End If
    End Sub

We are now ready to add mouse wheel zooming to the application.  We need to add two event handlers, one to set the focus to the photo if it is clicked on - the photo must have focus to raise a MouseWheel event.  The zoom event handler simply checks whether the user is scrolling forwards or backwards and adjusts the zoom factor accordingly and redraws the photo.  We adjust the zoom factor by multiplying it by either "1 + dZoomIncrements" or "1 - dZoomIncrements", i.e. 1.15 or 0.85 so we increase or decrease the zoom factor by 15% of its current value after each operation.  When we call the ReDrawPhoto function we will pass in the position of the mouse on the panel - these coordinates include any portion image currently not visible.  The event handlers are shown below.

     Private Sub picPhoto_MouseWheel(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) _
        Handles picPhoto.MouseWheel

        If e.Delta > 0 Then
            dZoomFactor *= (e.Delta / 120) * (1 + dZoomIncrements)
        ElseIf e.Delta < 0 Then
            dZoomFactor *= (e.Delta / -120) * (1 - dZoomIncrements)
        End If
        ReDrawPhoto(e.X, e.Y)
    End Sub

    Private Sub picPhoto_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles picPhoto.Click
    End Sub

It should be noted that this method of mouse wheel zooming is not perfect.  When the user scrolls the mouse wheel the panel that contains the picture is vertically scrolled before the MouseWheel event fires.  As a result the location to keep in the centre of the screen that is passed to the ReDrawPhoto function is not always the area the user had the mouse over, but instead a position either slightly above or below the target area.


Adding an "Image..." Menu

It's now time to start adding a new menu that will allow the user to access the new functionality we are adding.  Add a new menu "Image" to your application's main menu and name it "mnuImage".  Now add the following menu items to your Image menu: "Flip Vertical", "Flip Horizontal", "Rotate Left", "Rotate Right", "Zoom In", "Zoom Out" and "Fit to Screen".  Name these menu items "mnuImageFlipVertical", "mnuImageFlipHorizontal", "mnuRotateLeft", etc…  We'll start by adding event handlers for the zoom menus.  Add the following event handlers to your code:
    Private Sub ImageZoomIn(ByVal sender As System.Object, ByVal e As System.EventArgs) _
        Handles mnuImageZoomIn.Click
        ' Increases the zoom factor and redraws the image.
        ' Passes the cordinates of the middle of the screen into the ReDrawPhoto function so
        ' That the center of the image at present remains in the center of the screen.
        dZoomFactor *= 1 + dZoomIncrements
        ReDrawPhoto(CInt(pnlPhoto.Width / 2) - pnlPhoto.AutoScrollPosition.X, _
            CInt(pnlPhoto.Height / 2) - pnlPhoto.AutoScrollPosition.Y)
    End Sub

    Private Sub ImageZoomOut(ByVal sender As System.Object, ByVal e As System.EventArgs) _
        Handles mnuImageZoomOut.Click
        dZoomFactor *= 1 - dZoomIncrements
        ReDrawPhoto(CInt(pnlPhoto.Width / 2) - pnlPhoto.AutoScrollPosition.X, _
            CInt(pnlPhoto.Height / 2) - pnlPhoto.AutoScrollPosition.Y)
    End Sub
    Private Sub ImageFitToScreen(ByVal sender As System.Object, ByVal e As System.EventArgs) _
        Handles mnuImageFitToScreen.Click
        ReDrawPhoto(0, 0)
    End Sub

When we call the ReDrawPhoto function we have to specify the position on the screen that will remain in the same position after the zooming operation.  For the zoom menu items we will pass in the position of the centre of the portion of image that is currently being displayed.  Your user interface should now look like this:


Basic Image Manipulation

We are now going to add the ability to perform basic image manipulation.  The .NET Framework image object contains a method that will flip and/or rotate the image it represents - this makes our job relatively easy. 

We'll add event handlers to respond to the options available in the image menu.  All of these event handlers check that an image is loaded, call the "RotateFlip" method the image object that represents the photo then calls the ReDrawPhoto function so that the modified image is displayed.  All functions pass coordinates of the centre of the image to the ReDrawPhoto function.  Add the following to your code:

    Private Sub ImageFlipVeritcal(ByVal sender As System.Object, ByVal e As System.EventArgs) _
        Handles mnuImageFlipVertical.Click
        If Not picPhoto.Image Is Nothing Then
            ReDrawPhoto(CInt(pnlPhoto.Width / 2), CInt(pnlPhoto.Height / 2))
        End If
    End Sub

    Private Sub ImageFlipHorizontal(ByVal sender As System.Object, ByVal e As System.EventArgs) ) _
        Handles mnuImageFlipHorizontal.Click
        If Not picPhoto.Image Is Nothing Then
            ReDrawPhoto(CInt(pnlPhoto.Width / 2), CInt(pnlPhoto.Height / 2))
        End If
    End Sub

    Private Sub ImageRotateLeft(ByVal sender As System.Object, ByVal e As System.EventArgs) ) _
        Handles mnuImageRotateLeft.Click
        If Not picPhoto.Image Is Nothing Then
            ReDrawPhoto(CInt(pnlPhoto.Width / 2), CInt(pnlPhoto.Height / 2))
        End If
    End Sub

    Private Sub ImageRotateRight(ByVal sender As System.Object, ByVal e As System.EventArgs) ) _
        Handles mnuImageRotateRight.Click
        If Not picPhoto.Image Is Nothing Then
            ReDrawPhoto(CInt(pnlPhoto.Width / 2), CInt(pnlPhoto.Height / 2))
        End If
    End Sub

In Visual Basic.NET event handlers can handle events from more than one source.  The "Handles" statements in all of the event handlers above could be extended to also handle events from another source, for example a toolbar or context menu.


Rearranging Thumbnails at Run-Time

The final change to our application in this article is modifying it to rearrange the thumbnails displayed on the panel pnlThumbnails if it is resized.  We used the Dock Properties of many of the components on the form so they would automatically be easily resized with the form they are on - but if we want the thumbnails to be rearranged when the panel that contains them is resized we will have to do it ourselves!

We'll start by adding a function to rearrange the thumbnails if the panel containing them is resized.  This function calls the "SetThumbnailPosition" function we created earlier for each thumbnail on the panel containing them.  We also get the current AutoScrollPosition for the panel when we start the operation and reset it to this position when the redraw is complete; this will prevent the thumbnails panel from being scrolled back to the top after the redraw. Add the following function to your code:

    Private Sub RearrangeThumbnails(ByRef cntlThumbnailContainer As ScrollableControl)

        Dim pOldScrollPos As Point
        Dim iAddedThumbnails As Integer = 0

        ' Get the Current Autoscroll position of the control
        With cntlThumbnailContainer.AutoScrollPosition
            pOldScrollPos = New Drawing.Point(Math.Abs(.X), Math.Abs(.Y))
        End With

        ' Set the new position of each thumbnail to fill the ScrollableControl using
        ' the SetThumbnailPosition function
        For Each tnThumbnail As Thumbnail In cntlThumbnailContainer.Controls
            SetThumbnailPosition(tnThumbnail, cntlThumbnailContainer, iAddedThumbnails)
            iAddedThumbnails += 1  ' Increment the count of added thumbnails by one

        ' Set the containers AutoScroll position to where it was before it was resized
        cntlThumbnailContainer.AutoScrollPosition = pOldScrollPos
        cntlThumbnailContainer.AutoScroll = True

    End Sub

We now add an event handler that will call this function when the panel is resized.  The function is shown below.

    Private Sub pnlThumbnails_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles pnlThumbnails.Resize
        RearrangeThumbnails(DirectCast(pnlThumbnails, ScrollableControl))
    End Sub

All modifications are now complete and the application is ready for use!  Whilst its functionality is somewhat limited compared with many modern photo editing applications, it does allow the user to browse and view photos with relative ease. 

This is the end of this article, but obviously there is loads of scope for improvement in the application.  You are free to use the code in this article in any of your own projects.  The photo browser could be improved dramatically by relatively simple tasks like improving the user interface by adding context menus, toolbar or statusbar, or by adding the ability to save modified images (the Image object has methods for this too) - where you take it from here is up to you, but the possibilities are almost endless!