|
|||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionOne 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 BackgroundThe key to adding a user defined border around a control is to handle the
From Bob Powell, an MVP:
The class Handling the Windows Messages to the SubclassThe code below illustrates how to implement the cases as defined by Bob Powell. Note that we must use the When When [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 The following is the message handling code that I’ve used to adjust the appropriate 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 Also, I have added the behavior that whenever the textbox is marked as 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 BordersThe The inner border is only drawn when the 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 ControlFinally, to redraw the control, I’ve added a /// <summary> /// This is needed to get the control to repaint correctly. /// UpdateStyles is NOT sufficient since /// it leaves artifacts when the control is resized. /// </summary> private void Redraw() { // Make sure there is no recursion while recreating the handle: if (!this.RecreatingHandle) doRedraw = true; // doRedraw = !this.RecreatingHandle; }
Doing the Actual DrawingYou must have noticed that we still haven’t called 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 Please note that the Playing with the DemoThe 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 ProjectsFirst, 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: AcknowledgmentsI would like to thank Georgi Atanasov for his suggestions. He pointed out that
|
||||||||||||||||||||||||||||||||||||||||||||||