<

[Product update] Infrastructure Analysis Tool is now available with Business Accounts.Learn More

x

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

Published on
19,936 Points
10,036 Views
4 Endorsements
Last Modified:
Awarded
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
1 Comment

Expert Comment

by:RIAS
Very useful and out of the box ! Thanks!
0

Featured Post

Introduction to Web Design

Develop a strong foundation and understanding of web design by learning HTML, CSS, and additional tools to help you develop your own website.

A query can call a function, and a function can call Excel, even though we are in Access. This is Part 2, and steps you through the VBA that "wraps" Excel functionality so we can use its worksheet functions in Access. The declaration statement de…
Overview of OneDrive and collaboration.

Keep in touch with Experts Exchange

Tech news and trends delivered to your inbox every month