From 43c2f67171a885b5fa3e196954a7ff79f0e021df Mon Sep 17 00:00:00 2001 From: Dingping Zhang Date: Mon, 6 Sep 2021 21:39:04 +0800 Subject: [PATCH] feat(control): add the highlight-text-block control. --- .../Data/Model/DemoItemModel.cs | 13 +- .../UserControl/Main/LeftMainContent.xaml | 5 +- .../UserControl/Main/LeftMainContent.xaml.cs | 5 + .../Controls/Text/HighlightTextBlock.cs | 199 ++++++++++++++++++ .../HandyControl_Shared.projitems | 1 + 5 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 src/Shared/HandyControl_Shared/Controls/Text/HighlightTextBlock.cs diff --git a/src/Shared/HandyControlDemo_Shared/Data/Model/DemoItemModel.cs b/src/Shared/HandyControlDemo_Shared/Data/Model/DemoItemModel.cs index b0811427..3cbaab9c 100644 --- a/src/Shared/HandyControlDemo_Shared/Data/Model/DemoItemModel.cs +++ b/src/Shared/HandyControlDemo_Shared/Data/Model/DemoItemModel.cs @@ -4,6 +4,9 @@ namespace HandyControlDemo.Data { public class DemoItemModel : ObservableObject { + private bool _isVisible = true; + private string _queriesText = string.Empty; + public string Name { get; set; } public string GroupName { get; set; } @@ -14,7 +17,15 @@ namespace HandyControlDemo.Data public bool IsNew { get; set; } - private bool _isVisible = true; + public string QueriesText + { + get => _queriesText; +#if NET40 + set => Set(nameof(QueriesText), ref _queriesText, value); +#else + set => Set(ref _queriesText, value); +#endif + } public bool IsVisible { diff --git a/src/Shared/HandyControlDemo_Shared/UserControl/Main/LeftMainContent.xaml b/src/Shared/HandyControlDemo_Shared/UserControl/Main/LeftMainContent.xaml index aecee549..f03333d8 100644 --- a/src/Shared/HandyControlDemo_Shared/UserControl/Main/LeftMainContent.xaml +++ b/src/Shared/HandyControlDemo_Shared/UserControl/Main/LeftMainContent.xaml @@ -6,6 +6,7 @@ xmlns:langs="clr-namespace:HandyControlDemo.Properties.Langs" xmlns:ex="clr-namespace:HandyControlDemo.Tools.Extension" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:data="clr-namespace:HandyControlDemo.Data" Margin="16,16,0,16" CornerRadius="10" Effect="{StaticResource EffectShadow4}"> @@ -48,10 +49,10 @@ - + - + diff --git a/src/Shared/HandyControlDemo_Shared/UserControl/Main/LeftMainContent.xaml.cs b/src/Shared/HandyControlDemo_Shared/UserControl/Main/LeftMainContent.xaml.cs index adec3756..224a8ae3 100644 --- a/src/Shared/HandyControlDemo_Shared/UserControl/Main/LeftMainContent.xaml.cs +++ b/src/Shared/HandyControlDemo_Shared/UserControl/Main/LeftMainContent.xaml.cs @@ -68,6 +68,7 @@ namespace HandyControlDemo.UserControl foreach (var item in ViewModelLocator.Instance.Main.DemoInfoCurrent.DemoItemList) { item.IsVisible = true; + item.QueriesText = string.Empty; } } else @@ -78,10 +79,12 @@ namespace HandyControlDemo.UserControl if (item.Name.ToLower().Contains(key)) { item.IsVisible = true; + item.QueriesText = _searchKey; } else if (item.TargetCtlName.Replace("DemoCtl", "").ToLower().Contains(key)) { item.IsVisible = true; + item.QueriesText = _searchKey; } else { @@ -89,10 +92,12 @@ namespace HandyControlDemo.UserControl if (!string.IsNullOrEmpty(name) && name.ToLower().Contains(key)) { item.IsVisible = true; + item.QueriesText = _searchKey; } else { item.IsVisible = false; + item.QueriesText = string.Empty; } } } diff --git a/src/Shared/HandyControl_Shared/Controls/Text/HighlightTextBlock.cs b/src/Shared/HandyControl_Shared/Controls/Text/HighlightTextBlock.cs new file mode 100644 index 00000000..328a677e --- /dev/null +++ b/src/Shared/HandyControl_Shared/Controls/Text/HighlightTextBlock.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Media; + +namespace HandyControl.Controls +{ + public class HighlightTextBlock : TextBlock + { + public static readonly DependencyProperty SourceTextProperty = + DependencyProperty.Register("SourceText", typeof(string), typeof(HighlightTextBlock), new PropertyMetadata(null, OnSourceTextChanged)); + public static readonly DependencyProperty QueriesTextProperty = + DependencyProperty.Register("QueriesText", typeof(string), typeof(HighlightTextBlock), new PropertyMetadata(null, OnQueriesTextChanged)); + public static readonly DependencyProperty HighlightBrushProperty = + DependencyProperty.Register("HighlightBrush", typeof(Brush), typeof(HighlightTextBlock), new PropertyMetadata(Brushes.Yellow)); + + private static void OnSourceTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => + ((HighlightTextBlock) d).RefreshInlines(); + + private static void OnQueriesTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => + ((HighlightTextBlock) d).RefreshInlines(); + + /// + /// Replace the original property Text for binding text. + /// + /// + /// Don't use the property! + /// Because the has some unique behaviors + /// that is not needed at at , + /// which will cause some unexpected issue. + /// + public string SourceText + { + get => (string) GetValue(SourceTextProperty); + set => SetValue(SourceTextProperty, value); + } + + /// + /// Gets or sets the text that need to be highlighted. + /// It can be an array of text separated by spaces. + /// + public string QueriesText + { + get => (string) GetValue(QueriesTextProperty); + set => SetValue(QueriesTextProperty, value); + } + + /// + /// Gets or sets the of the background of the highlight text. + /// + public Brush HighlightBrush + { + get => (Brush) GetValue(HighlightBrushProperty); + set => SetValue(HighlightBrushProperty, value); + } + + private void RefreshInlines() + { + Inlines.Clear(); + + if (string.IsNullOrEmpty(SourceText)) return; + if (string.IsNullOrEmpty(QueriesText)) + { + Inlines.Add(SourceText); + return; + } + + var sourceText = SourceText; + var queries = QueriesText.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + var intervals = from query in queries.Distinct() + from interval in GetQueryIntervals(sourceText, query) + select interval; + var mergedIntervals = MergeIntervals(intervals.ToList()); + var fragments = SplitTextByOrderedDisjointIntervals(sourceText, mergedIntervals); + + Inlines.AddRange(GenerateRunElement(fragments)); + } + + private IEnumerable GenerateRunElement(IEnumerable fragments) + { + return from item in fragments + select item.IsQuery + ? GetHighlightRun(item.Text) + : new Run(item.Text); + } + + private Run GetHighlightRun(string highlightText) + { + var run = new Run(highlightText); + run.SetBinding(TextElement.BackgroundProperty, new Binding + { + Source = this, + Path = new PropertyPath(nameof(HighlightBrush)), + Mode = BindingMode.OneWay + }); + return run; + } + + private static IEnumerable SplitTextByOrderedDisjointIntervals(string sourceText, List mergedIntervals) + { + if (string.IsNullOrEmpty(sourceText)) yield break; + + if (!mergedIntervals?.Any() ?? true) + { + yield return new Fragment { Text = sourceText, IsQuery = false }; + yield break; + } + + var range0 = mergedIntervals.First(); + int start0 = range0.Start; + int end0 = range0.End; + + if (start0 > 0) yield return new Fragment { Text = sourceText.Substring(0, start0), IsQuery = false }; + yield return new Fragment { Text = sourceText.Substring(start0, end0 - start0), IsQuery = true }; + + int previousEnd = end0; + foreach (var range in mergedIntervals.Skip(1)) + { + int start = range.Start; + int end = range.End; + yield return new Fragment { Text = sourceText.Substring(previousEnd, start - previousEnd), IsQuery = false }; + yield return new Fragment { Text = sourceText.Substring(start, end - start), IsQuery = true }; + previousEnd = end; + } + + if (previousEnd < sourceText.Length) + yield return new Fragment { Text = sourceText.Substring(previousEnd), IsQuery = false }; + } + + private static List MergeIntervals(List intervals) + { + if (!intervals?.Any() ?? true) return new List(); + + intervals.Sort((x, y) => x.Start != y.Start ? x.Start - y.Start : x.End - y.End); + + var pointer = intervals[0]; + int startPointer = pointer.Start; + int endPointer = pointer.End; + + var result = new List(); + foreach (var range in intervals.Skip(1)) + { + int start = range.Start; + int end = range.End; + + if (start <= endPointer) + { + if (endPointer < end) + { + endPointer = end; + } + } + else + { + result.Add(new Range { Start = startPointer, End = endPointer }); + startPointer = start; + endPointer = end; + } + } + result.Add(new Range { Start = startPointer, End = endPointer }); + return result; + } + + private static IEnumerable GetQueryIntervals(string sourceText, string query) + { + if (string.IsNullOrEmpty(sourceText) || string.IsNullOrEmpty(query)) yield break; + + int nextStartIndex = 0; + while (nextStartIndex < sourceText.Length) + { + int index = sourceText.IndexOf(query, nextStartIndex, StringComparison.CurrentCultureIgnoreCase); + + if (index == -1) yield break; + + nextStartIndex = index + query.Length; + yield return new Range { Start = index, End = nextStartIndex }; + } + } + + private struct Fragment + { + public string Text { get; set; } + + public bool IsQuery { get; set; } + } + + private struct Range + { + public int Start { get; set; } + + public int End { get; set; } + } + } +} diff --git a/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems b/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems index da30695d..48caa907 100644 --- a/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems +++ b/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems @@ -81,6 +81,7 @@ +