smithvoice.com
 Y'herd thisun? 

“The main requirement is for commerce to replace politics as the driving force behind space policy”

from Spaceflight Revolution by David Ashford

Your own image control and App part 20

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.


20) A better toolbar

The choice of splitters is up to you, but the choice of toolbars is a different story. When I visit companies I always sneak glances at the software on all the screens and I can spot a VB app from yards away simply by the toolbars - big hokin' ugly always-up Win3.1 style buttons with tiny icons. Yeah, they do the trick and in many apps they're passable because in most database apps the real data doesn't take up all that much screenspace, but for an imaging app we need all the space we can get so we have to do some adjustment... too bad there's only so much you can adjust with the stock toolbar control.

Here're some of the issues it would be great to get around:

  • You can make the buttons smaller by setting the toolbar height but you hit a minimum limit of 28.
  • With the handy ButtonsCollection wizard, you can add buttons (ToolbarButtons, not Windows.Forms.Buttons) and have them visible at design time but the size and location of the buttons are readonly and managed by the toolbar control
  • If you use the wizard you can add ToolbarButtons with the Toggle style but there's no built-in group property to make toggles mutually exclusive. To get radiobutton toggle functionality you have to use the ToolbarButton Tag property (or Text property if you're not going to display text on the buttons) to hold a group flag and on each click event use code to iterate the toolbar buttons and force the state of the other toggles in the sets.
  • If you're going to use Text on any button you have to manually set the Toolbar button width to fit the largest text meaning all buttons will be set to the largest width, the other buttons won't size themselves to fit their own texts.
  • You can set the toolbar appearance to Flat so the buttons pop up but if you set the Text property on any button then later decide that you aren't going to use texts at all or if you want to make text a user-defined option, the toolbar buttons won't automatically go back to being nice squares when all of the texts are set to String.Empty. It's as if the control always wants to hold at least one character space in the text property if any text has ever been set.
  • You can add regular controls such as combos, labels and real buttons to the toolbar using the [Toolbar].Controls.Add method and you will have power over their placement but the ToolbarButtons will stay where they are. Since there's no way to place those ToolbarButtons explicitly you'll be adding a lot of Separators to open up space for your added controls or be forced to add them only to the rightmost edge of the bar using designtime hard-coded Left values.

In the end, the toolbar control is like the DBGrid, it gets you a quick, basic result cheap and if you need any extra power at all you should buy a 3rd party widget or make one yourself, as we're going to do.

 

If the control were going to be complex and save the dev-user a lot of low-level effort like the svImageEditor did then we could make a new ControlLibrary DLL project and go all out to make our toolbar bulletproof so that it can be used in any .Net project. We didn't do that for the splitter because it was just a minor enhancement and we won't do it right now for the toolbar for the same reason. Plus, in my experience it takes a few versions of controls to figure out all of the options a generic dll version should have and having a usercontrol in an exe project gives you freedom to quickly tweak if needed. One more thing: Components should work "The .Net Way" but a usercontrol in a GUI exe can take advantage of the specific development language being used for the project (In our case, VB) and as such can save you time and effort.

Like the toolbar, the lightweight panel control has a Controls collection so we'll base our "toolbar" on a panel. Right click on the project node of the solution explorer tree and click on Add>>Add User Control. Name the control "svToolbar" and when its designer appears, add a ToolTip control then switch to codeview on the UC and replace all of the stock text with the following:

Public Class svToolbar
Inherits System.Windows.Forms.Panel
  
#Region " Windows Form Designer generated code "
  
Public Sub New()
MyBase.New()
  
'This call is required by the Windows Form Designer.
InitializeComponent()
  
'Add any initialization after the InitializeComponent() call
  
End Sub
  
'UserControl overrides dispose to clean up the component list.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
 If Not (components Is Nothing) Then
 components.Dispose()
 End If
End If
MyBase.Dispose(disposing)
End Sub
  
'Required by the Windows Form Designer
Private components As System.ComponentModel.IContainer
  
'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
Friend WithEvents ToolTip1 As System.Windows.Forms.ToolTip
<system.diagnostics.debuggerstepthrough> Private Sub InitializeComponent()
Me.components = New System.ComponentModel.Container
Me.ToolTip1 = New System.Windows.Forms.ToolTip(Me.components)
  
End Sub
  
#End Region
  
#Region "Declarations"
  
#Region "Constants & Variables"
Private m_rbBackcolorSelected As Color = Color.FromName("ControlDark")
Private m_rbBackcolorUnselected As Color = Color.FromName("Control")
  
#End Region
  
#Region "Events"
Public Event ButtonClicked(ByVal but As Button)
Public Event RadioOptionChanged(ByVal rbut As RadioButton)
  
#End Region
  
#End Region
  
#Region "Public Methods"
  
Public Function ControlIndexByName(ByVal Value As String) As Integer
Dim bFound As Boolean = False
For i As Integer = 0 To Me.Controls.Count - 1
 If Me.Controls(i).Name = Value Then
 'use the VBClassic style
 'rather than immediately returning
 bFound = True
 ControlIndexByName = i
 Exit For
  
 End If
  
Next
  
If Not bFound Then
 ControlIndexByName = -1
  
End If
  
End Function
  
  
#End Region
  
#Region "Private Methods"
  
Private Sub DrawControls()
Dim ctl As Control
Dim previousControl As Control
For i As Integer = 0 To MyBase.Controls.Count - 1
 ctl = MyBase.Controls(i)
  
 'textboxes don't extend unless they're multiline
 'and multilines don't look nice short, just force to align middle
 ctl.Height = Me.ClientRectangle.Height - 6
 ctl.Top = (Me.ClientRectangle.Height \ 2) - (ctl.Height \ 2)
  
 If i = 0 Then
 ctl.Left = 0
  
 Else
 previousControl = PreviousVisibleControl(i)
 ctl.Left = previousControl.Left + previousControl.Width
  
 End If
  
Next
End Sub
  
  
Protected Overrides Sub OnControlAdded(ByVal e As System.Windows.Forms.ControlEventArgs)
If e.Control.Visible Then
 DrawControls()
End If
  
If e.Control.GetType Is GetType(Button) Then
 AddHandler CType(e.Control, Button).Click, AddressOf evtButtonClick
 ToolTip1.SetToolTip(e.Control, e.Control.Text)
 e.Control.Text = ""
  
ElseIf e.Control.GetType Is GetType(RadioButton) Then
 AddHandler CType(e.Control, RadioButton).CheckedChanged, AddressOf evtRadioOptionChanged
 ToolTip1.SetToolTip(e.Control, e.Control.Text)
 e.Control.Text = ""
  
 'make the selected rb easy to see
 If CType(e.Control, RadioButton).Checked Then
 e.Control.BackColor = m_rbBackcolorSelected
 Else
 e.Control.BackColor = m_rbBackcolorUnselected
 End If
  
End If
  
'all controls need to tell when to redraw the bar
AddHandler e.Control.VisibleChanged, AddressOf evtVisibleChanged
  
End Sub
  
  
Protected Overrides Sub OnControlRemoved(ByVal e As System.Windows.Forms.ControlEventArgs)
If e.Control.Visible Then
 DrawControls()
  
End If
  
If e.Control.GetType Is GetType(Button) Then
 RemoveHandler CType(e.Control, Button).Click, AddressOf evtButtonClick
  
ElseIf e.Control.GetType Is GetType(RadioButton) Then
 RemoveHandler CType(e.Control, RadioButton).CheckedChanged, AddressOf evtRadioOptionChanged
  
End If
  
'all controls need to tell when to redraw the bar
RemoveHandler e.Control.VisibleChanged, AddressOf evtVisibleChanged
  
End Sub
  
  
Private Function PreviousVisibleControl(ByVal currentControlIndex As Integer) As Control
'go backwards till you find a visible control
For i As Integer = currentControlIndex - 1 To 0 Step -1
 If MyBase.Controls(i).Visible Then
 Return MyBase.Controls(i) 
  
 End If 
  
Next
  
End Function
  
  
#End Region
  
#Region "Event Handlers"
Private Sub evtButtonClick(ByVal sender As Object, ByVal e As EventArgs)
RaiseEvent ButtonClicked(CType(sender, Button))
  
End Sub
  
  
Private Sub evtRadioOptionChanged(ByVal sender As Object, ByVal e As EventArgs)
Dim tmpSender As RadioButton = DirectCast(sender, RadioButton)
 
If tmpSender.Checked Then
 tmpSender.BackColor = m_rbBackcolorSelected
 RaiseEvent RadioOptionChanged(tmpSender)
  
Else
 tmpSender.BackColor = m_rbBackcolorUnselected
  
End If
  
End Sub
  
  
Private Sub evtVisibleChanged(ByVal sender As Object, ByVal e As EventArgs)
DrawControls()
  
End Sub
  
#End Region
  
End Class

The code is pretty straight forward, we have a panel that we can add controls to. We'll just run through it quickly...

As each control is added to the panel the base fires a protected OnControlAdded event, this is the major switchboard for the functionality. If the new control is going to be visible when it's added then we call on the DrawControls method which iterates through all of the controls in the collection and places them starting from the left edge of the panel. Each control is sized to fit in the height of the panel with three hard-coded pixels of room on the top and bottom and centered vertically. For the Location.X, there's a helper function "PreviousVisibleControl" that gives us what its name implies so we can set each control's Left property correctly.

Whether the new control is drawn or not, if it has events that the parent form needs to hear about then we'll have to wire those up and you see us doing that for buttons and radiobuttons in OnControlAdded. Here's the part where VB gets to be VB. We use the .Net dynamic event style of AddHandler to hook the buttons up to internal event subs ("evtButtonClick" and "evtRadioOptionChanged") and those subs just raise classic style VB events up to the parent form. Because the exe is written in VB there is no rule forcing us to create an eventargs class if we don't want to, and so our exposed events ("ButtonClicked" and "RadioOptionChanged") simply raise the control that was clicked. Some devs say that this is wrong but it's not, it's taking advantage of the abilities and fully-supported traditional features of our language.

The standard toolbar control lets you set a ToolbarButton to the Toggle style but the toolbar doesn't have any way for you to tell it what toggle buttons are in a relationship. The typical practice is to set the same Tag property for all of the toggles in a group and when the button click fires you have to iterate through all of the controls unchecking the ones that are of the same type AND that have the same tag. It's a little bit of work and we bulleted it earlier as being a negative, but annoying as it is, it's better than what we get by default with "Real RadioButtons so it's nice that they let us turn that same behavior on.

By default when a RadioButton is pressed the Windows Forms engine automatically unchecks all of the other RadioButtons on the same container (the form, panel or groupbox) so you don't have to uncheck them manually AND you get a CheckChanged event from all of the RadioButtons that are changing their CheckedState. Sounds nice till you need more than one group and really don't want to be putting them all in different panel controls. It would be far nicer if Microsoft had just done what Sheridan/Infragistics and other 3rd party widget makers have been doing for years with their option buttons: Add a "GroupName" property to link different buttons and sets of buttons together across containers.

In our project we've lucked out by having only one group for the MouseModes so we can use the default behaviour.

If we did need more than one group on the toolbar then we would need to put RadioButtons into panels or turn off the RadioButton Autocheck property. AutoCheck False gives you the functionality, and requires you to do all the same code, of a Toggled ToolbarButton.

What's the big deal? Isn't choice good?

Well, if you were going to compile the toolbar usercontrol into a dll, how would you handle hooking up events to cover both RadioButton styles? How would you know which RadioButtons should be hooked up to CheckChanges and which should be hooked up to Clicks? You don't want to add both events to the same button because the events would be fighting each other and could lead to race condition issues.

It's a good question to ponder. And it's why I made svToolbar a usercontrol. Months from now a user-dev will be able to deduce the rules because the code is right there in the exe.

At the same time we're hooking up the events you see that we're adding the Text value to the ToolTip control and clearing the Text property. This is something we wouldn't want to automatically do if we were releasing the UC as a component ;-), but it fits the particular needs of our app because we don't want to show any button text and we do we need a way to pass the tooltip text into the toolbar. We get the job done by using the Text property as a bucket.

The last child control event we need to watch for is the VisibleChanged, when it fires we have to redraw the toolbar controls.

Note: If we're always adding controls to the end of the collection instead of placing controls into specific indexes then DrawControls could be reworked to just add to the currently displayed controls rather than starting the placements over from scratch on every Add. Feel free to do the extra code for geek efficiency but because toolbars will only be holding a small number of children don't expect any noticeable difference in performance.

Lastly, we've added a public helper called ControlIndexByName. Container control collections unfortunately don't use the control Names as unique keys ... you can even get away with duplicating Name properties or not setting Name properties when you add controls dynamically - so this function comes in handy. Notice again that we're taking advantage of VB being VB in that we're returning the result using the function name rather than using "Return" ... in some situations this ability is a real lifesaver, in this case we're doing it just because we can :).

Rebuild the project so the svToolbar is available in the My UserControls toolbox tab.

Next: Connecting the toolbar to the project

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