Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / multimedia / GDI+

DropDownPanel - Custom DropDown Panel with a Graphic and a Pushpin (VB.NET)

4.84/5 (17 votes)
1 Oct 2008CPOL4 min read 1   2.7K  
A ComboBox like dropdown control that controls can be dropped onto and arranged at design time.

DropDownPanel

Introduction

The DropDownPanel is a User Control that inherits from Panel. I wanted a combobox like dropdown control that I could drop other controls onto and arrange them at design time. I also wanted to be able to have the dropdown part to be able to be a different size from the header portion.

The Header:

  • Push pin - Locks the panel open so it won't close when it loses focus.
  • Dropdown button - Opens and closes the panel.
  • Textbox - Displays string information.
  • Graphic - Displays a small image next to the text box.

The Dropdown Panel:

  • The area defined by the PanelSize property that holds the child controls.

Events

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

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

ControlDesigner Methods

  • SizePanelToControl
  • Resizes the panel area to fit the current bounds of the control.

  • SizeToChildControls
  • Resizes the panel area to fit the child controls with a margin of AutoControlMargin.

Image 2

How to Use it

Drop the control on the form and size the header area to the width desired. For the panel area, open it and resize it with the PanelSize property, or use one of the designer methods above. Drop what controls you want on it, and adjust the appearance properties to achieve the look you want. It's as easy as that.

Code #Regions

Initialize

Because the Panel does not have a Load event, I put the Load code in the HandleCreated event, which gave me the same results.

VB
Private Sub DropDownPanel_HandleCreated(ByVal sender As Object, _
      ByVal e As System.EventArgs) Handles Me.HandleCreated
    'I put this here because there is no Load Event for a Panel
    If Not Me.DesignMode Then

        Select Case Me.StartUpState
            Case eStartUpState.Closed
                Me.CloseDropDown()

            Case eStartUpState.Open
                Me.OpenDropDown()
        End Select

    ResizeMe()
    End If
End Sub

If you do not want to use the above event, delete it, and add the following code to each of the parent forms:

VB
Private Sub Form1_Load(ByVal sender As System.Object, _
      ByVal e As System.EventArgs) Handles MyBase.Load
    CloseDropdowns(Me.Controls)
End Sub

'Recursive Sub to Close all Dropdowns
Sub CloseDropdowns(ByVal cc As System.Windows.Forms.Control.ControlCollection)
    For Each c As Control In cc
        If c.GetType Is GetType(DropDownPanel.DropDownPanel) Then
            Dim ddp As DropDownPanel.DropDownPanel = c
            If ddp.StartUpState = _
              DropDownPanel.DropDownPanel.eStartUpState.Closed Then
                ddp.CloseDropDown()
            Else
                ddp.OpenDropDown()
            End If
        End If
        If c.HasChildren Then CloseDropdowns(c.Controls)
    Next
End Sub

Control Properties

Here is a list of the primary properties:

  • StartUpState
  • Is the DropDownPanel open or closed when it loads.

  • PanelSize
  • Get or set the size of the DropDownPanel.

  • PanelGradientType, PanelBackColorA, PanelBackColorB
  • What color and blend type to fill the panel with.

  • PanelBorderColor
  • Get or set the border color of the DropDownPanel.

  • AutoControlMargin
  • Get or set the right and bottom margin from the controls.

  • PanelCornerRadius, TextBoxCornerRadius
  • Get or set the corner radius.

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

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

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

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

  • 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.

Mouse Events

Track if the cursor is over one of the areas like the button or the push pin.

VB
Private Function IsMouseOverArea(ByVal X As Integer, _
      ByVal Y As Integer, ByVal GP As GraphicsPath) As Boolean
    'Convert to Region.
    Using Area As New Region(GP)
        'Is the point inside the region.
        Return Area.IsVisible(X, Y)
    End Using
End Function

Painting

Contains the routines to draw all the parts of the control. Any child control on the panel 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 = IIf(_GraphicAutoWidth, 20 * _
            (_GraphicImage.Width / _GraphicImage.Height), _GraphicWidth)
        e.Graphics.DrawImage(_GraphicImage, 0, 0, GW, 20)
        e.Graphics.DrawRectangle(New Pen(_GraphicBorderColor), 0, 0, GW, 19)
    End If

    'Draw the Text Box
    If rectTextBox.Width > _TextBoxCornerRadius * 2 Then

        If Me._TextBoxGradientType = eGradientType.Solid Then
            e.Graphics.FillPath(New SolidBrush(_TextBoxBackColorA), _
                GetRectPath(rectTextBox, TextBoxCornerRadius))
        Else
            Using lgbr As LinearGradientBrush = New LinearGradientBrush _
              (rectTextBox, _TextBoxBackColorA, _TextBoxBackColorB, _
              CType([Enum].Parse(GetType(LinearGradientMode), _
              _TextBoxGradientType.ToString), LinearGradientMode))

                e.Graphics.FillPath(lgbr, GetRectPath(rectTextBox, _
                    TextBoxCornerRadius))

            End Using
        End If

        e.Graphics.DrawPath(New Pen(_TextBoxBorderColor), _
            GetRectPath(rectTextBox, TextBoxCornerRadius))

        'Draw the Text if Available
        If _Text <> "" Then

            Using sf As StringFormat = New StringFormat
                sf.Alignment = StringAlignment.Center
                sf.LineAlignment = StringAlignment.Center
                e.Graphics.DrawString(_Text, Me.Font, _
                    New SolidBrush(Me.ForeColor), rectTextBox, sf)
            End Using

        End If
    End If

    'If the Panel is Open Paint the Panel Box
    If IsOpen Then
        Dim rect As Rectangle = New Rectangle(0, 21, _
            Me.PanelSize.Width - 1, Me.PanelSize.Height - 1)
        If Me._PanelGradientType = eGradientType.Solid Then
            e.Graphics.FillPath(New SolidBrush(_PanelBackColorA), _
                GetRectPath(rect, PanelCornerRadius))
        Else
            Using lgbr As LinearGradientBrush = New LinearGradientBrush _
              (rect, _PanelBackColorA, _PanelBackColorB, _
              CType([Enum].Parse(GetType(LinearGradientMode), _
              _PanelGradientType.ToString), LinearGradientMode))

                e.Graphics.FillPath(lgbr, GetRectPath(rect, _
                    PanelCornerRadius))

            End Using
        End If

        e.Graphics.DrawPath(New Pen(_PanelBorderColor), _
            GetRectPath(rect, PanelCornerRadius))
    End If

    DrawDropDownButton(e.Graphics)

    If _ShowPushPin Then e.Graphics.DrawImage(bmpPushPin, rectPushPin)

    'Adjust any miss placed control positioned under the Header
    For Each c As Control In Me.Controls
        If c.Location.Y < 21 Then
            If c.Tag IsNot "IgnoreMe" Then _ 
                c.Location = New Point(c.Location.X, 21)
        End If
    Next
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.

Public

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

Control Events

Contains the events for the DropDownPanel.

DropDownPanelDesigner

The control designer that makes the SmartTag. Go Here for Designer How to's: UITypeEditorsDemo[^].

End Regions

Anchoring and Resizing

This became a headache for a while, but I eventually figured out a way. By design, resizing is handled differently if the panel is open or closed. If the panel is closed, then the HeaderWidth adjusts with the control's width, but if it is open, then the HeaderWidth does not change. This is handled in Sub ResizeMe. In this, there is a variable blnIsResizeOK which is set to False to bypass this routine in the Resize event until all manual sizing is done, as in the setting of the IsOpen property, then the blnIsResizeOK is set back to True and ResizeMe is manually called to properly resize. All was good until I tried to anchor the left and right side down to make the control grow and shrink with the form. If a panel was open when the form was resized, then the panel won't resize. So, I needed to make sure the DropDownPanel is closed before resizing occurs. Unfortunately, the panel does not have a ResizeBegin event, so you have to add this code to the parent form of any anchored DropDownPanel.

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

    CloseDDP()

End Sub

Private Sub CloseDDP()

    If DropDownPanelAnchored.IsOpen Then _
        DropDownPanelAnchored.ForceCloseDropDown()
    If DropDownPanelDocked.IsOpen Then _
        DropDownPanelDocked.ForceCloseDropDown()

End Sub

Now, the DropDownPanel is closed before resizing begins, and all is well...until, I maximized or minimized the form. This does not trigger the form's Resize event. To catch this, you have to override the MessageWindow.WndProc method.

VB
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
    'MessageWindow.WndProc Method 
    If (m.Msg = WM_SYSCOMMAND) Then
        If m.WParam.ToInt64 = SC_MAXIMIZE _
            Or m.WParam.ToInt64 = SC_RESTORE _
            Or m.WParam.ToInt64 = SC_MINIMIZE _
            Or m.WParam.ToInt64 = SC_DblClckTitleBarMAX _
            Or m.WParam.ToInt64 = SC_DblClckTitleBarRestore Then
            CloseDDP()
        End If
    End If
    'Call the base
    MyBase.WndProc(m)
End Sub

Now again, all is well...until the form is minimized. The WndProc was working, but somewhere during minimizing, the control was resizing itself (in a bad way). So, I needed to block the resizing if minimized.

VB
Private Sub DropDownPanel_Resize( _
  ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Resize
    'Block resizing when the parent form minimizes 
    Try
        If Me.FindForm.WindowState <> FormWindowState.Minimized _
          And blnIsResizeOK Then
            ResizeMe()
        End If
    Catch ex As Exception
    End Try
End Sub

See Form2 for an example.

History

  • Version 1.0 - October 2008.

License

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