テキストバックライトTextBlock(WPF)

こんにちはHabr! TextBlockに基づいて、テキストを強調表示する機能を持つコントロールを作成しました。 はじめに、その使用例を示し、それがどのように作成されたかを説明します。



コントロールの使用例
<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>
      
      







開発開始



検索バーに入力されたTextBlockのテキストを強調表示するのに時間がかかりました。 一見、タスクは簡単に思えました。 テキストを3つのRun要素に分割し、すべてのテキスト、検索文字列、およびその位置(1/2/3)をコンバーターに転送しました。 Middle Runには背景があります



いくつかの偶然の一致があるかもしれないという考えが私の頭に浮かんだとき、私は実装を始める時間がありませんでした。 したがって、このアプローチは適合しません。



Xamlを 「オンザフライ」で作成し、 XamlReaderを使用して解析し、 TextBlockにスローするというアイデアがまだありました。 しかし、臭いがするので、この考えもすぐに落ちました。



次の(そして最後の)アイデアは、ルールを強調するシステムを作成し、それをTextBlockに固定することでした。 TextBlockまたはAttachedPropertyに基づいたブラックジャックと女の子を使用したコントロールの2つのオプションがあります。 いくつかの熟考の後、強調表示機能によってTextBlock自体の機能に制限が課される可能性があり、それを継承する場合は簡単に解決できるため、個別のコントロールを作成する方が良いと判断しました。



レディコントロールのソース



それでは始めましょう。 私はすぐに、最初のアイデアをテストするのと同じプロジェクトでコントロールを行ったことを警告しますので、名前空間に注意を払ってはいけません。 メインプロジェクトにコントロールを含めるときに(またはgithubにアップロードします)、これらのことを頭に入れておきます。



Xamlコントロールマークアップでは、 Loadedイベントハンドラーを除き、すべてがクリーンです



 <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>
      
      





コードに移動します。



ネタバレ見出し
  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); } }
      
      







私の意見では、コメントは冗長なので、ここではコードを説明しません。



タスクキューコードは次のとおりです。



ネタバレ見出し
  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(); } } } }
      
      







ここではすべてが非常に簡単です。 新しい挑戦が到着します。 キューに場所がある場合は、キューに配置されます。 そうでない場合、フィールド_deleteOld == trueの場合 、次のタスク(最新)を削除して新しいタスクを配置し、そうでない場合はfalse(タスクは追加されません)を返します。



ルールコレクションのコードは次のとおりです。 理論的には、 ObservableCollectionは不要になる可能性がありますが、このコレクションには将来追加の機能が必要になる場合があります。



ネタバレ見出し
  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); } }
      
      







ハイライトルールコードは次のとおりです。



ネタバレ見出し
  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; } }
      
      







ここにはロジックがほとんどないため、コメントはありません。



ハイライト用の抽象クラスは次のとおりです。



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





現在、フラグメントを強調表示する2つの方法を知っています。 SpanおよびTextRange経由。 これまでのところ、選択したメソッドは強調表示手順のコードで記述されていますが、将来的にはオプションでこれを行う予定です。



背景を強調する相続人です
  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)); }
      
      







さて、スレッドセーフを除き、コメントすることはありません。 実際には、インスタンスはメインスレッドでスピンする必要があり、メソッドはどこからでも呼び出すことができます。



そして、これはテキスト強調コードです
  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)); }
      
      







おわりに



まあ、それがおそらくすべてです。 あなたの意見を聞きたいです。



更新:



現在のバージョンのコードは、現在githubにあるものとわずかに異なりますが、一般的にコントロールは同じ原理で動作します。 コントロールは.net Framework 4.0用に作成されました。



スクリーンショット
画像

画像





GitHubリンク



All Articles