Silverlight 2.0 语法高亮 TextBox






4.91/5 (13投票s)
Silverlight 2.0 中的语法高亮 TextBox。
引言
首先,对于您在阅读本文时可能遇到的语言问题,我想提前致歉。英语是我的第二语言,所以对于任何拼写错误或语言错误,我深表歉意。
我之前发布过这篇文章,期待读者的回复。但没有收到。所以,这次我将尽力解释。另外,感谢名为 FIREBALL 的优秀项目作者。
我开始开发这样一个 TextBox 的第一个原因是,我想将我现有的为 Windows Forms 编写的代码(使用了 Sebastian Faltoni 库中的 CodeEditorControl
)移植到其 Microsoft WPF 克隆版本。由于我当时正在开发一些 Silverlight 的东西,而 WPF 几乎是相同的,所以我认为,如果我能在 Silverlight 中实现它,那么我也可以将其移植到 WPF。我开始研究 Silverlight/WPF 的 TextBox
功能,并将其与 Sebastian 的 SyntaxDocument
类结合起来。Sebastian 在他的 Windows Forms CodeEditorControl
中使用了完全自定义绘制的控件。所有绘制都通过 GDI+ 完成。如您所知,WPF 中没有可以直接使 Sebastian 的代码按原样工作的组件,也没有能力为 TextBox
文本块着色。我们只有一个 Foreground
属性来控制整个文档的文本颜色。唯一支持此类功能的控件是 TextBlock
。
TextBlock
有一个 Inlines
属性,您可以在其中设置由 Run
/LineBreak
块分隔的格式化文本。每个 Run
块都有自己的 Text
/Foreground
/Background
属性来控制 Text
/Document
的视觉外观。我决定使用 TextBlock
进行文本可视化,并自定义块的外观。但是有一个问题:我该如何输入文本?TextBlock
只支持可视化,不支持输入。我想到一个办法,就是将 TextBox
放在我的 User Control 前面,将 TextBlock
放在后面。TextBox
用于输入,TextBlock
用于文本可视化。将 TextBlock
和 TextBox
都设置好后,在渲染时会出现一个伪影,导致 TextBlock
的文本略有偏移。为了解决这个问题,我不得不将 TextBox
前景色画刷的 alpha 值设置为 0.01d,使其几乎不可见,但在选择文本时非常有用。如果我将其设置为 0.0d,文本选择会遇到另一个问题:它看起来只显示了选择背景,而没有反转的前景。这可能是 Silverlight 中控件的内部逻辑,因为即使将 SelectionForground
设置为白色,它仍然不可见。
下一部分是如何使 TextBlock
可滚动。这并不难。将 TextBlock
放在 ScrollViewer
中即可解决问题。
<Grid x:Name="LayoutRoot">
<ScrollViewer x:Name="_scroll" Margin="0,0,0,0" Visibility="Visible"
HorizontalScrollBarVisibility="Visible" HorizontalContentAlignment="Left"
VerticalContentAlignment="Top">
<TextBlock x:Name="_text_block" Height="Auto" Width="Auto"
Text="" TextWrapping="NoWrap" Margin="0,0,0,0"/>
</ScrollViewer>
</Grid>
接下来,让我们谈谈 Sebastian 的 SyntaxDocument
类。我将其从 Windows Forms 版本移植到了 WPF 版本。移植并不困难,但导致了某些处理 XML 和使用 Hashtable
作为文本块样式内部存储的类的完全重构。SyntaxDocument
类是一个解析器,它包含已解析的文档行/单词集合对象的集合,并包含样式。样式具有诸如文本的前景色和背景色之类的属性。设置 SyntaxDocument.Text
属性后,它会触发“Changed
”事件,因此您可以继续进行已解析文本的渲染。在我们的例子中,文本渲染由 TextBlock
完成。此处显示的方法是渲染已解析内容的处理程序代码
protected void OnDocument_Changed(object sender, EventArgs e)
{
List<Fireball.Syntax.Row> rows =
_document.VisibleRows.OfType<Fireball.Syntax.Row>().ToList();
_text_block.Inlines.Clear();
rows.ForEach(row =>
{
if (_document[rows.IndexOf(row)].RowState ==
Fireball.Syntax.RowState.SegmentParsed)
{
_document.Parser.ParseLine(rows.IndexOf(row), true);
}
if (_document[rows.IndexOf(row)].RowState ==
Fireball.Syntax.RowState.NotParsed)
{
_document.ParseRow(row, true);
}
Fireball.Syntax.WordCollection words = row.FormattedWords;
if ( words.Count > 0 )
{
words.OfType<Fireball.Syntax.Word>().ToList().ForEach(word =>
{
Run run = new Run()
{
Text = word.Text,
Foreground = word.Style != null ? new SolidColorBrush(
word.Style.ForeColor) : new SolidColorBrush(Colors.Black)
};
_text_block.Inlines.Add(run);
});
}
_text_block.Inlines.Add(new LineBreak());
});
}
代码中最重要的部分是创建一个“Run
”类的实例,并根据每个已解析行中 Row.FormattedWords
的 Word 对象中的属性来设置其属性,这些属性存在于 SyntaxDocument
对象实例中。
每次 TextBox
触发“TextChanged
”事件时都会进行文本渲染。我使用的技术可能不是最佳实践。如果有人发现优化此代码的方法,那将是太棒了。我将继续深入研究 Sebastian 的代码,寻找优化方法,并可能会在 WPF 中使用自定义绘制。
现在,再多说一点关于此类使用的 SyntaxDocument
语言模板。特定语言的字典存储在 XML 文件(SYN)中。我在示例中使用的仅用于 XML 语法高亮。其他字典必须移植到 SyntaxDocument
类的 Silverlight 版本。我对 XML 语法字典文件所做的更改是针对负责语法块颜色定义的属性。下面是对此 SyntaxDocument
引擎的 XML.SYN 文件所做更改的示例
<!--ORIGINAL DEFEINITION-->
<Style Name="Text" ForeColor="Black"
BackColor="" Bold="false"
Italic="false"
Underline="false"/>
<!--CHANGED DEFEINITION-->
<Style Name="Text" ForeColor="#FF000000"
BackColor="" Bold="false" Italic="false"
Underline="false"/>
如果您想使用其他语言字典,可以从 Sebastian 的 Fireball.Syntax 项目的原始源代码中获取:http://www.dotnetfireball.net 或 http://www.codeplex.com/dotnetfireball。
最后,我遇到了一个滚动条的大问题。当 TextBox
文本的长度超过其宽度或高度时,用户控件内的两个控件都会自动出现滚动条。如果我通过箭头键导航 TextBox
文本或输入文本,TextBox
的滚动条会自动滚动,而我的 TextBlock
实例(用于渲染已解析文本)则不会。在这种情况下,我必须对 TextBox
和 ScrollViewer
的滚动条进行控制,使其 Maximum
和 Value
属性相等。我们该怎么做?首先要深入研究 TextBox
的样式/模板。要获取 TextBox
的默认模板,我使用了 Microsoft Expression Blend,它做得非常容易。只需将控件放在页面上,右键单击左侧对象/时间轴树中的控件项,然后选择菜单项“编辑控件部件(模板)->编辑副本”。现在,您可以浏览模板部件以获取内部控件的名称。接下来,我必须重写/处理两个事件:我派生文本框实例的 OnApplyTemplate
/LayoutUpdated
,并获取这些子元素以找到垂直和水平滚动条的实例。下面的代码显示了如何做到这一点
//In this part we have Get a Template Child 'ContentElement'.
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_content = base.GetTemplateChild("ContentElement") as ScrollViewer;
if (_content == null)
return;
_content.LayoutUpdated += new EventHandler(OnContent_LayoutUpdated);
}
//Getting child content of scrollviewer
private void OnContent_LayoutUpdated(object sender, EventArgs e)
{
if (_content_border != null)
return;
_content_border = VisualTreeHelper.GetChild(_content, 0) as Border;
if (_content_border != null)
{
int count = VisualTreeHelper.GetChildrenCount(_content_border);
if (count > 0)
{
Grid grid = VisualTreeHelper.GetChild(_content_border, 0) as Grid;
if (grid != null)
{
IEnumerable<System.Windows.Controls.Primitives.ScrollBar> found = (
from child in grid.Children.ToList() where
child is System.Windows.Controls.Primitives.ScrollBar select child as
System.Windows.Controls.Primitives.ScrollBar);
if (found.Count() > 0)
{
VerticalScrollBar = (from sc in found where sc.Name == "VerticalScrollBar"
select sc).First();
HorizontalScrollBar = (from sc in found where sc.Name == "HorizontalScrollBar"
select sc).First();
if (ContentFound != null)
ContentFound(this, new RoutedEventArgs());
}
}
}
}
}
获取 TextBox
的滚动条实例后,我们可以获取其属性的实际值,并在滚动值更改时注册事件,这样我们就可以将 TextBlock
的 ScrollViewer
滚动条值设置为与 TextBox
相同的值。此技术使我们能够实现一个可以高亮显示语法的自定义 TextBox
控件。下面是 TextBoxExtended
类和实现代码语法高亮的 User Control 的完整代码
带有垂直和水平滚动条的 TextBoxExtended 类
public class TextBoxExtended : TextBox
{
//Template internal content scroll viewer
ScrollViewer _content = null;
//Content border
Border _content_border = null;
//Text calculations
TextBlock _size_block = null;
/// <summary>An Event beeing fired when template
/// content found and initialized</summary>
public event RoutedEventHandler ContentFound;
public TextBoxExtended()
{
//Since we do not use any own style for this
//control set DefaultStyleKey to TextBox
DefaultStyleKey = typeof(TextBoxExtended);
}
//In this part we have Get a Template Child 'ContentElement'.
//This is ScrollViewer of TexBox
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_content = base.GetTemplateChild("ContentElement") as ScrollViewer;
if (_content == null)
return;
_content.LayoutUpdated += new EventHandler(OnContent_LayoutUpdated);
}
//Getting child content of scrollviewer
private void OnContent_LayoutUpdated(object sender, EventArgs e)
{
if (_content_border != null)
return;
_content_border = VisualTreeHelper.GetChild(_content, 0) as Border;
if (_content_border != null)
{
int count = VisualTreeHelper.GetChildrenCount(_content_border);
if (count > 0)
{
Grid grid = VisualTreeHelper.GetChild(_content_border, 0) as Grid;
if (grid != null)
{
//OK NOW TRY TO CREATE A LITTLE TextBlock for text mesurament calculations
_size_block = new TextBlock()
{
Foreground = null,
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Left,
FontFamily = FontFamily,
FontSize = FontSize,
FontStretch = FontStretch,
FontStyle = FontStyle,
FontWeight = FontWeight
};
grid.Children.Add(_size_block);
IEnumerable<System.Windows.Controls.Primitives.ScrollBar> found =
(from child in grid.Children.ToList() where child is
System.Windows.Controls.Primitives.ScrollBar select
child as System.Windows.Controls.Primitives.ScrollBar);
if (found.Count() > 0)
{
VerticalScrollBar = (from sc in found where sc.Name ==
"VerticalScrollBar" select sc).First();
HorizontalScrollBar = (from sc in found where sc.Name ==
"HorizontalScrollBar" select sc).First();
if (ContentFound != null)
ContentFound(this, new RoutedEventArgs());
}
//_content.Clip = new RectangleGeometry()
// { Rect = new Rect(0, 0, ActualWidth, ActualHeight) };
}
}
}
}
public Size MesureText(string Text)
{
if (_size_block != null)
{
_size_block.Text = string.IsNullOrEmpty(Text.Replace("\r",
"").Replace("\n","")) ? " ":Text;
return new Size(_size_block.ActualWidth, _size_block.ActualHeight);
}
return Size.Empty;
}
public bool CanDoTextMesure
{
get { return _size_block != null; }
}
public System.Windows.Controls.Primitives.ScrollBar VerticalScrollBar
{
get;
set;
}
public System.Windows.Controls.Primitives.ScrollBar HorizontalScrollBar
{
get;
set;
}
}
支持语法高亮的 SyntaxTextBox 类
public partial class SyntaxTextBox : UserControl
{
Fireball.Syntax.SyntaxDocument _document;
bool _updated_locked = false;
bool _is_loaded = false;
public static readonly DependencyProperty IsReadOnlyProperty =
DependencyProperty.Register("IsReadOnly",
typeof(bool), typeof(SyntaxTextBox),
new PropertyMetadata(false, delegate(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
SyntaxTextBox box = d as SyntaxTextBox;
if (box != null && box._text_box != null )
{
box._text_box.IsReadOnly = (bool)e.NewValue;
}
}));
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string),
typeof(SyntaxTextBox), new PropertyMetadata("",
delegate(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null )
{
SyntaxTextBox box = d as SyntaxTextBox;
if (box != null)
{
string new_val = (string)e.NewValue;
string old_val = box._document.Text;
string[] new_lines = null;
string[] old_lines = null;
System.IO.StringReader r = new System.IO.StringReader(new_val);
string line = null;
List<string> l_lines = new List<string>();
while ((line = r.ReadLine()) != null)
{
l_lines.Add(line);
}
new_lines = l_lines.ToArray();
r = new System.IO.StringReader(old_val);
line = null;
l_lines = new List<string>();
while ((line = r.ReadLine()) != null)
{
l_lines.Add(line);
}
old_lines = l_lines.ToArray();
bool has_changes = false;
for (int i = 0; i < new_lines.Count(); i++)
{
if (i <= box._document.Count - 1)
{
if (box._document[i].Text != new_lines[i])
{
box._document[i].SetText(new_lines[i]);
box._document[i].IsRendered = false;
has_changes = true;
}
}
else
{
Fireball.Syntax.Row row = box._document.Add(new_lines[i], false);
box._document.ParseRow(row, true);
has_changes = true;
row.IsRendered = false;
}
}
if (old_lines.Count() > new_lines.Count())
{
for (int i = new_lines.Count(); ; )
{
if (box._document.Count == new_lines.Count())
break;
if (box._document.Count == 1)
{
box._document[i].SetText("");
has_changes = true;
break;
}
box._document.Remove(i);
}
}
if (has_changes)
{
box.RenderDocument();
}
}
}
}));
public SyntaxTextBox()
{
InitializeComponent();
Loaded += new RoutedEventHandler(OnLoaded);
}
//External Language Syntax Loading...
public void SetSyntax(string SyntaxSrc, System.Text.Encoding SrcEncoding,
Fireball.CodeEditor.SyntaxFiles.SyntaxLanguage language)
{
if (_is_loaded != true)
return;
System.IO.MemoryStream s = new System.IO.MemoryStream(SrcEncoding.GetBytes(SyntaxSrc));
SetSyntax(s, language);
}
//External Language Syntax Loading...
public void SetSyntax(System.IO.Stream SyntaxSrc,
Fireball.CodeEditor.SyntaxFiles.SyntaxLanguage language)
{
if (_is_loaded != true)
return;
_document.Parser.Init(Fireball.Syntax.Language.FromSyntaxFile(SyntaxSrc));
}
protected void OnLoaded(object sender, RoutedEventArgs e)
{
this.Focus();
_document = new Fireball.Syntax.SyntaxDocument();
Fireball.CodeEditor.SyntaxFiles.CodeEditorSyntaxLoader.SetSyntax(_document,
Fireball.CodeEditor.SyntaxFiles.SyntaxLanguage.XML);
//*****************TEST ONLY***********************//
XElement elm = new XElement("Objects",
new XAttribute("type", "None"),
new XElement("Hello")
);
//************************************************//
_text_box.ContentFound += OnContentFound;
_text_box.TextChanged += OnTextChanged;
_text_box.KeyUp += OnTextBox_KeyUp;
_text_box.Text = elm.ToString();
_text_box.LayoutUpdated += new EventHandler(OnTextLayoutUpdated);
_text_box.IsReadOnly = IsReadOnly;
}
protected void OnTextLayoutUpdated(object sender, EventArgs e)
{
if (_updated_locked)
{
_updated_locked = !_updated_locked;
return;
}
UpdateScrolls();
_updated_locked = true;
}
protected void OnContentFound(object sender, RoutedEventArgs e)
{
RenderDocument();
}
public void UpdateScrolls()
{
if (
_text_box.VerticalScrollBar != null &&
_text_box.HorizontalScrollBar != null
)
{
double pVt = 0;
double pHt = 0;
double pVs = 0;
double pHs = 0;
if( _text_box.VerticalScrollBar.Maximum > 0 &&
_text_box.VerticalScrollBar.Value > 0 )
pVt = (_text_box.VerticalScrollBar.Value /
_text_box.VerticalScrollBar.Maximum) * 100;
pVs = (_scroll.VertRange / 100) * pVt;
if( _text_box.HorizontalScrollBar.Maximum > 0 &&
_text_box.HorizontalScrollBar.Value > 0)
pHt = (_text_box.HorizontalScrollBar.Value /
_text_box.HorizontalScrollBar.Maximum) * 100;
pHs = (_scroll.HorzRange / 100) * pHt;
_scroll.HorzRange = _text_box.HorizontalScrollBar.Maximum;
_scroll.ScrollIntoPosition(_text_box.HorizontalScrollBar.Value/
*Math.Round(pHs)*/, Math.Round(pVs));
}
}
protected void OnTextBox_KeyUp(object sender, KeyEventArgs e)
{
UpdateScrolls();
}
protected void OnTextChanged(object sender, RoutedEventArgs e)
{
Text = _text_box.Text;
_text_box.Focus();
}
protected void RenderDocument()
{
//.VisibleRows.OfType<Fireball.Syntax.Row>().ToList();
List<Fireball.Syntax.Row> rows =
_document.Rows.OfType < Fireball.Syntax.Row>().ToList();
//.VisibleRows.OfType<Fireball.Syntax.Row>().ToList();
List<Fireball.Syntax.Row> total_rows =
_document.Rows.OfType<Fireball.Syntax.Row>().ToList();
rows.ForEach(row =>
{
if (_document[rows.IndexOf(row)].RowState ==
Fireball.Syntax.RowState.SegmentParsed)
{
row.IsRendered = false;
_document.Parser.ParseLine(rows.IndexOf(row), true);
}
if (_document[rows.IndexOf(row)].RowState ==
Fireball.Syntax.RowState.NotParsed)
{
row.IsRendered = false;
_document.ParseRow(row, true);
}
});
if (_text_box.CanDoTextMesure == false )
return;
_scroll.Locked = true;
bool ValidateRows = false;
rows.ForEach( row =>
{
if (row.IsRendered)
return;
if (row.Index > _scroll.Rows - 1)
{
ValidateRows = true;
_scroll.AddRow(true);
}
Fireball.Syntax.WordCollection words = row.FormattedWords;
row.IsRendered = true;
Scroller.ScrollRowCanvas block =
_scroll[row.Index] as Scroller.ScrollRowCanvas;
block.Clear();
if (words.Count > 0)
{
words.OfType<Fireball.Syntax.Word>().ToList().ForEach(word =>
{
if (_text_box.CanDoTextMesure)
_scroll.AddWord(row.Index, word, _text_box.MesureText(word.Text));
});
}
else
{
if (_text_box.CanDoTextMesure)
_scroll.AddWord(row.Index, null, _text_box.MesureText(""));
}
});
if ( total_rows.Count < _scroll.Rows )
{
while (_scroll.Rows > total_rows.Count)
{
_scroll.RemoveRow(_scroll.Rows - 1, true);
ValidateRows = true;
}
}
if (ValidateRows)
{
_scroll.InvalidateRows(true);
}
_scroll.Locked = false;
_scroll.InvalidateLayout();
_text_box.Focus();
UpdateScrolls();
}
public System.Windows.Controls.Primitives.ScrollBar VerticalScrollBar
{
get;
set;
}
public System.Windows.Controls.Primitives.ScrollBar HorizontalScrollBar
{
get;
set;
}
public string Text
{
get { return (string)base.GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public bool IsReadOnly
{
get { return (bool)base.GetValue(IsReadOnlyProperty); }
set { SetValue(IsReadOnlyProperty, value); }
}
}
SyntaxTextBox XAML
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="System.Windows.Controls.SyntaxTextBox" IsTabStop="True"
d:DesignWidth="400" d:DesignHeight="300"
xmlns:sc="clr-namespace:Scroller;assembly=SyntaxTextBox"
xmlns:local="clr-namespace:System.Windows.Controls;assembly=SyntaxTextBox">
<UserControl.Resources>
<LinearGradientBrush x:Key="TextBoxBorder"
EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFA3AEB9"/>
<GradientStop Color="#FF8399A9" Offset="0.375"/>
<GradientStop Color="#FF718597" Offset="0.375"/>
<GradientStop Color="#FF617584" Offset="1"/>
</LinearGradientBrush>
</UserControl.Resources>
<Border Height="Auto" Width="Auto" BorderThickness="1,1,1,1"
CornerRadius="2,2,2,2"
BorderBrush="{StaticResource TextBoxBorder}"
Background="#FFF0FFFF">
<Grid x:Name="LayoutRoot" Height="Auto" Width="Auto">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<sc:ScrollViewerEx Opacity="1.0"
x:Name="_scroll"
Margin="4,4,0,0"
Visibility="Visible"/>
<local:TextBoxExtended
Foreground="#05000000"
Background="{x:Null}"
x:Name="_text_box"
TextWrapping="NoWrap"
AcceptsReturn="True"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" Grid.ColumnSpan="1"
Grid.RowSpan="1" BorderBrush="{x:Null}"
Margin="0,0,0,0"/>
</Grid>
</Border>
</UserControl>
所有其他类和资源都包含在此文章的 zip 压缩包中。
待完成的工作
一旦我修复了任何问题或收到了修改建议,我将尽快更新本文。如果您需要更改或代码修改,请随时进行。如果您修复了错误,实现了有趣的功能,或者修改了算法,请告知我,以便我更新本文。
- 撤销/重做支持
- 行号
Using the Code
要嵌入 SyntaxTextBox
,只需创建一个您自己的 Silverlight 应用程序项目。添加对 SyntaxTextBox.dll 的引用,然后将以下行添加到您的 XAML 页面
<UserControl ...
...
xmlns:stb="clr-namespace:System.Windows.Controls;assembly=SyntaxTextBox">
...
<stb:SyntaxTextBox IsTabStop="True"
Margin="0,0,0,0"
Width="244"
Height="204"
VerticalAlignment="Top"
HorizontalAlignment="Left"/>
...
</UserControl>
更新
02/27/2009
我已经更新了源代码,它们包含 TextBlock
的内联渲染的新实现。现在,在编辑和滚动数千行时速度更快了一些。所有其他性能问题都与 Microsoft 的 Silverlight 实现有关。
1. 新属性
不多。只有一个(目前):O)。
IsReadOnly
- 允许控制内容的编辑。
2. 新方法
SetSyntax(string SyntaxSrc, Encoding SrcEncoding, SyntaxLanguage language)
- 此方法允许您从 XML 字符串源加载外部语言定义。SetSyntax(System.IO.Stream SyntaxSrc, SyntaxLanguage language)
- 此方法允许您从包含 XML 语言定义的源流加载外部语言定义。
3. 新渲染技术
我重新实现了渲染方法以提高滚动/编辑性能。新实现会比较旧的 Text
属性值与新的值之间的差异。只有发生更改的行才会被解析和渲染。请参见下面的代码
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string),
typeof(SyntaxTextBox), new PropertyMetadata("",
delegate(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null )
{
SyntaxTextBox box = d as SyntaxTextBox;
if (box != null)
{
string new_val = (string)e.NewValue;
string old_val = box._document.Text;
string[] new_lines = null;
string[] old_lines = null;
System.IO.StringReader r = new System.IO.StringReader(new_val);
string line = null;
List<string> l_lines = new List<string>();
while ((line = r.ReadLine()) != null)
{
l_lines.Add(line);
}
new_lines = l_lines.ToArray();
r = new System.IO.StringReader(old_val);
line = null;
l_lines = new List<string>();
while ((line = r.ReadLine()) != null)
{
l_lines.Add(line);
}
old_lines = l_lines.ToArray();
bool has_changes = false;
for (int i = 0; i < new_lines.Count(); i++)
{
if (i <= box._document.Count - 1)
{
if (box._document[i].Text != new_lines[i])
{
box._document[i].SetText(new_lines[i]);
box._document[i].IsRendered = false;
has_changes = true;
}
}
else
{
Fireball.Syntax.Row row = box._document.Add(new_lines[i], false);
box._document.ParseRow(row, true);
has_changes = true;
row.IsRendered = false;
}
}
if (old_lines.Count() > new_lines.Count())
{
for (int i = new_lines.Count(); ; )
{
if (box._document.Count == new_lines.Count())
break;
if (box._document.Count == 1)
{
box._document[i].SetText("");
has_changes = true;
break;
}
box._document.Remove(i);
}
}
if (has_changes)
{
box.RenderDocument();
}
}
}
}));
您可能已经注意到调用了一个新的渲染文档的方法:“RenderDocument()
”。代码如下
protected void RenderDocument()
{
//.VisibleRows.OfType<Fireball.Syntax.Row>().ToList();
List<Fireball.Syntax.Row> rows =
_document.Rows.OfType < Fireball.Syntax.Row>().ToList();
//.VisibleRows.OfType<Fireball.Syntax.Row>().ToList();
List<Fireball.Syntax.Row> total_rows =
_document.Rows.OfType<Fireball.Syntax.Row>().ToList();
rows.ForEach(row =>
{
if (_document[rows.IndexOf(row)].RowState ==
Fireball.Syntax.RowState.SegmentParsed)
{
row.IsRendered = false;
_document.Parser.ParseLine(rows.IndexOf(row), true);
}
if (_document[rows.IndexOf(row)].RowState ==
Fireball.Syntax.RowState.NotParsed)
{
row.IsRendered = false;
_document.ParseRow(row, true);
}
});
if (_text_box.CanDoTextMesure == false )
return;
_scroll.Locked = true;
bool ValidateRows = false;
rows.ForEach( row =>
{
if (row.IsRendered)
return;
if (row.Index > _scroll.Rows - 1)
{
ValidateRows = true;
_scroll.AddRow(true);
}
Fireball.Syntax.WordCollection words = row.FormattedWords;
row.IsRendered = true;
Scroller.ScrollRowCanvas block =
_scroll[row.Index] as Scroller.ScrollRowCanvas;
block.Clear();
if (words.Count > 0)
{
words.OfType<Fireball.Syntax.Word>().ToList().ForEach(word =>
{
if (_text_box.CanDoTextMesure)
_scroll.AddWord(row.Index, word, _text_box.MesureText(word.Text));
});
}
else
{
if (_text_box.CanDoTextMesure)
_scroll.AddWord(row.Index, null, _text_box.MesureText(""));
}
});
if ( total_rows.Count < _scroll.Rows )
{
while (_scroll.Rows > total_rows.Count)
{
_scroll.RemoveRow(_scroll.Rows - 1, true);
ValidateRows = true;
}
}
if (ValidateRows)
{
_scroll.InvalidateRows(true);
}
_scroll.Locked = false;
_scroll.InvalidateLayout();
_text_box.Focus();
UpdateScrolls();
}
为了使此代码正常工作,我不得不实现一个自定义 ScrollViewer
控件,该控件基于文章 Scroller.aspx?fid=1532323&df=90&mpp=25&noise=3&sort=Position&view=Quick&select=2845356[^] 中描述的技术。感谢 Jerry Evans 提供的示例。
文章中描述的控件使用固定的列/行大小,这在我们的情况下是不适用的。我已经重新设计了控件以支持动态添加、删除行等。水平滚动条可以通过 SyntaxTextBox
类进行控制。为了控制行的高度/宽度,我在 TextBoxExtended
类中添加了一个名为 MesureText(string Text)
的新方法。为了在 TextBox
中支持此类功能,我在 TextBoxExtended
控件中添加了一个 TextBlock
用于文本测量。这是 TextBoxExtended
类 OnContent_LayoutUpdated
方法的代码
//Getting child content of scrollviewer
private void OnContent_LayoutUpdated(object sender, EventArgs e)
{
if (_content_border != null)
return;
_content_border = VisualTreeHelper.GetChild(_content, 0) as Border;
if (_content_border != null)
{
int count = VisualTreeHelper.GetChildrenCount(_content_border);
if (count > 0)
{
Grid grid = VisualTreeHelper.GetChild(_content_border, 0) as Grid;
if (grid != null)
{
//OK NOW TRY TO CREATE A LITTLE TextBlock
//for text mesurament calculations
_size_block = new TextBlock()
{
Foreground = null,
VerticalAlignment = VerticalAlignment.Top,
HorizontalAlignment = HorizontalAlignment.Left,
FontFamily = FontFamily,
FontSize = FontSize,
FontStretch = FontStretch,
FontStyle = FontStyle,
FontWeight = FontWeight
};
grid.Children.Add(_size_block);
IEnumerable<System.Windows.Controls.Primitives.ScrollBar> found =
(from child in grid.Children.ToList() where child
is System.Windows.Controls.Primitives.ScrollBar select child
as System.Windows.Controls.Primitives.ScrollBar);
if (found.Count() > 0)
{
VerticalScrollBar = (from sc in found where sc.Name ==
"VerticalScrollBar" select sc).First();
HorizontalScrollBar = (from sc in found where sc.Name ==
"HorizontalScrollBar" select sc).First();
if (ContentFound != null)
ContentFound(this, new RoutedEventArgs());
}
//_content.Clip = new RectangleGeometry()
// { Rect = new Rect(0, 0, ActualWidth, ActualHeight) };
}
}
}
}
public Size MesureText(string Text)
{
if (_size_block != null)
{
_size_block.Text = string.IsNullOrEmpty(Text.Replace("\r",
"").Replace("\n","")) ? " ":Text;
return new Size(_size_block.ActualWidth, _size_block.ActualHeight);
}
return Size.Empty;
}
4. 新类“ScrollViewerEx”
public partial class ScrollViewerEx : UserControl//, IMouseWheelObserver
{
// set a fixed cell width
private int cellWidth = 1;
// and a fixed cell height
private int cellHeight = 1;
//
private int _rows = 0;
//
private int _cols = 0;
public int CellHeight
{
get { return cellHeight; }
set
{
cellHeight = value;
InvalidateLayout();
}
}
public int CellWidth
{
get { return cellWidth; }
set
{
cellWidth = value;
InvalidateLayout();
}
}
/// <summary>
/// Stores the current scroll bar position as an integral index
/// </summary>
public int VertPosition
{
get { return (int)VScroll.Value; }
set
{
VScroll.Value = value;
InvalidateLayout();
}
}
/// <summary>
/// Get the maximum range of the vertical scrollbar
/// </summary>
public double VertRange
{
get { return (int)VScroll.Maximum; }
set
{
VScroll.Maximum = value;
}
}
/// <summary>
/// Get the maximum range of the vertical scrollbar
/// </summary>
public double HorzRange
{
get { return HScroll.Maximum; } set { HScroll.Maximum = value; }
}
/// <summary>
/// Stores the current horizontal scrollbar position as an integral index
/// </summary>
public int HorzPosition
{
get { return (int)HScroll.Value; }
set
{
HScroll.Value = value;
InvalidateLayout();
}
}
private bool useClipper = true;
/// <summary>
/// Hows many rows can we display on a page? N.B. assumes fixed height
/// </summary>
private int RowsPerPage
{
get
{
if (useClipper)
return (int)(ElementContentClipper.ClippingRect.Height / cellHeight);
else
return _rows;
}
}
/// <summary>
/// How many columns can we display on a page? N.B. assumes fixed width
/// </summary>
private int ColsPerPage
{
get
{
if (useClipper)
return (int)(ElementContentClipper.ClippingRect.Width / cellWidth);
else
return _cols;
}
}
/// <summary>
/// List of all visible items
/// </summary>
public List<UIElement> VisibleItems
{
get;
private set;
}
/// <summary>
/// Lock for recursion in ArrangeOverride
/// </summary>
public bool Locked
{
get;
set;
}
/// <summary>
/// if FastMode == true then use fast scrolling ....
/// </summary>
public bool FastMode
{
get;
private set;
}
private TranslateTransform Translation
{
get;
set;
}
public int Rows
{
get { return _rows; }
private set { }
}
public void AddWord(int row, Fireball.Syntax.Word word, Size wordSize)
{
ScrollRowCanvas sr =
row >= 0 && row <= ElementContent.Children.Count-1
? ElementContent.Children[row] as ScrollRowCanvas: null;
if (sr == null)
AddRow();
sr = ElementContent.Children[row] as ScrollRowCanvas;
sr.AddWord(word, wordSize);
if (CellHeight < sr.Height)
CellHeight = (int)sr.Height;
}
public void RemoveRow(int Index, bool KeepLocked)
{
bool WasLocked = Locked;
_rows--;
double topDecrementer = (ElementContent.Children[Index] as FrameworkElement).Height;
ElementContent.Children.RemoveAt(Index);
IEnumerable<UIElement> rows = (from child in ElementContent.Children
where ElementContent.Children.IndexOf(child) > Index select child);
if (rows.Count() > 0)
{
rows.ToList().ForEach(row =>
{
double top = ((double)row.GetValue(Canvas.TopProperty)) - topDecrementer;
row.SetValue(Canvas.TopProperty, top);
});
}
if (KeepLocked == false)
Locked = false;
//
SwitchStrategy(false, WasLocked);
// force recalc etc
InvalidateLayout();
//
this.Cursor = Cursors.Arrow;
}
public void AddRow()
{
AddRow(false);
}
public void InvalidateRows(bool KeepLocked)
{
bool WasLocked = Locked;
double actualHeight = 0.0d;
ElementContent.Children.Cast<ScrollRowCanvas>().ToList().ForEach(rw =>
{
rw.SetValue(Canvas.TopProperty, actualHeight);
actualHeight += rw.ActualHeight;
});
Locked = false;
SwitchStrategy(false, WasLocked);
// force recalc etc
InvalidateLayout();
//
this.Cursor = Cursors.Arrow;
}
public void AddRow(bool KeepLocked)
{
bool WasLocked = Locked;
Locked = true;
_rows++;
ScrollRowCanvas sr = new ScrollRowCanvas(_rows-1);
// add to the canvas
double actualHeight =
ElementContent.Children.Sum(s => (s as ScrollRowCanvas).ActualHeight);
ElementContent.Children.Add(sr);
sr.SetValue(Canvas.LeftProperty, 0.0d);
// equivalent to <ScrollRowCanvas Canvas.Top="yoff">
sr.SetValue(Canvas.TopProperty, actualHeight);
if( KeepLocked == false )
Locked = false;
//
actualHeight = 0.0d;
ElementContent.Children.Cast<ScrollRowCanvas>().ToList().ForEach(rw =>
{
rw.SetValue(Canvas.TopProperty, actualHeight);
actualHeight += rw.ActualHeight;
});
SwitchStrategy(false, WasLocked);
// force recalc etc
InvalidateLayout();
//
this.Cursor = Cursors.Arrow;
}
public void RemoveRow(int index)
{
if (index <= ElementContent.Children.Count - 1 && index >= 0)
ElementContent.Children.RemoveAt(index);
else
return;
Locked = true;
_rows--;
double xoff = 0;
double yoff = 0;
for (int row = 0; row < _rows; row++)
{
// new item
ScrollRowCanvas sr = ElementContent.Children[row] as ScrollRowCanvas;
// equivalent to <ScrollRowCanvas Canvas.Left="xoff">
sr.SetValue(Canvas.LeftProperty, xoff);
// equivalent to <ScrollRowCanvas Canvas.Top="yoff">
sr.SetValue(Canvas.TopProperty, yoff);
// next vertical slot
yoff += cellHeight;
}
Locked = false;
//
SwitchStrategy(false);
// force recalc etc
InvalidateLayout();
//
this.Cursor = Cursors.Arrow;
}
public void ScrollIntoPosition(double hV, double vV)
{
bool update = false;
if( HScroll != null && (update=HScroll.Value != hV))
HScroll.Value = hV;
if (VScroll != null && (update=VScroll.Value != vV))
VScroll.Value = vV;
if( update )
InvalidateLayout();
}
public void Clear()
{
this.Cursor = Cursors.Wait;
Locked = true;
ElementContent.Children.Clear();
//
SwitchStrategy(false);
// force recalc etc
InvalidateLayout();
//
this.Cursor = Cursors.Arrow;
}
public ScrollRowCanvas this[int index]
{
get
{
if (index >= 0 && index <= ElementContent.Children.Count - 1)
return ElementContent.Children[index] as ScrollRowCanvas;
else
return null;
}
}
/// <summary>
/// Constructor
/// </summary>
public ScrollViewerEx()
{
InitializeComponent();
Debug.Assert(ElementContent != null);
this.Loaded += OnLoaded;
// event handlers
KeyDown += delegate(object sender, KeyEventArgs e)
{
OnKeyDown(e);
};
// list of *all* row items we manage
VisibleItems = new List<UIElement>();
// apply the scrolling translation
Translation = new TranslateTransform();
// mouse wheel listener - DISABLED
//WheelMouseListener.Instance.AddObserver(this);
}
/// <summary>
/// Ensure we get keyboard events
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected virtual void OnLoaded(object sender, RoutedEventArgs e)
{
// fast by default
FastMode = true;
// set up scroll bars - actual ranges get calculated in
// ArrangeOverride
VScroll.Value = 0;
HScroll.Value = 0;
VScroll.Minimum = 0;
VScroll.Maximum = _rows - 1;
VScroll.Value = 0;
HScroll.Minimum = 0;
HScroll.Maximum = _cols - 1;
HScroll.Value = 0;
//
SwitchStrategy(false);
}
public void SwitchStrategy(bool change)
{
SwitchStrategy(change, false);
}
/// <summary>Switch scrolling strategies</summary>
/// <param name="change"></param>
public void SwitchStrategy(bool change, bool WasLocked)
{
if (change)
{
FastMode = !FastMode;
}
int limit = ElementContent.Children.Count;
Locked = true;
if (FastMode)
{
Color color = Color.FromArgb(0xFF, 0x80, 0x00, 0x00);
SolidColorBrush br = new SolidColorBrush(color);
ColHeaderContent.Background = br;
RowHeaderContent.Background = br;
for (int row = 0; row < limit; row++)
{
ElementContent.Children[row].Visibility = Visibility.Collapsed;
}
}
else
{
Color color = Color.FromArgb(0xFF, 0x40, 0x00, 0x00);
SolidColorBrush br = new SolidColorBrush(color);
ColHeaderContent.Background = br;
RowHeaderContent.Background = br;
for (int row = 0; row < limit; row++)
{
ElementContent.Children[row].Visibility = Visibility.Visible;
}
}
if( WasLocked == false )
Locked = false;
InvalidateLayout();
}
//// mouse wheel - move vertical scroll bar as appropriate
//public void OnMouseWheel(MouseWheelArgs args)
//{
// // update the scrollbar thumb according to wheel motion
// double pos = VScroll.Value;
// pos += -args.Delta;
// VScroll.Value = pos;
// //
// InvalidateLayout();
// //_strategy.Layout(HorzPosition, VertPosition, RowsPerPage, ColsPerPage);
//}
/// <summary>
/// N.B Simplified for the sake of example - we are only interested in thumb events
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void VScroll_Scroll(object sender,
System.Windows.Controls.Primitives.ScrollEventArgs e)
{
InvalidateLayout();
}
/// <summary>
/// N.B Simplified for the sake of example - we are only interested in thumb events
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void HScroll_Scroll(object sender,
System.Windows.Controls.Primitives.ScrollEventArgs e)
{
InvalidateLayout();
}
protected override Size MeasureOverride(Size availableSize)
{
return base.MeasureOverride(availableSize);
}
public void InvalidateLayout()
{
if (Locked)
return;
InvalidateArrange();
}
// establish how many rows and columns we can display and
// set scroll bars accordingly
protected override Size ArrangeOverride(Size finalSize)
{
// let the base class handle the arranging
finalSize = base.ArrangeOverride(finalSize);
// here's the magic ...
ApplyLayoutOptimizer();
//
return finalSize;
}
/// <summary>
/// Set the vertical and horizontal scroll bar ranges
/// </summary>
protected void SetScrollRanges()
{
// what is the view-port size?
Rect clipRect = ElementContentClipper.ClippingRect;
// how many integral lines can we display ?
int rowsPerPage = (int)(clipRect.Height / cellHeight);
// set the scroll count
VScroll.Maximum = (_rows - rowsPerPage);
}
/// <summary>
/// Use the Translation to scroll the content canvas
/// </summary>
protected void HandleScrolling()
{
// offset by scroll positions
Translation.X = -(HScroll.Value);
Translation.Y = -((VScroll.Value * cellHeight) - (VScroll.Value > 0 ? 5 : 0 ));
// apply the transform to the content container
ElementContent.RenderTransform = Translation;
}
public void ApplyLayoutOptimizer()
{
// beware recursion - settings visibility will trigger
// another ArrangeOverride invocation
if (Locked == false)
{
// lock
Locked = true;
// set up the scroll bars
SetScrollRanges();
// hide the visible items
foreach (UIElement uie in VisibleItems)
{
uie.Visibility = Visibility.Collapsed;
}
// remove from list
VisibleItems.Clear();
// layout a page worth of rows
int maxRow = System.Math.Min(VertPosition + RowsPerPage,
ElementContent.Children.Count);
for (int row = VertPosition; row < maxRow; row++)
{
UIElement uie = ElementContent.Children[row];
//
uie.Visibility = Visibility.Visible;
//
VisibleItems.Add(uie);
}
// scroll the canvas
HandleScrolling();
// unlock
Locked = false;
}
}
}
好了,我觉得就是这样了。很抱歉对更新的文章进行如此简略的解释。我没有太多时间进行详细解释。一旦有足够的时间,我会做的。
您可以尝试此新实现,并向我反馈任何建议或错误报告。
02/04/2009
性能问题。我已经修改了控件渲染部分的主算法。我将在几天内更新源代码。新实现仅更新和渲染已更改的行。
我发现了一个 WPF 问题,当同时渲染大量 TextBlock
时会出现。如果您想测试此问题,可以创建一个页面,然后以编程方式将 1000 个 TextBlock
实例放入位于 ScrollViewer
中的 StackPanel
中,您会发现滚动内容几乎是不可能的。我的 P4 3200 gHz CPU 负载超过 60%。看来 Microsoft 在渲染方面存在严重的性能问题。这使得我的控件有点无用。:(