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).
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));
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With