Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I paint custom borders on .Net WinForms controls

I've been trying to paint custom borders for existing .Net WinForms controls. I've attempted this by creating a class which from the control I want to change the border color of, and then try several things during painting. I've tried the following:

1. Catch WM_NCPAINT. This works, somewhat. The problem with the code below is that when the control resizes, the border will be cut off on the right and bottom side. Not good.

protected override void WndProc(ref Message m)
{
  if (m.Msg == NativeMethods.WM_NCPAINT) {
    WmNcPaint(ref m);
    return;
  }
  base.WndProc(ref m);
}

private void WmNcPaint(ref Message m)
{
  if (BorderStyle == BorderStyle.None) {
    return;
  }

  IntPtr hDC = NativeMethods.GetWindowDC(m.HWnd);
  if (hDC != IntPtr.Zero) {
    using (Graphics g = Graphics.FromHdc(hDC)) {
      ControlPaint.DrawBorder(g, new Rectangle(0, 0, this.Width, this.Height), _BorderColor, ButtonBorderStyle.Solid);
    }
    m.Result = (IntPtr)1;
    NativeMethods.ReleaseDC(m.HWnd, hDC);
  }
}

2. Override void OnPaint. This works for some controls, but not all. This also requires that you set BorderStyle to BorderStyle.None, and you have to manually clear the background on paint, otherwise you get this when you resize.

protected override void OnPaint(PaintEventArgs e)
{
  base.OnPaint(e);
  ControlPaint.DrawBorder(e.Graphics, new Rectangle(0, 0, this.Width, this.Height), _BorderColor, ButtonBorderStyle.Solid);
}

3. Overriding void OnResize and void OnPaint (like in method 2). This way, it paints well with resizing, but not when the Panel has AutoScroll enabled, in which case it will look like this when scrolling down. If I try to use WM_NCPAINT to paint the border, Refresh() has no effect.

protected override void OnResize(EventArgs eventargs)
{
  base.OnResize(eventargs);
  Refresh();
}

Suggestions are more than welcome. I'd like to know what the best way to go about this is, for multiple types of controls (I'll have to do this for multiple default WinForms controls).

like image 546
Codecat Avatar asked Sep 29 '14 17:09

Codecat


1 Answers

EDIT: So I figured out what was causing my initial problems. After a very long time of tinkering, experimenting, and looking into the .Net framework source code, here's a definitive way to do it (considering you have a control that inherits from the control you want to draw a custom border on):

[DllImport("user32.dll")]
public static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, RedrawWindowFlags flags);

[Flags()]
public enum RedrawWindowFlags : uint
{
  Invalidate = 0X1,
  InternalPaint = 0X2,
  Erase = 0X4,
  Validate = 0X8,
  NoInternalPaint = 0X10,
  NoErase = 0X20,
  NoChildren = 0X40,
  AllChildren = 0X80,
  UpdateNow = 0X100,
  EraseNow = 0X200,
  Frame = 0X400,
  NoFrame = 0X800
}

// Make sure that WS_BORDER is a style, otherwise borders aren't painted at all
protected override CreateParams CreateParams
{
  get
  {
    if (DesignMode) {
      return base.CreateParams;
    }
    CreateParams cp = base.CreateParams;
    cp.ExStyle &= (~0x00000200); // WS_EX_CLIENTEDGE
    cp.Style |= 0x00800000; // WS_BORDER
    return cp;
  }
}

// During OnResize, call RedrawWindow with Frame|UpdateNow|Invalidate so that the frame is always redrawn accordingly
protected override void OnResize(EventArgs e)
{
  base.OnResize(e);
  if (DesignMode) {
    RecreateHandle();
  }
  RedrawWindow(this.Handle, IntPtr.Zero, IntPtr.Zero, RedrawWindowFlags.Frame | RedrawWindowFlags.UpdateNow | RedrawWindowFlags.Invalidate);
}

// Catch WM_NCPAINT for painting
protected override void WndProc(ref Message m)
{
  if (m.Msg == NativeMethods.WM_NCPAINT) {
    WmNcPaint(ref m);
    return;
  }
  base.WndProc(ref m);
}

// Paint the custom frame here
private void WmNcPaint(ref Message m)
{
  if (BorderStyle == BorderStyle.None) {
    return;
  }

  IntPtr hDC = NativeMethods.GetWindowDC(m.HWnd);
  using (Graphics g = Graphics.FromHdc(hDC)) {
    g.DrawRectangle(new Pen(_BorderColor), new Rectangle(0, 0, this.Width - 1, this.Height - 1));
  }
  NativeMethods.ReleaseDC(m.HWnd, hDC);
}

So in a nutshell, leave OnPaint as is, make sure WS_BORDER is set, then catch WM_NCPAINT and draw the border via the hDC, and make sure that RedrawWindow is called in OnResize.

This could maybe even be extended in order to draw a custom scrollbar, because that's part of the window frame that you can draw on during WM_NCPAINT.

I removed my old answer from this.

EDIT 2: For ComboBox, you have to catch WM_PAINT in WndProc(), because for some reason the .Net source for painting the ComboBox doesn't use OnPaint(), but WM_PAINT. So something like this:

protected override void WndProc(ref Message m)
{
  base.WndProc(ref m);

  if (m.Msg == NativeMethods.WM_PAINT) {
    OnWmPaint();
  }
}

private void OnWmPaint()
{
  using (Graphics g = CreateGraphics()) {
    if (!_HasBorders) {
      g.DrawRectangle(new Pen(BackColor), new Rectangle(0, 0, this.Width - 1, this.Height - 1));
      return;
    }
    if (!Enabled) {
      g.DrawRectangle(new Pen(_BorderColorDisabled), new Rectangle(0, 0, this.Width - 1, this.Height - 1));
      return;
    }
    if (ContainsFocus) {
      g.DrawRectangle(new Pen(_BorderColorActive), new Rectangle(0, 0, this.Width - 1, this.Height - 1));
      return;
    }
    g.DrawRectangle(new Pen(_BorderColor), new Rectangle(0, 0, this.Width - 1, this.Height - 1));
  }
}
like image 192
Codecat Avatar answered Oct 03 '22 23:10

Codecat