Introduction
First of all, I want to apologize for language problems you may experience while reading this article. English is my second language, so sorry for any miss-spellings or language mistakes.
I posted this article before, expecting responses from readers. There were none. So, this time I will try to explain as much as I can. Additional thanks to the author of the great project code named FIREBALL.
The first reason I started working on such a textbox is to convert my existing code written for Windows Forms, which uses the CodeEditorControl
from Sebastian Faltoni's library, to its Microsoft WPF clone. Since I was working on some Silverlight stuff and WPF is almost the same, I thought that, if I make it work in Silverlight, then I can port it to WPF as well. I started to dig into the Silverlight/WPF possibilities of TextBox
to glue it up with Sebastian’s SyntaxDocument
class. Sebastian uses a totally custom-drawn control in his CodeEditorControl
for Windows Forms. All drawing is done by GDI+. As you know, we do not have anything in WPF to make Sebastian’s code work as is, and we do not have the possibilities to colorize the TextBox
text blocks with different colors. We only have a Foreground
property to control the text color for a whole document. The only control that supports such a thing is TextBlock
.
The TextBlock
has a property Inlines
where you can set a formatted text separated in Run
/LineBreak
blocks. Each Run
block has its own Text
/Foreground
/Background
properties to control the visual appearance of a Text
/Document
. I decided to use a TextBlock
for text visualization with custom block appearance. But there was an issue: how was I going to type text in my control? The TextBlock
supports only visualization, no typing. I came up with the idea to put the TextBox
in front of my User Control and the TextBlock
in the back. The TextBox
is used for typing, and TextBlock
for text visualization. Setting both TextBlock
and TextBox
resulted in an artifact while rendering, with a little text offset of a TextBlock
. To get it fixed, I had to set the TextBox
foreground brush alpha value to 0.01d, so it appears hardly visible, but very useful while selecting text. If I set it to 0.0d, there is another problem with the text selection: that it looks like only the selection background is visible and there is no inverted foreground. This might be an internal logic of controls in Silverlight, because even when SelectionForground
was set to White, it was still not visible.
The next part is how to make the TextBlock
scrollable. That was not so hard. Putting TextBlock
inside the ScrollViewer
solves the issue.
<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>
Next, let’s talk about Sebastian’s SyntaxDocument
class. I ported it from the Windows Forms version to a WPF version. The porting was not hard, but resulted in complete reengineering of some classes that work with XML and uses Hashtable
s as an internal storage for text block styles and so on. The SyntaxDocument
class is parser that contains the parsed content of the document rows/words collection objects with styles included. The style has properties such as foreground and background of the text. After setting the SyntaxDocument.Text
property, it fires up an event “Changed
”, so you can go for the parsed text rendering. In our case, the text rendering is done by TextBlock
. The method shown here is the handler code that renders the parsed content:
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());
});
}
The most important part of the code above is creating an instance of the "Run
" class and setting its properties according to those inside the Word objects that reside in Row.FormattedWords
of each parsed row in the SyntaxDocument
object instance.
The text rendering is done every time the TextBox
fires an event “TextChanged
”. The technique I used might not be the best practice to do this. If anyone finds a way to optimize this code, it would be just great. I will keep searching and digging deeper inside of Sebastian’s code to find a way for optimizations, and might use custom drawing in WPF.
And now, a little bit more about the SyntaxDocument language templates that are used in this class. The language specific dictionaries are stored in XML files (SYN). The one I used in my sample is only for XML syntax highlighting. Other dictionaries have to be ported to the Silverlight version of the SyntaxDocument
class. The changes I have made to the XML syntax dictionary file blocks properties that are responsible for the color definition of a syntax block. Shown below is a sample of such changes made to the XML.SYN file of the SyntaxDocument
engine:
<!---->
<Style Name="Text" ForeColor="Black"
BackColor="" Bold="false"
Italic="false"
Underline="false"/>
<!---->
<Style Name="Text" ForeColor="#FF000000"
BackColor="" Bold="false" Italic="false"
Underline="false"/>
If you want to use other language dictionaries, you may obtain them from the original source codes of Sebastian’s Fireball.Syntax project: http://www.dotnetfireball.net or http://www.codeplex.com/dotnetfireball.
Finally, I got a big problem with scroll bars. When the length of TextBox
text is over its width or height, the scroll bars appear automatically for both controls inside my User Control. If I navigate through the TextBox
text with arrow keys or type a text, the TextBox
’s scroll bars scroll automatically, witch is not happing to my TextBlock
instance that renders the parsed text. In this case, I had to get a control over the TextBox
and the ScrollViewer
scroll bars to make their Maximum
and Value
properties be equal. How can we do this? The first thing is to dig into the TextBox
style/template. To get the default template of a TextBox
, I used Microsoft Expression Blend that does this very easily. Just put the control on a page, right click on the item of your control in the objects/timeline tree on the left, and go for the menu item “Edit Control Parts (Template) -> Edit Copy”. Now, you can go through template parts to get the names of the controls inside. Next, I had to override/handle two events: OnApplyTemplate
/LayoutUpdated
of my derived textbox instance, and get those child elements to find an instance of both the vertical and the horizontal scrollbars. The code below shows how to do this:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_content = base.GetTemplateChild("ContentElement") as ScrollViewer;
if (_content == null)
return;
_content.LayoutUpdated += new EventHandler(OnContent_LayoutUpdated);
}
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());
}
}
}
}
}
After we get an instance of both the scrollbars of a TextBox
, we can get the actual values of its properties and signup for events when the scroll values are changed, so we can set our TextBlock
’s ScrollViewer
scrollbar values to the same value as the TextBox
has. This technique gives us the ability to implement a custom TextBox
control that can highlight syntax. Below is the complete code for the TextBoxExtended
class and the User Control that implements code syntax highlighting:
The TextBoxExtended class with vertical and horizontal scrollbars available
public class TextBoxExtended : TextBox
{
ScrollViewer _content = null;
Border _content_border = null;
TextBlock _size_block = null;
public event RoutedEventHandler ContentFound;
public TextBoxExtended()
{
DefaultStyleKey = typeof(TextBoxExtended);
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_content = base.GetTemplateChild("ContentElement") as ScrollViewer;
if (_content == null)
return;
_content.LayoutUpdated += new EventHandler(OnContent_LayoutUpdated);
}
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)
{
_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());
}
}
}
}
}
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;
}
}
The SyntaxTextBox class with syntax highlighting support
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);
}
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);
}
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);
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()
{
List<Fireball.Syntax.Row> rows =
_document.Rows.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); }
}
}
The 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>
All other classes and resources are included in the zip archive of this article.
The work to do
I will try to update this article as soon as I fix any issue I find or include a suggestion for modification. If you want changes or code modifications, feel free to do it. If you fix errors or implement something interesting, or modify an algorithm, please let me know, so I can update this article.
- Undo/Redo support
- Line numbering
Using the Code
To embed SyntaxTextBox
, simply create your own Silverlight Application project. Add a reference to SyntaxTextBox.dll and then add the following lines to your XAML page:
<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>
Updates
02/27/2009
I have updated the sources and they contain a new implementation for TextBlock
's inline rendering. Now, it is a bit faster when editing and scrolling 1000s of lines. All other performance issues are related to Microsoft's Silverlight implementation.
1. New properties
Not much. Just one (for now) :O).
IsReadOnly
- Allows to control editing of content.
2. New methods
SetSyntax(string SyntaxSrc, Encoding SrcEncoding, SyntaxLanguage language)
- This method allows you to load an external language definition from an XML string source.
SetSyntax(System.IO.Stream SyntaxSrc, SyntaxLanguage language)
- This method allows you to load an external language definition from a source stream that contains an XML language definition.
3. New rendering technique
I have re-implemented the rendering methods to improve scrolling/editing performance. The new implementation uses differences between the old Text
property value and the new one. Only lines that are different will be parsed and rendered. See the code below:
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();
}
}
}
}));
You may have noticed that there is a call to a new method that renders the document: "RenderDocument()
". The code is shown below:
protected void RenderDocument()
{
List<Fireball.Syntax.Row> rows =
_document.Rows.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();
}
In order to make this code work, I had to implement a custom ScrollViewer
control that is based on a technique described in the article Scroller.aspx?fid=1532323&df=90&mpp=25&noise=3&sort=Position&view=Quick&select=2845356[^]. Thanks to Jerry Evans for the sample.
The control described in the article uses fixed column/row sizes and that is not valid in our case. I have redesigned the control to support dynamic row addition, removing, and etc. The horizontal scrollbar is controllable through the SyntaxTextBox
class. To control the row's width/height, I have added a new method to the TextBoxExtended
class called MesureText(string Text)
. To support such functionality in TextBox
, I have added a TextBlock
to the TextBoxExtended
control for the text measurement. Here is the code for the TextBoxExtended
class OnContent_LayoutUpdated
method:
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)
{
_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());
}
}
}
}
}
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. New class "ScrollViewerEx"
public partial class ScrollViewerEx : UserControl
{
private int cellWidth = 1;
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();
}
}
public int VertPosition
{
get { return (int)VScroll.Value; }
set
{
VScroll.Value = value;
InvalidateLayout();
}
}
public double VertRange
{
get { return (int)VScroll.Maximum; }
set
{
VScroll.Maximum = value;
}
}
public double HorzRange
{
get { return HScroll.Maximum; } set { HScroll.Maximum = value; }
}
public int HorzPosition
{
get { return (int)HScroll.Value; }
set
{
HScroll.Value = value;
InvalidateLayout();
}
}
private bool useClipper = true;
private int RowsPerPage
{
get
{
if (useClipper)
return (int)(ElementContentClipper.ClippingRect.Height / cellHeight);
else
return _rows;
}
}
private int ColsPerPage
{
get
{
if (useClipper)
return (int)(ElementContentClipper.ClippingRect.Width / cellWidth);
else
return _cols;
}
}
public List<UIElement> VisibleItems
{
get;
private set;
}
public bool Locked
{
get;
set;
}
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);
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);
InvalidateLayout();
this.Cursor = Cursors.Arrow;
}
public void AddRow(bool KeepLocked)
{
bool WasLocked = Locked;
Locked = true;
_rows++;
ScrollRowCanvas sr = new ScrollRowCanvas(_rows-1);
double actualHeight =
ElementContent.Children.Sum(s => (s as ScrollRowCanvas).ActualHeight);
ElementContent.Children.Add(sr);
sr.SetValue(Canvas.LeftProperty, 0.0d);
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);
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++)
{
ScrollRowCanvas sr = ElementContent.Children[row] as ScrollRowCanvas;
sr.SetValue(Canvas.LeftProperty, xoff);
sr.SetValue(Canvas.TopProperty, yoff);
yoff += cellHeight;
}
Locked = false;
SwitchStrategy(false);
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);
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;
}
}
public ScrollViewerEx()
{
InitializeComponent();
Debug.Assert(ElementContent != null);
this.Loaded += OnLoaded;
KeyDown += delegate(object sender, KeyEventArgs e)
{
OnKeyDown(e);
};
VisibleItems = new List<UIElement>();
Translation = new TranslateTransform();
}
protected virtual void OnLoaded(object sender, RoutedEventArgs e)
{
FastMode = true;
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);
}
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();
}
private void VScroll_Scroll(object sender,
System.Windows.Controls.Primitives.ScrollEventArgs e)
{
InvalidateLayout();
}
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();
}
protected override Size ArrangeOverride(Size finalSize)
{
finalSize = base.ArrangeOverride(finalSize);
ApplyLayoutOptimizer();
return finalSize;
}
protected void SetScrollRanges()
{
Rect clipRect = ElementContentClipper.ClippingRect;
int rowsPerPage = (int)(clipRect.Height / cellHeight);
VScroll.Maximum = (_rows - rowsPerPage);
}
protected void HandleScrolling()
{
Translation.X = -(HScroll.Value);
Translation.Y = -((VScroll.Value * cellHeight) - (VScroll.Value > 0 ? 5 : 0 ));
ElementContent.RenderTransform = Translation;
}
public void ApplyLayoutOptimizer()
{
if (Locked == false)
{
Locked = true;
SetScrollRanges();
foreach (UIElement uie in VisibleItems)
{
uie.Visibility = Visibility.Collapsed;
}
VisibleItems.Clear();
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);
}
HandleScrolling();
Locked = false;
}
}
}
Well, I think that's it. Sorry for such sparse explanation of my updated article. I do not have a lot of time for a descent explanation. I will do this as soon as I have enough time for this.
You may try this new implementation, and post a feedback to me for any suggestions or bug reports.
02/04/2009
The performance issues. I have modified the main algorithm of the rendering part of my control. I will update the sources in a couple of days. The new implementation updates and renders only the changed rows.
I have found a WPF issue when rendering a huge amount of TextBlock
s at the same time. If you want to test this issue, you may create a page and then programmatically put 1000 TextBlock
instances into a StackPanel
that resides inside a ScrollViewer
and you will see that scrolling the content is almost impossible. CPU load is over 60 % on my P4 3200 gHz. It seems that Microsoft has a big performance problem in rendering. This makes my control kind of useless. :O(