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

Silverlight 2.0 语法高亮 TextBox

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (13投票s)

2009年2月2日

LGPL3

9分钟阅读

viewsIcon

157285

downloadIcon

1180

Silverlight 2.0 中的语法高亮 TextBox。

Image4.png

引言

首先,对于您在阅读本文时可能遇到的语言问题,我想提前致歉。英语是我的第二语言,所以对于任何拼写错误或语言错误,我深表歉意。

我之前发布过这篇文章,期待读者的回复。但没有收到。所以,这次我将尽力解释。另外,感谢名为 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 用于文本可视化。将 TextBlockTextBox 都设置好后,在渲染时会出现一个伪影,导致 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.nethttp://www.codeplex.com/dotnetfireball

最后,我遇到了一个滚动条的大问题。当 TextBox 文本的长度超过其宽度或高度时,用户控件内的两个控件都会自动出现滚动条。如果我通过箭头键导航 TextBox 文本或输入文本,TextBox 的滚动条会自动滚动,而我的 TextBlock 实例(用于渲染已解析文本)则不会。在这种情况下,我必须对 TextBoxScrollViewer 的滚动条进行控制,使其 MaximumValue 属性相等。我们该怎么做?首先要深入研究 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 的滚动条实例后,我们可以获取其属性的实际值,并在滚动值更改时注册事件,这样我们就可以将 TextBlockScrollViewer 滚动条值设置为与 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 压缩包中。

待完成的工作

一旦我修复了任何问题或收到了修改建议,我将尽快更新本文。如果您需要更改或代码修改,请随时进行。如果您修复了错误,实现了有趣的功能,或者修改了算法,请告知我,以便我更新本文。

  1. 撤销/重做支持
  2. 行号

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 用于文本测量。这是 TextBoxExtendedOnContent_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 在渲染方面存在严重的性能问题。这使得我的控件有点无用。:(

© . All rights reserved.