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
- Drop a
DropDownContainer
on the Form
. - Click on the dropdown button to open the drop surface. (Yes, in this version, the button works in Design Time! Details later.)
- Drop a control on the drop surface (a single control like a
Calendar
control, or a collection of controls on a Panel
). - 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)
Control's Appearance
- Modify the Appearance properties to suit your needs.
- Click on the dropdown button to close the drop surface and un-clutter the design surface.
Run Time Appearance
- The
DDAlignment
and DDShadow
are the two properties that will not be visible at Design Time.
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
.
TSHost = New ToolStripControlHost(_DropControl)
Then, add the ToolStripControlHost
to the ToolStripDropDown
and remove the DropControl
from the DropDownContainer
control.
TSDropDown.Items.Add(TSHost)
Me.Controls.Remove(_DropControl)
Here is the complete method:
Private Sub DDContainer_HandleCreated(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.HandleCreated
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.
Private Sub TSDropDown_Opening(ByVal sender As Object, _
ByVal e As CancelEventArgs)
RaiseEvent DropDown(Me, True)
End Sub
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.
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.
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:
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
.
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.
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
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
If rectTextBox.Width > _TextBoxCornerRadius * 2 Then DrawTextBox(e.Graphics)
DrawDropDownButton(e.Graphics)
If _ShowPushPin Then DrawPushPin(e.Graphics)
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
.
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.
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:
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.