Posted December 01, 2002 12:00AM by Robert Smith
Tagged: Coding, 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 imageThe 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 powerfulWe'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. Just for looksLike 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 NamingOnce 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. |