1

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
Jimi
  • 29,621
  • 8
  • 43
  • 61
Simon
  • 1,384
  • 2
  • 10
  • 19
  • *"it's not working"*. That is never an adequate description of a problem. You need to describe exactly what you expect to happen and exactly what does happen. Maybe we can work it out from reading the code or testing it ourselves but we shouldn't have to do that. For instance, some people might be viewing this question on their phone. ALWAYS provide a FULL and CLEAR explanation of the problem. – jmcilhinney Aug 27 '21 at 00:57
  • @jmcilhinney Actually it is for this scenario because there is no error. It builds perfectly fine and once I load the dll it appears fine in the toolbox, so do all the properties. As also stated in the question I am trying to create a customizable border for the textbox. All of this is stated in the question so I'm not sure what else you expect. – Simon Aug 27 '21 at 01:14
  • *"it is for this scenario because there is no error"*. No it isn't. It never is. If you know it's not working then there must be a specific reason for that. Describe that reason. What are you seeing that you don't expect? What are you not seeing that you do expect? If an error message was required then you wouldn't know that there was an issue at all. Do you go to the doctor and tell them that you can't describe your symptoms because there's no error message? Do you go to the mechanic and tell them that you can't describe what's wrong with your car because there's no error message? Think! – jmcilhinney Aug 27 '21 at 01:27

1 Answers1

1

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
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • Thank you for your help. This definitely works. One thing though is the original border still shows up. Is there a way we can get rid of it? When I loaded I saw the BorderStyle wasn't there (the normal one with None,Single & Fixed3d), so I changed the `BorderStyle` in the code as I thought maybe it was conflicting with it. It seemed to be as the original borderstyle showed up in the properties now but upon selecting 'None' my coloured border disappears as well. The coloured border will only show up on `Fixed3d` borderstyle. – Simon Aug 27 '21 at 04:28
  • Ok I figured I could just add a new property for the bordercolor of the rest. E.g. If I select border style of bottom, then the other 3 sides are coloured to another colour which defaults to the background colour. This worked! However, the original borderstyle is still on 'Fixed3d' which leaves a white line around for the 3d look I guess. Gotta figure out how to make it work with single border or get rid of the original border entirely. – Simon Aug 27 '21 at 06:48
  • I've updated the class definition. Use this update to replace the previous code (all of it). -- I've added a note about the base class `BorderStyle`: it needs to be the default `Fixed3D`. I've added a piece of code that removes the standard gray-ish border before a new border is drawn. – Jimi Aug 27 '21 at 10:43