Introduction
Last month, me and my colleague (Afshin Khashei) worked on a document management
system that deals with lots of FlowDocuments. Flow documents are designed to optimize viewing and readability. Rather than being
set to one predefined layout, flow documents dynamically adjust and reflow their
content based on run-time variables such as window size, device resolution, and
optional user preferences. FlowDocument Hosts and formats flow content with advanced
document features, such as pagination and columns [MSDN]. The Blocking parts of a FlowDocument are instances of the Block class which has many derivations such as Figure, Floater, List, ListItem, Section and Table. Alongside the Block class there is another class called Inline. Each inline represents a piece of text or a special inline-level format for its
child inlines. Run, Span, Bold, and Underline are Examples of Inline classes. Block level and Inline Level classes together provide
a consistent way for generating custom flow documents. Here is a sample flowdocument.
<FlowDocument
Name="flowDoc"
TextAlignment="Justify"
IsOptimalParagraphEnabled="True"
IsHyphenationEnabled="True"
IsColumnWidthFlexible="True"
Background="AliceBlue"
ColumnWidth="300"
ColumnGap="20"
>
<Paragraph>
<Italic>
<Run>
"One of the most important operations necessary when text materials
are prepared for printing or display is the task of dividing long
paragraphs into individual lines. When this job has been done well,
people will not be aware of the fact that the words they are reading
have been broken apart arbitrarily and placed into a somewhat rigid
and unnatural rectangular framework; but if the job has been done
poorly, readers will be distracted by bad breaks that interrupt
their train of thought."
</Run></Italic>
</Paragraph>
<Paragraph>
<Hyperlink NavigateUri="http://www-cs-faculty.stanford.edu/~knuth/">
- Donald E. Knuth
</Hyperlink>
</Paragraph>
<Paragraph>
<Bold>
Principle of Optimal Paragraph
</Bold>
</Paragraph>
<Paragraph>
Knuth started his revolutionary work of developing a computer program to
produce high quality publication in the late ’70. The program he developed
called TEX (pronounced "tek") is highly regarded as the work that helps
shape the field of digital typography and desktop publishing. It is still
being widely used and constantly extended by many others up to today. One
of the most intriguing inventions in this program is its ability to produce
superior quality paragraph layout for printing and reading purpose – arguably
comparable to the work manually done by most respectable publishers of the
modern times. This typographically beautiful paragraph layout is driven by
a line breaking algorithm known as
<Italic>total-fit</Italic> or
<Italic>optimum-fit</Italic> algorithm. Some call the kind of paragraph
produced by this algorithm
<Italic>Optimal Paragraph</Italic> .
</Paragraph>
<Paragraph>
In principle, the task of text formatting consists of two main components:
choosing where to end individual lines and how to justify the lines. The first
component may involve the task of word division called
<Italic>hyphenation</Italic> .
Line justification is the task of fitting a line into a desired width. Usually
it is done by distributing the extra space into or taking out excess space
from inter-word spaces in the line. Line justification is strongly dependent
to line breaking. When line breaking is done properly, justification can be
done without the need to change the spacing too much and thereby avoiding
holes between words that would distress the eyes of the reader.
</Paragraph>
<Paragraph>
Unlike a standard line breaking algorithm which breaks the line without taking
into account the line that may come after it, the total-fit algorithm breaks
line by looking ahead on what may come later in the paragraph and make a single
decision to break all the lines at once. The main idea of the algorithm is to
provide a way to break a paragraph into lines so that the inter-word spacing is
balanced between all the lines of the paragraph. This is attained by choosing
the sequence of breakpoints with the minimal total cost over all lines. The
cost of a line depends on many factors that can affect the visual appearance
of the line, such as the measure of inter-word space changing, the division of
the last word of the line, etc.
</Paragraph>
<Paragraph>
A paragraph is considered optimal when all inter-word spaces over all lines are
set as
close as possible to the ideal inter-word spaces. Therefore, an optimum paragraph
is in
fact a paragraph composed in such a way that the total contrast of inter-word spaces
set
in all lines with the ideal inter-word spaces cannot be reduced anymore.
</Paragraph>
</FlowDocument>
The above FlowDocument looks like as follow in run time.

WPF provides a couple of controls that can save/load flowdocuments including FlowDocumentReader, RichTextBox, FlowDocumentPageViewer and FlowDocumentScrollViewer. One of the shining features of those controls is their Find option that allows
users to find a word or a phrase in a document. Find Control provides a nice
user interface and using it is easy.
Last month, we faced a situation that we need to customize the UI and functionality
of the build in Find control. After investigating the ControlTemplate of the FlowDocumentReader, we realized that the original Find Control can’t be customized,
because Microsoft defines it as Internal and there is no way to access it! And
shamefully, there is no news about it in the dotnet Framework 4 too. As a result,
me and my colleague (Afshin Khashei) decided to implement a find control from
the scratch. We expect the following functionalities from the new control.
· Expanding and collapsing the control similar to the original one.
· Highlighting the founded search term in the FlowDocument.
· Highlighting the Paragraph of the founded search term by a different color.
· The ability to navigate through founded patters.
· The ability to customize Font, Color, Background image and Background text of the
control.
· The ability to put the control outside of the FlowDocumentReader.
The blue items don’t support by the current Find Control. We define our control as
a custom UserControl that gets the FlowDocument via its Document property and
starts the search operation whenever user expand the control and press the Enter
after entering the search term. The control is flexible and using it is easy
too.
The rest of the paper is organized as follows. In the first section, I will illustrate
the process of finding a word inside a FlowDocument. In the second part, I will
describe the process of how to highlight a word inside a FlowDocument. There
is a light glance at the code of the Control in the third section and finally
the last section is dedicated to using of the control.
Finding Text inside a FlowDocument
A FlowDocument can contain lots of different Blocks and Inlines, but, all of the
texts inside a FlowDocument have been represented by the Run class. The other
Blocks and Inlines are used for formatting and structuring the layout. An experienced
reader who worked on FlowDocuments may say “No, you are wrong, I can put text
inside other Inlines or Blocks like the following:
<Paragraph>
<Italic>
"One of the most important operations necessary when text materials
are prepared for printing or display is the task of dividing long
paragraphs into individual lines. When this job has been done well,
people will not be aware of the fact that the words they are reading
have been broken apart arbitrarily and placed into a somewhat rigid
and unnatural rectangular framework; but if the job has been done
poorly, readers will be distracted by bad breaks that interrupt
their train of thought."
</Italic>
</Paragraph>
But the reality is, as soon as the WPF loads the above paragraph, it changes the
code as follow implicitly.
Paragraph>
<Italic>
<Run>
"One of the most important operations necessary when text materials
are prepared for printing or display is the task of dividing long
paragraphs into individual lines. When this job has been done well,
people will not be aware of the fact that the words they are reading
have been broken apart arbitrarily and placed into a somewhat rigid
and unnatural rectangular framework; but if the job has been done
poorly, readers will be distracted by bad breaks that interrupt
their train of thought."
</Run></Italic>
As a matter of fact, the WPF inserts the Run instances apparently whenever it is
needed. So, in order to find a special text inside a FlowDocument, we should
navigate through the Run instances and check their Text properties to find a
matching with the search phrase. The following class contains the methods that
can be used to return all Run instances inside a FlowDoucment.
public static class LogicalTreeUtility
{
public static IEnumerable GetChildren(DependencyObject obj, Boolean allChildrenInHierachy)
{
if (!allChildrenInHierachy)
return LogicalTreeHelper.GetChildren(obj);
else
{
List<object> ReturnValues = new List<object>();
RecursionReturnAllChildren(obj, ReturnValues);
return ReturnValues;
}
}
private static void RecursionReturnAllChildren(DependencyObject obj, List<object> returnValues)
{
foreach (object curChild in LogicalTreeHelper.GetChildren(obj))
{
returnValues.Add(curChild);
if (curChild is DependencyObject)
RecursionReturnAllChildren((DependencyObject)curChild, returnValues);
}
}
public static IEnumerable<ReturnType> GetChildren<ReturnType>(DependencyObject obj, Boolean allChildrenInHierachy)
{
foreach (object child in GetChildren(obj, allChildrenInHierachy))
if (child is ReturnType)
yield return (ReturnType)child;
}
}
The above codes are based on a dotnet framework class called LogicalTreeHelper. This class has the GetChildren method that returns all of the direct childes of
a given DependencyObject. In order to return all childes including grand childes and …, we have to call the
GetChildren method recursively as follows:
private static void RecursionReturnAllChildren(DependencyObject obj, List<object> returnValues)
{
foreach (object curChild in LogicalTreeHelper.GetChildren(obj))
{
returnValues.Add(curChild);
if (curChild is DependencyObject)
RecursionReturnAllChildren((DependencyObject)curChild, returnValues);
}
}
Using the LogicalTreeUtility.GetChildren method, one could get all of the Runs inside a document and check whether
their Text properties contain the given search term or not. Seems to be easy,
isn’t it?
But the story isn’t finished and finding the matched Runs is not sufficient. We need
to highlight the founded words too!
TextRange, TextPointers and TextEffect
The TextPointer class is intended to facilitate traversal and manipulation of content that is represented
by Windows Presentation Foundation (WPF) flow content elements. It provides a
way to address the positions inside a FlowDocument. Some of the operations that TextPointer facilitates include the following:
· Perform an ordinal comparison of the current position with a second specified position.
· Determine the type of content adjacent to the current position in a specified direction.
· Get the TextElement that scopes or is adjacent to the current position.
· Get the text container that scopes the current document.
· Get a specified number of characters preceding or following the current position.
· Insert a string of characters at the current position.
· Find line boundaries in content.
· Translate between TextPointer positions and symbol offsets into content. See the
GetOffsetToPosition and GetPositionAtOffset methods.
[MSDN]
On the other hand, a TextRange specifies a selection of content surrounded by two TextPointers. In order to address the search term inside a FlowDocument, we have to find a TextRange
that its starting pointer points to the start of the word and its End pointer
points to the end of the word in the document. t
After finding the TextRange, we have to highlight it. The highlighting process should
be done without affecting the foregrounds and styles of the underlying document.
Fortunately, text highlighting can be done transparently using the TextEffect class. The TextEffect object allows you to add effects such as animations to text
objects such as TextBlock, TextElement, and FlowDocument objects. The following
code assign a foreground color to a target that specified by a TextRange using
a TextEffect.
public static TextEffect HighLightText(TextRange range, Brush brush)
{
TextEffect effect = new TextEffect();
effect.Foreground = brush;
if (range.Start.Parent is Inline)
{
Inline parent = range.Start.Parent as Inline;
effect.PositionStart = Math.Abs(range.Start.GetOffsetToPosition(parent.ContentStart));
effect.PositionCount = Math.Abs(range.End.GetOffsetToPosition(range.Start));
}
else if (range.Start.Parent is Block)
{
Block parent = range.Start.Parent as Block;
effect.PositionStart = Math.Abs(range.Start.GetOffsetToPosition(parent.ContentStart));
effect.PositionCount = Math.Abs(range.End.GetOffsetToPosition(range.Start));
}
TextEffectTarget[] targets = TextEffectResolver.Resolve(range.Start, range.End, effect);
foreach (TextEffectTarget target in targets)
{
target.Enable();
}
return effect;
}
In case user collapses the Find control, the TextEffects should be sweep off from
the document. And the following code, clear all of the TextEffects inside a document.
public static void ClearTextDecorations(DependencyObject document)
{
// Traverse all run tags
foreach (Run run in LogicalTreeUtility.GetChildren<Run>(document, true))
{
run.TextEffects = new TextEffectCollection();
}
}
Find Control
And finally here is the code of the FindControl. Here is the code of FindControl
itself.
<UserControl x:Name="findControl" x:Class="ICP.Controls.Document.FindControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:flowDocumentControls="clr-namespace:ICP.Controls.Document"
xmlns:converters="clr-namespace:ICP.Controls.Converters"
xmlns:controls="clr-namespace:ICP.Controls"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
<converters:BooleanToVisibilityConverter x:Key="booleanToVisibilityConverter"></converters:BooleanToVisibilityConverter>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<ToggleButton Grid.Column="0" Click="FindButton_Click" Name="FindButton" Margin="3,0,3,0" ToolTip="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type flowDocumentControls:FindControl}}, Path=ToolTip}" x:Uid="ToggleButton_1" Focusable="True">
<ToggleButton.Style>
<Style TargetType="ToggleButton">
<Style.BasedOn>
<Style TargetType="ButtonBase">
<Style.Triggers>
<Trigger Property="UIElement.IsEnabled">
<Setter Property="UIElement.Opacity">
<Setter.Value>
<s:Double>0.3</s:Double>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>False</s:Boolean>
</Trigger.Value>
</Trigger>
<Trigger Property="UIElement.IsMouseOver">
<Setter Property="UIElement.Opacity">
<Setter.Value>
<s:Double>1</s:Double>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>
</Style.Triggers>
<Style.Resources>
<ResourceDictionary />
</Style.Resources>
<Setter Property="UIElement.Focusable">
<Setter.Value>
<s:Boolean>False</s:Boolean>
</Setter.Value>
</Setter>
<Setter Property="UIElement.Opacity">
<Setter.Value>
<s:Double>0.5</s:Double>
</Setter.Value>
</Setter>
<Setter Property="FrameworkElement.Cursor">
<Setter.Value>
<Cursor>Hand</Cursor>
</Setter.Value>
</Setter>
<Setter Property="Panel.Background">
<Setter.Value>
<SolidColorBrush>#00FFFFFF</SolidColorBrush>
</Setter.Value>
</Setter>
<Setter Property="Control.Padding">
<Setter.Value>
<Thickness>3,1,3,1</Thickness>
</Setter.Value>
</Setter>
<Setter Property="Border.BorderBrush">
<Setter.Value>
<x:Null />
</Setter.Value>
</Setter>
<Setter Property="Border.BorderThickness">
<Setter.Value>
<Thickness>0,0,0,0</Thickness>
</Setter.Value>
</Setter>
<Setter Property="FrameworkElement.MinWidth">
<Setter.Value>
<s:Double>0</s:Double>
</Setter.Value>
</Setter>
<Setter Property="FrameworkElement.MinHeight">
<Setter.Value>
<s:Double>0</s:Double>
</Setter.Value>
</Setter>
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="ButtonBase">
<Border Background="{TemplateBinding Panel.Background}" x:Uid="Border_39">
<ContentPresenter Content="{TemplateBinding ContentControl.Content}" ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}" ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}" Name="Content" RenderTransformOrigin="0.5,0.5" x:Uid="ContentPresenter_2" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="UIElement.IsMouseOver">
<Setter Property="UIElement.RenderTransform" TargetName="Content">
<Setter.Value>
<ScaleTransform ScaleX="1.1" ScaleY="1.1" />
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>
<Trigger Property="ButtonBase.IsPressed">
<Setter Property="UIElement.RenderTransform" TargetName="Content">
<Setter.Value>
<ScaleTransform ScaleX="0.9" ScaleY="0.9" />
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Style.BasedOn>
<Style.Triggers>
<Trigger Property="UIElement.IsEnabled">
<Setter Property="UIElement.Visibility">
<Setter.Value>
<x:Static Member="Visibility.Collapsed" />
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>False</s:Boolean>
</Trigger.Value>
</Trigger>
<Trigger Property="ToggleButton.IsChecked">
<Setter Property="UIElement.Opacity">
<Setter.Value>
<s:Double>1</s:Double>
</Setter.Value>
</Setter>
<Trigger.Value>
<s:Boolean>True</s:Boolean>
</Trigger.Value>
</Trigger>
</Style.Triggers>
<Style.Resources>
<ResourceDictionary />
</Style.Resources>
</Style>
</ToggleButton.Style>
<Path Stroke="Black" VerticalAlignment="Center" x:Uid="Path_23">
<Path.Data>
<GeometryGroup>
<GeometryGroup.Children>
<RectangleGeometry RadiusX="1" RadiusY="1" Rect="0.5,0.5,19,19" />
<EllipseGeometry RadiusX="5" RadiusY="5" Center="12,8" />
<EllipseGeometry RadiusX="4" RadiusY="4" Center="12,8" />
<LineGeometry StartPoint="2.5,16.5" EndPoint="9,10" />
<LineGeometry StartPoint="3,17" EndPoint="9.5,10.5" />
<LineGeometry StartPoint="3.5,17.5" EndPoint="10,11" />
</GeometryGroup.Children>
</GeometryGroup>
</Path.Data>
</Path>
</ToggleButton>
<Grid Grid.Column="1" x:Name="findTextBoxGrid" Background="{Binding ElementName=findControl, Path=TextBoxBackgroundBrush}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="10*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Label
FontFamily="{Binding ElementName=findControl, Path=FontFamily}"
FontSize="{Binding ElementName=findControl, Path=FontSize}"
FontStyle="{Binding ElementName=findControl, Path=FontStyle}"
FontWeight="{Binding ElementName=findControl, Path=FontWeight}"
FontStretch="{Binding ElementName=findControl, Path=FontStretch}"
Name="backgroundLabel"
Grid.Column="0"
Foreground="{Binding ElementName=findControl, Path=BackgroundTextBrush}"
Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type flowDocumentControls:FindControl}}, Path=BackgroundText}"
Visibility="{Binding ElementName=findTextBox, Path=Text.IsEmpty, Converter={StaticResource booleanToVisibilityConverter}}"
HorizontalAlignment="Left" VerticalAlignment="Center"/>
<TextBox Background="Transparent" Grid.Column="0"
Padding="0"
FontFamily="{Binding ElementName=findControl, Path=FontFamily}"
FontSize="{Binding ElementName=findControl, Path=FontSize}"
FontStyle="{Binding ElementName=findControl, Path=FontStyle}"
FontWeight="{Binding ElementName=findControl, Path=FontWeight}"
FontStretch="{Binding ElementName=findControl, Path=FontStretch}"
BorderThickness="0"
Name="findTextBox"
KeyDown="findTextBox_KeyDown"
HorizontalAlignment="Left"
VerticalAlignment="Center"
/>
<controls:ContentButton Grid.Column="1" Width="15px" x:Name="previousButton" Click="previousButton_Click">
<Path VerticalAlignment="Center" HorizontalAlignment="Center" Fill="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type flowDocumentControls:FindControl}}, Path=NextPreviousButtonsForegroundBrush}" x:Uid="Path_18">
<Path.Data>
<PathGeometry Figures="M5,0L5,10L0,5z" />
</Path.Data>
</Path>
<controls:ContentButton.ToolTip>
<TextBlock Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type flowDocumentControls:FindControl}}, Path=PreviousButtonHintText}"></TextBlock>
</controls:ContentButton.ToolTip>
</controls:ContentButton>
<controls:ContentButton Grid.Column="2" Width="15px" x:Name="nextButton" Click="nextButton_Click">
<Path VerticalAlignment="Center" HorizontalAlignment="Center" Fill="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type flowDocumentControls:FindControl}}, Path=NextPreviousButtonsForegroundBrush}" x:Uid="Path_19">
<Path.Data>
<PathGeometry Figures="M0,0L0,10L5,5z" />
</Path.Data>
</Path>
<controls:ContentButton.ToolTip>
<TextBlock Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type flowDocumentControls:FindControl}}, Path=NextButtonHintText}"></TextBlock>
</controls:ContentButton.ToolTip>
</controls:ContentButton>
</Grid>
</Grid>
</UserControl>
public partial class FindControl : UserControl, INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void notifyPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
#endregion
public FindControl()
{
InitializeComponent();
setFindTextBoxVisibilty();
}
#region Properties
public string SearchText
{
get
{
return (string)this.GetValue(SearchTextProperty);
}
set
{
this.SetValue(SearchTextProperty, value);
}
}
public static readonly DependencyProperty SearchTextProperty = DependencyProperty.Register(
"SearchText", typeof(string), typeof(FindControl), new FrameworkPropertyMetadata());
public FlowDocument Document
{
get
{
return (FlowDocument)this.GetValue(DocumentProperty);
}
set
{
this.SetValue(DocumentProperty, value);
}
}
public static readonly DependencyProperty DocumentProperty = DependencyProperty.Register(
"Document", typeof(FlowDocument), typeof(FindControl), new FrameworkPropertyMetadata());
public Brush TextBoxBackgroundBrush
{
get
{
return (Brush)this.GetValue(TextBoxBackgroundBrushProperty);
}
set
{
this.SetValue(TextBoxBackgroundBrushProperty, value);
}
}
public static readonly DependencyProperty TextBoxBackgroundBrushProperty = DependencyProperty.Register(
"TextBoxBackgroundBrush", typeof(Brush), typeof(FindControl), new FrameworkPropertyMetadata());
public Brush BackgroundTextBrush
{
get
{
return (Brush)this.GetValue(BackgroundTextBrushProperty);
}
set
{
this.SetValue(BackgroundTextBrushProperty, value);
}
}
public static readonly DependencyProperty BackgroundTextBrushProperty = DependencyProperty.Register(
"BackgroundTextBrush", typeof(Brush), typeof(FindControl), new FrameworkPropertyMetadata());
public string BackgroundText
{
get
{
return (string)this.GetValue(BackgroundTextProperty);
}
set
{
this.SetValue(BackgroundTextProperty, value);
}
}
public static readonly DependencyProperty BackgroundTextProperty = DependencyProperty.Register(
"BackgroundText", typeof(string), typeof(FindControl), new FrameworkPropertyMetadata());
public string NextButtonHintText
{
get
{
return (string)this.GetValue(NextButtonHintTextProperty);
}
set
{
this.SetValue(NextButtonHintTextProperty, value);
}
}
public static readonly DependencyProperty NextButtonHintTextProperty = DependencyProperty.Register(
"NextButtonHintText", typeof(string), typeof(FindControl), new FrameworkPropertyMetadata());
public string PreviousButtonHintText
{
get
{
return (string)this.GetValue(PreviousButtonHintTextProperty);
}
set
{
this.SetValue(PreviousButtonHintTextProperty, value);
}
}
public static readonly DependencyProperty PreviousButtonHintTextProperty = DependencyProperty.Register(
"PreviousButtonHintText", typeof(string), typeof(FindControl), new FrameworkPropertyMetadata());
public Brush HighlightedTextBrush
{
get
{
return (Brush)this.GetValue(HighlightedTextBrushProperty);
}
set
{
this.SetValue(HighlightedTextBrushProperty, value);
}
}
public static readonly DependencyProperty HighlightedTextBrushProperty = DependencyProperty.Register(
"HighlightedTextBrush", typeof(Brush), typeof(FindControl), new FrameworkPropertyMetadata());
public Brush HighlightedParagraphBrush
{
get
{
return (Brush)this.GetValue(HighlightedParagraphBrushProperty);
}
set
{
this.SetValue(HighlightedParagraphBrushProperty, value);
}
}
public static readonly DependencyProperty HighlightedParagraphBrushProperty = DependencyProperty.Register(
"HighlightedParagraphBrush", typeof(Brush), typeof(FindControl), new FrameworkPropertyMetadata());
public Brush NextPreviousButtonsForegroundBrush
{
get
{
return (Brush)this.GetValue(NextPreviousButtonsForegroundBrushProperty);
}
set
{
this.SetValue(NextPreviousButtonsForegroundBrushProperty, value);
}
}
public static readonly DependencyProperty NextPreviousButtonsForegroundBrushProperty = DependencyProperty.Register(
"NextPreviousButtonsForegroundBrush", typeof(Brush), typeof(FindControl), new FrameworkPropertyMetadata());
private int currentFindedPatternPosition;
private List<TextRange> findedPatterns;
private DependencyObject highlightedParagraph;
private string filterText = "";
public Action RefreshHighLightedWordsHandler
{
get
{
return (Action)this.GetValue(RefreshHighLightedWordsHandlerProperty);
}
set
{
this.SetValue(RefreshHighLightedWordsHandlerProperty, value);
}
}
public static readonly DependencyProperty RefreshHighLightedWordsHandlerProperty = DependencyProperty.Register(
"RefreshHighLightedWordsHandler", typeof(Action), typeof(FindControl), new FrameworkPropertyMetadata());
#endregion
#region Methods
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property.Name == "SearchText")
{
if (SearchText != null && SearchText.Length > 0)
{
FindButton.IsChecked = true;
setFindTextBoxVisibilty();
findTextBox.Text = SearchText;
DoSearch();
}
}
}
private void refreshHighlightedwordsSearch()
{
if (FindButton.IsChecked.HasValue && FindButton.IsChecked.Value &&
findedPatterns != null)
{
foreach (TextRange range in findedPatterns)
{
FlowDocumentExtension.HighLightText(range, HighlightedTextBrush);
}
}
}
private void setFindTextBoxVisibilty()
{
if (FindButton.IsChecked.HasValue && FindButton.IsChecked.Value)
{
findTextBoxGrid.Visibility = Visibility.Visible;
findTextBox.Text = "";
filterText = "";
currentFindedPatternPosition = -1;
findedPatterns = null;
findTextBox.Focus();
CreateButtonToolTips();
RefreshHighLightedWordsHandler = new Action(refreshHighlightedwordsSearch);
notifyPropertyChanged("RefreshSearchHandler");
}
else
{
findTextBoxGrid.Visibility = Visibility.Collapsed;
SearchText = "";
if (Document != null)
{
SetBackGround(highlightedParagraph, null);
FlowDocumentExtension.ClearTextDecorations(Document);
}
}
}
private void CreateButtonToolTips()
{
if (NextButtonHintText != null && NextButtonHintText.Length > 0)
{
ToolTip tooltip = new ToolTip();
TextBlock textBlock = new TextBlock();
textBlock.Text = NextButtonHintText;
tooltip.Content = textBlock;
nextButton.ToolTip = tooltip;
}
if (PreviousButtonHintText != null && PreviousButtonHintText.Length > 0)
{
ToolTip tooltip = new ToolTip();
TextBlock textBlock = new TextBlock();
textBlock.Text = PreviousButtonHintText;
tooltip.Content = textBlock;
previousButton.ToolTip = tooltip;
}
}
private void SetBackGround(DependencyObject target, Brush brush)
{
if (target != null)
{
if (target is Paragraph)
{
((Paragraph)target).Background = brush;
}
else if (target is TextElement)
{
((TextElement)target).Background = brush;
}
}
}
private void BringIntoViewNextPattern()
{
currentFindedPatternPosition++;
if (findedPatterns == null || findedPatterns.Count == 0)
{
return;
}
if (findedPatterns.Count <= currentFindedPatternPosition)
{
currentFindedPatternPosition = findedPatterns.Count - 1;
}
if (highlightedParagraph != null)
{
SetBackGround(highlightedParagraph, null);
}
highlightedParagraph = findedPatterns[currentFindedPatternPosition].Start.Paragraph;
if (highlightedParagraph == null)
{
highlightedParagraph = findedPatterns[currentFindedPatternPosition].Start.Parent;
}
LogicalTreeHelper.BringIntoView(highlightedParagraph);
SetBackGround(highlightedParagraph, HighlightedParagraphBrush);
}
private void BringIntoViewPreviousPattern()
{
currentFindedPatternPosition--;
if (findedPatterns == null || currentFindedPatternPosition < 0)
{
currentFindedPatternPosition = 0;
return;
}
if (highlightedParagraph != null)
{
SetBackGround(highlightedParagraph, null);
}
highlightedParagraph = findedPatterns[currentFindedPatternPosition].Start.Paragraph;
if (highlightedParagraph == null)
{
highlightedParagraph = findedPatterns[currentFindedPatternPosition].Start.Parent;
}
LogicalTreeHelper.BringIntoView(highlightedParagraph);
SetBackGround(highlightedParagraph, HighlightedParagraphBrush);
}
private void DoSearch()
{
if (Document == null)
{
return;
}
if ((filterText == findTextBox.Text && findedPatterns != null))
{
BringIntoViewNextPattern();
return;
}
if (filterText != findTextBox.Text)
{
FlowDocumentExtension.ClearTextDecorations(Document);
}
filterText = findTextBox.Text;
findedPatterns = FlowDocumentExtension.GetAllMatchingInParagraph(Document, filterText, false);
foreach (TextRange range in findedPatterns)
{
FlowDocumentExtension.HighLightText(range, HighlightedTextBrush);
}
currentFindedPatternPosition = -1;
BringIntoViewNextPattern();
}
#endregion
#region Events
private void findTextBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
DoSearch();
}
else if (e.Key == Key.Escape)
{
FindButton.IsChecked = false;
setFindTextBoxVisibilty();
}
}
private void FindButton_Click(object sender, RoutedEventArgs e)
{
setFindTextBoxVisibilty();
}
private void nextButton_Click(object sender, RoutedEventArgs e)
{
if (filterText != findTextBox.Text)
{
DoSearch();
}
else
{
BringIntoViewNextPattern();
}
}
private void previousButton_Click(object sender, RoutedEventArgs e)
{
if (filterText != findTextBox.Text)
{
DoSearch();
}
else
{
BringIntoViewPreviousPattern();
}
}
#endregion
}
Using the Code
Using the code is quite easy. The only thing that needs to be done is setting the
properties of the Find control including the Document as follows:
<icp:FindControl
HorizontalAlignment="Left"
BorderThickness="1"
HighlightedTextBrush="Yellow"
HighlightedParagraphBrush="Aqua"
NextPreviousButtonsForegroundBrush="Blue"
Grid.Row="0"
Width="150"
Height="25"
BackgroundText="Form Search"
TextBoxBackgroundBrush="White"
BackgroundTextBrush="Gray"
Document="{Binding ElementName=docReader, Path=Document}"></icp:FindControl>
You can download the code here. At the end, I like to appreciate the efforts and design guidances of my colleague,
Afshin Khashei, that help me to implement the control.