📜 ⬆️ ⬇️

TextBlock with backlit text (WPF)

Hi Habr! I created a TextBlock based control with the ability to highlight the text. To begin, I will give an example of its use, then I will describe how it was created.

Example of using control
<local:HighlightTextBlock TextWrapping="Wrap"> <local:HighlightTextBlock.HighlightRules> <local:HighlightRule HightlightedText="{Binding Filter, Source={x:Reference thisWindow}}"> <local:HighlightRule.Highlights> <local:HighlightBackgroung Brush="Yellow"/> <local:HighlightForeground Brush="Black"/> </local:HighlightRule.Highlights> </local:HighlightRule> </local:HighlightTextBlock.HighlightRules> <Run FontWeight="Bold">Property:</Run> <Run Text="{Binding Property}"/> </local:HighlightTextBlock> 


Start of development


It took me to highlight the text in the TextBlock entered in the search bar. At first glance, the task seemed simple. It occurred to me to divide the text into 3 Run elements, which would transfer to the converter all the text, the search string and its position (1/2/3). Medium Run has a backgroung .

I didn’t have time to start the implementation, when I had the idea that there could be several coincidences. So this approach does not fit.
')
There was also the idea to form Xaml “on the fly”, parse it with the help of XamlReader and throw it into TextBlock . But this thought also immediately fell off, because it smacks.

The next (and final) idea was to create a system of rules for highlighting and tie it to TextBlock . There are 2 options: your control with blackjack and girls based on TextBlock or AttachedProperty . After some deliberation, I decided that it would still be better to create a separate control, because the backlight functionality may impose some restrictions on the functionality of the TextBlock itself, and it’s easier to resolve if inherited from it.

Sources of ready control


So let's get started. Immediately I’ll warn you that I did the control in the same project where I was going to test the first idea, so don’t pay attention to namespaces. I’ll bring such things to mind when I include the control in the main project (or I’ll post it on the githab).

In Xaml, the control layout is clean, except for the Loaded event handler.

 <TextBlock x:Class="WpfApplication18.HighlightTextBlock" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Loaded="TextBlock_Loaded"> </TextBlock> 

Go to the code:

Spoiler header
  public partial class HighlightTextBlock : TextBlock { //      TextBlock // (         TextBlock) string _content; //           Dictionary<HighlightRule, TaskQueue> _ruleTasks; /// <summary> ///    /// </summary> public HighlightRulesCollection HighlightRules { get { return (HighlightRulesCollection)GetValue(HighlightRulesProperty); } set { SetValue(HighlightRulesProperty, value); } } public static readonly DependencyProperty HighlightRulesProperty = DependencyProperty.Register("HighlightRules", typeof(HighlightRulesCollection), typeof(HighlightTextBlock), new FrameworkPropertyMetadata(null) { PropertyChangedCallback = HighlightRulesChanged }); static void HighlightRulesChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { var col = e.NewValue as HighlightRulesCollection; var tb = sender as HighlightTextBlock; if (col != null && tb != null) { col.CollectionChanged += tb.HighlightRules_CollectionChanged; foreach (var rule in col) { rule.HighlightTextChanged += tb.Rule_HighlightTextChanged; } } } public HighlightTextBlock() { _ruleTasks = new Dictionary<HighlightRule, TaskQueue>(); HighlightRules = new HighlightRulesCollection(); InitializeComponent(); } //        void HighlightRules_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { switch (e.Action) { case System.Collections.Specialized.NotifyCollectionChangedAction.Add: foreach (HighlightRule rule in e.NewItems) { _ruleTasks.Add(rule, new TaskQueue(1)); SubscribeRuleNotifies(rule); BeginHighlight(rule); } break; case System.Collections.Specialized.NotifyCollectionChangedAction.Remove: foreach (HighlightRule rule in e.OldItems) { rule.HightlightedText = string.Empty; _ruleTasks.Remove(rule); UnsubscribeRuleNotifies(rule); } break; case System.Collections.Specialized.NotifyCollectionChangedAction.Reset: foreach (HighlightRule rule in e.OldItems) { rule.HightlightedText = string.Empty; _ruleTasks.Remove(rule); UnsubscribeRuleNotifies(rule); } break; } } //      void SubscribeRuleNotifies(HighlightRule rule) { rule.HighlightTextChanged += Rule_HighlightTextChanged; } //      void UnsubscribeRuleNotifies(HighlightRule rule) { rule.HighlightTextChanged -= Rule_HighlightTextChanged; } //  ,  ,      void Rule_HighlightTextChanged(object sender, HighlightTextChangedEventArgs e) { BeginHighlight((HighlightRule)sender); } //         . //   ,    /  , //      ,    //   .       ,      //    .     . void BeginHighlight(HighlightRule rule) { _ruleTasks[rule].Add(new Action(() => Highlight(rule))); } //   void Highlight(HighlightRule rule) { //     ,   if (rule == null) return; //        Xaml ,     ,    , //     /    ObservableCollection<Highlight> highlights = null; Application.Current.Dispatcher.Invoke(new ThreadStart(() => { highlights = rule.Highlights; })); //    ,     ,  ,    if (highlights.Count == 0) return; //         var exitFlag = false; exitFlag = exitFlag || string.IsNullOrWhiteSpace(_content); Application.Current.Dispatcher.Invoke(new ThreadStart(() => { exitFlag = exitFlag || Inlines.IsReadOnly || Inlines.Count == 0 || HighlightRules == null || HighlightRules.Count == 0; })); if (exitFlag) return; //  .      ,      //   TextBlock ,       var par = new Paragraph(); //  _content,      Span    TextBlock'a. var parsedSp = (Span)XamlReader.Parse(_content); //  Span   ,        par.Inlines.AddRange(parsedSp.Inlines.ToArray()); //    (  )    TextBlock'a  . //         var firstPos = par.ContentStart; var curText = string.Empty; Application.Current.Dispatcher.Invoke(new ThreadStart(() => { curText = Text; })); //        var hlText = string.Empty; Application.Current.Dispatcher.Invoke(new ThreadStart(() => { hlText = rule.HightlightedText; })); //             ,   , //  ,       if (!string.IsNullOrEmpty(hlText) && hlText.Length <= curText.Length) { //        IgnoreCase. //      ,       //      :) var comparison = StringComparison.CurrentCulture; Application.Current.Dispatcher.Invoke(new ThreadStart(() => { comparison = rule.IgnoreCase ? StringComparison.CurrentCultureIgnoreCase : StringComparison.CurrentCulture; })); //   ,        var indexes = new List<int>(); var ind = curText.IndexOf(hlText, comparison); while (ind > -1) { indexes.Add(ind); ind = curText.IndexOf(hlText, ind + hlText.Length, StringComparison.CurrentCultureIgnoreCase); } TextPointer lastEndPosition = null; //           foreach (var index in indexes) { //            , //     string     TextPointer'a. //  ,    . var curIndex = index; //         TextPointer  //  ,       var pstart = lastEndPosition ?? firstPos.GetInsertionPosition(LogicalDirection.Forward).GetPositionAtOffset(curIndex); // startInd      TextPointer      var startInd = new TextRange(pstart, firstPos.GetInsertionPosition(LogicalDirection.Forward)).Text.Length; //    ,  startInd   curIndex while (startInd != curIndex) { //  ,     ,    startInd  curIndex,  //           if (startInd < curIndex) { //       curIndex - startInd var newpstart = pstart.GetPositionAtOffset(curIndex - startInd); //  TextPointer   \r  \n,      //  .   ,        if (newpstart.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementEnd) newpstart = newpstart.GetInsertionPosition(LogicalDirection.Forward); var len = new TextRange(pstart, newpstart).Text.Length; startInd += len; pstart = newpstart; } else { var newpstart = pstart.GetPositionAtOffset(curIndex - startInd); var len = new TextRange(pstart, newpstart).Text.Length; startInd -= len; pstart = newpstart; } } //      ,    var pend = pstart.GetPositionAtOffset(hlText.Length); var delta = new TextRange(pstart, pend).Text.Length; while (delta != hlText.Length) { if (delta < hlText.Length) { var newpend = pend.GetPositionAtOffset(hlText.Length - delta); var len = new TextRange(pend, newpend).Text.Length; delta += len; pend = newpend; } else { var newpend = pend.GetPositionAtOffset(hlText.Length - delta); var len = new TextRange(pend, newpend).Text.Length; delta -= len; pend = newpend; } } //  ,      Hyperlink. //      ,     , // ,         , //     .        , //     var sHyp = (pstart?.Parent as Inline)?.Parent as Hyperlink; var eHyp = (pend?.Parent as Inline)?.Parent as Hyperlink; if (sHyp != null) pstart = pstart.GetNextContextPosition(LogicalDirection.Forward); if (eHyp != null) pend = pend.GetNextContextPosition(LogicalDirection.Backward); //       . if (pstart.GetOffsetToPosition(pend) > 0) { var sp = new Span(pstart, pend); foreach (var hl in highlights) hl.SetHighlight(sp); } lastEndPosition = pend; } } //             TextBlock var parStr = XamlWriter.Save(par); Application.Current.Dispatcher.BeginInvoke(new ThreadStart(() => { Inlines.Clear(); Inlines.AddRange(((Paragraph)XamlReader.Parse(parStr)).Inlines.ToArray()); })).Wait(); } void TextBlock_Loaded(object sender, RoutedEventArgs e) { //    TextBlock'a     , //      . //      ,     . var sp = new Span(); sp.Inlines.AddRange(Inlines.ToArray()); var tr = new TextRange(sp.ContentStart, sp.ContentEnd); using (var stream = new MemoryStream()) { tr.Save(stream, DataFormats.Xaml); stream.Position = 0; using(var reader = new StreamReader(stream)) { _content = reader.ReadToEnd(); } } Inlines.AddRange(sp.Inlines.ToArray()); //      foreach (var rule in HighlightRules) BeginHighlight(rule); } } 


I will not describe the code here, because comments, in my opinion, are redundant.

Here is the task queue code:

Spoiler header
  public class TaskQueue { Task _worker; Queue<Action> _queue; int _maxTasks; bool _deleteOld; object _lock = new object(); public TaskQueue(int maxTasks, bool deleteOld = true) { if (maxTasks < 1) throw new ArgumentException("TaskQueue:       0"); _maxTasks = maxTasks; _deleteOld = deleteOld; _queue = new Queue<Action>(maxTasks); } public bool Add(Action action) { if (_queue.Count() < _maxTasks) { _queue.Enqueue(action); DoWorkAsync(); return true; } if (_deleteOld) { _queue.Dequeue(); return Add(action); } return false; } void DoWorkAsync() { if(_queue.Count>0) _worker = Task.Factory.StartNew(DoWork); } void DoWork() { lock (_lock) { if (_queue.Count > 0) { var currentTask = Task.Factory.StartNew(_queue.Dequeue()); currentTask.Wait(); DoWorkAsync(); } } } } 


Everything is pretty simple here. A new challenge is coming. If there is a place in the queue, then it is placed in the queue. Otherwise, if the _deleteOld field is == true , then we delete the next task (the latest one) and place a new one, otherwise we return false (the task is not added).

Here is the code for the collection of rules. In theory, an ObservableCollection could have been avoided, but additional functionality may be required from this collection in the future.

Spoiler header
  public class HighlightRulesCollection : DependencyObject, INotifyCollectionChanged, ICollectionViewFactory, IList, IList<HighlightRule> { ObservableCollection<HighlightRule> _items; public HighlightRulesCollection() { _items = new ObservableCollection<HighlightRule>(); _items.CollectionChanged += _items_CollectionChanged; } public HighlightRule this[int index] { get { return ((IList<HighlightRule>)_items)[index]; } set { ((IList<HighlightRule>)_items)[index] = value; } } object IList.this[int index] { get { return ((IList)_items)[index]; } set { ((IList)_items)[index] = value; } } public int Count { get { return ((IList<HighlightRule>)_items).Count; } } public bool IsFixedSize { get { return ((IList)_items).IsFixedSize; } } public bool IsReadOnly { get { return ((IList<HighlightRule>)_items).IsReadOnly; } } public bool IsSynchronized { get { return ((IList)_items).IsSynchronized; } } public object SyncRoot { get { return ((IList)_items).SyncRoot; } } public event NotifyCollectionChangedEventHandler CollectionChanged; public int Add(object value) { return ((IList)_items).Add(value); } public void Add(HighlightRule item) { ((IList<HighlightRule>)_items).Add(item); } public void Clear() { ((IList<HighlightRule>)_items).Clear(); } public bool Contains(object value) { return ((IList)_items).Contains(value); } public bool Contains(HighlightRule item) { return ((IList<HighlightRule>)_items).Contains(item); } public void CopyTo(Array array, int index) { ((IList)_items).CopyTo(array, index); } public void CopyTo(HighlightRule[] array, int arrayIndex) { ((IList<HighlightRule>)_items).CopyTo(array, arrayIndex); } public ICollectionView CreateView() { return new CollectionView(_items); } public IEnumerator<HighlightRule> GetEnumerator() { return ((IList<HighlightRule>)_items).GetEnumerator(); } public int IndexOf(object value) { return ((IList)_items).IndexOf(value); } public int IndexOf(HighlightRule item) { return ((IList<HighlightRule>)_items).IndexOf(item); } public void Insert(int index, object value) { ((IList)_items).Insert(index, value); } public void Insert(int index, HighlightRule item) { ((IList<HighlightRule>)_items).Insert(index, item); } public void Remove(object value) { ((IList)_items).Remove(value); } public bool Remove(HighlightRule item) { return ((IList<HighlightRule>)_items).Remove(item); } public void RemoveAt(int index) { ((IList<HighlightRule>)_items).RemoveAt(index); } IEnumerator IEnumerable.GetEnumerator() { return ((IList<HighlightRule>)_items).GetEnumerator(); } void _items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke(this, e); } } 


Here is the highlight rule code:

Spoiler header
  public class HighlightRule : DependencyObject { public delegate void HighlightTextChangedEventHandler(object sender, HighlightTextChangedEventArgs e); public event HighlightTextChangedEventHandler HighlightTextChanged; public HighlightRule() { Highlights = new ObservableCollection<Highlight>(); } /// <summary> /// ,    /// </summary> public string HightlightedText { get { return (string)GetValue(HightlightedTextProperty); } set { SetValue(HightlightedTextProperty, value); } } public static readonly DependencyProperty HightlightedTextProperty = DependencyProperty.Register("HightlightedText", typeof(string), typeof(HighlightRule), new FrameworkPropertyMetadata(string.Empty, HighlightPropertyChanged)); public static void HighlightPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var me = d as HighlightRule; if (me != null) me.HighlightTextChanged?.Invoke(me, new HighlightTextChangedEventArgs((string)e.OldValue, (string)e.NewValue)); } /// <summary> ///  ? /// </summary> public bool IgnoreCase { get { return (bool)GetValue(IgnoreCaseProperty); } set { SetValue(IgnoreCaseProperty, value); } } public static readonly DependencyProperty IgnoreCaseProperty = DependencyProperty.Register("IgnoreCase", typeof(bool), typeof(HighlightRule), new PropertyMetadata(true)); /// <summary> ///   /// </summary> public ObservableCollection<Highlight> Highlights { get { return (ObservableCollection<Highlight>)GetValue(HighlightsProperty); } set { SetValue(HighlightsProperty, value); } } public static readonly DependencyProperty HighlightsProperty = DependencyProperty.Register("Highlights", typeof(ObservableCollection<Highlight>), typeof(HighlightRule), new PropertyMetadata(null)); } public class HighlightTextChangedEventArgs : EventArgs { public string OldText { get; } public string NewText { get; } public HighlightTextChangedEventArgs(string oldText,string newText) { OldText = oldText; NewText = newText; } } 


There is almost no logic here, so no comments.

Here is an abstract class for highlighting:

  public abstract class Highlight : DependencyObject { public abstract void SetHighlight(Span span); public abstract void SetHighlight(TextRange range); } 

I currently know two ways to highlight a fragment. Via Span and via TextRange . So far, the chosen method is registered in the code in the illumination procedure, but in the future I plan to do this optionally.

Here is the heir to highlight the background
  public class HighlightBackgroung : Highlight { public override void SetHighlight(Span span) { Brush brush = null; Application.Current.Dispatcher.BeginInvoke(new ThreadStart(() => { brush = Brush; })).Wait(); span.Background = brush; } public override void SetHighlight(TextRange range) { Brush brush = null; Application.Current.Dispatcher.BeginInvoke(new ThreadStart(() => { brush = Brush; })).Wait(); range.ApplyPropertyValue(TextElement.BackgroundProperty, brush); } /// <summary> ///     /// </summary> public Brush Brush { get { return (Brush)GetValue(BrushProperty); } set { SetValue(BrushProperty, value); } } public static readonly DependencyProperty BrushProperty = DependencyProperty.Register("Brush", typeof(Brush), typeof(HighlightBackgroung), new PropertyMetadata(Brushes.Transparent)); } 


Well, there is nothing to comment, except for the safety of threads. The fact is that the instance must spin in the main thread, and the method can be called from anywhere.

And this is the code for highlighting the text color
  public class HighlightForeground : Highlight { public override void SetHighlight(Span span) { Brush brush = null; Application.Current.Dispatcher.BeginInvoke(new ThreadStart(() => { brush = Brush; })).Wait(); span.Foreground = brush; } public override void SetHighlight(TextRange range) { Brush brush = null; Application.Current.Dispatcher.BeginInvoke(new ThreadStart(() => { brush = Brush; })).Wait(); range.ApplyPropertyValue(TextElement.ForegroundProperty, brush); } /// <summary> ///     /// </summary> public Brush Brush { get { return (Brush)GetValue(BrushProperty); } set { SetValue(BrushProperty, value); } } public static readonly DependencyProperty BrushProperty = DependencyProperty.Register("Brush", typeof(Brush), typeof(HighlightForeground), new PropertyMetadata(Brushes.Black)); } 


Conclusion


Well, perhaps that's all. I would like to hear your opinion.

UPDATE:

The code of the current version is slightly different from what is now on the githaba, but in general, the control works on the same principle. Control was created for .net Framework 4.0 .

Screenshots
image
image


GitHub link

Source: https://habr.com/ru/post/314060/


All Articles