If you do any WinForms programming then chances are you have used comboboxes, and perhaps at one point or another, you disabled one of those comboboxes because you did not want the user to be able to make a selection. This works fine, however I actually was getting some complaints from users of some of my software apps saying it was too hard to read the disabled text in a combo. Sometimes just because a feature is disabled, does not mean that the data being displayed in it is of no importance.
So today lets see how we can change this standard functionality to make things a bit easier on our end users.
We will use Visual Basic .NET to extended the standard WinForms .NET combobox control.
Lets start by taking a look at the demo. Here we see 2 disabled comboboxes. The top one is a standard WinForm combobox, which has a gray on gray appearance, and can be pretty hard to read. This demo uses larger fonts, so at a smaller font size, it becomes even more difficult. The second one is the extended ComboBox, and you can see that the ForeColor has stayed black, and the BackColor is gray indicating it is disabled. Much easier to read. So lets see what the code looks like to accomplish this.
Like all the extended controls you can make from the standard WinForm controls, we need to inherit from the control we want to extend, in this case Combobox.
The real trick here is that we are never going to actually make the control disabled. We are simply going to keep track of if it SHOULD be disabled, and stop certain actions from happening in that case. Those actions would be the ability to drop down the combo to make a selection, or to type in the textbox portion of the combo. Take those away, and the combobox is essentially disabled. The other thing we need to take into account, is the default context menu, which gives the user access to features like paste.
In order to do this, we setup a few private fields to keep track of the disabled/enabled state manually.
Here is the start of our class
' 2008 - ZerosAndTheOne.com
' Matthew Kleinwaks
'Extended Combobox Control for better
'handling of disabled appearance
Option Strict On
Option Explicit On
Imports System.Windows.Forms
Public Class ComboboxEx
Inherits ComboBox
#Region "Private Backing Fields"
''' <summary>
''' Private backing field for the shadowed enabled property
''' </summary>
Private _Enabled As Boolean = True
''' <summary>
''' BackColor of control when it is enabled
''' </summary>
Private _enabledBackcolor As Color = MyBase.BackColor
''' <summary>
''' BackColor of a control when it is disabled
''' </summary>
''' <remarks></remarks>
Private _disabledBackColor As Color = Color.LightGray
#End Region
Note it is just 3 private fields we will use. One to keep track of the enabled state, and 2 to keep track of the BackColor, depending on the enabled state. The reason we use these 2 BackColor fields, is because we set the controls underlying BackColor property to one of these when the enabled state changes, so we need to keep track of what each color should be when enabled does change.
Next lets define our public fields that will be exposed to the consumer of this control.
#Region "Public Exposed Fields"
''' <summary>
''' Gets or Sets the value indicating if this control is enabled
''' </summary>
''' <value>Boolean</value>
''' <returns>True of control is enabled, otherwise false</returns>
''' <remarks>This property shadows the ComboBox base class enabled property</remarks>
Public Shadows Property Enabled() As Boolean
Get
Return _Enabled
End Get
Set(ByVal Value As Boolean)
If _Enabled <> Value Then
_Enabled = Value
OnEnabledChanged(New EventArgs)
End If
End Set
End Property
''' <summary>
''' Gets or Sets the BackColor of the control when it is disabled
''' </summary>
''' <value>Color Structure</value>
Public Property DisabledBackColor() As Color
Get
Return _disabledBackColor
End Get
Set(ByVal value As Color)
_disabledBackColor = value
If Not _Enabled Then
MyBase.BackColor = _disabledBackColor
End If
End Set
End Property
''' <summary>
''' Gets or Sets the BackColor of the control when it is enabled
''' </summary>
''' <value>Color Structure</value>
''' <remarks>Shadows the base class BackColor property</remarks>
Public Shadows Property BackColor() As Color
Get
Return _enabledBackcolor
End Get
Set(ByVal value As Color)
If _enabledBackcolor <> value Then
_enabledBackcolor = value
MyBase.BackColor = _enabledBackcolor
End If
End Set
End Property
#End Region
So just like we have 3 backing fields, we have 3 public properties. The biggest thing to note is that 2 of these properties (BackColor and Enabled) are declared shadows. This means they hide and replace the functionality that the base Combobox class provided for these given properties. It is now totally up to us to handle these properties in our new class.
To do the magic here, we will override a few protected methods from the combobox base class, and add our custom functionality.
Lets start with OnEnabledChanged. This is the method that is called when Enabled is changed. In the Enabled public property we defined above, we call this method.
''' <summary>
''' When the shadowed enabled property changes, this method is called
''' </summary>
Protected Overrides Sub OnEnabledChanged(ByVal e As System.EventArgs)
'COMMON ROUTINE FOR TOGGLING ENABLED STATUS
ToggleEnabled()
'SEND NOTIFICATION TO BASE CLASS
MyBase.OnEnabledChanged(e)
End Sub
Pretty simple (we will define the ToggleEnabled method after we define all our overrides).
Next is the PreProcessMessage override. We use this to look at key presses and decide if we want to throw them out, or let them pass to the control to do whatever the given keypress might do. Basically in this example, if the control is enabled, we don't do anything at all, so that is the first thing we check. Next we allow through key presses like Tab, Left, and Right. You could customize what you allow based on the needs of your program.
'OVERRIDE PreProcessMessage TO LOOK FOR KEY PRESSES AND FILTER THEM
Public Overrides Function PreProcessMessage(ByRef msg As Message) As Boolean
'PREVENT KEYBOARD ENTRY IF CONTROL IS DISABLED
If Not _Enabled Then
'CHECK IF ITS A KEYDOWN MESSAGE (&H100)
If msg.Msg = &H100 Then
'GET THE KEY THAT WAS PRESSED
Dim key As Int32 = msg.WParam.ToInt32
'ALLOW TAB, LEFT, OR RIGHT KEYS
If key <> Keys.Tab OrElse _
key <> Keys.Left OrElse _
key <> Keys.Right Then
Return True
End If
End If
End If
'CALL BASE METHOD SO DELEGATES RECEIVE EVENT
Return MyBase.PreProcessMessage(msg)
End Function
Next we override the WndProc method. In here we are looking for 2 possible messages, which have to deal with the drop down of the list in the combobox. We don't want that to happen when the control is disabled, so we filter out those specific messages, and allow all other messages to go through as normal.
'OVERRIDE WndProc TO LOOK FOR DROP DOWN MESSAGES AND FILTER THEM
Protected Overrides Sub WndProc(ByRef m As Message)
'PREVENT DROPDOWN LIST DISPLAYING IF READONLY
If Not _Enabled Then
If m.Msg = &H201 OrElse m.Msg = &H203 Then
Return
End If
End If
'CALL BASE METHOD SO DELEGATES RECEIVE EVENT
MyBase.WndProc(m)
End Sub
Finally, the last override we need to do, is the OnParentEnabledChanged method. This is because if the control is in a container, like a groupbox or panel, and you set the container's enabled property to false, you would expect the combobox to also have it's enabled property set to false. However, the parent control does not simply loop its child controls and set their enabled properties. It actually calls the OnParentEnabledChanged for each child, and lets the child handle what it want's to do in that case. If the control is enabled, we pass through the message like normal. If it is disabled, then we call the ToggleEnabled routine, which is our last routine to define.
'WHEN THE CONTROL IS IN A CONTAINER, AND THE CONTAINER'S ENABLED PROPERTY
'IS SET TO FALSE, THIS CONTROL GETS ITS OnParentEnabledChanged CALLED
'NOT JUST THE CONTROLS ENABLED PROPERTY SET SO WE OVERRIDE THIS, AND
'TOGGLE ENABLED STATE ACCORDINGLY
Protected Overrides Sub OnParentEnabledChanged(ByVal e As System.EventArgs)
_Enabled = MyBase.Parent.Enabled
If _Enabled Then
MyBase.OnParentEnabledChanged(e)
Else
ToggleEnabled()
End If
End Sub
So it is time to define our ToggleEnabled routine. It is actually pretty simple. We set tabstop to false if the control is disabled, and true if it is enabled, so users won't set focus to it with the tab key. We also set the ContextMenuStrip equal to a New ContextMenuStrip. This actually causes NO menu to appear when the control is right clicked, since the NEW context menu we make has no menu items on it. Setting this property to nothing will bring back the standard cut/copy/paste context menu. Keep this all in mind if you happen to use a custom context menu on this control. We also set the BackColor property to our desired colors were, and also set the SelectionLength to 0. This just deselects any selected text, and is really optional and up to you.
#Region "Support Methods"
'COMMON ROUTINE FOR TOGGLING ENABLED STATE
Private Sub ToggleEnabled()
'IF THE CONTROL IS DISABLED, TURN OFF ITS TABSTOP
MyBase.TabStop = _Enabled
'IF THE CONTROL IS DISABLED, SET ITS CONTEXT MENU TO
'A DUMMY NEW CONTEXT MENU SO WE DON'T GET THE
'DEFAULT CONTEXT MENU, OTHERWISE SETTING IT TO
'NOTHING REAPPLIES THE DEFAULT CONTEXT MENU
'ALSO SET BACK COLOR ACCORDINGLY
If Not _Enabled Then
MyBase.ContextMenuStrip = New ContextMenuStrip
MyBase.BackColor = _disabledBackColor
Else
MyBase.ContextMenuStrip = Nothing
MyBase.BackColor = _enabledBackcolor
End If
'DESELECT ANY TEXT FROM COMBOBOX
Me.SelectionLength = 0
End Sub
#End Region
So that all there is to it. We end up with these results:


Attached is the source code for this project in Visual Studio 2008 format, targetting .NET 2.0
As always, please leave your comments and suggestions.
Posted
May 12 2008, 10:51 PM
by
Matthew Kleinwaks