smithvoice.com
 Y'herd thisun? 

“Learning how to do in something new what you do without thinking in something old strengthens you in both.”

from this by smith

Your own image control and app part 17b

TaggedCoding, VB, Imaging

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.


 

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 it 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


jump to:

  • 1) The spec
  • 2) Setting up the workspace
  • 3) Feature 1: Loading an image
  • 4) Custom Exceptions
  • 5) "Fax images" and Multipage TIFFs
  • 6) Custom events
  • 7) Selecting specific fax pages
  • 8) Feature 2: Rotating image displays
  • 9) The most useful tool in GDI+: DrawImage
  • 10) Feature 3: Zooming
  • 11) Handling the unhandleable exception
  • 12) Fixing the squish
  • 13) Zooming to fit the control
  • 14) You're already beating the Pros
  • 15) Feature 4: Cropping
  • 16) Bonus Feature: StickyMouse
  • 17a) Final Cleanup
  • 17b) Passing the current display image
  • 18) Making the application
  • 19) Source and result viewports
  • 20) A better toolbar
  • 21) Hooking the toolbar to the project
  • 22) Adding ImageEditors
  • 23) The toolbar ZoomCombo
  • 24) The final solution
  • 25) Saving to image files
  • 26) An integer-only textbox
  • 27) Passing save options between forms
  • 28) Dealing with that last exception
  • 29) Offer more options with menus
  • 30 The downloads and ebook


  • home     who is smith    contact smith     rss feed π
    Since 1997 a place for my stuff, and it if helps you too then all the better smithvoice.com