Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

DropDownContainer - Custom DropDown Container Using the ToolStripDropDown (VB.NET)

4.96/5 (34 votes)
1 Jan 2009CPOL7 min read 119.7K   4.6K  
This control makes it possible to layout the controls in a ToolStripDropDown at design time.

Image 1

Introduction

You may think you have seen this one before, and you will be partially correct. I wanted a ComboBox like dropdown control that I could drop other controls onto and arrange them at design time. Because the ToolStripDropDown couldn't be handled directly at design time and I didn't fully understand it yet, I made the DropDownPanel[^] which is a User Control that Inherits Panel and has a fancy size change routine. It is a good control, and works well for certain situations, but it still wasn't a true dropdown like I wanted. Then, I got the idea you will find here. I decided to create a separate article for this control because I felt there are enough different techniques in each to not just update and replace the DropDownPanel article.

The ToolStripDropDown had two main stumbling points. First, you cannot see it on the design surface, and second, it contains only one control at a time. You can put a bunch of controls on a Panel and then put that "one" Panel control in the ToolStripDropDown, but then, you have these extra controls laying around your design surface, cluttering it up, or you have to hide them somewhere. Then, I got the idea to hide it in a DropDownContainer, and show it like I did in the DropDownPanel control at Design Time, and in the ToolStripDropDown at Run Time, depending on the DesignMode property.

Control's Design Time Setup in the IDE

  1. Drop a DropDownContainer on the Form.
  2. Click on the dropdown button to open the drop surface. (Yes, in this version, the button works in Design Time! Details later.)
  3. Drop a control on the drop surface (a single control like a Calendar control, or a collection of controls on a Panel).
  4. Assign the control to the DropControl property. This property uses a UITypeEditor DropControlsEditor to narrow the list of controls to just the ones on the DropDownContainer (usually just one)
  5. Image 2

Control's Appearance

  1. Modify the Appearance properties to suit your needs.
  2. Click on the dropdown button to close the drop surface and un-clutter the design surface.
  3. Image 3

Run Time Appearance

  1. The DDAlignment and DDShadow are the two properties that will not be visible at Design Time.
  2. Image 4

Control Layout

The Header:

  • Push pin - Locks the Panel open so it won't close when it loses focus (now animated!).
  • Dropdown button - Opens and closes the Panel.
  • Textbox - Displays string information.
  • Graphic - Displays a small image next to the textbox.

The Design Time Dropdown Panel:

  • The area defined by the PanelSize property that holds the DropControl.

Events

  • Public Event DropDown(ByVal sender As Object, ByVal IsOpen As Boolean)
  • This event will fire when the dropdown button is clicked.

  • Public Event TextBoxChanged(ByVal sender As Object)
  • This event will fire when the text in the textbox changes.

Code #Regions

Initialize

Because there is no Load event, I put the Load code in the HandleCreated event, which gave me the same result.

To setup a ToolStripDropDown first, put the DropControl into a ToolStripControlHost.

VB
TSHost = New ToolStripControlHost(_DropControl)

Then, add the ToolStripControlHost to the ToolStripDropDown and remove the DropControl from the DropDownContainer control.

VB
TSDropDown.Items.Add(TSHost)
Me.Controls.Remove(_DropControl)

Here is the complete method:

VB
Private Sub DDContainer_HandleCreated(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.HandleCreated

    'I put this here because there is no Load Event
    If Not Me.DesignMode Then

        Me.CloseDesignDropDown()
        Me.Region = Nothing

        blnIsResizeOK = True

        If _DropControl IsNot Nothing Then

            TSHost = New ToolStripControlHost(_DropControl)
            TSHost.Margin = Padding.Empty
            TSHost.Padding = Padding.Empty
            TSHost.AutoSize = False
            TSHost.Size = _DropControl.Size

            TSDropDown.Size = TSHost.Size
            TSDropDown.Items.Add(TSHost)
            TSDropDown.BackColor = _DDBackColor
            TSDropDown.DropShadowEnabled = _DDShadow
            Me.Controls.Remove(_DropControl)
        End If
    End If

    ResizeMe()
End Sub

Control properties

Here is a list of the primary properties:

  • DropControl
  • Get or set the control to show in the dropdown.

  • PanelSize
  • Get or set the size of the dropdown panel (Auto-sizes to DropControl if set).

  • DDBackColor
  • What color to fill the panel with. (Only shows if DDPadding is greater than 0.)

  • DDShadow
  • Get or set if the shadow appears at Run Time.

  • DDAlignment
  • Get or Set where the DropDown appears at Runtime

  • DDOpacity
  • Get or Set the Opacity of the DropDown at Runtime

  • ShowPushPin
  • Get or set if the push pin is visible.

  • Pin, PinBody, PinHighlight, PinOutline
  • Get or set the color scheme for the pushpin.

  • ButtonShape
  • Get or set the shape of the dropdown button.

  • ButtonBackColor, ButtonHighlight, ButtonForeColor, ButtonBorder
  • Get or Set the Color scheme for the DropDown Button

  • TextBoxGradientType, TextBoxBackColorA, TextBoxBackColorB
  • What color and blend type to fill the textbox with.

  • TextBoxBorderColor
  • Get or set the border color of the textbox.

  • Text, ForeColor, TextShadow, TextShadowColor
  • Get or set the text format.

  • GraphicBorderColor
  • Get or set the border color around the graphic.

  • GraphicImage
  • Get or set the image next to the textbox.

  • GraphicWidth
  • Get or set the width of the graphic image.

  • GraphicAutoWidth
  • Get or set to automatically size the width from the image aspect ratio.

ToolStripDropDown

Contains the open and close events for the ToolStripDropDown. Closing the dropdown is easy and automatic if you just let it close when it loses focus. However, if you want to close it by clicking the button again, you have a problem because when you click the button, you are outside the dropdown area, which will automatically close it before the button actually clicks, so it just re-opens it. To fix this, I had to check if the mouse is over the button area when the dropdown tries to close and cancel the closing.

VB
Private Sub TSDropDown_Opening(ByVal sender As Object, _
        ByVal e As CancelEventArgs)
    RaiseEvent DropDown(Me, True)
End Sub
VB
Private Sub TSDropDown_Closing(ByVal sender As Object, _
        ByVal e As ToolStripDropDownClosingEventArgs)
    Try
        If (Not GetButtonPath.IsVisible(PointToClient(Control.MousePosition)) _
            Or (e.CloseReason = ToolStripDropDownCloseReason.Keyboard)) _
            And Not CBool(_PushPinState) Then
            IsOpen = False
        End If

        e.Cancel = CBool(_PushPinState)

        If Not e.Cancel Then
            Me.Invalidate()
            RaiseEvent DropDown(Me, False)

        End If
    Catch ex As Exception

    End Try
End Sub

Mouse events

Track if the cursor is over one of the areas like the button or the pushpin. If the control is in design mode, then resize to show the panel, and if in runtime, then show the ToolStripDropDown. But wait, normally, when you click the control at design time, it just selects the control. By adding the ControlDesigner.GetHitTest() method to the DDContainerDesigner, the mouse position can be checked to see if it is over the button, and pass the click through, allowing it to fire the mouse events in the designer. Now, clicking the dropdown button opens and closes the panel at design time.

VB
Protected Overrides Function GetHitTest _
         (ByVal point As System.Drawing.Point) As Boolean

    Dim DDC As DDContainer = CType(Component, DDContainer)
    point = DDC.PointToClient(point)
    Return DDC.GetButtonPath.IsVisible(point)

End Function

Another thing you may notice is in design time, when the control is closed, it can only resize its width (like the single line TextBox), but when it is open, you can resize all. This is also done in the DDContainerDesigner by overriding the ControlDesigner.SelectionRules property.

VB
Public Overrides ReadOnly Property SelectionRules() _
       As System.Windows.Forms.Design.SelectionRules
    Get
        Dim DDC As DDContainer = CType(Component, DDContainer)
        If DDC.IsOpen Then
            Return MyBase.SelectionRules
        Else
            Return SelectionRules.LeftSizeable _
                   Or SelectionRules.RightSizeable _
                   Or Windows.Forms.Design.SelectionRules.Visible _
                   Or Windows.Forms.Design.SelectionRules.Moveable
        End If
    End Get
End Property

To refresh the selection rectangle, I reset the ISelectionService in the MouseUp event:

VB
Dim selectservice As ISelectionService = _
    CType(GetService(GetType(ISelectionService)), ISelectionService)
Dim selection As New ArrayList
selection.Clear()
selectservice.SetSelectedComponents(selection, SelectionTypes.Replace)
selection.Add(Me)
selectservice.SetSelectedComponents(selection, SelectionTypes.Add)

Since we are talking about the DDContainerDesigner, let's get the OnPaintAdornments method out of the way. This is where we can add an additional painting routine that occurs after the control paints itself and occurs only at design time. Here is where a representation of the drop surface is painted at design time, so you can see where to drop the DropControl.

VB
Protected Overrides Sub OnPaintAdornments _
         (ByVal pe As System.Windows.Forms.PaintEventArgs)
    MyBase.OnPaintAdornments(pe)
    Dim DDC As DDContainer = CType(Component, DDContainer)
    If DDC.IsOpen Then
        Dim rect As Rectangle = New Rectangle(0, DDC.HeaderHeight + 2, _
            DDC.PanelSize.Width - 1, DDC.PanelSize.Height - 1)
        Using g As Graphics = pe.Graphics
            g.FillRectangle(New SolidBrush(DDC.DDBackColor), rect)
            Using pn As Pen = New Pen(Color.Gray, 1)
                pn.DashStyle = DashStyle.Dash
                g.DrawRectangle(pn, rect)
            End Using
        End Using
    End If
End Sub

Painting

Contains the routines to draw each part of the control. Any control added to the DropDownContainer that is in the header area will be automatically moved down. To override this behavior, add the string "IgnoreMe" in the child control's Tag property, and it will be ignored.

VB
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
    'Draw the Graphic if available and resize
    If _GraphicImage IsNot Nothing Then
        Dim GW As Integer = CInt(IIf(_GraphicAutoWidth, _
             _HeaderHeight * (_GraphicImage.Width / _GraphicImage.Height) _
             , _GraphicWidth))
        e.Graphics.DrawImage(_GraphicImage, 0, 0, GW, _HeaderHeight)
        e.Graphics.DrawRectangle(New Pen(_GraphicBorderColor), _
                                 0, 0, GW, _HeaderHeight - 1)
    End If

    'Draw the Text Box
    If rectTextBox.Width > _TextBoxCornerRadius * 2 Then DrawTextBox(e.Graphics)

    'Draw the Drop Down Button
    DrawDropDownButton(e.Graphics)

    'Draw the Push Pin
    If _ShowPushPin Then DrawPushPin(e.Graphics)

    'Adjust any miss placed control positioned on the Header
    For Each c As Control In Me.Controls
        If c.Location.Y < _HeaderHeight + 1 Then
            If CStr(c.Tag) <> "IgnoreMe" Then
                c.Location = New Point(c.Location.X, _HeaderHeight + 1)
            End If
        End If
    Next
End Sub

In the DropDownContainer, the pushpin now rotates instead of just flipping, and the colors can be changed. Instead of a canned bitmap, the pushpin uses a GraphicsPath to draw itself manually in any color on a rotated Graphics surface. The rotation is done by transforming the Graphics object with a rotated Matrix incremented with a Timer.

VB
Sub DrawPushPin(ByRef g As Graphics)
    g.SmoothingMode = SmoothingMode.AntiAlias
    Dim gp As New GraphicsPath
    Dim mx As New Matrix
    mx.RotateAt(PushPinRotate, New Point(rectPushPin.X + 10, 9))
    g.Transform = mx

    gp.FillMode = FillMode.Winding
    gp.AddEllipse(rectPushPin.X + 6, 0, 8, 4)
    gp.AddEllipse(rectPushPin.X + 3, 7, 14, 6)
    gp.AddRectangle(New Rectangle(rectPushPin.X + 7, 3, 6, 8))
    g.FillPath(New LinearGradientBrush _
        (New Rectangle(rectPushPin.X + 2, 0, 14, 18), _
        _PinHighlight, _PinBody, _
         LinearGradientMode.Horizontal), gp)

    Using pn As Pen = New Pen(_Pin, 3)
        pn.EndCap = LineCap.Triangle
        g.DrawLine(pn, rectPushPin.X + 10, 13, rectPushPin.X + 10, 18)
    End Using

    gp.Reset()
    gp.AddEllipse(rectPushPin.X + 6, 0, 8, 4)
    gp.AddArc(rectPushPin.X + 3, 7, 14, 6, 326, 246)
    gp.StartFigure()
    gp.AddLine(rectPushPin.X + 7, 3, rectPushPin.X + 7, 9)
    gp.StartFigure()
    gp.AddLine(rectPushPin.X + 13, 3, rectPushPin.X + 13, 9)
    gp.StartFigure()
    gp.AddArc(rectPushPin.X + 7, 7, 6, 3, 0, 150)
    g.DrawPath(New Pen(_PinOutline, 1), gp)

    gp.Dispose()
    mx.Dispose()
End Sub

Methods

    Private

  • UpdateTextArea - Sets up what appears in the header.
  • UpdateRegion - Sets up the header and panel regions to show what is behind the control through the cut out area.
  • ResizeMe - Resizes the control to fit the current state.
  • OpenDesignDropDown - Opens the panel in design mode.
  • CloseDesignDropDown - Closes the panel in design mode
  • .

    Public

  • OpenDropDown - Opens the panel.
  • CloseDropDown - Closes the panel only if the pushpin is up.
  • ForceCloseDropDown - Flips the pushpin up and closes the panel.

Control events

Contains the events for the DropDownContainer.

DropDownContainer designer and editors

The ControlDesigner that makes the SmartTag. Go here for Designer Howto's not already discussed in this article: UITypeEditorsDemo[^].

End Regions

Anchoring and resizing

Anchoring does not pose a big problem in this control like it did in the DropDownPanel, just make sure it is closed in design mode when resizing the form. With form resizing, moving or switching to a different window can't be detected by the DropDownContainer, which is only a problem if the pushpin is down. To eliminate this problem, add two events in the parent form to force the DropDownContainer to close if you are using the pushpin.

Add each DropDownContainer to close them.

VB
Private Sub Form1_ResizeBegin(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.ResizeBegin
    DropDownContainer1.ForceCloseDropDown()
    DropDownContainer2.ForceCloseDropDown()
    ...
End Sub

Private Sub Form_Move(ByVal sender As Object, _
  ByVal e As System.EventArgs) Handles Me.Move
    DropDownContainer1.ForceCloseDropDown()
    DropDownContainer2.ForceCloseDropDown()
    ...
End Sub

Or, if you are not sure, use the below routines:

VB
Private Sub Form1_Move(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.Move
    For Each c As Control In Me.Controls
        If c.GetType Is GetType(DropDownContainer.DDContainer) Then
            CType(c, DropDownContainer.DDContainer).ForceCloseDropDown()
        End If
    Next
End Sub

Private Sub Form1_ResizeBegin(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.ResizeBegin
    For Each c As Control In Me.Controls
        If c.GetType Is GetType(DropDownContainer.DDContainer) Then
            CType(c, DropDownContainer.DDContainer).ForceCloseDropDown()
        End If
    Next
End Sub

History

  • Version 1.0 - January 2009.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)