Click here to Skip to main content
14,638,114 members
Articles » Desktop Development » Edit Controls » General
Article
Posted 25 Nov 2007

Stats

63.1K views
1.4K downloads
39 bookmarked

A Padded Rich Text Box Subclass

Rate this:
4.25 (12 votes)
Please Sign up or sign in to vote.
4.25 (12 votes)
15 May 2008CPOL
A subclass which adds new display properties to a rich text box control
Image 1Image 2Image 3
Image 4Image 5Image 6

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.

  1. with wParam = 0
  2. In this case, you should adjust the client rectangle to be some sub-rectangle of the window rectangle, and return zero.

  3. with wParam = 1
  4. In this case, you have an option. You can simply adjust the first rectangle in the array of RECTs 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].

-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:

  1. Rect[0] is the proposed new client position.
  2. Rect[1] is the source rectangle or the current window, in case you want to preserve the graphics that are already drawn there.
  3. 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)]
// This is the default layout for a structure
public struct NCCALCSIZE_PARAMS
{
    // Can't use an array here so simulate one
    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 RECTs in the structure, since that would allocate the RECTs 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)     // False
            {

                RECT rect = (RECT)Marshal.PtrToStructure(m.LParam, typeof(RECT));

                // Adjust (shrink) the client rectangle to accommodate the border:
                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) // True
            {

                nccsp = (NCCALCSIZE_PARAMS)Marshal.PtrToStructure(m.LParam, 
                                                typeof(NCCALCSIZE_PARAMS));

                // Adjust (shrink) the client rectangle to accommodate the border:
                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:
    // Hide the caret if the text is readonly:
    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;  // Without this test there may be artifacts

    IntPtr hDC = GetWindowDC(hWnd);
    using (Graphics g = Graphics.FromHdc(hDC))
    {
        using (Pen p = new Pen(color, width))
        {
            p.Alignment = System.Drawing.Drawing2D.PenAlignment.Inset;
            // 2634 -- Start
            // There is a bug when drawing a line of width 1
            // so we have to special case it and adjust
            // the height and width down 1 to circumvent it:
            int adjustment = (width == 1 ? 1 : 0);
            g.DrawRectangle(p, new Rectangle(0, 0, Width - adjustment, 
                            Height - adjustment));
            // 2634 -- End
            
             // Draw the border line if a color is specified and there is room:
            if (borderLineColor != null && width >= m_FixedSingleLineWidth 
                                && m_FixedSingleLineWidth > 0)   // 2635
            {
                p.Color = (Color)borderLineColor;
                p.Width = m_FixedSingleLineWidth;
                // Overlay the inner border edge with the border line
                int offset = width - m_FixedSingleLineWidth;
                // 2634 -- Start
                // There is a bug when drawing a line of width 1
                // so we have to special case it and adjust
                // the height and width down 1 to circumvent it:
                adjustment = (m_FixedSingleLineWidth == 1 ? 1 : 0);
                g.DrawRectangle(p, new Rectangle(offset, offset, 
                                Width - offset - offset - adjustment, 
                                Height - offset - offset - adjustment)); 
                // 2634 -- End
            }
        }
    }
    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.

/// <span class="code-SummaryComment"><summary></span>
/// This is needed to get the control to repaint correctly.
/// UpdateStyles is NOT sufficient since
/// it leaves artifacts when the control is resized.
/// <span class="code-SummaryComment"></summary></span>
private void Redraw()
{
    // Make sure there is no recursion while recreating the handle:
    if (!this.RecreatingHandle) doRedraw = true;
    // doRedraw = !this.RecreatingHandle;
}

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;
        // Draw the inner border if BorderStyle.FixedSingle
        // is selected. Null means no border.
        PaintBorderRect(this.Handle, m_BorderWidth, m_BorderColor,
            (BorderStyle == BorderStyle.FixedSingle) ? 
            (object)FixedSingleLineColor : null);
    }
    if (doRedraw)
    {
        // 2633 -- Start
        // We use RecreateHandle for the Fixed3D border
        // style to force the control to be recreated. 
        // It calls DestroyHandle and CreateHandle setting
        // RecreatingHandle to true. The downside of this is that it
        // will cause the control to flash.
        if (BorderStyle == BorderStyle.Fixed3D)
        {
            // This is only needed to prevent
            // artifacts for the Fixed3D border style
            RecreateHandle();
        }
        else
        {
            // The SWP_FRAMECHANGED (SWP_DRAWFRAME) flag will
            // generate WM_NCCALCSIZE and WM_NCPAINT messages among others.
            // uint setWindowPosFlags = (uint)(SWP.SWP_NOMOVE | 
            //         SWP.SWP_NOSIZE | SWP.SWP_NOZORDER | SWP.SWP_FRAMECHANGED)
            SetWindowPos(Handle, IntPtr.Zero, 0, 0, 0, 0, setWindowPosFlags);
        }
        // 2633 -- End
        doRedraw = false;    // This must follow RecreateHandle()
    }
}

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

  • Release 2.6.2.8 – 09/06/2007
  • Release 2.6.3.5 – 05/13/2008
  • This release fixes several problems:

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Tony Zackin
Software Developer (Senior) Takamomto, LLC
United States United States
I have been doing database related programming in the financial services industry for over 20 years on various platforms. I used to be a UNIX Sybase DBA but now prefer programming in the .NET environment using C# and SQL Server.

Comments and Discussions

 
QuestionThe link for the source code is down Is that intentional ? Pin
Leach72383-Jun-16 6:30
MemberLeach72383-Jun-16 6:30 
GeneralMy vote of 5 Pin
Danny Wood2-Nov-11 2:11
MemberDanny Wood2-Nov-11 2:11 
GeneralMy vote of 4 Pin
Dave_Morton21-Jul-10 10:20
MemberDave_Morton21-Jul-10 10:20 
GeneralSome Suggestions Pin
Georgi Atanasov26-Nov-07 0:22
MemberGeorgi Atanasov26-Nov-07 0:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.