Tuesday, January 06, 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.


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


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