Monday, January 05, 2009
Home
.Net
Return of the Repeater
Complete image control & app
Part 2 - Starting the control
Part 3 - Loading images
Part 4 - Custom exceptions
Part 5 - Fax/Multipage tifs
Part 6 - Custom events
Part 7 - Selecting fax pages
Part 8 - Rotating displays
Part 9 - The power of GDI+
Part 10 - Zooming
Part 11 - .Net's fatal exception
Part 12 - Fix the squishies
Part 13 - Zoom to fit
Part 14 - You beat the pros
Part 15 - Cropping
Part 16 - Bonus: StickyMouse
Part 17 - Finishing touches?
Part 18 - Make the application
Part 19 - Adding viewports
Part 20 - A better toolbar
Part 21 - Connect the toolbar
Part 22 - Adding ImageEditors
Part 23 - Toolbar ZoomCombo
Part 24 - VB3 style ease
Part 25 - Saving images to files
Part 26 - Integer-only textBox
Part 27 - Passing save settings
Part 28 - The last exception
Part 29 - Menus offer more
Part 30 - Book, app & source
Couple of CS Snippets
XML processing quickies
File extension extensions
McAfee.Not
cs IntBox
Floating Holidays
Code snippets
Flexible sorts
Converted deserializations
Autodeploy not found
Autodeploy just stops
VB must be killed
Media file attributes
Fastest file searches
Webservicing custom objects
Aspect correct resize
Funky thumbnails
Wide interval timing
VB2005 or bust?
Recurring events
Single instances
Proper proper casing
Simple address formating
Combo filling reminder
Easy gradient forms
Grrrr on interops?
Winform memory usage
Windows service your ISP
Pretty up VS code printing
Remote configs with no BS
GDI+ printing cd cases
Your own flat combo
Where's the splitter?
Full autoemail
ReessppoonncceeWwrriittee
Kill the back cache
Color to hex
Source to web
Recode without recompile
Prop snippet for VS2008
Database tricks

Your own image control

Originally published December 2002 on DoItIn.net using VB7.0/2002. Updated for VB7.1 February 2005
Links for compiled demo versions, all required resources and source code are included at the end of this article.

Plus, get the complete eBook in Adobe Acrobat 7 format ... all here.


17) Final Cleanup

The big chunk of the work is done, now we just need to patch up those little things that make the difference between "I can use my control" and "Anyone can use my control"

Clear the control

Let's add a method that initializes the control; clearing the picturebox, setting the picturebox square to fill the visible area, forcing the page count variable to zero and resetting the imagepage array so all of those "m_ImagePages Is Nothing" lines actually do something.

This routine should be called by the control's Load event and we'll make the sub that does the work Public so the dev-user can easily re-initialize the widget on-demand.

Public Sub InitializeControl()
        m_CurrentPageNumber = 0
        ''init the array 
        m_ImagePages = Nothing
        pbDisplay.Image = Nothing
        pbDisplay.Size = Me.Size

    End Sub

Now add the call to the control's Load event:

Private Sub svImageEditor_Load(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load

        InitColorDepth()
InitializeControl()

    End Sub
Resize the picturebox when the control is resized

When the dev-user first adds an instance of the control to a form it will appear at the same size you see it in development and the picturebox won't be re-sized by the InitializeControl method. You can tweak UC's GUI designer to a specific size, and you should use the property sheet to explicitly make the picturebox size the same as the UC's size (setting the picturebox BorderStyle to None is up to you).

When the dev-user is resizing the control in their IDE or the user is resizing their form with the control fully anchored the above InitializeControl code won't keep the picturebox positioned correctly at the edges of the control. Patch this issue with code in the UC's private OnResize event. Notice that the event you want to use is the protected OnResize from the "(Overrides)" event list, not the control's own "Resize" event, if you use the standard Resize then the picturebox won't keep up with the UC and you'll get flashing and intermittent scrollbars during resizes.

Protected Overrides Sub OnResize(ByVal e As System.EventArgs)

        If m_ImagePages Is Nothing Then
            pbDisplay.Size = Me.Size

        Else
            'this is bit of a hack
            'an anchored usercontrol gets the event
            'but the autoscroll bars don't respond
            'to it if the internal control (pbDisplay)
            'isn't also resized
            'by "reminding" the UC of the setting
            'the bars get the repaint requests 
            Me.AutoScroll = True

        End If

    End Sub

Tip: You should be fine with the above but if even with the above you get scrollbar flashing on your typical user machines, you can try using a static variable in the OnResize event as we always had to do in VBClassic. For an example of that technique, click here.

Clean up the property sheet

As you've been doing your unit tests, you've probably just ignored all of the greyed out custom properties, the ones that are either readonly or not applicable at designtime (CurrentDisplayImage, CurrentPageNumber, TotalPageCount). Another dev-user down the line might find those positively distracting and they're easy to "hide" with the System.ComponentModel.Browseable attribute. Just add the attribute to each of the properties as shown here with CurrentDisplayImage:

<System.ComponentModel.Browseable(False)> _
    Public ReadOnly Property CurrentDisplayImage() As Image
        Get
            Return DirectCast(m_ImagePages(m_CurrentPageNumber).WorkingImage, Image)
        End Get

    End Property

The properties will still pop up in designtime intellisense and they'll be fully available to runtime code, all we're doing here is removing them from the Visual Studio property sheet grid.

In a similar vein, there are some usercontrol base properties that don't make sense in the property sheet and also you don't want to have mucked with in code. You can't hide them from code, but you can short circuit them so even if the dev-user does try to use them they won't hurt the control.

A good example of such a property is "AutoScroll". Our control depends on this property of the usercontrol always being set to True and so we don't want it showing up in the property sheet AND we want to ignore any external requests to set it to False. The trick is to use the Browseable attribute on a public property of the same name declared as "Shadows". Using Shadows lets you trap the base class's properties and methods and process them yourself (like hooking windows messages in VBClassic). There are a bunch that get in the way in my opinion, so I created a new sub-region in "Public Properties" to keep them all "hidden" together:

#Region "Public Properties"

#Region "Hidden Base Properties"

    'hide this from the property sheet
    <System.ComponentModel.Browseable(False)> _
    Public Shadows Property AutoScroll() As Boolean
        Get
            Return MyBase.AutoScroll

        End Get

        Set(ByVal Value As Boolean)
            'we need to use this internally
            'external attempts should be ignored
            'so do nothing

        End Set
    End Property

    'hide this from the property sheet
    <System.ComponentModel.Browseable(False)> _
    Public Shadows Property AutoScrollMargin() As Size
        Get
            Return MyBase.AutoScrollMargin

        End Get

        Set(ByVal Value As Size)
            'we need to use this internally
            'external attempts should be ignored
            'so do nothing

        End Set
    End Property

    'hide this from the property sheet
    <System.ComponentModel.Browseable(False)> _
    Public Shadows Property AutoScrollMinSize() As Size
        Get
            Return MyBase.AutoScrollMinSize

        End Get

        Set(ByVal Value As Size)

            'we need to use this internally
            'external attempts should be ignored
            'so do nothing

        End Set
    End Property

    'hide this from the property sheet
    <System.ComponentModel.Browseable(False)> _
    Public Shadows Property BackgroundImage() As Image
        Get
            Return MyBase.BackgroundImage

        End Get

        Set(ByVal Value As Image)
            'we need to use this internally
            'external attempts should be ignored
            'so do nothing

        End Set
    End Property


    'devs are used to this not doing anything
    'why not just dump it since it makes no sense
    'hide this from the property sheet
    <System.ComponentModel.Browseable(False)> _
    Public Shadows Property RightToLeft() As RightToLeft
        Get
            Return RightToLeft.No

        End Get

        Set(ByVal Value As RightToLeft)
            'we need to use this internally
            'external attempts should be ignored
            'so do nothing

        End Set
    End Property


    'like a picturebox, we don't want a tabstop
    'hide this from the property sheet
    <System.ComponentModel.Browseable(False)> _
    Public Shadows Property TabStop() As Boolean
        Get
            Return False

        End Get

        Set(ByVal Value As Boolean)
            'we need to use this internally
            'external attempts should be ignored
            'so do nothing
            MyBase.TabStop = False

        End Set
    End Property


#End Region

....

Kinda funny huh? It's just this kind of silent property "eating" that we said we hated about Visual Studio's control size properties ... ah well, at least ours won't crash the framework if they're used ;-).

Some folks say to also use the EditorBrowseable() attribute if you want to hide base properties from the Visual Studio code editor. I say stick to just Browseable() and protecting the input, EditorBrowseable() is only handled in the VB.Net code editor (C# ignores it) and even under VB I've found it doesn't always work.

One more thing on the look of the control in dev-user mode: we needed the yellow for visibility during development but now we should change the UC's backcolor to make it look a bit more professional when it first gets dropped on a form; Change it to System>Control with the IDE property sheet.

Protecting image loads

We did a lot of work to protect the control from hitting an unhandleable exception during Zooms but our lowballed limit may be far below what is acceptable to Windows at different dimension ratios. Until someone figures out an algorithm to quickly test the actual graphics limits, our lowballing will keep zooms from crashing programs and we need similar protection in the LoadImage method.

The easy way out is to test the Image dimensions and if either hit our limit then raise an exception and don't do the load. That works, but there's another almost-as-easy way: If the image object can be created then resize it to fit the maximum allowable dimensions. The good news is that we've already laid the foundation for this route in the LoadImage's use of CalcZoomPercentLimits. All we need to do is call that method before we set the svImagePage's CurrentZoom member, test the source image sizes against the limits and if they're gong to be exceeded then set CurrentZoom to the MaximumZoomPercent; at the same time we can protect tiny images by checking against the MIN_ZOOM_PIXELS constant. Because our ResetImage will also be initializing zoom percentages we'll need to do the calculation there too, so it's best to create a new private sub that both methods can use:

Private Sub InitImagePercent(ByRef imagePage As svImagePage)
        If imagePage.WorkingImage.Width >= m_MaxControlDimension OrElse _
            imagePage.WorkingImage.Height >= m_MaxControlDimension Then
            imagePage.CurrentZoom = imagePage.MaxZoomPercent

        ElseIf imagePage.WorkingImage.Width <= MIN_ZOOM_PIXELS OrElse _
            imagePage.WorkingImage.Height <= MIN_ZOOM_PIXELS Then
            imagePage.CurrentZoom = imagePage.MinZoomPercent

        Else
            imagePage.CurrentZoom = 100

        End If

    End Sub

Now you can update the LoadImage (and replace its call to ResetImage with a call to UpdateDisplay, ResetImage was redundant in this situation anyway)

Public Function LoadImage(ByVal img As Image) As Boolean
        Dim pageCount As Integer
        Dim tmpFileImage As Image
        Dim tmpFrameImage As Image

        Try
            m_CurrentPageNumber = 0
            tmpFileImage = DirectCast(img.Clone, Image)
            pageCount = SourceFilePageCount(tmpFileImage)

            ''reset the array to the number of pages
            ReDim m_ImagePages(pageCount - 1)

            For i As Integer = 0 To UBound(m_ImagePages)
                tmpFileImage.SelectActiveFrame(FrameDimension.Page, i)
                tmpFrameImage = DirectCast(tmpFileImage.Clone, Image)
                tmpFrameImage = CopyImage(tmpFrameImage)
                m_ImagePages(i).OriginalImage = tmpFrameImage
                m_ImagePages(i).WorkingImage = DirectCast(tmpFrameImage.Clone, Image)
                    
CalcZoomPercentLimits(m_ImagePages(i))
                InitImagePercent(m_ImagePages(i))

            Next

UpdateZoomDisplay()

            RaiseEvent ImageLoaded(Me, New svImageLoadEventArgs(pageCount))

        Catch ex As Exception
            'todo:  test to figure errors
            '       and remove catch-all
            Throw ex

        Finally
            If Not tmpFrameImage Is Nothing Then
                tmpFrameImage = Nothing
            End If
            If Not tmpFileImage Is Nothing Then
                tmpFileImage = Nothing
            End If

        End Try

    End Function
Public Sub ResetImage(Optional ByVal resetRotation As Boolean = True, _
        Optional ByVal resetZoom As Boolean = True, _
        Optional ByVal resetAll As Boolean = False)

        If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
            If resetAll Then
                For i As Integer = 0 To UBound(m_ImagePages)
                    If resetRotation Then
                        m_ImagePages(i).WorkingImage = _
                           DirectCast(m_ImagePages(i).OriginalImage.Clone, Image)
                    End If
                    If resetZoom Then
InitImagePercent(m_ImagePages(i))

                    End If

                Next

            Else
                If resetRotation Then
                    m_ImagePages(m_CurrentPageNumber).WorkingImage = _
                        DirectCast(m_ImagePages(m_CurrentPageNumber).OriginalImage.Clone, Image)

                End If
                If resetZoom Then
InitImagePercent(m_ImagePages(m_CurrentPageNumber))

                End If
            End If

            UpdateZoomDisplay()

        End If

    End Sub

Use your favorite drawing program to create a 32767(w)x2000(h) image and give it a test. On my machine this will set the max and the initial display to 40%. Trying it again with 2000(w)x32767(h), however, generates a trappable exception in the Image.FromImage call of the file version of LoadImage:

Oy veh. It's not the big fatal kahuna but "Generic Exception occurred in GDI+" isn't a great message to pop up to the user. We'll lay a trap for this in the filepath version of LoadImage, and convert it to an svInvalidImage exception, using a slightly different message.

Here's hoping we eventually get the time to go deep into GDI+ to figure out the image by image limits on-demand. Until that time realistic limits and usable exception information will get projects to the users and the majority of users will be more than satisfied (Over the years, did you ever hit that control size limit bug in your apps? It's been there from the beginning but you had to hit an unrealistic size to find it).

Public Function LoadImage(ByVal fileName As String) As Boolean
        If File.Exists(fileName) Then
            Try
                Return LoadImage(Image.FromFile(fileName))

            Catch ex As OutOfMemoryException
                Throw New svImageInvalidImageFileException(ex)

Catch ex As System.Runtime.InteropServices.ExternalException
                Throw New svImageInvalidImageFileException( _
                    "Source image could not be loaded." & vbCrLf & _
                    "Width or height may exceed allowable limits.", _
                    ex)

            Catch ex As Exception
                'todo:  test to figure errors
                '       and remove catch-all
                Throw ex

Finally
                'even though we are technically making a copy
                '.Net will lock the source file while the app is 
                'is running
                'the way around this is another necessary evil
                GC.Collect

            End Try

        Else
            Throw New FileNotFoundException

        End If

    End Function

We've also added a GC.Collect in LoadImage's Finally. If you don't do this and your dev-user tries to allow overwriting of a loaded image file they'll get a "Generic GDI+ Exception". While many devs frown on ever forcing the garbage collector to run, there are times when it is the right thing to do and releasing the lock on a loaded image file is definitely one of them. Even though we're creating clones of the file's images and not working directly with the file, .Net isn't smart enough to release the unmanaged resource till our app is closed (.Net2's garbage collector supposedly will release unmanaged resources more effectively).

And on the subject of memory...

Dispose the image objects

Here's one last that will make a huge difference to your users: Implement the hook for the control's Disposed event and use it to pass Dispose calls to the image objects in memory. My sincerest thanks goes to Riaan Greyling of South Africa for emailing me about the absolute need for this. Riaan added the hook in a new region of the UC and I'll quote you his code exactly:

#Region " General Component Events "

    Private Sub svImageEditor_Disposed(ByVal sender As Object, ByVal e As System.EventArgs) _
        Handles MyBase.Disposed

        Dim ThisLoop As Integer

        For ThisLoop = 0 To m_ImagePages.Length - 1
            m_ImagePages(ThisLoop).WorkingImage.Dispose()
            m_ImagePages(ThisLoop).OriginalImage.Dispose()

        Next

    End Sub
 
#End Region

As mentioned, while the CLR garbage collector should eventually kick in and reclaim even unmanaged resources (like memory used for images), before that happens your program could slow to a crawl and really upset your users. Implementing this hook will instantly give your control's apps a visible snappiness.

Passing the current display image

The control fits its part of the application spec and all that's left for the dev-user is to hook up some menus and buttons and relatively simple image saving code to the ImageCropped event. They probably won't want to make a file right at that event so to save them having to hold their own image variable we'll add a quick public property that let's them get at the currently displayed image at any time.

Public ReadOnly Property CurrentDisplayImage() As Image
        Get
            Return DirectCast(m_ImagePages(m_CurrentPageNumber).WorkingImage, Image)

        End Get

    End Property

Alternatively you could use a clone of pbDisplay's Image, but if you do remember to check for Nothing and return Nothing explicitly if pbDisplay has no image. If you don't then the dev-user will get a NullReferenceException (what devs from VBClassic will always call "A 91").

At one presentation, a person asked "why use a method for loading and a readonly property for reading instead of a R/W Image property?" The answer, as always in this project is: "MultiPage TIffs". I'm of the opinion that it would be confusing to the dev-user to have a single "Image" property that lets you pass in a four page file and only passes out a single page, and the flipside option of passing out an array of all of the WorkingImages would be even more confusing since now they'd need to check the CurrentPageNumber property to work with the image they care about. With the method and RO property what they're doing and what they're getting are obvious.

We don't save images but we can make saving more powerful

We'd just mentioned that it's up to the dev-user to save off the image objects, the control's purpose is to display, rotate and crop and keeping image file format code out of it means it's less likely to need maintenance as file format desires change. We just export an "Image" type and the rest is almost script code.

That being said, we can provide some optional functionality that to export a resized Image object so they don't have to do their own DrawImage code for thumbnailing or other resize requirements. For this, we'll use two slightly modified semi-intelligent versions of the generic Smithvoice.com Aspect-correct resize routine. One that returns an aspect correct new image and one that returns an image set to a specific size.

Both of these new routines are essentially copying images so we could overload the CopyImage function, but in my mind the aspect correct version would be more understandable with a unique name.

Open the ImageUtilities.vb file and add the new functions:

Friend Function AspectResizeImage(ByVal sourceImage As Image, _
        ByVal maxSize As Integer, _
        Optional ByVal ByWidth As Boolean = True, _
        Optional ByVal interpolation As InterpolationMode = InterpolationMode.HighQualityBicubic) _
        As Image

        Dim ratio As Single = 1
        Dim newWidth, newHeight As Integer
        Dim bmpSource As Bitmap
        Dim bmpDest As Bitmap
        Dim gfx As Graphics

        Try

            bmpSource = New Bitmap(sourceImage)
            ratio = CSng(bmpSource.Width / bmpSource.Height)

            If ByWidth Then
                newWidth = maxSize
                newHeight = CInt(maxSize / ratio)

            Else 'by height
                newWidth = CInt(maxSize * ratio)
                newHeight = maxSize

            End If

            'make bitmap for result
            bmpDest = New Bitmap(newWidth, newHeight)

            'create Graphics object for final bitmap
            gfx = Graphics.FromImage(bmpDest)
            gfx.InterpolationMode = interpolation

            'blast the source image into the dest
            gfx.DrawImage(bmpSource, 0, 0, bmpDest.Width, bmpDest.Height)

            Return DirectCast(bmpDest.Clone, Image)

        Catch ex As OutOfMemoryException
            Throw ex
        Catch ex As ArgumentOutOfRangeException
            Throw ex
        Catch ex As Exception
            'ToDo: remove after complete testing
            Throw ex
        Finally

            If Not (sourceImage Is Nothing) Then
                sourceImage.Dispose() ' get rid of the old one if it exists.
            End If

            If Not (bmpSource Is Nothing) Then
                bmpSource.Dispose() ' get rid of the old one if it exists.
            End If

            If Not (bmpDest Is Nothing) Then
                bmpDest.Dispose() ' get rid of the old one if it exists.
            End If

            If Not gfx Is Nothing Then
                gfx.Dispose()
            End If

        End Try

    End Function


    Friend Function CopyImage(ByVal sourceImage As Image, _
        ByVal finalwidth As Integer, ByVal finalHeight As Integer, _
        Optional ByVal interpolation As svInterpolationMode = _
            svInterpolationMode.HighQualityBicubic) As Image

        Dim bmpSource As Bitmap
        Dim bmpDest As Bitmap
        Dim gfx As Graphics

        Try
            bmpSource = New Bitmap(sourceImage)

            ''make bitmap for result
            bmpDest = New Bitmap(finalwidth, finalHeight)

            'create Graphics object for final bitmap
            gfx = Graphics.FromImage(bmpDest)
            gfx.InterpolationMode = CType(interpolation, InterpolationMode)

            'blast the source image into the dest
            gfx.DrawImage(sourceImage, 0, 0, bmpDest.Width, bmpDest.Height)


            Return DirectCast(bmpDest.Clone, Image)
        Catch ex As OutOfMemoryException
            Throw ex
        Catch ex As Exception
            Throw ex
            'Todo: remove after complete testing
        Finally

            If Not (bmpSource Is Nothing) Then
                bmpSource.Dispose() ' get rid of the old one if it exists.
            End If

            If Not (bmpDest Is Nothing) Then
                bmpDest.Dispose() ' get rid of the old one if it exists.
            End If

            If Not gfx Is Nothing Then
                gfx.Dispose()
            End If


        End Try

    End Function

Unlike the generic smithvoice.com version, these have no size protection code and they're declared as Friend so making them only accessible inside the UC's assembly. The user will get at them via wrapper functions in the public Methods region of the UC. Notice that we're not limiting the minimum size to MIN_ZOOM_PIXELS since that is a limit we defined only for our display, instead we've hard coded a lower limit of 1, that may not be a very useful size but many web pages use 1x1 images so it's at least got precedent.

Public Function ResizedDisplayImageAspectCorrect( _
        ByVal maxSize As Integer, _
        Optional ByVal byWidth As Boolean = True, _
        Optional ByVal interpolation As svInterpolationMode = _
        svInterpolationMode.HighQualityBilinear) _
        As Image

        Dim tmpPageImage As svImagePage

        If Not interpolation.IsDefined(GetType(svInterpolationMode), interpolation) Then
            Throw New ArgumentOutOfRangeException("interpolation", _
                "Invalid svInterpolationMode")
        Else

            If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
                tmpPageImage = m_ImagePages(m_CurrentPageNumber)

                If (maxSize >= tmpPageImage.WorkingImage.Width * tmpPageImage.MaxZoomPercent) OrElse _
                     (maxSize >= tmpPageImage.WorkingImage.Height * tmpPageImage.MaxZoomPercent) OrElse _
                     maxSize < 1 Then

                    Throw New ArgumentOutOfRangeException("maxSize", _
                            maxSize, _
                            "Supported resize widths are between 1 and " _
                                & m_MaxControlDimension.ToString & " pixels")

                Else
                    Return AspectResizeImage(tmpPageImage.WorkingImage, maxSize, byWidth, interpolation)

                End If

            End If
        End If

    End Function


    Public Function ResizedDisplayImageSpecific( _
        ByVal finalWidth As Integer, _
        ByVal finalHeight As Integer, _
        Optional ByVal interpolation As svInterpolationMode = _
            svInterpolationMode.HighQualityBilinear) _
        As Image

        Dim tmpPageImage As svImagePage

        If Not interpolation.IsDefined(GetType(svInterpolationMode), interpolation) Then
            Throw New ArgumentOutOfRangeException("interpolation", _
                "Invalid svInterpolationMode")
        Else

            If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
                tmpPageImage = m_ImagePages(m_CurrentPageNumber)

                If (finalWidth >= tmpPageImage.WorkingImage.Width * tmpPageImage.MaxZoomPercent) OrElse _
                   finalWidth < 1 Then

                    Throw New ArgumentOutOfRangeException("finalWidth", finalWidth, _
                        "Supported resize widths are between 1 and " _
                        & m_MaxControlDimension.ToString & " pixels")

                ElseIf (finalHeight >= tmpPageImage.WorkingImage.Height * tmpPageImage.MaxZoomPercent) OrElse _
                    finalHeight < 1 Then

                    Throw New ArgumentOutOfRangeException("finalHeight", finalHeight, _
                        "Supported resize heights are between 1 and " _
                        & m_MaxControlDimension.ToString & " pixels")

                Else
                    Return CopyImage(tmpPageImage.WorkingImage, finalWidth, finalHeight, interpolation)

                End If

            End If

        End If


    End Function

In these wrappers we're doing the size checking and ... darn it, you're right, tossing exceptions may do the trick but it'd be better if we told the dev up front what the limits are so they wouldn't have to apply and hope. And exposing the interpolation mode is risky too because the enumeration includes a silly value named "Invalid" that will crash DrawImage if applied (why the heck is that in the enum?)

Ok, back to front, we need a wrapper around the InterpolationMode enum just to get rid of the "Invalid". Put it in the svTypes.vb file. Note the required brackets around the VB keyword "Default", and that I made it with bytes because I really hate the -1 Invalid member that much ;-).

Public Enum svInterpolationMode As Byte
    [Default] = 0
    Low = 1
    High = 2
    Bilinear = 3
    Bicubic = 4
    NearestNeighbor = 5
    HighQualityBilinear = 6
    HighQualityBicubic = 7

End Enum

Now in svTypes.vb define a class to hold the properties. Although it is going to end up very similar to the svImagePage structure we're doing it as a class this time because we're exposing it to the dev-user and OPP is the correct way to do that in .Net and also a class lets us make all the properties readonly and the constructors "hidden" by sign Friend declarations:

Public Class svDisplayImageDetail
    Private m_PageNumber As Integer = 0
    Private m_PagesInFile As Integer = 0
    Private m_Width As Integer = 0
    Private m_Height As Integer = 0
    Private m_CurrentZoom As Integer = 0
    Private m_MaxZoom As Integer = 0

    Friend Sub New()

    End Sub
    Friend Sub New(ByVal pageNumber As Integer, ByVal pagesInFile As Integer, _
        ByVal width As Integer, ByVal height As Integer, _
        ByVal currentZoom As Integer, ByVal maxZoom As Integer)

        m_PageNumber = pageNumber
        m_PagesInFile = pagesInFile
        m_Width = width
        m_Height = height
        m_CurrentZoom = currentZoom
        m_MaxZoom = maxZoom

    End Sub

    Public ReadOnly Property PageNumber() As Integer
        Get
            Return m_PageNumber

        End Get

    End Property

    Public ReadOnly Property PagesInFile() As Integer
        Get
            Return m_PagesInFile

        End Get

    End Property

    Public ReadOnly Property Width() As Integer
        Get
            Return m_Width

        End Get

    End Property

    Public ReadOnly Property Height() As Integer
        Get
            Return m_Height

        End Get

    End Property

    Public ReadOnly Property CurrentZoom() As Integer
        Get
            Return m_CurrentZoom

        End Get

    End Property

    Public ReadOnly Property MaxZoom() As Integer
        Get
            Return m_MaxZoom

        End Get

    End Property

    Public ReadOnly Property MaxHeight() As Integer
        Get
            Return m_Width * m_MaxZoom

        End Get

    End Property

    Public ReadOnly Property MaxWidth() As Integer
        Get
            Return m_Height * m_MaxZoom

        End Get

    End Property

End Class

Now just add the readonly public property to the UC (hiding it from the property sheet since it makes no sense at designtime and will only show a null reference then anyway):

<System.ComponentModel.Browseable(False)> _
    Public ReadOnly Property CurrentImageDetail() As svDisplayImageDetail
        Get
            Dim oID As svDisplayImageDetail
            Dim tmpImagePage As svImagePage
            If Not m_ImagePages Is Nothing AndAlso m_ImagePages.Length > 0 Then
                tmpImagePage = m_ImagePages(m_CurrentPageNumber)
                oID = New svDisplayImageDetail( _
                    m_CurrentPageNumber + 1, _
                    m_ImagePages.Length, _
                    tmpImagePage.WorkingImage.Width, _
                    tmpImagePage.WorkingImage.Height, _
                    tmpImagePage.CurrentZoom, _
                    tmpImagePage.MaxZoomPercent)
                Return oID
            Else
                oID = New svDisplayImageDetail()
            End If
            Return oID

        End Get

    End Property

Give it a fast test in testHarness with this kind of code in a button click:

Private Sub butShowDetails_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles butShowDetails.Click
        Dim oID As svDisplayImageDetail = SvImageEditor1.CurrentImageDetail
        MsgBox("Page: " & oID.PageNumber.ToString & " of " & oID.PagesInFile.ToString & vbCrLf & _
            "width: " & oID.Width.ToString & vbCrLf & _
            "height: " & oID.Height.ToString & vbCrLf & _
            "max width: " & oID.MaxWidth.ToString & vbCrLf & _
            "max height: " & oID.MaxHeight.ToString & vbCrLf & _
            "cur zoom: " & oID.CurrentZoom.ToString & vbCrLf & _
            "max zoom: " & oID.MaxZoom.ToString)

    End Sub

That's more useful.


This class's object is being created on every call to the CurrentImageDetail property, and on each call it's just tossing up a lot of values copied over from the page structure. I don't think any end user is going to notice the performance hit but it's not very efficient.

Welcome to Experience.

Way back in the beginning we decided to be "efficient" by internally using an array of structures for the page information instead of a collection of objects and now it seems that had we done OOP internally it would been more efficient in the long run because we could be exposing a readonly interface of a page object for the CurrentImageDetail.

Going back and changing this fundamental building block now would be a lot of work for little real benefit so we're not going to do it, but keep the scenario in the back of your mind because the vast majority of applications have internal collections and there will be similar ramifications of the code styles you choose.

Just for looks

Like the yellow background, the FixedSingle border around the picturebox was helpful for design but might not be wanted in an app. We could expose the full borderstyle enumeration but the inset Fixed3D style is even more useless in this context so instead we'll make a boolean property to turn the FixedSingle display on and off:

Public Property ShowImageBorder() As Boolean
        Get
            Return pbDisplay.BorderStyle = BorderStyle.FixedSingle

        End Get

        Set(ByVal Value As Boolean)
            Select Case Value
                Case True
                    pbDisplay.BorderStyle = BorderStyle.FixedSingle

                Case Else
                    pbDisplay.BorderStyle = BorderStyle.None

            End Select

        End Set

    End Property

I won't bother creating a special icon for the control. IDE Toolbox icons are truly important for shareware and retail releases - which my exact code given freely here for educational purposes is NOT allowed to be used for - but for corporate releases it's up to you. If you want to add an icon you can find lots of tips on the web.

Versioning and the importance of Strong Naming

Once you've got the code set and you're happy with the unit tests and you've gone through and removed all of the "ToDo" comment lines you can pop it in an app ... AFTER you've got the version system set.

Similar to VB6, VB.Net allows you to switch on and off an autoincrement for your version numbers but for some reason C# got the full old VB6 functionality and VB.Net got some pain.

To set your versions, you have to edit the text in the AssemblyInfo.vb file. Open that file and look down near the bottom, this is the "autoincrement" area:

' You can specify all the values or you can default the Build and Revision Numbers 
' by using the '*' as shown below:

<Assembly: AssemblyVersion("1.0.*")>

From that you'd assume that every time you build you'd get an updated Revision and Build value but that's the case only in C#. VB.Net doesn't automatically increment the assembly versions when you use wildcards in the version attribute of the assemblyinfo file if you're running the same instance of the IDE (unless you switch from a non-strongnamed build to a strongnamed build) and VB developers are urged to set version numbers explicitly, whereas rebuilding with C# will give a new version number on every build in the same IDE instance. This isn't a bug - it's not well documented, but it's not a bug.

At first that pissed me off, how dare MS give C# the decade-old "VB way" and make VBers have to jump through hoops. It seemed to me that Microsoft was thinking that VB people were going to do more rebuilds than C# people and so they made the IDE ignore our updates. But the more I pondered it the more I found that VB got the good end of the stick. In VBClassic we did mostly need to shut off autoincrement while we were working out code and only turn it on again when we thought we were ready for releasing to testers - if we didn't do this then testers got upset that we'd go from version XX.01 to XX.27. On code complete some people sometimes forgot to turn the incrementer back on and ended up giving the testers a new build with an old version number - not that I ever did that of course ;-). With the new IDE, all you have to do is build build build and THEN, just before you release to your test team you manually increment. I think it's easier to remember to manually update the version number than it was to remember to turn off the autoincrementer.

So, we're happy with the code and we're going to manually set the version number:

' You can specify all the values or you can default the Build and Revision Numbers 
' by using the '*' as shown below:

<Assembly: AssemblyVersion("1.0.0.0")>

That alone might be good enough for some quickie XCOPY apps where the company or user isn't concerned about security and where the control dll is always going to be in the same folder as the exe. Remember though, that even manual versioning is only helping humans and only helping to a point, when you're making components you should use manual versioning AND Strong Naming.

Consider your new control compiled without strong naming. A dev-user will grab the compiled dll and pop it in their WinForm and hook it all up to their GUI and everything is great. At the end they follow their corporate security mandate and add a strong name to their exe (because corporate machines are rightly set to only trust exes from known sources and the company's key is given high trust) and they hit the build button and they get an error. They can't build a strong named exe unless all of the components it uses are strong named. It really isn't a security thing (just because something has a key file doesn't mean it's safe) it's just the way it is, so component dlls, controls or code libraries, should be strong named or they'll be worthless to devs who have to make strong named apps.


There are a number of places to get good information on strong naming, both the reasons and best practices, I think Brent Rector did a nice and concise step-through including tips for obfuscated assemblies at http://www.developer.com/net/vb/article.php/3292231, and of course you can't beat MS for giving it to you to you dry: http://msdn.microsoft.com/library/en-us/cpguide/html/cpconworkingwithstrongly-namedassemblies.asp

Btw: If you're going to do Autodeploys - my personal BIG Favorite tech advantage from .Net - then you have to get a handle on strong naming and versioning, you won't be autodeploying anything for long without both bases covered.

Version and strong name key file noted in the AssemblyInfo.vb file, set the build type to "Release" and build your svImageEdit control project. Put all of the solution files into your source control system (If you're not running Perforce or SourceSafe put it all in a zip and save it off somewhere you won't lose it). Close the IDE and start a new instance, it's time to wrap the app.

Next: Making the application

Robert Smith
Kirkland, WA

 

added to smithvoice march 2005


Print  

pagecomment
  Add Comment



Submit Comment
  View Ratings
50.00%0
40.00%0
30.00%0
20.00%0
10.00%0

Number of Comments 0 , Average of Ratings
  View Comments
No comment.


Privacy Statement  |  Terms Of Use
Copyright 2008 by Robert C. Smith