CodeBox






4.91/5 (33投票s)
一个快速的 WPF 文本框控件,支持文本着色、高亮显示、下划线和删除线。
引言
本文介绍了一种增强型文本框控件,旨在方便文本着色、高亮显示、下划线和删除线。由于它派生自 TextBox
控件而非 RichTextBox
,因此速度很快。
背景
我正在升级我的一个正则表达式生成工具。这是我第 n 次考虑将其从 WinForms 迁移到 WPF。我的问题是 RichTextBox
控件的痛苦缓慢。我曾多次尝试以某种方式获得与 WinForms RichTextBox
相媲美的性能,但都失败了。这一次,我尝试了不同的方法。为了好玩,我重写了 OnRender
方法,并在 TextBox
中写入了一些文本。我惊讶地发现文本框的文本和我的额外文本都可见,因为我原以为只会出现我重写的文本。我花了几分钟才从“哦,这很奇怪”进展到“哇,我的问题解决了”,因为我现在可以执行以下操作。
- 重新创建与文本框中相同的文本,但带有颜色和其他装饰
- 由于两组文本都可见,我可以确保它们完全对齐
- 原始文本的画刷可以设置为透明,这样它就不会覆盖装饰文本
- 选择和编辑将由基础文本框功能处理
我猜这不可能比 RichTextBox
慢,所以我试了一下。它超出了我的预期,所以它就在这里。
Using the Code
此控件可以像常规文本框一样使用,但有以下注意事项。背景和前景画刷都应设置为透明。这在构造函数中完成,因此您只需不设置它们即可。要设置默认文本颜色,请使用 BaseForeground
属性。要设置背景颜色,只需将其包装在边框中。希望我能很快移除这种非标准行为。文本着色规则通过 Decorations
属性设置。
代码
有几个问题需要处理。由于此控件的最初目的是以各种方式突出显示受正则表达式影响的文本,因此我需要一种可扩展的方法来进行文本选择。我预计随着时间的推移,我可能会有很多选择。我还需要处理滚动、文本着色、背景着色、下划线和删除线。
文本选择和装饰
用于选择文本进行装饰的类都派生自抽象基类 Decoration
。
public abstract class Decoration:DependencyObject
{
public static DependencyProperty DecorationTypeProperty
= DependencyProperty.Register("DecorationType",
typeof(EDecorationType), typeof(Decoration),
new PropertyMetadata(EDecorationType.TextColor));
public EDecorationType DecorationType
{
get { return (EDecorationType)GetValue(DecorationTypeProperty); }
set { SetValue(DecorationTypeProperty, value); }
}
public static DependencyProperty BrushProperty
= DependencyProperty.Register("Brush",
typeof(Brush), typeof(Decoration),
new PropertyMetadata(null));
public Brush Brush
{
get { return (Brush)GetValue(BrushProperty); }
set { SetValue(BrushProperty, value); }
}
public abstract List<pair> Ranges(string Text);
}
Decoration
类型指的是枚举 EDecorationType
的成员,该枚举列出了可用的各种样式。
public enum EDecorationType
{
TextColor,
Hilight,
Underline,
Strikethrough
}
Ranges
是重要的方法。它返回一个 Pair
对象列表,表示字符选择的起始点和长度。
public class Pair
{
public int Start { get; set; }
public int Length { get; set; }
public Pair(){}
public Pair(int start, int length)
{
Start = start;
Length = length;
}
}
请注意,该列表取决于所涉及的文本。我包含了八个 Decoration
类。
ExplicitDecoration
:根据起始字符位置和字符数进行选择MultiExplicitDecoration
:根据起始字符位置和字符数列表进行选择StringDecoration
:根据给定字符串的String.IndexOf
进行选择MultiStringDecoration
:根据给定字符串列表的String.IndexOf
进行选择RegexDecoration
:根据匹配正则表达式进行选择MultiRexexDecoration
:根据匹配正则表达式列表中的任何一个进行选择RegexWordDecoration
:根据由单词边界包围的字符串进行选择MultiRegexWordDecoration
:根据由单词边界包围的字符串列表进行选择
如果将其用于语法着色,MultiRegexWordDecoration
可能是最有用的。
public class MultiRegexWordDecoration:Decoration
{
private List<string> mWords = new List<string>();
public List<string> Words
{
get { return mWords; }
set { mWords = value; }
}
public bool IsCaseSensitive { get; set; }
public override List<pair> Ranges(string Text)
{
List<pair> pairs = new List<pair>();
foreach (string word in mWords)
{
string rstring = @"(?i:\b" + word + @"\b)";
if (IsCaseSensitive)
{
rstring = @"\b" + word + @"\b)";
}
Regex rx = new Regex(rstring);
MatchCollection mc = rx.Matches(Text);
foreach (Match m in mc)
{
pairs.Add(new Pair(m.Index, m.Length));
}
}
return pairs;
}
}
请注意,使用此 Decoration 将找不到像 @@fetch_status
这样的“单词”。在这种情况下,单词边界在“f”之前,而不是第一个“@”。值得注意的是,即使有超过一百个单词,此 Decoration 的性能也相当好。
文本创建
文本创建及后续所有操作都依赖于 FormattedText
类。以下将为我们提供与原始 TextBox
中相同的文本。
FormattedText formattedText = new FormattedText(
this.Text,
CultureInfo.GetCultureInfo("en-us"),
FlowDirection.LeftToRight,
new Typeface(this.FontFamily.Source),
this.FontSize,
BaseForeground); //Text that matches the textbox's
文本着色
一旦我们有了文本,着色就很容易了。我们使用 SetForegroundBrush
方法,并使用 Decoration
对象中的信息。
foreach (Decoration dec in mDecorations)
{
if (dec.DecorationType == EDecorationType.TextColor)
{
List<pair> ranges = dec.Ranges(this.Text);
foreach (Pair p in ranges)
{
formattedText.SetForegroundBrush(dec.Brush, p.Start, p.Length);
}
}
}
这如人们所希望的那样。稍后,SetForegroundBrush
调用有效地推翻了之前的调用。在 SQL 文本示例中,sys.Comments 被视为一个单词并设置为绿色画刷。稍后,. 被设置为灰色画刷。
我们有绿色文本,中间有灰色句点。即使它可能会提高性能,但清理文本颜色更改也不是必要的。
滚动
处理滚动有点棘手。第一个也是最大的障碍是 TextBox
不会公开一个事件来指示滚动。这与用于保持文本同步的 TextChanged
事件不同。但是,文本框的 ControlTemplate
中确实有一个滚动查看器。
<ControlTemplate TargetType="c:CodeBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}"
BorderBrush="{TemplateBinding Border.BorderBrush}"
Background="{TemplateBinding Panel.Background}"
Name="Bd" SnapsToDevicePixels="True">
<ScrollViewer Name="PART_ContentHost"
SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
</Border >
<ControlTemplate.Triggers>...
</ControlTemplate.Triggers>
</ControlTemplate>
创建可视化树后,我们可以将处理程序附加到可视化树中 ScrollViewer
的 ScrollChanged
事件。
private void EnsureScrolling()
{
if (!mScrollingEventEnabled)
{
DependencyObject dp = VisualTreeHelper.GetChild(this, 0);
ScrollViewer sv = VisualTreeHelper.GetChild(dp, 0) as ScrollViewer;
sv.ScrollChanged += new ScrollChangedEventHandler(ScrollChanged);
mScrollingEventEnabled = true;
}
}
private void ScrollChanged(object sender, ScrollChangedEventArgs e)
{
this.InvalidateVisual();
}
在 Render
方法的开头调用 EnsureScrolling
,以确保控件已准备好滚动。
为了使渲染文本在滚动时与 TextBox
文本匹配,还需要执行其他几项操作。需要调整格式化文本的宽度和高度。
double leftMargin =4.0 + this.BorderThickness.Left ;
double topMargin = 2.0 + this.BorderThickness.Top;
formattedText.MaxTextWidth = this.ViewportWidth; // space for scrollbar
formattedText.MaxTextHeight = this.ActualHeight + this.VerticalOffset;
//Adjust for scrolling
ViewportWidth
需要用于宽度,因为它考虑了滚动条的存在与否。左右边距的数字是通过反复试验找到的。文本在控件上绘制如下:
drawingContext.DrawText(formattedText,
new Point(leftMargin, topMargin - this.VerticalOffset));
唯一需要处理的另一个问题是裁剪渲染的文本,使其仅在文本框内可见。
drawingContext.PushClip(new RectangleGeometry(new Rect(0, 0,
this.ActualWidth, this.ActualHeight)));
这在调用 DrawingContext
的任何绘图方法之前完成。
背景着色
FormattedText
类还提供了一个 BuildHighlightGeometry
方法,以提供高亮文本背景所需的几何图形。值得注意的是,它适用于单行和多行高亮。它们添加如下:
foreach (Decoration dec in mDecorations)
{
if (dec.DecorationType == EDecorationType.Hilight)
{
List<pair> ranges = dec.Ranges(this.Text);
foreach (Pair p in ranges)
{
Geometry geom =
formattedText.BuildHighlightGeometry(
new Point(leftMargin, topMargin - this.VerticalOffset),
p.Start, p.Length);
if (geom != null)
{
drawingContext.DrawGeometry(dec.Brush, null, geom);
}
}
}
}
下划线和删除线
在文本高亮显示的情况下,绘制的几何图形与从 FormattedText
对象生成的几何图形完全相同。我们可以在调用 DrawGeometry
方法之前转换几何图形。下划线可以被认为是矩形底部的一部分,当聚合时形成几何图形。同样,删除线可以被认为是中间部分。高亮显示不限于单行的情况有点复杂。StackedRectangleGeometryHelper
和 PointCollectionHelper
用于此任务。计划是获取表示几何图形的点集,更改它们,然后从它们生成新的几何图形。
public List<geometry> BottomEdgeRectangleGeometries()
{
List<geometry> geoms = new List<geometry>();
PathGeometry pg = (PathGeometry)mOrginalGeometry;
foreach (PathFigure fg in pg.Figures)
{
PolyLineSegment pls = (PolyLineSegment)fg.Segments[0];
PointCollectionHelper pch =
new PointCollectionHelper(pls.Points, fg.StartPoint);
List<double> distinctY = pch.DistinctY;
for (int i = 0; i < distinctY.Count - 1; i++)
{
double bottom = distinctY[i + 1] - 3;
double top = bottom + 2;
// ordered values of X that are present for both Y values
List<double> rlMatches = pch.XAtY(distinctY[i], distinctY[i + 1]);
double left = rlMatches[0];
double right = rlMatches[rlMatches.Count - 1];
PathGeometry rpg = CreateGeometry(top, bottom, left, right);
geoms.Add(rpg);
}
}
return geoms;
}
有三件事需要注意。如果我们从中获取几何图形的文本中存在一些换行符,则 Figures
集合中可能存在多个元素。其次,Pathfigure
由单个 PolyLineSegment
组成。HilightGeometry
是通过组合矩形的路径创建的。不会发生简化。
左中部的额外点没有被移除。这非常重要,因为它确保以下过程有效:根据 Y 值将点分组到行中。然后,获取相邻行的交集。最小值和最大值将是矩形的左右两侧。
结论
希望这能填补框架中存在的小空白。我最初无意发布此内容,但后来我记得我为此制作的应用程序的原始版本使用了改编自 C# - 通过解析富文本格式化 RichTextBox 中的文本 的代码,所以我决定回报一下。
修订
- 2009年3月10日 - 增加了边框厚度调整;将默认前景和背景设置为
Colors.Transparent
。