C#自定义控件:在RichTextBox左侧添加行号与顶部标尺

Mason
Mason
发布时间:2026-10-15 | 5人浏览过
原创

在开发文本编辑器或IDE插件时,标准的RichTextBox缺乏类似VS或Sublime中的行号标记和坐标标尺。通过继承UserControl并重构OnPaint事件,可以低成本地实现这一需求,帮助开发者更直观地进行排版对齐和内容校对。

实践过程

效果展示

设计思路

实现该功能的核心在于“重绘”而非修改原有控件的内部行为。我们需要构建一个继承自 UserControl 的自定义类 GuageRichTextBox,其内部包含一个标准 RichTextBox。主要的视觉元素(边框、标尺背景、刻度线、行号)均由 PictureBox 的 Paint 事件触发绘制。

关键操作逻辑如下:

  1. 布局隔离:将 RichTextBox 嵌入控件边缘,预留出左侧(用于行号)和顶部(用于横标尺)的空间。
  2. 状态同步:监听 RichTextBox 的横向和纵向滚动事件(HScroll 和 VScroll),记录当前的滚动偏移量,以便在重绘标尺时正确定位刻度起始点。
  3. 精准绘图:根据当前分辨率(DPI)和单位设置(厘米/像素),循环绘制刻度线和数字,并根据 RulerStyle 枚举切换显示模式。

核心实现代码

以下是实现行号显示与双轴标尺(横/纵)的完整 C# 代码。代码采用了较旧式的 C# Windows Forms 语法,保留了原有的逻辑结构以确保兼容性。

public partial class GuageRichTextBox : UserControl
{
    public GuageRichTextBox()
    {
        InitializeComponent();
        richTextBox1.WordWrap = false;
        // 初始化时设定基础边距
        richTextBox1.Top = Distance_X;
        richTextBox1.Left = Distance_Y;
        richTextBox1.Width = this.Width - Distance_X - 2;
        richTextBox1.Height = this.Height - Distance_Y - 2;
    }

    #region 变量及常量
    // 预留空间:左侧宽Distance_Y,顶部高Distance_X
    const int Distance_X = 30;
    const int Distance_Y = 30;
    const int SpaceBetween = 3;

    public static float thisleft = 0; 
    public static float StartBitH = 0; // 记录横向滚动条的位置
    public static float StartBitV = 0; // 记录纵向滚动条的位置
    
    const int Scale1 = 3; // 短刻度线长
    const int Scale5 = 6; // 中刻度线长
    const int Scale10 = 9; // 长刻度线长
    
    public static float Degree = 0; 
    public static float CodeSize = 1; 

    #endregion

    #region 属性

    [Browsable(true), Category("设置标尺控件"), Description("设置RichTextBox控件的相关属性")]
    public RichTextBox NRichTextBox
    {
        get { return richTextBox1; }
    }

    public enum Ruler
    {
        Graduation = 0, // 仅刻度
        Rule = 1,      // 完整标尺样式
    }

    private bool TCodeShow = false;

    [Browsable(true), Category("设置标尺控件"), Description("是否在RichTextBox控件的前面添加代码的行号")]
    public bool CodeShow
    {
        get { return TCodeShow; }
        set
        {
            TCodeShow = value;
            this.Invalidate();
        }
    }

    private Ruler TRulerStyle = Ruler.Graduation;

    [Browsable(true), Category("设置标尺控件"), Description("设置标尺样式:\nGraduation为刻度\nRule为尺子")]
    public Ruler RulerStyle
    {
        get { return TRulerStyle; }
        set
        {
            TRulerStyle = value;
            this.Invalidate();
        }
    }

    public enum Unit
    {
        Cm = 0,
        Pels = 1,
    }

    private Unit TUnitStyle = Unit.Cm;

    [Browsable(true), Category("设置标尺控件"), Description("设置标尺的单位:\nCm为厘米\nPels为像素")]
    public Unit UnitStyle
    {
        get { return TUnitStyle; }
        set
        {
            TUnitStyle = value;
            this.Invalidate();
        }
    }

    #endregion

    #region 事件处理 - 绘制逻辑核心

    private void GuageRichTextBox_Paint(object sender, PaintEventArgs e)
    {
        // 1. 绘制外边框
        e.Graphics.DrawRectangle(new Pen(Color.DarkGray), 0, 0, this.Width - 1, this.Height - 1);
        
        // 2. 计算并调整 RichTextBox 的位置(取决于是否显示行号)
        if (CodeShow)
        {
            // 获取行号的宽度
            float tem_code = (float) StringSize(
                (Convert.ToInt32(CodeSize + (float) (richTextBox1.Height /
                    StringSize(CodeSize.ToString(), richTextBox1.Font, false)))).ToString(), 
                this.Font, true);
            
            richTextBox1.Top = Distance_X;
            richTextBox1.Left = Distance_Y + (int) Math.Ceiling(tem_code);
            richTextBox1.Width = this.Width - Distance_X - 2 - (int) Math.Ceiling(tem_code);
            richTextBox1.Height = this.Height - Distance_Y - 2;
            thisleft = Distance_Y + tem_code;
        }
        else
        {
            richTextBox1.Top = Distance_X;
            richTextBox1.Left = Distance_Y;
            richTextBox1.Width = this.Width - Distance_X - 2;
            richTextBox1.Height = this.Height - Distance_Y - 2;
            thisleft = Distance_Y;
        }

        // 3. 绘制控件背景及边框装饰
        e.Graphics.DrawRectangle(new Pen(Color.LightSteelBlue), richTextBox1.Location.X - 1, thisleft - 1,
            richTextBox1.Width + 1, richTextBox1.Height + 1);
        e.Graphics.FillRectangle(new SolidBrush(Color.Silver), 1, 1, this.Width - 2, Distance_Y - 2); 
        e.Graphics.FillRectangle(new SolidBrush(Color.Silver), 1, 1, Distance_X - 2, this.Height - 2);
        e.Graphics.FillRectangle(new SolidBrush(Color.Gray), 3, 3, Distance_X - 7, Distance_Y - 8);
        e.Graphics.DrawRectangle(new Pen(SystemColors.Control), 3, 3, Distance_X - 8, Distance_Y - 8);
        
        // 4. 若为标尺模式,绘制标尺背景板
        if (RulerStyle == Ruler.Rule)
        {
            // 上边标尺背景
            e.Graphics.FillRectangle(new SolidBrush(Color.Gray), thisleft - 3, 3, this.Width - (thisleft - 2),
                Distance_Y - 9);
            e.Graphics.DrawLine(new Pen(SystemColors.Control), thisleft - 3, 3, this.Width - 2, 3);
            e.Graphics.DrawLine(new Pen(SystemColors.Control), thisleft - 3, Distance_Y - 5, this.Width - 2, Distance_Y - 5);
            e.Graphics.FillRectangle(new SolidBrush(Color.WhiteSmoke), thisleft - 2, 9,
                this.Width - (thisleft - 2) - 1, Distance_Y - 19);

            // 左边标尺背景
            e.Graphics.FillRectangle(new SolidBrush(Color.Gray), 3, Distance_Y - 3, Distance_X - 7,
                this.Height - (Distance_Y - 3) - 2);
            e.Graphics.DrawLine(new Pen(SystemColors.Control), 3, Distance_Y - 3, 3, this.Height - 2);
            e.Graphics.DrawLine(new Pen(SystemColors.Control), Distance_X - 5, Distance_Y - 3, Distance_X - 5, this.Height - 2);
            e.Graphics.FillRectangle(new SolidBrush(Color.WhiteSmoke), 9, Distance_Y - 3, Distance_X - 19,
                this.Height - (Distance_Y - 3) - 2);
        }

        // 5. 绘制横向刻度(Top Ruler / Graduation)
        int tem_temHeight = 0;
        string tem_value = "";
        int divide = 5;
        Pen tem_p = new Pen(new SolidBrush(Color.Black));
        
        if (UnitStyle == Unit.Cm) 
            Degree = e.Graphics.DpiX / 25.4F; 
        if (UnitStyle == Unit.Pels) 
            Degree = 10; 

        int tem_width = this.Width - 3;
        int tem_n = (int) StartBitH;
        if (tem_n != StartBitH) StartBitH = (int) StartBitH;

        for (float i = 0; i < tem_width;)
        {
            tem_temHeight = Scale1;
            float j = (i + (int) StartBitH) / Degree;
            tem_value = "";
            j = (int) j;

            if (j % (divide * 2) == 0)
            {
                tem_temHeight = Scale10;
                if (UnitStyle == Unit.Cm) tem_value = Convert.ToString(j / 10);
                if (UnitStyle == Unit.Pels) tem_value = Convert.ToString((int) j * 10);
            }
            else if (j % divide == 0)
            {
                tem_temHeight = Scale5;
            }

            tem_p.Width = 1;
            if (RulerStyle == Ruler.Graduation)
            {
                e.Graphics.DrawLine(tem_p, i + 1 + thisleft, SpaceBetween, i + 1 + thisleft, SpaceBetween + tem_temHeight);
                if (tem_value.Length > 0)
                    ProtractString(e.Graphics, tem_value.Trim(), i + 1 + thisleft, SpaceBetween, i + 1 + thisleft, SpaceBetween + tem_temHeight, 0);
            }

            if (RulerStyle == Ruler.Rule)
            {
                if (tem_value.Length > 0)
                {
                    e.Graphics.DrawLine(tem_p, i + 1 + thisleft, 4, i + 1 + thisleft, 7);
                    e.Graphics.DrawLine(tem_p, i + 1 + thisleft, Distance_Y - 9, i + 1 + thisleft, Distance_Y - 7);
                    float tem_space = 3 + (Distance_X - 19F - 9F - StringSize(tem_value.Trim(), this.Font, false)) / 2F;
                    ProtractString(e.Graphics, tem_value.Trim(), i + 1 + thisleft, (float) Math.Ceiling(tem_space), i + 1 + thisleft, (float) Math.Ceiling(tem_space) + tem_temHeight, 0);
                }
            }
            i += Degree;
        }

        // 6. 绘制纵向刻度(Left Ruler / Graduation)
        if (UnitStyle == Unit.Cm) 
            Degree = e.Graphics.DpiX / 25.4F; 
        if (UnitStyle == Unit.Pels) 
            Degree = 10; 

        int tem_height = this.Height - 3;
        int tem_n2 = (int) StartBitV;
        if (tem_n2 != StartBitV) StartBitV = (int) StartBitV;

        for (float i = 0; i < tem_height;)
        {
            tem_temHeight = Scale1;
            float j = (i + (int) StartBitV) / Degree;
            tem_value = "";
            j = (int) j;
            if (j % 10 == 0)
            {
                tem_temHeight = Scale10;
                if (UnitStyle == Unit.Cm) tem_value = Convert.ToString(j / 10);
                if (UnitStyle == Unit.Pels) tem_value = Convert.ToString((int) j * 10);
            }
            else if (j % 5 == 0)
            {
                tem_temHeight = Scale5;
            }

            tem_p.Width = 1;
            if (RulerStyle == Ruler.Graduation)
            {
                e.Graphics.DrawLine(tem_p, SpaceBetween, i + 1 + Distance_Y, SpaceBetween + tem_temHeight, i + 1 + Distance_Y);
                if (tem_value.Length > 0)
                    ProtractString(e.Graphics, tem_value.Trim(), SpaceBetween, i + 1 + Distance_Y, SpaceBetween + tem_temHeight, i + 1 + Distance_Y, 1);
            }

            if (RulerStyle == Ruler.Rule)
            {
                if (tem_value.Length > 0)
                {
                    e.Graphics.DrawLine(tem_p, 4, i + 1 + Distance_Y, 7, i + 1 + Distance_Y);
                    e.Graphics.DrawLine(tem_p, Distance_Y - 9, i + 1 + Distance_Y, Distance_Y - 7, i + 1 + Distance_Y);
                    float tem_space = 3 + (Distance_X - 19F - 9F - StringSize(tem_value.Trim(), this.Font, false)) / 2F;
                    ProtractString(e.Graphics, tem_value.Trim(), (float) Math.Floor(tem_space), i + 1 + Distance_Y, (float) Math.Floor(tem_space) + tem_temHeight, i + 1 + Distance_Y, 1);
                }
            }
            i += Degree;
        }

        // 7. 绘制行号(CodeShow == true)
        if (CodeShow)
        {
            float tem_FontHeight = (float) (richTextBox1.Height / (StringSize(CodeSize.ToString(), richTextBox1.Font, false)));
            float tem_tep = richTextBox1.Top;
            int tem_mark = 0;
            for (int i = 0; i < (int) tem_FontHeight; i++)
            {
                tem_mark = i + (int) CodeSize;
                e.Graphics.DrawString(tem_mark.ToString(), this.Font, new SolidBrush(Color.Red),
                    new PointF(richTextBox1.Left - StringSize(tem_mark.ToString(), this.Font, true) - 2, tem_tep));
                tem_tep = tem_tep + StringSize("懂", richTextBox1.Font, false);
            }
        }
    }

    #endregion

    #region 辅助事件与方法

    private void GuageRichTextBox_Resize(object sender, EventArgs e)
    {
        richTextBox1.Top = Distance_X;
        richTextBox1.Left = Distance_Y;
        richTextBox1.Width = this.Width - Distance_X - 2;
        richTextBox1.Height = this.Height - Distance_Y - 2;
        this.Invalidate();
    }

    private void richTextBox1_HScroll(object sender, EventArgs e)
    {
        StartBitH = (int) (Math.Abs((float) richTextBox1.GetPositionFromCharIndex(0).X - 1));
        this.Invalidate();
    }

    private void richTextBox1_VScroll(object sender, EventArgs e)
    {
        StartBitV = (int) (Math.Abs((float) richTextBox1.GetPositionFromCharIndex(0).Y - 1));
        if (CodeShow)
            CodeSize = (int) Math.Abs((richTextBox1.GetPositionFromCharIndex(0).Y /
                                       StringSize("懂", richTextBox1.Font, false)));
        this.Invalidate();
    }

    /// <summary>
    /// 在指定区域绘制文字
    /// </summary>
    public void ProtractString(Graphics e, string str, float x1, float y1, float x2, float y2, float n)
    {
        float TitWidth = StringSize(str, this.Font, true);
        if (n == 0)
            e.DrawString(str, this.Font, new SolidBrush(Color.Black), new PointF(x2 - TitWidth / 2, y2 + 1));
        else
        {
            StringFormat drawFormat = new StringFormat();
            drawFormat.FormatFlags = StringFormatFlags.DirectionVertical;
            e.DrawString(str, this.Font, new SolidBrush(Color.Black), new PointF(x2 + 1, y2 - TitWidth / 2), drawFormat);
        }
    }

    /// <summary>
    /// 测量文本尺寸
    /// </summary>
    public float StringSize(string str, Font font, bool n) 
    {
        Graphics TitG = this.CreateGraphics();
        SizeF TitSize = TitG.MeasureString(str, font);
        if (n) return TitSize.Width;
        else return TitSize.Height;
    }

    #endregion
}

使用建议

  • 性能优化:上述代码在滚动时频繁调用 Invalidate() 会引发重绘。对于超长文本,建议引入双缓冲机制或使用 UpdateStyles 减少闪烁。
  • 单位校准:Degree 变量的计算依赖于当前窗口的 Dpi。若应用支持高 DPI 缩放,建议在启动时动态读取 SystemInformation 进行校正。
  • 滚动同步:如果同时实现了垂直滚动条的跟随,需额外监听 VScroll 事件以更新行号区域的顶部基准线。

总结

通过上述步骤,我们成功为一个简单的 WinForms 组件添加了专业的编辑环境外观。虽然未使用复杂的第三方库,但这种基于 GDI+ 重绘的方法提供了极高的灵活性,可以根据项目需求自由调整标尺样式、颜色以及行号的交互逻辑。

以上就是C#实现带行数和标尺的RichTextBox的详细内容,更多关于C# RichTextBox自定义控件的资料请关注本站其它相关文章!
相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表