Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations Mike Lewis on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Getting behind Windows forms system colors 1

Status
Not open for further replies.

SBendBuckeye

Programmer
May 22, 2002
2,166
US
Hello All,

Windows has some defined colors (eg Window) that change depending upon the context. As an example, if you change a textbox to readonly, the forecolor and backcolor properties do NOT change, even though the appearance of the control on the form does change.

Here is the problem:

Readonly - dark forecolor on disabled backcolor
Disabled - light forecolor on disabled backcolor

ComboBoxes do not have a read only property but I want to simulate the coloring because the dark forecolor is easier to read and to be consistent with ReadOnly textboxes which surround it.

Any ideas and/or suggestions about how to do this? Thanks!

Have a great day!

j2consulting@yahoo.com
 
I hate that too:

Code:
   Public Class MyComboBox

        'new combobox that can be set to readonly so that its forcolor is black whien not in use.
        Inherits System.Windows.Forms.ComboBox
        ' make a variable to store readonly status.
        Private m_ReadOnly As Boolean = False
        ' we want to catch when readonly gets changed:
        Public Event ReadonlyChanged As EventHandler

        Public Property [ReadOnly]() As Boolean
            'if something wants to know the controls readonly status then return it:
            Get
                Return m_ReadOnly
            End Get
            'if something wants to change the Readonly status then see if it is a new value
            'if it is then chnage the readonly status and call OnReadonlyChanged
            Set(ByVal Value As Boolean)
                If m_ReadOnly <> Value Then
                    m_ReadOnly = Value
                    OnReadOnlyChanged()
                End If
            End Set
        End Property

        Protected Overridable Sub OnReadOnlyChanged()
            'we don't want the control to be a tab stop when it is read only
            MyBase.TabStop = Not m_ReadOnly
            'for some reason it keeps getting its text selected when it is set readonly. 
            'this sets it so that no text is selected whenever the readonly status changes:
            Me.SelectionLength = 0
            If Me.ReadOnly Then
                'change the backcolor so that it looks disabled when set to readonly
                Me.BackColor = System.Drawing.SystemColors.Control
            Else
                'change the backcolor so that it looks enabled when it is.
                Me.BackColor = System.Drawing.SystemColors.Window
            End If
            'fire the event ReadOnlyChanged when the readonly setting is changed
            'you can then handle the event comboox.readonlychanged in the forms code.
            RaiseEvent ReadonlyChanged(Me, New EventArgs)

        End Sub

        Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
            'wndproc processes the messages sent to a window
            If m_ReadOnly Then
                'see if the message sent is WM_LBUTTONDOWN (0x0201) or WM_LBUTTONDBLCLK (0x0203)
                'the constant value for this can be found in the file WinUser.h in the SDK.
                '(the msdn library says to look in windows.h bt its not there...)
                'Read about these messages in the msdn library - filter for windows SDK
                If m.Msg = &H201 OrElse m.Msg = &H203 Then m.Msg = &H0 'change the message to WM_NULL - this gets ignored
            End If
            'process the messages normally - call wndproc proper with the message
            MyBase.WndProc(m)
        End Sub

    End Class
 
Hello jo0ls,

Very nice work, well deserving of a star. I have extended a handful of controls and it is encouraging to see someone else's work that is pretty similar to my own way of doing it. Maybe someday I'll yet get the hang of all of this.

If you've never been to CodeProject.com, they have some great articles over there. Drop me a line at my sigature if you are doing a lot of control extending. It would be interesting to compare notes.

Thanks again!

Have a great day!

j2consulting@yahoo.com
 
Hello again jo0ls,

Thanks again for the excellent code! It worked the first time. I tweaked it just a bit as below in case anyone is interested:

1. Saved and restored original TabStop value instead of using Not ReadOnly
Code:
Private m_TabStop As Boolean

Protected Overrides Sub InitLayout()
    MyBase.InitLayout()
    m_TabStop = Me.TabStop
End Sub

Protected Overridable Sub OnReadOnlyChanged()
    'Not a TabStop when ReadOnly, otherwise restore original value
    If Me.ReadOnly Then
        MyBase.TabStop = False
    Else
        MyBase.TabStop = m_TabStop
    End If
    'Rest of Code
End Sub
2. Added Attributes to the ReadOnly property
Code:
<Category("Behavior"), _
 DefaultValue(False), _
 Description("Implemented similarly to TextBox.ReadOnly property")> _
Public Property [ReadOnly]() As Boolean
The statement Imports System.ComponentModel is required to use attributes. Once set, you can get the list by typing a left angle bracket < (left square bracket [ in C#) - intellisense is a wonderful thing.

If a Category attribute is not specified, then custom properties get lumped into Misc when Category view is specified. In this case, I used Behavior because that is where the TextBox ReadOnly property is listed. For new custom properties, I will use something like <Category("CustomProperties")> to group them all togoether.

DefaultValue defines, but does NOT set the default value. If a non-default value is specified, it will show up in the property browser as Bold. As an aside, binding will NOT work if an explicit default value is not specified.

Description is what shows up in the bottom of the property browser when you click a property.

A couple other attributes of interest include ReadOnly and Browsable(False) which makes a Run Time only property invisible at Design Time. If you include Browsable(False) and Description, it will show up in the property browser as ReadOnly in Design view.

3. Changed WndProc to use Case statement and Defined Constants to enhance readability
Code:
'Found in C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\PlatformSDK\Include\winuser.h
Private Const WM_LBUTTONDOWN As Long = &H201
Private Const WM_LBUTTONDBLCLK As Long = &H203

'WndProc processes the messages sent to a window
Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
    If m_ReadOnly Then
        Select Case m.Msg
            Case WM_LBUTTONDOWN, WM_LBUTTONDBLCLK
                'Ignore the message
                m.Msg = &H0
        End Select
    End If
    MyBase.WndProc(m)
End Sub
Enjoy!



Have a great day!

j2consulting@yahoo.com
 
I more tweak (sigh). The ReadOnly property is actually called before InitLayout so it cannot be set in InitLayout. Instead, I added this code:
Code:
Private m_TabStop As Boolean
Private m_FirstTime As Boolean = True

Protected Overridable Sub OnReadOnlyChanged()
    'Save original TabStop state
    If m_FirstTime Then
        m_FirstTime = False
        m_TabStop = Me.TabStop
    End If
    'Rest of Code
End Sub

Have a great day!

j2consulting@yahoo.com
 
I also found that you can still move into the combobox with arrow keys when it is read only. The only way I found to get around it so far was to trap the arrow keypresses on the controls next to the comboboxes. And to do that you need to mess with IsInputKey.
Code:
    Public Class MyButton
        'new button that intercepts arrow key events 
        'used for controls placed next to  cboboxes as normally you can move into them
        'with keys even if tabstop is disabled.
        Inherits Button

        Protected Overrides Function IsInputKey(ByVal keyData As System.Windows.Forms.Keys) As Boolean
            'check what key has been pressed
            'if it is an arrow key then make IsInputKey true so that
            'we can handle the keypress in the forms code with the keydown event
            '(normally arrow keys do NOT fire the keydown event as the are not inputkeys)
            If keyData = Keys.Right OrElse keyData = Keys.Down _
            OrElse keyData = Keys.Up OrElse keyData = Keys.Left Then IsInputKey = True
        End Function

    End Class

And then in the form:

Code:
   Private Sub check_keypresses(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) _
            Handles Button1.KeyDown, Button2.KeyDown, Button3.KeyDown
        ' Here we intercept both buttons keydown events.
        ' Arrow keys ARE passed to this sub as the custom button has made them input keys.
        '
        ' Check if it is a down or right arrow key: 

        If e.KeyCode = Keys.Down OrElse e.KeyCode = Keys.Right Then
            'if it is then move forwards to the next control that is a tabstop:
            SelectNextControl(sender, True, True, True, True)
            Exit Sub
        End If
        'check if it is left or up
        If e.KeyCode = Keys.Left OrElse e.KeyCode = Keys.Up Then
            'and move backwards to the next control that is a tabstop.
            SelectNextControl(sender, False, True, True, True)
        End If
    End Sub

I've tried all sorts of api calls and wndproc message tricks but that is the only way I can get it to skip the control when an arrow key is pressed.
 
I'm not seeing using the arrow keys to enter from other controls but it did alert me to the arrows being able to navigate the list. Another issue is that Ctl+X and Ctl+V continue to work. In each of the above situations, I overrode OnKeyDown. With Ctl+X and Ctl+Y I had to use a System.Timer to reset the Text property as it does not appear to be trappable.
Code:
'Handles resetting text if Ctl+X (Cut) or Ctl+V (Paste) detected on ReadOnly control
Private m_SystemTimer As System.Timers.Timer

'Check for Up and Down arrow keys as well as Delete and Alt+X (Cut) and Alt+V (Paste)
Protected Overrides Sub OnKeyDown(ByVal e As System.Windows.Forms.KeyEventArgs)
    If Me.ReadOnly Then
        If e.KeyCode = Keys.Up OrElse e.KeyCode = Keys.Down Then
            e.Handled = True
        ElseIf e.KeyCode = Keys.Delete Then
            e.Handled = True
        ElseIf e.KeyCode = Keys.ControlKey Then
            'Save Text in case Ctl+X (Cut) or Ctl+V (Paste)
            m_SaveText = Me.Text
        ElseIf e.KeyCode = Keys.X OrElse e.KeyCode = Keys.V Then
            'Fire timer to reset Text
            CreateSystemTimer()
        End If
    End If
End Sub 

'Used to reset Text when Ctl+X or Ctl+V is detected on ReadOnly control
Private Sub CreateSystemTimer()
    m_SystemTimer = New System.Timers.Timer
    With m_SystemTimer
        .Interval = 2
        .Enabled = True
        AddHandler .Elapsed, _
           New System.Timers.ElapsedEventHandler(AddressOf Me.SystemTimerElapsed)
    End With
End Sub

'Used to reset Text when Ctl+X or Ctl+V is detected on ReadOnly control
Private Sub SystemTimerElapsed(ByVal sender As System.Object, _
                               ByVal e As System.Timers.ElapsedEventArgs)
    Me.Text = m_SaveText
    m_SystemTimer.Enabled = False
    m_SystemTimer.Dispose()
End Sub
I'm not sure about the .Dispose call. This could potentially be called mutliple times and it is a one time only firing for each timer, but I'm still fuzzy on the indeterminate garbage collection in .Net.


Have a great day!

j2consulting@yahoo.com
 
you can get the keys by overriding processcmdkey
so for paste keys:

Code:
 Protected Overrides Function ProcessCmdKey(ByRef m As System.Windows.Forms.Message, _ 
 ByVal keyData As System.Windows.Forms.Keys) As Boolean


        Const WM_KEYDOWN As Integer = &H100
        Const WM_SYSKEYDOWN As Integer = &H104
        If (m.Msg = WM_KEYDOWN) OrElse (m.Msg = WM_SYSKEYDOWN) Then
            
            Select Case keyData
  Case Keys.Control Or Keys.V
                    'Paste operation ctrl + v
'if you return true here then the keypress is ignored.
 Case Keys.Shift Or Keys.Insert
                    'Paste operation shift + insert
  End Select
        End If
    End Function

You can also catch WM_PASTE in wndproc:
(and ban the context menu, but we blocked right clicks anyway)
Code:
 Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)

        'another trap - will catch paste from context menu and disallow the drop
        If m.Msg = &H302 Then  'WM_PASTE
            'block paste then
            'set m.msg to WM_NULL 
            m.msg = &H0
        End If

        If m.Msg = &H7B Then 'WM_CONTEXTMENU
            'block the right click context menu:
                m.Msg = 0
        End If
        MyBase.WndProc(m)
    End Sub

similarly WM_CUT (&H300), WM_COPY (&H301)
WM_CLEAR (&H303) - delete key
WM_UNDO (&H304) - ctrl-z


 
<I also found that you can still move into the combobox with arrow keys when it is read only.

I don't have a great deal that I can contribute to this discussion, but read only is intended to still allow you to, say copy and paste the contents. As such, the tabindex functionality isn't disabled. However, if a control's enabled is set to false, it's no longer focusable. jo0ls, I wonder if you could do some of the same cool things you're doing here with coloration, but on a control with enabled set to false? That might obviate the need to mess with keystroke capturing and redirecting to make the control non-focusable.

Bob
 
Hello jo0ls,

WM_CUT, WM_PASTE, WM_RBUTTONDOWN, WM_CONTEXTMENU, etc do not appear to work in WndProc when I click in the comboboxes text portion. Any ideas on this? Thanks!


Have a great day!

j2consulting@yahoo.com
 
I just found that out too. The combobox is apparently two controls. You have a listbox like control with the text, and the arrow. So wndproc is overriding the drop arrow. Urgh.
This means you can use wndproc to stop the drop by blocking the mouse on the drop arrow. But it doesn't work to block cut/copy/paste.
If you catch WM_Contextmenu you will see it fires when you right click on the drop-arrow.
I think the best that can be done is to disable the drop in wnd proc, and disable keypresses in processcmdkey. I just swapped from trying to make it readonly to overriding onenabledchanged after reading BobRodes post...

Ahhh...
you can disable the context menu completely by setting it to a new, blank context menu.
.. and save the original one and swap them when the enabled property changes.

Code:
Public Class myCombobox
    Inherits ComboBox
    Private _enabledContextMenu As ContextMenu
    Private _diabledContextMenu As New ContextMenu
    Public Sub New()
        _enabledContextMenu = Me.ContextMenu
    End Sub

    Protected Overrides Sub OnEnabledChanged(ByVal e As System.EventArgs)
        If Me.Enabled Then
            Me.BackColor = System.Drawing.SystemColors.Window
            Me.ContextMenu = _enabledContextMenu
        Else
            Me.BackColor = System.Drawing.SystemColors.Control
            Me.DroppedDown = False
            Me.ContextMenu = _diabledContextMenu
        End If
    End Sub

    Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
        'wndproc processes the messages sent to a window
        If Not Me.Enabled Then
            'WM_LBUTTONDOWN (0x0201)
            'WM_LBUTTONDBLCLK (0x0203)
            If m.Msg = &H201 OrElse m.Msg = &H203 Then
                'reset msg to WM_NULL
                m.Msg = &H0
            End If
        End If
        MyBase.WndProc(m)
    End Sub

    Protected Overrides Function ProcessCmdKey(ByRef m As System.Windows.Forms.Message, _
     ByVal keyData As System.Windows.Forms.Keys) As Boolean
        If Me.Enabled Then
            Return False 'allow normal behaviour
        End If
        Const WM_KEYDOWN As Integer = &H100
        Const WM_SYSKEYDOWN As Integer = &H104
        If (m.Msg = WM_KEYDOWN) OrElse (m.Msg = WM_SYSKEYDOWN) Then
            Select Case keyData
                Case Keys.Shift Or Keys.C
                    'copy operation shift+c
                    Return False 'allow, if you want it like a readonly textbox
                Case Keys.Control Or Keys.Insert
                    'copy operation ctrl + insert
                    Return False
                Case Else
                    'don't let the user type into the control.
                    'or cut copy etc etc etc
                    Return True
            End Select
        End If
    End Function


End Class

lol...
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top