Custom borders
-
Hi there. I'm working on a custom control where each side can have a different border thickness. I found this code while googling which allows me to modify the controls non-client area so I can draw my borders:
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}enum WM_MESSAGE
{
WM_NCCALCSIZE = 131,
WM_NCPAINT = 133,
WM_NCHITTEST = 132,
WM_NCLBUTTONDOWN = 161,
}[StructLayout(LayoutKind.Sequential)]
struct NCCALCSIZE_PARAMS
{
public RECT rgrc0, rgrc1, rgrc2;
public IntPtr lppos;
}[DllImport("User32.dll")]
private extern static IntPtr GetWindowDC( IntPtr hWnd );[DllImport("User32.dll")]
private extern static int ReleaseDC( IntPtr hWnd, IntPtr hDC );private Rectangle _rcButton = Rectangle.Empty;
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case (int)WM_MESSAGE.WM_NCCALCSIZE :
if (m.WParam == IntPtr.Zero)
{
// shrink the client area
NCCALCSIZE_PARAMS csp;
csp = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));
csp.rgrc0.Left += 1;
csp.rgrc0.Top += 5;
csp.rgrc0.Right -= 1;
csp.rgrc0.Bottom -= 1;
Marshal.StructureToPtr( csp, m.LParam, false );
}
break;case (int)WM\_MESSAGE.WM\_NCPAINT : { // draw the borders in the non client area IntPtr hDC = GetWindowDC(m.HWnd); Graphics g = Graphics.FromHdc(hDC); g.FillRectangle(SystemBrushes.Highlight, 0, 0, this.Width, 5); // top g.FillRectangle(SystemBrushes.Highlight, 0, 0, 1, this.Height); // left g.FillRectangle(SystemBrushes.Highlight, this.Width-1, 0, 1, this.Height); // right g.FillRectangle(SystemBrushes.Highlight, 0, this.Height-1, this.Width, 1); // bottom ReleaseDC( m.HWnd, hDC ); break; }
}
base.WndProc(ref m);
}This works fine until the control is resized - it's client area gets reset. Any ideas on how to fix this? "I think I speak on behalf of everyone here when I say huh?" - Buffy
-
Hi there. I'm working on a custom control where each side can have a different border thickness. I found this code while googling which allows me to modify the controls non-client area so I can draw my borders:
[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}enum WM_MESSAGE
{
WM_NCCALCSIZE = 131,
WM_NCPAINT = 133,
WM_NCHITTEST = 132,
WM_NCLBUTTONDOWN = 161,
}[StructLayout(LayoutKind.Sequential)]
struct NCCALCSIZE_PARAMS
{
public RECT rgrc0, rgrc1, rgrc2;
public IntPtr lppos;
}[DllImport("User32.dll")]
private extern static IntPtr GetWindowDC( IntPtr hWnd );[DllImport("User32.dll")]
private extern static int ReleaseDC( IntPtr hWnd, IntPtr hDC );private Rectangle _rcButton = Rectangle.Empty;
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case (int)WM_MESSAGE.WM_NCCALCSIZE :
if (m.WParam == IntPtr.Zero)
{
// shrink the client area
NCCALCSIZE_PARAMS csp;
csp = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));
csp.rgrc0.Left += 1;
csp.rgrc0.Top += 5;
csp.rgrc0.Right -= 1;
csp.rgrc0.Bottom -= 1;
Marshal.StructureToPtr( csp, m.LParam, false );
}
break;case (int)WM\_MESSAGE.WM\_NCPAINT : { // draw the borders in the non client area IntPtr hDC = GetWindowDC(m.HWnd); Graphics g = Graphics.FromHdc(hDC); g.FillRectangle(SystemBrushes.Highlight, 0, 0, this.Width, 5); // top g.FillRectangle(SystemBrushes.Highlight, 0, 0, 1, this.Height); // left g.FillRectangle(SystemBrushes.Highlight, this.Width-1, 0, 1, this.Height); // right g.FillRectangle(SystemBrushes.Highlight, 0, this.Height-1, this.Width, 1); // bottom ReleaseDC( m.HWnd, hDC ); break; }
}
base.WndProc(ref m);
}This works fine until the control is resized - it's client area gets reset. Any ideas on how to fix this? "I think I speak on behalf of everyone here when I say huh?" - Buffy
My guess is that the reason it works until the control is resized is that the client area was never successfully change the the size you are wanting. I took a look at the WM_NCCALCSIZE documentation here[^]. It says that if WParam is FALSE (zero), that a RECT is passed in LParam, not an NCCALCSIZE_PARAMS structure. On the other hand, if WParam is TRUE (non-zero), then you are supposed to get an NCCALCSIZE_PARAMS in LParam. So, it looks like the WM_NCCALCSIZE handling code is never doing anything. I suspect that if you put a breakpoint on the first line of the WM_NCCALCSIZE handling code, then stepped through it, that it would throw an exception at
csp.rgrc0.Left += 1;
John
"You said a whole sentence with no words in it, and I understood you!" -- my wife as she cries about slowly becoming a geek. -
My guess is that the reason it works until the control is resized is that the client area was never successfully change the the size you are wanting. I took a look at the WM_NCCALCSIZE documentation here[^]. It says that if WParam is FALSE (zero), that a RECT is passed in LParam, not an NCCALCSIZE_PARAMS structure. On the other hand, if WParam is TRUE (non-zero), then you are supposed to get an NCCALCSIZE_PARAMS in LParam. So, it looks like the WM_NCCALCSIZE handling code is never doing anything. I suspect that if you put a breakpoint on the first line of the WM_NCCALCSIZE handling code, then stepped through it, that it would throw an exception at
csp.rgrc0.Left += 1;
John
"You said a whole sentence with no words in it, and I understood you!" -- my wife as she cries about slowly becoming a geek.Thanks for the tip. I managed to get it to "remember" the resized non-client area by changing the offending case statememt to:
case (int)WM_MESSAGE.WM_NCCALCSIZE :
if ( m.WParam == IntPtr.Zero )
{
RECT rect = (RECT) Marshal.PtrToStructure(m.LParam, typeof(RECT));
rect.Left += 1;
rect.Top += 5;
rect.Right -= 1;
rect.Bottom -= 1;
Marshal.StructureToPtr(rect, m.LParam, false);
}
else
{
NCCALCSIZE_PARAMS csp;
csp = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));
csp.rgrc0.Left += 1;
csp.rgrc0.Top += 5;
csp.rgrc0.Right -= 1;
csp.rgrc0.Bottom -= 1;
Marshal.StructureToPtr(csp, m.LParam, false);
}Only problem was that sometimes the right and bottom border widths were out by 1. Managed to fix this by overriding OnSizeChanged:
[DllImport("User32.dll")]
public static extern int RedrawWindow(IntPtr hwnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, int fuRedraw);protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
// force redraw of the entire control (including non-client area)
RedrawWindow(this.Handle, IntPtr.Zero, IntPtr.Zero, RDW_FRAME | RDW_UPDATENOW | RDW_INVALIDATE);
}Works like a charm :) "I think I speak on behalf of everyone here when I say huh?" - Buffy
-
Thanks for the tip. I managed to get it to "remember" the resized non-client area by changing the offending case statememt to:
case (int)WM_MESSAGE.WM_NCCALCSIZE :
if ( m.WParam == IntPtr.Zero )
{
RECT rect = (RECT) Marshal.PtrToStructure(m.LParam, typeof(RECT));
rect.Left += 1;
rect.Top += 5;
rect.Right -= 1;
rect.Bottom -= 1;
Marshal.StructureToPtr(rect, m.LParam, false);
}
else
{
NCCALCSIZE_PARAMS csp;
csp = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, typeof(NCCALCSIZE_PARAMS));
csp.rgrc0.Left += 1;
csp.rgrc0.Top += 5;
csp.rgrc0.Right -= 1;
csp.rgrc0.Bottom -= 1;
Marshal.StructureToPtr(csp, m.LParam, false);
}Only problem was that sometimes the right and bottom border widths were out by 1. Managed to fix this by overriding OnSizeChanged:
[DllImport("User32.dll")]
public static extern int RedrawWindow(IntPtr hwnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, int fuRedraw);protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
// force redraw of the entire control (including non-client area)
RedrawWindow(this.Handle, IntPtr.Zero, IntPtr.Zero, RDW_FRAME | RDW_UPDATENOW | RDW_INVALIDATE);
}Works like a charm :) "I think I speak on behalf of everyone here when I say huh?" - Buffy
Great! John
"You said a whole sentence with no words in it, and I understood you!" -- my wife as she cries about slowly becoming a geek.