Simon
Simon

Reputation: 1375

Create a custom TextBox to add to the Toolbox and use in a Form

I'm very new to Visual Studio and I'm wanting to create my own TextBox that can have customizable borders (and maybe other stuff in future).
Right now, my method is merging a couple things together to sort of Frankenstein my own creation.

I've created a Class, built it, browsed and added it to the Toolbox in my Form (another project) and placed it in the Form. All the controls show up in the properties and the TextBox loads when I build and open the Form (no errors) but of course, it just shows up as a regular TextBox (no colored border).

I can't figure out why it's not working. Is it the rectangle that I create the problem, or some sort of painting problem, something to do with Usercontrols or something else that I'm missing?

Code:

Imports System.Windows.Forms
Imports System.Drawing

Public Class CustomTextBox
    Inherits System.Windows.Forms.TextBox
    Public Enum BorderSideOptions
        Left
        Right
        Top
        Bottom
        All
    End Enum

    Dim BrdrColor As Color = Color.Blue
    Dim BrdrSize As Single = 1
    Dim BrdrStyle As ButtonBorderStyle = ButtonBorderStyle.Solid
    Dim BorderSide As BorderSideOptions = BorderSideOptions.All

    Public Sub New()
        Me.Width = 120
        Me.Height = 20
        Me.BackColor = Color.White
        Me.ForeColor = Color.Black
    End Sub


    Property BorderSides As BorderSideOptions
        Get
            Return BorderSide
        End Get
        Set(value As BorderSideOptions)
            BorderSide = value
        End Set
    End Property

    Property BorderColor() As Color
        Get
            Return BrdrColor
        End Get
        Set(value As Color)
            BrdrColor = value
        End Set
    End Property

    Property BorderSize() As Single
        Get
            Return BrdrSize
        End Get
        Set(value As Single)
            BrdrSize = value
        End Set
    End Property

    Overloads Property BorderStyle() As ButtonBorderStyle
        Get
            Return BrdrStyle
        End Get
        Set(value As ButtonBorderStyle)
            BrdrStyle = value
        End Set
    End Property

    Private Sub CustomTextBox_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
        Dim txtRect As Rectangle, B As Color = Color.Black
        txtRect = New Rectangle(Location, Size)

        Select Case BorderSides
            Case BorderSideOptions.All
                ControlPaint.DrawBorder(e.Graphics, txtRect, BrdrColor, BrdrSize, BrdrStyle, BrdrColor, BrdrSize, BrdrStyle, BrdrColor, BrdrSize, BrdrStyle, BrdrColor, BrdrSize, BrdrStyle)
            Case BorderSideOptions.Bottom
                ControlPaint.DrawBorder(e.Graphics, txtRect, B, 0, ButtonBorderStyle.None, B, 0, ButtonBorderStyle.None, B, 0, ButtonBorderStyle.None, BrdrColor, BrdrSize, BrdrStyle)
            Case BorderSideOptions.Left
                ControlPaint.DrawBorder(e.Graphics, txtRect, BrdrColor, BrdrSize, BrdrStyle, B, 0, ButtonBorderStyle.None, B, 0, ButtonBorderStyle.None, B, 0, ButtonBorderStyle.None)
            Case BorderSideOptions.Right
                ControlPaint.DrawBorder(e.Graphics, txtRect, B, 0, ButtonBorderStyle.None, B, 0, ButtonBorderStyle.None, BrdrColor, BrdrSize, BrdrStyle, B, 0, ButtonBorderStyle.None)
            Case BorderSideOptions.Top
                ControlPaint.DrawBorder(e.Graphics, txtRect, B, 0, ButtonBorderStyle.None, BrdrColor, BrdrSize, BrdrStyle, B, 0, ButtonBorderStyle.None, B, 0, ButtonBorderStyle.None)
        End Select
    End Sub
End Class

Upvotes: 1

Views: 1066

Answers (1)

Jimi
Jimi

Reputation: 32278

Some modifications and a few pointers:

  • You have a Paint event handler that comes from nowhere in the OP; that won't work and, in any case, with a Custom Control you override the methods that raise the events (OnPaint(), in this case), you don't subscribe to the events
  • The Border thickness is very limited, 1 or 2 pixels: this is the size of the non-client area (if you need a thicker border, you need an UserControl that hosts a TextBox and expands outwards when the thickness is > 2).
    The property setter needs to consider this limitation. There's a Min/Max check for this in the modified code.
  • The BorderSize of the base class should be set to the default BorderStyle.Fixed3D only. You can set other styles, but: 1) there's actually no reason 2) you'd need to also handle WM_PAINT to draw inside the client area (not recommended and, as mentioned, not exactly useful here).
  • Each time you change a property value that affects the graphics, you need to invalidate the Control for the effect to be applied right away, both in the Designer and at run-time.
    The modified code uses Parent?.Invalidate(Bounds, True) (forces the Parent of the Control, if any, to invalidate the section of its Client Area where this Control is positioned. The True argument instructs to invalidate the children).
    If there's no Parent, the Custom Control most probably also has no Handle, at this time, so no painting actually occurs.
    This works in both contexts: when any of the affected properties is changed, in the PropertyGrid or in code, the new state is applied and becomes immediately visible (as mentioned, if the Control has a Handle).
  • Custom Properties that have a default value should be decorated with the DefaultValue Attribute. The default property value is not serialized in the Designer (in the Designer.vb / Designer.cs file) and can be used in custom Designers, Type converters and other stuff that are beyond the scope of this post
  • Setting Option Strict to ON is always a good idea (check your code when you do).

  • The TextBox Control doesn't raise Paint events (without tweaking it, not exactly useful here anyway), so overriding OnPaint() does nothing
  • Since you want to draw the Border outside the Client Area, you have trap the WM_NCPAINT message, which is sent to the Control when its non-client area needs repainting
  • You can draw a Border (or anything else really) inside the Client Area by handling WM_PAINT.
    See for example: TextBox with dotted lines for typing
  • To handle these messages, you have to override the Control's WndProc (you may have seen LRESULT CALLBACK WindowProc(...) somewhere else)
  • To draw on the non-client area, you need the Device Context of this Control. WM_NCPAINT passes a pointer to the clipping region in WParam, but it's simpler to use the GeWindowDC() function, then derive a Graphics object from the returned HDC using the managed Graphics.FromHdc() method. The HDC needs to be released calling ReleaseDC()

If you want to create a Custom Control that can be used in any other Solution / Project, build a Class Library and add this or other Controls to this assembly (pick the namespaces with care).
Set the Target CPU in Project->Properties->Compile to AnyCPU.


Imports System.ComponentModel
Imports System.Drawing
Imports System.Runtime.InteropServices
Imports System.Windows.Forms

<DesignerCategory("Code")>
Public Class CustomTextBox
    Inherits TextBox

    Private Const WM_NCPAINT As Integer = &H85
    Private m_BorderColor As Color = Color.Blue
    Private m_BorderSize As Integer = 1
    Private m_BorderStyle As ButtonBorderStyle = ButtonBorderStyle.Solid
    Private m_BorderSides As BorderSideOptions = BorderSideOptions.All

    Public Sub New()
    End Sub

    <DefaultValue(BorderSideOptions.All)>
    Public Property BorderSides As BorderSideOptions
        Get
            Return m_BorderSides
        End Get
        Set
            If m_BorderSides <> Value Then
                m_BorderSides = Value
                Parent?.Invalidate(Bounds, True)
            End If
        End Set
    End Property

    <DefaultValue(KnownColor.Blue)>
    Public Property BorderColor As Color
        Get
            Return m_BorderColor
        End Get
        Set
            If m_BorderColor <> Value Then
                m_BorderColor = Value
                Parent?.Invalidate(Bounds, True)
            End If
        End Set
    End Property

    <DefaultValue(1)>
    Public Property BorderSize As Integer
        Get
            Return m_BorderSize
        End Get
        Set
            Dim newValue = Math.Max(Math.Min(Value, 2), 1)
            If m_BorderSize <> newValue Then
                m_BorderSize = newValue
                Parent?.Invalidate(Bounds, True)
            End If
        End Set
    End Property

    <DefaultValue(ButtonBorderStyle.Solid)>
    Public Overloads Property BorderStyle As ButtonBorderStyle
        Get
            Return m_BorderStyle
        End Get
        Set
            If m_BorderStyle <> Value Then
                m_BorderStyle = Value
                Parent?.Invalidate(Bounds, True)
            End If
        End Set
    End Property

    Protected Overrides Sub OnHandleCreated(e As EventArgs)
        MyBase.OnHandleCreated(e)
        MyBase.BorderStyle = Windows.Forms.BorderStyle.Fixed3D
    End Sub

    Protected Overrides Sub WndProc(ByRef m As Message)
        MyBase.WndProc(m)
        Select Case m.Msg
            Case WM_NCPAINT
                If Not IsHandleCreated Then Return
                Dim rect = New Rectangle(0, 0, Width, Height)
                Dim hDC = GetWindowDC(Handle)
                Try
                    Using g = Graphics.FromHdc(hDC),
                       p As New Pen(BackColor, 2)
                        g.DrawRectangle(p, rect)
                        Select Case BorderSides
                            Case BorderSideOptions.All
                                ControlPaint.DrawBorder(g, rect, m_BorderColor, m_BorderSize, m_BorderStyle, m_BorderColor, m_BorderSize, m_BorderStyle, m_BorderColor, m_BorderSize, m_BorderStyle, m_BorderColor, m_BorderSize, m_BorderStyle)
                            Case BorderSideOptions.Bottom
                                ControlPaint.DrawBorder(g, rect, Nothing, 0, 0, Nothing, 0, 0, Nothing, 0, 0, m_BorderColor, m_BorderSize, m_BorderStyle)
                            Case BorderSideOptions.Left
                                ControlPaint.DrawBorder(g, rect, m_BorderColor, m_BorderSize, m_BorderStyle, Nothing, 0, 0, Nothing, 0, 0, Nothing, 0, 0)
                            Case BorderSideOptions.Right
                                ControlPaint.DrawBorder(g, rect, Nothing, 0, 0, Nothing, 0, 0, m_BorderColor, m_BorderSize, m_BorderStyle, Nothing, 0, 0)
                            Case BorderSideOptions.Top
                                ControlPaint.DrawBorder(g, rect, Nothing, 0, 0, m_BorderColor, m_BorderSize, m_BorderStyle, Nothing, 0, 0, Nothing, 0, 0)
                        End Select
                    End Using
                Finally
                    ReleaseDC(Handle, hDC)
                End Try
                m.Result = IntPtr.Zero
        End Select
    End Sub

    ' This could use a file of its own
    Public Enum BorderSideOptions
        Left
        Right
        Top
        Bottom
        All
    End Enum

    ' Native methods
    <DllImport("user32")>
    Private Shared Function GetWindowDC(hwnd As IntPtr) As IntPtr
    End Function

    <DllImport("user32")>
    Private Shared Function ReleaseDC(hwnd As IntPtr, hDc As IntPtr) As Integer
    End Function

End Class

Upvotes: 1

Related Questions