smithvoice.com
 Y'herd thisun? 

“International competition rather than international cooperation motivated states to open terrestrial frontiers for centuries, and that motivation will have to be harnessed again for our species to permanently occupy other worlds of the solar system.”

from Reopening the Space Frontier by John Hickman

Your own image control and app part 17a

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.


 

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.

Next: Cleanup continued

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