Introduction
One property that the text box controls are lacking is an “inner padding” property, which I have added here. This allows us to place a border around the actual text, which can improve the look of the text box. The border color may be a different color from the text background, in which case, it acts to “frame” the text, or it can be the same color, in which case, it acts as a uniform margin.
In my first attempt to remedy the lack of “inner padding” for the rich text box control, I followed Microsoft’s recommendations and added a rich text box to a panel and simulated a border. Once I had done that, I decided to create a user control so that I wouldn’t have to reinvent the wheel in the future. I called the control a “PaddedTextBox
” (even though it was a wrapper for a rich text box), and the code for it can be found here.
I got a few constructive comments about this control and its limitations, to wit, the rich text box has to be accessed indirectly via the user control, and the fact that it is not the most optimal solution to the problem. The recommendation was to subclass the rich text box instead and handle the drawing of the border in the subclass.
In this article, I have created a second, more sophisticated, solution to the problem based on reader feedback. And, while I was at it, I added some additional eye candy, viz., the ability to optionally display a second, adjustable inner-border of the text with a user-specified color. The PaddedTextBox
subclasses the RichTextBox
and adds some additional properties: BorderWidth
, BorderColor
, FixedSingleLineColor
, and FixedSingleLineWidth
.
Background
The key to adding a user defined border around a control is to handle the WM_NCCALCSIZE
window message and make the client area of the control smaller to accommodate the border. According to the MSDN documentation:
The WM_NCCALCSIZE
message is sent when the size and position of a window's client area must be calculated. By processing this message, an application can control the content of the window's client area when the size or position of the window changes.
From Bob Powell, an MVP:
There are two ways that WM_NCCALCSIZE
is raised.
In this case, you should adjust the client rectangle to be some sub-rectangle of the window rectangle, and return zero.
In this case, you have an option. You can simply adjust the first rectangle in the array of RECT
s in the same way as you did for the first case, and return zero. If you do this, the current client rectangle is preserved, and moved to the new position specified in Rect[0]
.
- with
wParam = 0
- with
wParam = 1
-or-
You can return any combination of the WVR_XXX
flags to specify how the window should be redrawn. One of these flags is WVR_VALIDRECTS
, which means that you must also update the rectangles in the rest of the NCCALCSIZE_PARAMS
structure so that:
Rect[0]
is the proposed new client position. Rect[1]
is the source rectangle or the current window, in case you want to preserve the graphics that are already drawn there. Rect[2]
is the destination rectangle where the source graphics will be copied to. If this rectangle is a different size to the source, the top and left will be copied, but the graphics will be clipped, not resized. You can, for example, copy only a relevant subset of the current client to the new place.
The class System.Windows.Forms.RichTextBox
provides a method, protected override void WndProc(ref System.Windows.Forms.Message m)
, which enables us to handle messages directed to this control instance. I’ve utilized that to handle the WM_NCCALCSIZE
event.
Handling the Windows Messages to the Subclass
The code below illustrates how to implement the cases as defined by Bob Powell. Note that we must use the Marshal
class methods to move unmanaged data structures to managed code, and vice versa.
When WParam
is 0
, we merely shrink the client portion of the window as defined by the RECT
structure by the specified size of the border.
When WParam
is 1
, we do basically the same thing. The major difference is the struct referenced in the LParam
field of the Message
struct
. In this case, the struct is a bit more complicated, to wit.
[StructLayout(LayoutKind.Sequential)]
public struct NCCALCSIZE_PARAMS
{
public RECT rect0, rect1, rect2;
public IntPtr lppos;
}
This struct contains, effectively, a vector of three RECT
structures, the first of which, rect0
, is modified as above to reset the client area. For this particular purpose, the rest of the structure can be safely ignored. (Note that we cannot use an actual array of RECT
s in the structure, since that would allocate the RECT
s on the heap and not as part of the structure itself.)
The following is the message handling code that I’ve used to adjust the appropriate RECT
structs accordingly:
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case (int)Win32Messages.WM_NCCALCSIZE:
int adjustment = this.BorderStyle == BorderStyle.FixedSingle ? 2 : 0;
if ((int)m.WParam == 0)
{
RECT rect = (RECT)Marshal.PtrToStructure(m.LParam, typeof(RECT));
rect.Top += m_BorderWidth - adjustment;
rect.Bottom -= m_BorderWidth - adjustment;
rect.Left += m_BorderWidth - adjustment;
rect.Right -= m_BorderWidth - adjustment;
Marshal.StructureToPtr(rect, m.LParam, false);
m.Result = IntPtr.Zero;
}
else if ((int)m.WParam == 1)
{
nccsp = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam,
typeof(NCCALCSIZE_PARAMS));
nccsp.rect0.Top += m_BorderWidth - adjustment;
nccsp.rect0.Bottom -= m_BorderWidth - adjustment;
nccsp.rect0.Left += m_BorderWidth - adjustment;
nccsp.rect0.Right -= m_BorderWidth - adjustment;
Marshal.StructureToPtr(nccsp, m.LParam, false);
m.Result = IntPtr.Zero;
}
base.WndProc(ref m);
break;
There are two more specific messages that we handle here. Whenever the control is painted, we want to paint our border as well. Whenever the non-client area is to be painted, we set a flag, which will ultimately result in a call to a general purpose routine called PaintBorderRect
(see below) to do this using the user specified border widths and colors.
Also, I have added the behavior that whenever the textbox is marked as readonly
, the caret is hidden. I don’t want a visible caret for uneditable text, so this seemed like a reasonable thing to do.
case (int)Win32Messages.WM_PAINT:
hideCaret = this.ReadOnly;
base.WndProc(ref m);
break;
case (int)Win32Messages.WM_NCPAINT:
base.WndProc(ref m);
doPaint = true;
break;
Note that we don’t actually do anything when these messages are detected. We merely set a couple of flags to indicate that something needs to get done. I’ll discuss the rationale below.
default:
base.WndProc(ref m);
break;
}
}
Painting the Borders
The PaintBorderRect
routine does the actual drawing of the borders. There are two potential hollow rectangles to draw. First is the standard border around the text box. The width
argument, as defined by the caller, determines the pen width which, in turn, determines the number of pixels for each edge of the rectangle to actually draw within the Width
and Height
of the rectangle defining the control’s size.
The inner border is only drawn when the BorderStyle
is FixedSingle
. It allows you to add a differently colored, variable size, outline around the text which overlays the innermost pixels of the border rectangle. Note that this implies that the BorderWidth
property must be at least as large as the line width of the inner line border. You can change the new FixedSingleLineWidth
property if you want a heavier or thinner inner line border. The color of this rectangle is defined by the new property, FixedSingleLineColor
, which is passed via the argument borderLineColor
. (borderLineColor
is defined as an object
since, if BorderStyle
is not FixedSingle
, a null
value is passed in lieu of a color, and a Color
being a struct
, cannot be null
.)
private void PaintBorderRect(IntPtr hWnd, int width, Color color,
object borderLineColor)
{
if (width == 0) return;
IntPtr hDC = GetWindowDC(hWnd);
using (Graphics g = Graphics.FromHdc(hDC))
{
using (Pen p = new Pen(color, width))
{
p.Alignment = System.Drawing.Drawing2D.PenAlignment.Inset;
int adjustment = (width == 1 ? 1 : 0);
g.DrawRectangle(p, new Rectangle(0, 0, Width - adjustment,
Height - adjustment));
if (borderLineColor != null && width >= m_FixedSingleLineWidth
&& m_FixedSingleLineWidth > 0)
{
p.Color = (Color)borderLineColor;
p.Width = m_FixedSingleLineWidth;
int offset = width - m_FixedSingleLineWidth;
adjustment = (m_FixedSingleLineWidth == 1 ? 1 : 0);
g.DrawRectangle(p, new Rectangle(offset, offset,
Width - offset - offset - adjustment,
Height - offset - offset - adjustment));
}
}
}
ReleaseDC(hWnd, hDC);
}
Setting Up a Redraw of the Control
Finally, to redraw the control, I’ve added a Redraw
routine, which basically sets a flag to ultimately force a call to SetWindowPos
or, for the Fixed3D
style, to call the control’s RecreateHandle
method. In the latter case, I found that this was the only reliable way to ensure that the borders would be redrawn correctly without any artifacts when the BorderStyle
is Fixed3D
. By the way, if you look at the disassembled code for the RichTextBox
, you’ll see a call to RecreateHandle
under certain circumstances when the BorderStyle
property is changed.
private void Redraw()
{
if (!this.RecreatingHandle) doRedraw = true;
}
Redraw
is invoked in response to a change in one of the new border properties, as well as whenever the control is resized. Note that Redraw
itself has to be invoked after the window has been resized, not in the actual resize code. To guarantee that the resize had been completed, I originally posted an application defined message in the OnSizeChanged
event, and then did the actual redraw once the message was received in the message handler. However, I ultimately opted for a more general approach, described below.
Doing the Actual Drawing
You must have noticed that we still haven’t called PaintBorderRect
, SetWindowPos
, or RecreateHandle
anywhere in the code samples. Instead, we have merely set the flags doRedraw
or doPaint
. Also, to hide the caret, I’ve just set the hideCaret
flag. So, where and when are these functions actually being performed? The answer is in a timer routine.
void timer_Tick(object sender, EventArgs e)
{
if (hideCaret)
{
hideCaret = false;
HideCaret(this.Handle);
}
if (doPaint)
{
doPaint = false;
PaintBorderRect(this.Handle, m_BorderWidth, m_BorderColor,
(BorderStyle == BorderStyle.FixedSingle) ?
(object)FixedSingleLineColor : null);
}
if (doRedraw)
{
if (BorderStyle == BorderStyle.Fixed3D)
{
RecreateHandle();
}
else
{
SetWindowPos(Handle, IntPtr.Zero, 0, 0, 0, 0, setWindowPosFlags);
}
doRedraw = false;
}
}
Doing the actual work in a timer routine (it is arbitrarily set to be invoked every 200 ms) solves a number of problems. It lets us deal with the resize issue above without having to define an application specific WndProc
message but, more importantly, it obviates the need to call SetWindowPos
/ RecreateHandle
and PaintBorderRect
unnecessarily. This is particularly important in the case of RecreateHandle
since this causes the control to flash, so the goal was to minimize calls, both for efficiency and appearance. 200 ms is a very long time, relatively, and allows multiple redraws and border paints to be compressed into a single call.
Please note that the RecreateHandle
call is only needed when the BorderStyle
is Fixed3D
. Since I imagine that most users of this control will be using one of the other two border styles primarily anyway, the excess overhead and flashing caused by this shouldn’t be an issue, in practice. For the other border styles of None
and FixedSingle
, the SetWindowPos
call with the SWP_DRAWFRAME
/ SWP_FRAMECHANGED
flag set will cause a WM_NCPAINT
message, which will set doPaint
to true
.
Playing with the Demo
The attached demo will let you play with the control’s properties so that you can see what the various combinations of colors, sizes, and border styles will display. It’s a useful program in its own right to help in selecting the appropriate text box border styles, colors, and sizes if you are going to use the control in your own programs. Check out the images at the beginning of the article for some examples.
Using the Control in your Projects
First, compile the control, and put the resulting DLL in the folder of your choice, preferably, one containing your reusable assemblies. Alternatively, you can just copy the files PaddedRichTextBox.dll and PaddedRichTextBox.xml from the supplied zip file.
Open a project and display the Toolbox. Go to the Common Controls section, right click, and select “Choose items”. In the “Choose Toolbox Items” dialog box, press the Browse button, go to the folder containing PaddedTextBox.dll, and select it for your project. Now, you can treat this control as if it were the built-in rich text box with a few additional properties.
History
This release fixes several problems:
- Release 2.6.2.8 – 09/06/2007
- Release 2.6.3.5 – 05/13/2008
- The use of
RecreateHandle
is now limited to the Fixed3D
style, improving the efficiency and display of a redraw for the most common styles. - A GDI bug, that displays single pixel lines incorrectly, was circumvented, allowing single pixel border lines to be rendered correctly.
- If the
FixedSingleLineWidth
is zero, then the inner border line is not drawn, rather than being drawn with a width of zero.
Acknowledgments
I would like to thank Georgi Atanasov for his suggestions. He pointed out that RecreateHandle
could be replaced by the SetWindowPos
API, and the GDI bug regarding single pixel line widths.