65.9K
CodeProject 正在变化。 阅读更多。
Home

带衬垫的富文本框子类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.25/5 (12投票s)

2007年11月25日

CPOL

9分钟阅读

viewsIcon

71978

downloadIcon

1659

一个添加新显示属性到富文本框控件的子类。

引言

文本框控件缺少一个“内边距”属性,我在这里添加了它。这允许我们在实际文本周围放置一个边框,可以改善文本框的外观。边框颜色可以与文本背景色不同,在这种情况下,它起到“框住”文本的作用,或者它可以与背景色相同,在这种情况下,它起到均匀的边距作用。

在我首次尝试弥补富文本框控件缺少“内边距”的不足时,我遵循了微软的建议,将一个富文本框添加到面板中并模拟了一个边框。完成之后,我决定创建一个用户控件,这样以后我就不必重复造轮子了。我称该控件为“PaddedTextBox”(尽管它是一个富文本框的包装器),其代码可以在此处找到。

我收到了一些关于此控件及其局限性的建设性意见,即必须通过用户控件间接访问富文本框,并且它不是解决该问题的最优化方案。建议是子类化富文本框,然后在子类中处理边框的绘制。

在本文中,我根据读者反馈创建了一个更复杂的第二种解决方案。同时,我也添加了一些额外的视觉效果,即可以选择性地显示用户指定颜色的第二层可调的内边框。PaddedTextBox子类化了RichTextBox,并添加了一些额外的属性:BorderWidthBorderColorFixedSingleLineColorFixedSingleLineWidth

背景

在控件周围添加用户定义边框的关键是处理WM_NCCALCSIZE窗口消息,并缩小控件的客户区以适应边框。根据MSDN文档

当需要计算窗口客户区的大小和位置时,会发送WM_NCCALCSIZE消息。通过处理此消息,应用程序可以在窗口大小或位置更改时控制窗口客户区的内容。

来自MVP Bob Powell

WM_NCCALCSIZE有两种触发方式。

  1. wParam = 0
  2. 在这种情况下,您应该调整客户区矩形,使其成为窗口矩形的一个子矩形,并返回零。

  3. wParam = 1
  4. 在这种情况下,您有一个选择。您可以像第一个情况一样简单地调整RECT数组中的第一个矩形,并返回零。如果您这样做,当前的客户区矩形将被保留,并移动到Rect[0]中指定的新位置。

-或-

您可以返回任何WVR_XXX标志的组合来指定窗口的重绘方式。其中一个标志是WVR_VALIDRECTS,这意味着您还必须更新NCCALCSIZE_PARAMS结构中其余的矩形,以便

  1. Rect[0]是提议的新客户区位置。
  2. Rect[1]是源矩形或当前窗口,以防您想保留已绘制的图形。
  3. Rect[2]是源图形将被复制到的目标矩形。如果此矩形与源矩形大小不同,则将复制顶部和左侧,但图形将被裁剪,而不是缩放。例如,您可以只将当前客户区的一个相关子集复制到新位置。

System.Windows.Forms.RichTextBox类提供了一个方法protected override void WndProc(ref System.Windows.Forms.Message m),该方法使我们能够处理定向到此控件实例的消息。我已经利用它来处理WM_NCCALCSIZE事件。

处理子类的Windows消息

下面的代码说明了如何实现Bob Powell定义的各种情况。请注意,我们必须使用Marshal类的方法来移动非托管数据结构到托管代码,反之亦然。

WParam为0时,我们只需按照RECT结构定义的指定边框大小来缩小窗口的客户区。

WParam为1时,我们基本上做同样的事情。主要区别在于Message结构中的LParam字段引用的结构。在这种情况下,结构有点复杂,即。

[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;
}

该结构有效地包含一个三个RECT结构的向量,第一个是rect0,它如上所述被修改以重置客户区。对于此特定目的,结构的其他部分可以安全地忽略。(请注意,我们不能在结构中使用实际的RECT数组,因为这将把RECT分配到堆上而不是作为结构本身的一部分。)

以下是我用于相应调整RECT结构的消息处理代码

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;

我们在此处理另外两个特定消息。每当控件被绘制时,我们也想绘制边框。每当绘制非客户区时,我们设置一个标志,该标志最终将调用一个通用例程PaintBorderRect(见下文)来使用用户指定的边框宽度和颜色进行绘制。

此外,我还添加了以下行为:每当文本框被标记为readonly时,插入符就会隐藏。我不希望在不可编辑的文本中出现可见的插入符,所以这似乎是合理的。

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;

请注意,当检测到这些消息时,我们实际上并没有任何事情。我们只是设置了几个标志,表明需要做一些事情。我将在下面讨论其原理。

        default:
            base.WndProc(ref m);
            break;
    }
}

绘制边框

PaintBorderRect例程负责实际的边框绘制。有两类可能需要绘制的空心矩形。第一类是文本框周围的标准边框。调用者定义的width参数决定了画笔的宽度,进而决定了矩形的每个边缘在控件大小定义的WidthHeight内实际绘制的像素数量。

内边框仅在BorderStyleFixedSingle时绘制。它允许您在文本周围添加一个不同颜色、不同尺寸的轮廓,该轮廓会覆盖边框矩形的最内层像素。请注意,这意味着BorderWidth属性必须至少与内线框的线宽一样大。如果您想要更粗或更细的内线框,可以更改新的FixedSingleLineWidth属性。此矩形的颜色由新属性FixedSingleLineColor定义,该属性通过参数borderLineColor传递。(borderLineColor定义为object,因为如果BorderStyle不是FixedSingle,则会传递null值而不是颜色,而Color是结构体,不能为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);
}

设置控件的重绘

最后,为了重绘控件,我添加了一个Redraw例程,它基本上设置一个标志,最终强制调用SetWindowPos,或者对于Fixed3D样式,则调用控件的RecreateHandle方法。在后一种情况下,我发现这是确保边框能够正确重绘且没有伪影的唯一可靠方法,当BorderStyleFixed3D时。顺便说一句,如果您查看RichTextBox的反汇编代码,您会看到在BorderStyle属性更改的某些情况下会调用RecreateHandle

/// <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;
}

Redraw响应新的边框属性之一的变化而被调用,以及当控件被调整大小时。请注意,Redraw本身必须在窗口调整大小后调用,而不是在实际的调整大小代码中。为了确保调整大小已完成,我最初在OnSizeChanged事件中发布了一个应用程序定义的邮件,然后在消息处理程序中收到邮件后进行实际的重绘。然而,我最终选择了一种更通用的方法,如下所述。

执行实际绘制

您一定已经注意到,我们在代码示例中还没有调用PaintBorderRectSetWindowPosRecreateHandle。相反,我们只是设置了doRedrawdoPaint标志。此外,为了隐藏插入符,我只设置了hideCaret标志。那么,这些函数在哪里以及何时被实际执行?答案是在一个定时器例程中。

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()
    }
}

在定时器例程中执行实际工作(它被任意设置为每200毫秒调用一次)可以解决许多问题。它使我们能够处理上述大小调整问题,而无需定义应用程序特定的WndProc消息,但更重要的是,它消除了不必要地调用SetWindowPos / RecreateHandlePaintBorderRect的需要。这在RecreateHandle的情况下尤其重要,因为它会导致控件闪烁,所以目标是最小化调用次数,以提高效率和外观。200毫秒相对来说是一段很长的时间,可以压缩多个重绘和边框绘制调用为一次。

请注意,只有在BorderStyleFixed3D时才需要调用RecreateHandle。由于我估计这个控件的大多数用户主要会使用其他两种边框样式中的一种,因此由此产生的额外开销和闪烁在实践中不应该成为问题。对于其他边框样式NoneFixedSingle,带有SWP_DRAWFRAME / SWP_FRAMECHANGED标志的SetWindowPos调用将导致WM_NCPAINT消息,该消息会将doPaint设置为true

玩转演示

附带的演示允许您调整控件的属性,以便您可以查看各种颜色、大小和边框样式的组合显示效果。它本身就是一个有用的程序,可以帮助您在自己的程序中使用该控件时选择合适的文本框边框样式、颜色和大小。请查看文章开头的图片以获取一些示例。

在您的项目中安装控件

首先,编译控件,然后将生成的DLL放在您选择的文件夹中,最好是包含可重用程序集的文件夹。或者,您可以直接从提供的zip文件中复制PaddedRichTextBox.dllPaddedRichTextBox.xml文件。

打开一个项目并显示工具箱。转到常用控件部分,右键单击,然后选择“选择项”。在“选择工具箱项”对话框中,按浏览按钮,转到包含PaddedTextBox.dll的文件夹,然后为您的项目选择它。现在,您可以像使用内置的富文本框一样使用此控件,只是它增加了一些额外的属性。

历史

  • 发布 2.6.2.8 – 2007/09/06
  • 发布 2.6.3.5 – 2008/05/13
  • 此版本修复了几个问题

    • RecreateHandle的使用现在仅限于Fixed3D样式,提高了大多数常用样式的重绘效率和显示效果。
    • 一个GDI错误,它不正确地显示单像素线条,已被规避,允许单像素边框线正确渲染。
    • 如果FixedSingleLineWidth为零,则内边框线不会绘制,而不是绘制宽度为零。

致谢

我想感谢Georgi Atanasov的建议。他指出RecreateHandle可以用SetWindowPos API代替,并且指出了关于单像素线宽的GDI错误。

© . All rights reserved.