9

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).

joelimus
  • 39
  • 8
Codecat
  • 2,213
  • 3
  • 28
  • 39
  • I'm sure you have heard it before, but my honest suggestion would be to just use WPF instead of WinFroms. Beyond that, you have my wishes for good luck in doing this, and my +1 for a well-written question. – BradleyDotNET Sep 29 '14 at 17:45
  • Thank you! And yes, I have heard that before, many times :) I still need to find the time to learn WPF, but this project is far too deep into WinForms to convert it into WPF. Maybe in the future. – Codecat Sep 29 '14 at 17:48
  • protected override void OnResize(EventArgs eventargs) { base.OnResize(eventargs); Refresh(); } – houssam Sep 29 '14 at 20:35
  • houssam, that unfortunately doesn't work. (For the Panel, at least.) – Codecat Sep 29 '14 at 20:44
  • Actually, with some fiddling around I did get it to work. Let me do some more testing. Edit: Nope, unfortunately this breaks autoscroll painting on panels due to drawing the border in `OnPaint` (as seen in method 2 in my question) scrolling down: http://4o4.nl/20140929R67rl.png I'll update my question. – Codecat Sep 29 '14 at 20:48
  • Just a thought: Whereever you can turn all borders off, do that and place the controls on a Panel, for which you can paint your border.. Not sure how to turn borders off for each control, though, a Tab for example won't let yu do that.. – TaW Sep 29 '14 at 21:09
  • @TaW Even if I could do that, I still need to be able to paint the custom border on the Panels, which as noted doesn't work well with `AutoScroll`. (Unless you're implying I should put a panel inside of a panel.. :P) – Codecat Sep 29 '14 at 22:47
  • Hm, not sure I can follow. I meant to add the Panel only to paint its borders, not to provide AutoScroll features. So , yes, where needed I did mean to put a Panel inside a Panel. This kind of stacking is not really a bad thing .. Of course this ain't WPF, where stacking is normal and goes to much more extreme levels, but where needed it might help solve the border problem, at least for some cases. Given the disparate bunch the Winforms Controls make up, I doubt you can find a one for all solution.. – TaW Sep 29 '14 at 22:54
  • Hm, I'm afraid that you're right and that might be the only solution, which is a shame. Gonna keep the question open for a bit longer, maybe someone else has a better idea. – Codecat Sep 30 '14 at 10:25
  • If the only issue with option #1 is cut-off, why not simply subtract 1 from both width and height? That's common, btw, with border painting. – DonBoitnott Sep 30 '14 at 11:36
  • That doesn't work, unfortunately. I know that border painting often requires `Width-1` and `Height-1` for borders, but this is not needed for `ControlPaint.DrawBorder`. Note that it paints fine unless you resize the control (via anchor or something similar) – Codecat Sep 30 '14 at 12:07

2 Answers2

2

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));
  }
}
Codecat
  • 2,213
  • 3
  • 28
  • 39
-2

Actually you may use WPF interoperability controls to create any border you want .

  1. Create Form
  2. Place ElementHost control (from WPF Interoperability) on the form
  3. Create a WPF User Control(or use existing panel) with custom border
  4. Place WindowsFormsHost control inside WPF User Control (this control will be used later to host your control )
  5. Set the ElementHost Child property with WPF User Control from previous step

    I agree that my solution contains lot of nested controls , but from my point of view it significantly reduces amount of problems related to OnPaint nested controls WPF+WinForm

tarasn
  • 19
  • 1
  • 5