5

I'm trying to create a custom onPaint for my textbox, it is working... it is working But when I try to type something, a textbox is rendering above the textbox.

This is my constructor:

public TextBox()
{
  Font = new Font("Segoe UI", 11F, FontStyle.Regular, GraphicsUnit.Point, ((byte)(0)));
  BackColor = Color.White;
  BorderColor = Color.Gray;
  BorderStyle = BorderStyle.None;
  SetStyle(ControlStyles.UserPaint, true);
}

And the onPaint:

protected override void OnPaint(PaintEventArgs e)
{
  Graphics g = e.Graphics;
  g.FillRectangle(backgroundBrush, 0, 0, this.Width, this.Height);

  SizeF fontSize = g.MeasureString(Text, Font);
  g.DrawString(Text, Font, new SolidBrush(ForeColor), new PointF(5, 5), cFormat);

  g.DrawRectangle(borderPen, borderPen.Width / 2, borderPen.Width / 2,
                  this.Width - borderPen.Width, this.Height - borderPen.Width);
}
LarsTech
  • 80,625
  • 14
  • 153
  • 225
gerard
  • 174
  • 1
  • 4
  • 17
  • 1
    The TextBox control does not use the Paint event, so you are seeing the control's version and your own. – LarsTech Jul 01 '13 at 22:33
  • Right, so I do need to make my own control? Or is there another way? – gerard Jul 01 '13 at 22:38
  • If all you are trying to do is make a border, try putting the TextBox inside a Panel with a 2 pixel padding and set the TextBox to Dock.Fill and MultiLine=true. Otherwise, it's not clear *why* you are trying to paint the TextBox. – LarsTech Jul 01 '13 at 22:41
  • @LarsTech We wants a TextBox with custom border color. – King King Jul 01 '13 at 22:46
  • @gerard `SetStyle(ControlStyles.UserPaint, true);` will make you paint everything yourself, that's too complex to do. If you want a `TextBox` with custom border, I think there is another approach than painting that way. – King King Jul 01 '13 at 22:48
  • 3
    @HighCore you again, I wonder why you like to be involved in `Winforms` questions while you think it's dead. Persuade people to make `winforms` dead sooner? I don't think you have to do that. Many people will surely recognize that `WPF` is better for them. I also bet you that `WPF` is going to be my choice for my next commercial projects. However I will still learn/study `Winforms` to practice other skills, such as GDI+, Win32, ... and simply it's for fun. – King King Jul 02 '13 at 03:15

1 Answers1

9

If you just want a custom TextBox with some custom border (width and color), I have 2 solutions here:

  1. Using ControlPaint, this will allow you to draw border with some style and color but can't use Brush to draw more variously (like a HatchBrush can do):

    public class CustomTextBox : TextBox
    {
        [DllImport("user32")]
        private static extern IntPtr GetWindowDC(IntPtr hwnd);
        struct RECT
        {
          public int left, top, right, bottom;
        }
        struct NCCALSIZE_PARAMS
        {
          public RECT newWindow;
          public RECT oldWindow;
          public RECT clientWindow;
          IntPtr windowPos;
        }            
        float clientPadding = 2;  
        float actualBorderWidth = 4;
        Color borderColor = Color.Red;      
        protected override void WndProc(ref Message m)
        {
          //We have to change the clientsize to make room for borders
          //if not, the border is limited in how thick it is.
          if (m.Msg == 0x83) //WM_NCCALCSIZE   
          {
            if (m.WParam == IntPtr.Zero)
            {
                RECT rect = (RECT)Marshal.PtrToStructure(m.LParam, typeof(RECT));
                rect.left += clientPadding;
                rect.right -= clientPadding;
                rect.top += clientPadding;
                rect.bottom -= clientPadding;
                Marshal.StructureToPtr(rect, m.LParam, false);
            }
            else
            {
                NCCALSIZE_PARAMS rects = (NCCALSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALSIZE_PARAMS));
                rects.newWindow.left += clientPadding;
                rects.newWindow.right -= clientPadding;
                rects.newWindow.top += clientPadding;
                rects.newWindow.bottom -= clientPadding;
                Marshal.StructureToPtr(rects, m.LParam, false);
            }
          }
          if (m.Msg == 0x85) //WM_NCPAINT    
          {         
             IntPtr wDC = GetWindowDC(Handle);
             using(Graphics g = Graphics.FromHdc(wDC)){                                                      
               ControlPaint.DrawBorder(g, new Rectangle(0,0,Size.Width, Size.Height), borderColor, actualBorderWidth, ButtonBorderStyle.Solid,
             borderColor, actualBorderWidth, ButtonBorderStyle.Solid, borderColor, actualBorderWidth, ButtonBorderStyle.Solid,
             borderColor, actualBorderWidth, ButtonBorderStyle.Solid); 
             }   
             return;          
          }
          base.WndProc(ref m);
        }
    }
    

    Here is the textbox snapshot:

  2. using FillRegion method of a Graphics to paint the border with various kinds of Brush, here I use HatchBrush:

    public class CustomTextBox : TextBox
    {
      [DllImport("user32")]
      private static extern IntPtr GetWindowDC(IntPtr hwnd);
      struct RECT
      {
        public int left, top, right, bottom;
      }
      struct NCCALSIZE_PARAMS
      {
        public RECT newWindow;
        public RECT oldWindow;
        public RECT clientWindow;
        IntPtr windowPos;
      }         
      int clientPadding = 2;   
      int actualBorderWidth = 4;     
      protected override void WndProc(ref Message m)
      {
          //We have to change the clientsize to make room for borders
          //if not, the border is limited in how thick it is.
          if (m.Msg == 0x83) //WM_NCCALCSIZE   
          {
            if (m.WParam == IntPtr.Zero)
            {
                RECT rect = (RECT)Marshal.PtrToStructure(m.LParam, typeof(RECT));
                rect.left += clientPadding;
                rect.right -= clientPadding;
                rect.top += clientPadding;
                rect.bottom -= clientPadding;
                Marshal.StructureToPtr(rect, m.LParam, false);
            }
            else
            {
                NCCALSIZE_PARAMS rects = (NCCALSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALSIZE_PARAMS));
                rects.newWindow.left += clientPadding;
                rects.newWindow.right -= clientPadding;
                rects.newWindow.top += clientPadding;
                rects.newWindow.bottom -= clientPadding;
                Marshal.StructureToPtr(rects, m.LParam, false);
            }
          }
          if (m.Msg == 0x85) //WM_NCPAINT
          {                 
            IntPtr wDC = GetWindowDC(Handle);
            using(Graphics g = Graphics.FromHdc(wDC)){                                                
              Rectangle rect = new Rectangle(0,0,Width,Height);
              Rectangle inner = new Rectangle(0, 0, Width, Height);
              inner.Offset(actualBorderWidth + 2, actualBorderWidth + 2);
              inner.Width -= 2 * actualBorderWidth + 4;
              inner.Height -= 2 * actualBorderWidth + 4;
              Region r = new Region(rect);
              r.Xor(inner);
              using (System.Drawing.Drawing2D.HatchBrush brush = new System.Drawing.Drawing2D.HatchBrush(System.Drawing.Drawing2D.HatchStyle.SmallCheckerBoard, Color.Green, Color.Red))
              {                    
                g.FillRegion(brush, r);
              }
            }
            return;
          }
          base.WndProc(ref m);
      }
    }
    

    Here is the textbox snapshot:
    with scrollbars

King King
  • 61,710
  • 16
  • 105
  • 130
  • I tried the code but it doesn't work: System.ArgumentException was unhandled, invalid parameter. On line 62 (g.ReleaseHdc(wDC)). Also it said that it cannot cast float as int, so I changed actualBorderWidth to a int. – gerard Jul 02 '13 at 09:36
  • @gerard which code doesn't work? The first or the second? the `actualBorderWidth` should be declared as `int` in the second code, I updated it, sorry, but in the first code it should be `float`. – King King Jul 02 '13 at 10:02
  • @gerard please see my update, just remove the line `g.ReleaseHdc...` and add the line `return;`. I don't know why it worked before but in this case we don't need to ReleaseDC, if you want you can use the `ReleaseDC` win32 api function, it doesn't throw any exception. The `return;` is needed so that the default non-client area painting is not executed. – King King Jul 02 '13 at 10:46
  • It works amazing now, only I need to change the float into int, else it doesn't working. I'm using .Net 2.0. – gerard Jul 02 '13 at 11:26
  • 1
    @gerard This code leaks badly. You need to use [ReleaseDC](http://www.pinvoke.net/default.aspx/user32.releasedc), not the `.G.ReleaseHdc`, and call it `ReleaseDC(Handle, wDC);` after disposing the graphic object, which should be wrapped in a `using` branch, `using (Graphics g = Graphics.FromHdc(wDC)) {...}`. I think (I could be wrong) but there might be issues with the return statement preventing the `base.WndProc(ref m);` method from being called: I don't think you always get the scrollbars drawn in a multiline box on Vista or Win7. – LarsTech Jul 02 '13 at 14:42
  • @LarsTech thank you, you're right in the leakage but the ScrollBars are drawn OK. I think the ScrollBars belongs to some kind of special region and not part of non-client area. – King King Jul 02 '13 at 21:29
  • Scrollbars are in the non-client region. The scrollbars do not always redraw properly - I just tested your control in Win2008 to verify. In your solution, you should call `base.WndProc(ref m);` first so the control can draw itself properly, but that leads to a nasty flicker problem. – LarsTech Jul 02 '13 at 21:45
  • @LarsTech I'm not sure but it seems to be OK in Windows 7, at first loading, the scrollbars may be darken (black totally) but after being focused, it is drawn OK. calling `base.WndProc(ref m)` will make it draw 2 times (one by default, one by my code) and the result is only affected by my code. I can see a little flicker, so there is no other choice at the moment. – King King Jul 02 '13 at 21:50
  • 1
    Don't ignore the advice on ReleaseDC. You are leaking handles with your current code. – LarsTech Jul 02 '13 at 22:01
  • @KingKing I am using MultiLine=True, and it happens that my text was overdrawn by the border, how can I change the height of the TextBox, or just give padding to the text area? – Aesthetic Jul 15 '16 at 05:09