Community Pick: Many members of our community have endorsed this article.
Editor's Choice: This article has been selected by our editors as an exceptional contribution.

A New Approach for Custom Colors in a Disabled VB.Net WinForms TextBox

Mike TomlinsonHigh School Computer Science, Computer Applications, Digital Design, and Mathematics Teacher
CERTIFIED EXPERT
Published:
A request I've seen often here on Experts-Exchange and out on the internet is for the ability to customize the BackColor and ForeColor of a Disabled TextBox.  A common complaint is that the text in a disabled TextBox is hard to read because the default forecolor is a gray on top of a light gray background.  A good number of these requests are immediately met with fierce opposition to changing the default colors as it might confuse the user since your disabled TextBox will look different from other standard disabled TextBoxes.  This article will not take a stand on this issue.  Let's assume that you've carefully weighed the drawbacks, or have a valid reason to deviate from the norm, and still want to change the colors of your disabled TextBox.  How, then, can we accomplish our goal?

Let's first take a look at the two most common solutions to this problem...

Common Solution #1 - The ReadOnly() Property Approach

Many solutions suggest that instead of setting Enabled() to False, we instead set ReadOnly() to True which prevents changes to the TextBox, but allows the BackColor and ForeColor to be customized.  A small caveat to that approach is that the BackColor must be set before changes to the ForeColor are honored.  Setting the BackColor to the already assigned BackColor seems to do the trick:

        TextBox1.BackColor = TextBox1.BackColor
        TextBox1.ForeColor = Color.Red

A problem with the ReadOnly() approach, though, is that the TextBox still responds to interaction from the user.  The cursor changes when the mouse enters, and the text can be selected and copied to the clipboard (using both the mouse and the keyboard).  Setting ShortcutsEnabled() on the TextBox to False prevents the text from being copied to the clipboard, but the text can still be selected by the user.  For many, this approach is good enough, and the issue is deemed resolved.  The ReadOnly() approach gives a functionally "disabled" TextBox since the text within cannot technically be changed by the user.  The fact that the TextBox still responds to user interaction, and the text can be selected, is a minor caveat that many simply choose to ignore.

Common Solution #2 - The Paint() Event Approach

Another common solution to customizing the colors in a disabled TextBox is to derive from the TextBox class, and then manually draw the text in the desired colors via the Paint() event.  This approach typically boils down to something like this:
Public Class TextBoxEx
                          Inherits System.Windows.Forms.TextBox
                      
                          ' ... other code ...
                      
                          Protected Overrides Sub OnEnabledChanged(ByVal e As System.EventArgs)
                              MyBase.OnEnabledChanged(e)
                      
                      	' *We only UserPaint when in a DISABLED state*
                              Me.SetStyle(ControlStyles.UserPaint, Not Me.Enabled)
                              Me.Refresh()
                          End Sub
                      
                          Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
                              MyBase.OnPaint(e)
                      
                              ' *We only UserPaint when in a DISABLED state*
                              Using BackBrush As New SolidBrush(Me.BackColorDisabled)
                                  e.Graphics.FillRectangle(BackBrush, Me.ClientRectangle)
                              End Using
                      
                              Using ForeBrush As New SolidBrush(Me.ForeColorDisabled)
                                  Dim sf As New StringFormat
                                  Select Case Me.TextAlign
                                      Case HorizontalAlignment.Left
                                          sf.Alignment = StringAlignment.Near
                                      Case HorizontalAlignment.Center
                                          sf.Alignment = StringAlignment.Center
                                      Case HorizontalAlignment.Right
                                          sf.Alignment = StringAlignment.Far
                                  End Select
                      
                                  e.Graphics.DrawString(Me.Text, Me.Font, ForeBrush, Me.ClientRectangle, sf)
                              End Using
                          End Sub
                      
                          ' ... other code ...
                      		
                      End Class

Open in new window


This approach has a slight bug in that the text shifts by a couple pixels between an enabled/disabled state.  The bug is quite noticeable if a larger font size is used (try 48 pt, for instance, and observe what happens).  Additionally, the text will not render correctly when disabled if Multiline() has been set to True and the window has been scrolled.  In that case, disabling the TextBox will result in the text being drawn as if the scroll position were at the top, while the actual scroll position remains in the currently scrolled position.  A normal TextBox stays in the same scroll position when disabled.  A third anomaly in using the Paint() event approach manifests itself when the inherited TextBox control starts in an initially disabled state, and then is switched to an enabled state.  Under those conditions, the enabled state renders the text using the wrong font.  This bug can be fixed with more shenanigans in the OnEnabledChanged() event, but I won't get into that here.  Long story short, the Paint() event approach requires a lot of tweaking that results in a still imperfect control.


"And now for something completely different!" --Monty Python

The Suppressed WM_ENABLE Approach

According to MSDN, the EnableWindow() API is used to enable and disable a control:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms646291(v=vs.85).aspx

      
"Enables or disables mouse and keyboard input to the specified window or control. When input is disabled, the window does not receive input such as mouse clicks and key presses. When input is enabled, the window receives all input."

Why do we care?  The Enabled() Property of the TextBox follows the same procedure as the EnableWindow() API.  It may even use it directly, I'm not sure!
      
When the EnableWindow() API is used to disable a control, it takes the following three steps:
(1) Sends the control a WM_CANCELMODE message.
(2) Adds WS_DISABLED to the window styles for the control.
(3) Sends the control a WM_ENABLED message.

The WM_CANCELMODE message is used to notify the control that it should no longer process input:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms632615(v=vs.85).aspx

      
"When the WM_CANCELMODE message is sent, the DefWindowProc function cancels internal processing of standard scroll bar input, cancels internal menu processing, and releases the mouse capture."

The WS_DISABLED window style prevents mouse feedback and focus:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms632600(v=vs.85).aspx

      
"A disabled window cannot receive input from the user."
     
The WM_ENABLED message is sent to notify the control that it should change its enabled state:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms632621(v=vs.85).aspx

      
"Sent when an application changes the enabled state of a window. It is sent to the window whose enabled state is changing. This message is sent before the EnableWindow function returns, but after the enabled state (WS_DISABLED style bit) of the window has changed.  A window receives this message through its WindowProc function."
     
It is this step, the WM_ENABLED message, that we are most interested in because that is the action that actually causes the TextBox BackColor and ForeColor to change to a disabled state.  Notice the last part of the WM_ENABLED documentation that states, "a window receives this message through its WindowProc function."  Thus when we derive from the normal TextBox class, we can override WndProc() to trap the desired WM_ENABLED message.  Furthermore, from within WndProc(), we can prevent WM_ENABLED from being processed normally simply by exiting WndProc() without executing "MyBase.WndProc(m)".  This is the crux of my approach as suppressing the WM_ENABLED message prevents the ForeColor/BackColor values from automatically changing to the disabled colors.  The other two steps of the EnableWindow() API process occur normally, though, resulting in a TextBox that is functionally disabled, but behaves as if it were enabled with respect to allowing changes to ForeColor/BackColor.  Visually speaking, the TextBox frame will still subdue if the BorderStyle() is set to Fixed3D.  Having suppressed WM_ENABLED, all we have to do is change the standard BackColor/ForeColor properties and we have achieved our goal of a custom colored disabled TextBox!

The suppressed WM_ENABLED approach addresses the ReadOnly() shortcoming by not giving any visual feedback with the mouse, and not allowing the TextBox to be focused.  It addresses the Paint() event approach shortcomings because the control paints itself as it normally would.  We are not attempting to recreate how the control should look with Graphics calls, the control simply draws itself as it always did before.

Presenting the DisTextBox() Control!
Public Class DisTextBox
                          Inherits System.Windows.Forms.TextBox
                      
                          Private _ForeColorBackup As Color
                          Private _BackColorBackup As Color
                          Private _ColorsSaved As Boolean = False
                          Private _SettingColors As Boolean = False
                      
                          Private _BackColorDisabled As Color = SystemColors.Control
                          Private _ForeColorDisabled As Color = SystemColors.WindowText
                      
                          Private Const WM_ENABLE As Integer = &HA
                      
                          Private Sub DisTextBox_VisibleChanged(sender As Object, e As System.EventArgs) Handles Me.VisibleChanged
                              If Not Me._ColorsSaved AndAlso Me.Visible Then
                                  ' Save the ForeColor/BackColor so we can switch back to them later
                                  _ForeColorBackup = Me.ForeColor
                                  _BackColorBackup = Me.BackColor
                                  _ColorsSaved = True
                      
                                  If Not Me.Enabled Then ' If the window starts out in a Disabled state...
                                      ' Force the TextBox to initialize properly in an Enabled state,
                                      ' then switch it back to a Disabled state
                                      Me.Enabled = True
                                      Me.Enabled = False
                                  End If
                      
                                  SetColors() ' Change to the Enabled/Disabled colors specified by the user
                              End If
                          End Sub
                      
                          Protected Overrides Sub OnForeColorChanged(e As System.EventArgs)
                              MyBase.OnForeColorChanged(e)
                      
                              ' If the color is being set from OUTSIDE our control,
                              ' then save the current ForeColor and set the specified color
                              If Not _SettingColors Then
                                  _ForeColorBackup = Me.ForeColor
                                  SetColors()
                              End If
                          End Sub
                      
                          Protected Overrides Sub OnBackColorChanged(e As System.EventArgs)
                              MyBase.OnBackColorChanged(e)
                      
                              ' If the color is being set from OUTSIDE our control,
                              ' then save the current BackColor and set the specified color
                              If Not _SettingColors Then
                                  _BackColorBackup = Me.BackColor
                                  SetColors()
                              End If
                          End Sub
                      
                          Private Sub SetColors()
                              ' Don't change colors until the original ones have been saved,
                              ' since we would lose what the original Enabled colors are supposed to be
                              If _ColorsSaved Then
                                  _SettingColors = True
                                  If Me.Enabled Then
                                      Me.ForeColor = Me._ForeColorBackup
                                      Me.BackColor = Me._BackColorBackup
                                  Else
                                      Me.ForeColor = Me.ForeColorDisabled
                                      Me.BackColor = Me.BackColorDisabled
                                  End If
                                  _SettingColors = False
                              End If
                          End Sub
                      
                          Protected Overrides Sub OnEnabledChanged(e As System.EventArgs)
                              MyBase.OnEnabledChanged(e)
                      
                              SetColors() ' change colors whenever the Enabled() state changes
                          End Sub
                      
                          Public Property BackColorDisabled() As System.Drawing.Color
                              Get
                                  Return _BackColorDisabled
                              End Get
                              Set(ByVal Value As System.Drawing.Color)
                                  If Not Value.Equals(Color.Empty) Then
                                      _BackColorDisabled = Value
                                  End If
                                  SetColors()
                              End Set
                          End Property
                      
                          Public Property ForeColorDisabled() As System.Drawing.Color
                              Get
                                  Return _ForeColorDisabled
                              End Get
                              Set(ByVal Value As System.Drawing.Color)
                                  If Not Value.Equals(Color.Empty) Then
                                      _ForeColorDisabled = Value
                                  End If
                                  SetColors()
                              End Set
                          End Property
                      
                          Protected Overrides ReadOnly Property CreateParams As System.Windows.Forms.CreateParams
                              Get
                                  Dim cp As System.Windows.Forms.CreateParams
                                  If Not Me.Enabled Then ' If the window starts out in a disabled state...
                                      ' Prevent window being initialized in a disabled state:
                                      Me.Enabled = True ' temporary ENABLED state
                                      cp = MyBase.CreateParams ' create window in ENABLED state
                                      Me.Enabled = False ' toggle it back to DISABLED state 
                                  Else
                                      cp = MyBase.CreateParams
                                  End If
                                  Return cp
                              End Get
                          End Property
                      
                          Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
                              Select Case m.Msg
                                  Case WM_ENABLE
                                      ' Prevent the message from reaching the control,
                                      ' so the colors don't get changed by the default procedure.
                                      Exit Sub ' <-- suppress WM_ENABLE message
                      
                              End Select
                      
                              MyBase.WndProc(m)
                          End Sub
                      
                      End Class

Open in new window


Spare me the details!  You've shown me the code; how do I use this thing?

If you just want to use the control without getting into the gory details, paste the code above into your project and re-build it.  You should then see the new DisTextBox control at the top of your ToolBox.  Simply use the new DisTextBox in place of the old TextBox where ever you need it.  The new DisTextBox control has two additional properties, ForeColorDisabled() and BackColorDisabled(), that allow you to set your desired colors for when the TextBox is in a disabled state.  I've set the default ForeColorDisabled() value to Black, which makes it much easier to read.  The default BackColorDisabled() value is the same as the normal disabled TextBox.  To disable the control, use the inherited Enabled() property as usual:
	DisTextBox1.Enabled = False ' Disable the control (automatically switches to disabled colors)
                      	DisTextBox1.Enabled = True ' Enable the control (automatically switches to enabled colors)

Open in new window


If you're interested in the implementation itself, please continue reading below.


"The Devil is in the Details" --Anonymous

     
Swapping Enabled and Disabled Colors

The first two variables in the class are of type Color and are called "_ForeColorBackup" and "_BackColorBackup".  Since the control never really disables, and simply stays in the enabled state, we are actually changing the normal ForeColor and BackColor Properties when we want to display the disabled colors.  This means we must save the current ForeColor and BackColor values, or they will be lost when we change them to the disabled colors.  The "_ForeColorBackup" and "_BackColorBackup" variables, then, do exactly what their names suggest, and store backups of the enabled colors so we can later switch back to them when necessary.

The next variable is the "_ColorsSaved" boolean flag, which starts out set to false.  It indicates whether the initial ForeColor and BackColor values have been saved to the backup variables.  These colors are saved in the VisibleChanged() event where the flag is toggled to true.  Also in the VisibleChanged() event, if the control starts in a disabled state, we must toggle the Enabled() property to True and then back to False to correctly initialize the control.  Failure to do so results in the TextBox being displayed in the disabled colors, but still allowing user interaction in an enabled state.  This initial backup of the colors, and toggling of the enabled state, only occur once when the control first appears.

Declared next is another boolean flag called "_SettingColors".  It is initially false, and only becomes true when the colors are being set by the SetColors() method.  Since enabled and disabled colors are both displayed with the normal ForeColor/BackColor properties, we need to distinguish whether those values are being set externally, or as a result of being internally changed by the SetColors() method.  This distinction is important because changing the ForeColor/BackColor properties will trigger the OnForeColorChanged()/OnBackColorChanged() methods, which is where we save the new values to the backup variables mentioned previously.  If we are in a disabled state, the new values placed into the the ForeColor/BackColor properties should not be saved, as doing so would cause the enabled colors to be lost.  To prevent this from happening, backups of the colors are only made when "_SettingColors" is false.

The SetColors() method is used to display the correct set of colors based on whether the control is currently enabled or not.  It is a simple routine that first verifies that the enabled colors have been saved to the backup variables, then sets the ForeColor/BackColor properties to the backup colors if the control is enabled, and to the desired disabled colors when the control is in a disabled state.  It wraps the color setting logic in a block that toggles "_SettingColors" to true and then back to false.  When the Enabled() property of the control is set, the OnEnabledChanged() method fires.  This method simply invokes the base class handler, then calls SetColors() to update the control with the desired state colors.

Wrapping up the code related to swapping enabled and disabled colors are the variables that store the desired disabled colors.  These two private variables are called "_BackColorDisabled" and "_ForeColorDisabled".  Not surprisingly, they are wrapped in public properties called BackColorDisabled() and ForeColorDisabled().  When those two properties are set, their new values are stored in their respective private variables and then the control is updated (if necessary) with a call to the SetColors() method.

CreateParams() and WndProc()

The CreateParams() override is necessary only for when DisTextBox() starts out in an initially disabled state.  When starting disabled, we have to switch the Enabled() property to true, call the base class CreateParams() method, then switch Enabled() back to false.  Without this override switcheroo, the control will default to the standard disabled state and ignore our custom disabled colors.

Lastly we come to the WndProc() method which processes all standard windows messages for our control.  Remember, the whole premise of this disabled TextBox being able to have custom colors was trapping the WM_ENABLE message and suppressing it.  That is exactly all this method does.  All other messages are processed normally by calling "MyBase.WndProc(m)".  The WM_ENABLE message is suppressed by simply exiting the sub preventing the base class WndProc() from executing.

If any of the code seems pointless, it's probably related to getting the control to work when starting in a disabled state.

Conclusion

That's it!  Hopefully I've explained the concept and code well enough for everyone to understand it; or at least know why the code was put in place.  The DisTextBox() should give you the ability to customize both the fore and back colors of the standard TextBox when it is in a disabled state.
4
12,188 Views
Mike TomlinsonHigh School Computer Science, Computer Applications, Digital Design, and Mathematics Teacher
CERTIFIED EXPERT

Comments (1)

Commented:
Very useful and out of the box ! Thanks!

Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.