Introduction
When I tried (for the first time) to understand in a practical way how one could display a UserControl beyond the limits of any eventual Parent Container and Parent Form, I came across several examples. Most of them very good, some with stunning styles and visual effects, however all complicated, tangled, sometimes dependent on external actions to the UserControl itself.
As I am fan of a simple coding, the aim of this article is to show how you can apply this technique to a "DropDown" UserControl simply by adding very few lines of code in three different "Key Parts" of the code:
- Key Part 1 - Variables definition
- Key Part 2 - UserControl loading
- Key Part 3 - "DropDown" action controller Event
This technique can be applied to Containers (such as Panel, GroupBox, TabControl, etc.) or Controls (such as ListView, ListBox, TreeView, DataGridView, etc.).
Using the Code
In this example we'll build a kind of DropDownList ComboBox (called CbBox
), consisting of three Controls:
- A Label (called
LBox
), to display the selected item.
- A Button (called
bDrop
), to Open/Show or Close/Hide the Items List.
- A ListView (called
_List
), without Headers, to store the selectable items.
● The Label and the Button are created by the Designer and are the visible and resizable part of the UserControl.
● The ListView, declared as an Instance Variable, is set up (with only one Column) when the UserControl is loaded, and only at Runtime.
Key Part 1 - Variables definition
Just define 3 variables:
_List
(class ListView), to store the list of data items. In VB.NET, Friend WithEvents
, in order to handle the ListView event ItemSelectionChanged
.
tsHost
(class ToolStripControlHost), to host the Control (_List
).
tsDrop
(class ToolStripDropDown), to display the list of data items stored in the hosted Control (_List
).
private ListView _List;
private ToolStripControlHost tsHost;
private ToolStripDropDown tsDrop = new ToolStripDropDown();
Friend WithEvents _List As ListView
Private tsHost As ToolStripControlHost
Private tsDrop As ToolStripDropDown = New ToolStripDropDown
Key Part 2 - UserControl loading
In the UserControl's Load Event
, but only at Runtime:
- Set up the ListView with the appropriate
Properties
, no Headers, just one Column (in this example).
- In C#, add the ListView's
ItemSelectionChanged
EventHandler
- Attach the ListView to the ToolStripControlHost.
private void CbBox_Load(object sender, System.EventArgs e)
{
if (this.DesignMode == false) {
_List = new ListView();
_List.View = View.Details;
_List.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
_List.MultiSelect = false;
_List.FullRowSelect = true;
_List.HideSelection = false;
_List.HeaderStyle = ColumnHeaderStyle.None;
_List.GridLines = true;
_List.Columns.Add(new ColumnHeader());
_List.Columns[0].Width = this.Width - 21;
_List.ItemSelectionChanged += _List_ItemSelectionChanged;
tsHost = new ToolStripControlHost(_List);
}
}
Private Sub CbBox_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
If Me.DesignMode = False Then _List = New ListView
_List.View = View.Details
_List.BorderStyle = Windows.Forms.BorderStyle.FixedSingle
_List.MultiSelect = False
_List.FullRowSelect = True
_List.HideSelection = False
_List.HeaderStyle = ColumnHeaderStyle.None
_List.GridLines = True
_List.Columns.Add(New ColumnHeader)
_List.Columns(0).Width = Me.Width - 21
tsHost = New ToolStripControlHost(_List)
End If
End Sub
Key Part 3 - "DropDown" action controller Event
In the Button (or Label) Click Event
:
- If
_IsOpen
is false, the ToolStripDropDown is to be shown:
- Resize the ListView:
Width
equals the UserControl's Width.
Height
is the lowest number of rows between the predefined _MaxItemsDisp
and the total data items (or 1 if no data items),
multiplied by 17 (16 pixels per row plus 1 pixel for the gridline) plus 1 pixel for final line.
- Prevent unnecessary lines display (Margin, Padding).
- Get the items to display by the ToolStripDropDown, from the ToolStripControlHost (which hosts the ListView).
- Show the ToolStripDropDown, relative to the UserControl's visible part (Label & Button), at the specified upper left corner location (horizontal, vertical).
- If
_IsOpen
is true, the ToolStripDropDown is to be hidden.
private void bDrop_Click(object sender, EventArgs e)
{
if (_IsOpen == false) {
_List.Size = new Size(this.Width, Math.Min(_MaxItemsDisp, Math.Max(_List.Items.Count, 1)) * 17 + 1);
if (tsHost != null)
{
tsHost.Margin = new Padding(0);
tsDrop.Padding = new Padding(0);
tsDrop.Items.Add(tsHost);
tsDrop.Show(this, new Point(-1, this.Height - 2));
}
}
else {
tsDrop.Hide();
}
_IsOpen = !_IsOpen; bDrop.Focus();
}
Private Sub bDrop_Click(sender As Object, e As EventArgs) Handles bDrop.Click, LBox.Click
If _IsOpen = False Then _List.Size = New Size(Me.Width, Math.Min(_MaxItemsDisp, Math.Max(_List.Items.Count, 1)) * 17 + 1)
If tsHost IsNot Nothing Then
tsHost.Margin = New Padding(0)
tsDrop.Padding = New Padding(0)
tsDrop.Items.Add(tsHost)
tsDrop.Show(Me, New Point(-1, Me.Height - 2))
End If
Else tsDrop.Hide()
End If
_IsOpen = Not (_IsOpen) bDrop.Focus()
End Sub
Some other features
The UserControl is actually very simple. But beyond the 3 Key Parts shown above, there are a few more pieces of code:
● The ListView's ItemSelectionChanged
Event
When the ToolStripDropDown is shown, this Event is needed, to:
- Save the Selected Item into a variable
Item
- Hide the ToolStripDropDown
- If the Item really changed:
- Copy the User's selection to the UserControl's Label Control
- Raise the UserControl's
SelectedItemChanged
Event
private void _List_ItemSelectionChanged(object sender, ListViewItemSelectionChangedEventArgs e)
{
string Item = e.Item.Text;
tsDrop.Hide();
_IsOpen = false;
if (LBox.Text != Item)
{
LBox.Text = Item;
if (SelectedItemChanged != null)
{
SelectedItemChanged(Item);
}
}
}
Private Sub _List_ItemSelectionChanged(sender As Object, e As ListViewItemSelectionChangedEventArgs) _
Handles _List.ItemSelectionChanged
Dim Item As String = e.Item.Text
tsDrop.Hide()
_IsOpen = False
If LBox.Text <> Item Then
LBox.Text = Item
RaiseEvent SelectedItemChanged(Item)
End If
End Sub
● The UserControl's Resize
Event
When the UserControl is resized, the Height
may not decrease below 15, otherwise the text would be cut.
private void CbBox_Resize(object sender, System.EventArgs e)
{
if (this.Height < 15)
{
this.Height = 15;
}
}
Private Sub CbBox_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Resize
If Me.Height < 15 Then
Me.Height = 15
End If
End Sub
When the Width
is resized, the Button's Width
is not affected (19 pixels). But when the Height
is decreased or increased, the Button's Height
changes accordingly:
(Height = 15 pixels)
(Height = 29 pixels)
● The MouseHover
and MouseLeave
Events
Just a very simple visual effect. When the Mouse passes over the Button Control, the Mouse pointer and the Button's BackColor are changed:
private void bDrop_MouseHover(object sender, EventArgs e)
{
this.Cursor = Cursors.Hand;
bDrop.BackColor = Color.Orange;
}
private void bDrop_MouseLeave(object sender, EventArgs e)
{
this.Cursor = Cursors.Default;
bDrop.BackColor = Color.Lavender;
}
Private Sub bDrop_MouseHover(sender As Object, e As EventArgs) Handles bDrop.MouseHover
Me.Cursor = Cursors.Hand
sender.BackColor = Color.Orange
End Sub
Private Sub bDrop_MouseLeave(sender As Object, e As EventArgs) Handles bDrop.MouseLeave
Me.Cursor = Cursors.Default
sender.BackColor = Color.Lavender
End Sub
● The Properties
A few Properties to add some functionality...
BackColor
- (Read/Write) Gets or Sets the UserControl's Back Color
ForeColor
- (Read/Write) Gets or Sets the UserControl's Fore Color
MaxItemsDisp
- (Read/Write) Gets or Sets the Maximum number of Rows to display
Text
- (Read/Write) Gets or Sets the UserControl's Text (Label Control)
Count
- (ReadOnly) Gets the total number of Items in the ListView's Item Collection
public override Color BackColor
{
get { return LBox.BackColor; }
set { LBox.BackColor = value; }
}
public override Color ForeColor
{
get { return LBox.ForeColor; }
set { LBox.ForeColor = value; }
}
public int MaxItemsDisp
{
get { return _MaxItemsDisp; }
set { _MaxItemsDisp = value; }
}
public override string Text
{
get { return LBox.Text; }
set { LBox.Text = value; }
}
public int Count
{
get { return _List == null ? 0 : _List.Items.Count; }
}
Public Overrides Property BackColor As Color
Get
Return LBox.BackColor
End Get
Set(value As Color)
LBox.BackColor = value
End Set
End Property
Public Overrides Property ForeColor As Color
Get
Return LBox.ForeColor
End Get
Set(value As Color)
LBox.ForeColor = value
End Set
End Property
Public Property MaxItemsDisp() As Integer
Get
Return _MaxItemsDisp
End Get
Set(ByVal value As Integer)
_MaxItemsDisp = value
End Set
End Property
Public Overrides Property Text() As String
Get
Return LBox.Text
End Get
Set(ByVal value As String)
LBox.Text = value
End Set
End Property
Public ReadOnly Property Count() As Integer
Get
Return _List.Items.Count
End Get
End Property
● The Methods
Only one Method is needed:
AddRow
- Adds a String to the ListView's ItemCollection
.
public void AddRow(string NewItem)
{
_List.Items.Add(NewItem);
}
Public Sub AddRow(ByRef NewItem As String)
_List.Items.Add(NewItem)
End Sub
● The Events
Only one Event:
SelectedItemChanged
- Is raised when the ListViews's ItemSelectionChanged
Event occurs and the Selected Item really changed.
public event SelectedItemChangedEventHandler SelectedItemChanged;
public delegate void SelectedItemChangedEventHandler(string Item);
Public Event SelectedItemChanged(Item As String)
The Demo project
Is a simple Windows Forms Application, where all the UserControl's features can be tested.
The UserControl DLL is located at ...\bin\Debug\CbBox.dll
Although I prefer VB.NET for the sake of simplicity and readability (braces and semicolons are a bummer),
I have to recognize that the performance of C# is much higher... DLL file sizes:
- VB.NET version is 28 KB
- C# version is 12 KB
- Items: each one of the groups can be added only once.
- Other player: Type and Add whatever you want.
- To Set a Property, first Click the white box and type or choose the desired value. Colors will show a ColorDialog.
- Event: Whenever the Selected Item changes, a message will be displayed:
History
03.Aug.2016 - First posted.