1

I've made a custom TextBox class to have a text box with custom border in my application, based on this other SO post. If I set any of the new custom properties in the form designer, they appear momentarily, until I change the control focus, and when I run the application, the new border settings are not displayed. I did update my form's InitializeComponent method, so that the text box initializes a new BorderedTextBox instead of TextBox. Does anyone know what's wrong here?

public class BorderedTextBox : TextBox
{
    private Color _borderColor = Color.Black;
    private int _borderWidth = 2;
    private int _borderRadius = 5;

    public BorderedTextBox() : base()
    {
        InitializeComponent();
        this.Paint += this.BorderedTextBox_Paint;
    }

    public BorderedTextBox(int width, int radius, Color color) : base()
    {
        this._borderWidth = Math.Max(1, width);
        this._borderColor = color;
        this._borderRadius = Math.Max(0, radius);
        InitializeComponent();
        this.Paint += this.BorderedTextBox_Paint;
    }

    public Color BorderColor
    {
        get => this._borderColor;
        set
        {
            this._borderColor = value;
            DrawTextBox();
        }
    }

    public int BorderWidth
    {
        get => this._borderWidth;
        set
        {
            if (value > 0)
            {
                this._borderWidth = Math.Min(value, 10);
                DrawTextBox();
            }
        }
    }

    public int BorderRadius
    {
        get => this._borderRadius;
        set
        {   // Setting a radius of 0 produces square corners...
            if (value >= 0)
            {
                this._borderRadius = value;
                this.DrawTextBox();
            }
        }
    }

    private void BorderedTextBox_Paint(object sender, PaintEventArgs e) => DrawTextBox(e.Graphics);

    private void DrawTextBox() => this.DrawTextBox(this.CreateGraphics());

    private void DrawTextBox(Graphics g)
    {
        Brush borderBrush = new SolidBrush(this.BorderColor);
        Pen borderPen = new Pen(borderBrush, (float)this._borderWidth);
        Rectangle rect = new Rectangle(
            this.ClientRectangle.X,
            this.ClientRectangle.Y,
            this.ClientRectangle.Width - 1,
            this.ClientRectangle.Height - 1);

        // Clear text and border
        g.Clear(this.BackColor);

        // Drawing Border
        g.DrawRoundedRectangle(
            borderPen,
            (0 == this._borderWidth % 2) ? rect.X + this._borderWidth / 2 : rect.X + 1 + this._borderWidth / 2,
            rect.Y,
            rect.Width - this._borderWidth,
            (0 == this._borderWidth % 2) ? rect.Height - this._borderWidth / 2 : rect.Height - 1 - this._borderWidth / 2,
            (float)this._borderRadius);
    }

    #region Component Designer generated code
    /// <summary>Required designer variable.</summary>
    private System.ComponentModel.IContainer components = null;

    /// <summary>Clean up any resources being used.</summary>
    /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
            components.Dispose();

        base.Dispose(disposing);
    }

    /// <summary>Required method for Designer support - Don't modify!</summary>
    private void InitializeComponent() => components = new System.ComponentModel.Container();
    #endregion
}
Jim Fell
  • 13,750
  • 36
  • 127
  • 202
  • Just to clarify, the control does not perform as expected, or the properties become inaccessible? – Trey Oct 02 '18 at 17:39
  • TextBox is legacy and not willing to support drawing. – TaW Oct 02 '18 at 17:44
  • @Trey I can still access the properties. The custom border is not rendering. – Jim Fell Oct 02 '18 at 17:45
  • @TaW That's good to know. Can you advise on what should be used? – Jim Fell Oct 02 '18 at 17:45
  • 1
    I disagree with @TaW , but you should be overriding the drawn method...its been awhile but I believe that is the issue. this.Paint += this.BorderedTextBox_Paint; is wrong, you want to override Paint – Trey Oct 02 '18 at 17:50
  • 1
    If possible you should nest the tbox borderless in a panel and draw the border there – TaW Oct 02 '18 at 18:03
  • 1
    @Trey: Look [here](https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.textbox?view=netframework-4.7.2). Neither `Paint` nor `OnPaint` are documented to work. And they won't do so reliably even though you can code them. – TaW Oct 02 '18 at 18:10
  • @TaW It's a bit more work than I was hoping for, but a rough test of placing a borderless TextBox on top of a Panel does seem to do what I need. Feel free to post it as an answer. Thanks. – Jim Fell Oct 02 '18 at 18:20
  • You need to override `WndProc` and in `WM_PAINT` message do your drawings. – γηράσκω δ' αεί πολλά διδασκόμε Oct 02 '18 at 18:24

2 Answers2

3

You need to override WndProc:

private const int WM_PAINT = 0x000F;

protected override void WndProc( ref Message m ) {

    if(m.Msg == WM_PAINT ) {

            base.WndProc( ref m );

            Graphics gr = this.CreateGraphics();

            //draw what you want


            gr.Dispose();

            return;
        }

        base.WndProc( ref m );
    }

Works fine without any issues. It draws in client area though. A complete version drawing a custom border, textbox need to have border:

[DllImport( "user32.dll" )]
static extern IntPtr GetWindowDC( IntPtr hWnd );

[DllImport( "user32.dll" )]
static extern bool ReleaseDC( IntPtr hWnd, IntPtr hDC );

[DllImport( "gdi32.dll" )]
static extern bool FillRgn( IntPtr hdc, IntPtr hrgn, IntPtr hbr );

[DllImport( "gdi32.dll" )]
static extern IntPtr CreateRectRgn( int nLeftRect, int nTopRect, int nRightRect,
        int nBottomRect );

[DllImport( "gdi32.dll" )]
static extern IntPtr CreateSolidBrush( uint crColor );

[DllImport( "gdi32.dll" )]
static extern bool DeleteObject( IntPtr hObject );

private const int WM_NCPAINT = 0x0085;
private const int WM_PAINT = 0x000F;
private const int RGN_DIFF = 0x4;
private int p_border = 2;

protected override void WndProc( ref Message m ) {

    if(m.Msg == WM_PAINT ) {
        base.WndProc( ref m );

        IntPtr hdc = GetWindowDC( this.Handle ); //gr.GetHdc();
        IntPtr rgn = CreateRectRgn( 0, 0, this.Width, this.Height );
        IntPtr brush = CreateSolidBrush( 0xFF0000 ); //Blue : B G R

        CombineRgn( rgn, rgn, CreateRectRgn( p_border, p_border, this.Width - p_border,
                                             this.Height - p_border ), RGN_DIFF );

        FillRgn( hdc, rgn, brush );

        ReleaseDC( this.Handle, hdc );
        DeleteObject( rgn );
        DeleteObject( brush );

        m.Result = IntPtr.Zero;

        return;
    }

    if( m.Msg == WM_NCPAINT ) {
        return;
    }

    base.WndProc( ref m );
}
1

Winforms TextBox is a legacy control I think even from way back before the .Net framework.

It doesn't support owner-drawing and as one can see on MSDN neither Paint not OnPaint are documented to work.

Yes, you can code them, and yes, they will have some effect. But TextBox doesn't play by the normal rules and will mess up your drawing without triggering a paint event.

Possibly you can hook yourself into the windows message queue (WndProc) but it is generally not recommended, especially for something like adorning it with a border.

Usually nesting a TextBox in a Panel and letting the Panel draw a nice Border is the simplest solution..

TaW
  • 53,122
  • 8
  • 69
  • 111