TextBlock с подсветкой текста (WPF)

Привет Хабр! Я создал контрол на основе 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). Средний Run имеет Backgroung.

Не успел я приступить к реализации, как пришла в голову мысль, что совпадений может быть несколько. А значит такой подход не подходит.

Была еще мысль формировать Xaml «на лету», парсить его при помощи XamlReader и кидать в TextBlock. Но эта мысль тоже сразу отвалилась, потому что попахивает.

Следующей (и окончательной) идеей стало создать систему правил подсветки и прикрутить ее к TextBlock. Тут 2 варианта: свой контрол с блэкджеком и девочками на основе TextBlock или AttachedProperty. После недолгих раздумий, я решил, что все таки лучше создать отдельный контрол, потому что функционал подсветки может наложить некоторые ограничения на функциональность самого TextBlock, а разруливать это проще, если от него унаследоваться.

Исходники готового контрола


Итак, приступим. Сразу предупрежу, что контрол я делал в том же проекте, где собирался тестировать первую идею, поэтому не обращайте внимание на неймспейсы. До ума такие вещи я доведу уже, когда буду включать контрол в основной проект (или буду выкладывать на гитхаб).

В 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);
    }

Мне на данный момент известно два способа подсветить фрагмент. Через 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));
    }


Заключение


Ну вот пожалуй и все. Хотелось бы услышать ваше мнение.

UPDATE:

Код текущей версии немного отличается от того, что сейчас на гитхабе, но в целом контрол работает по тому же принципу. Контрол создавался для .net Framework 4.0.

Скрины
image
image


Ссылка на GitHub
Support the author
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 14

    +3
    Нет скринов.
      0
      И ссылки на GitHub
        0
        Извиняюсь за неудобства. Конечно выложу, но чуть позже.
    • UFO just landed and posted this here
        0

        Да, чем не устроил, например, AvalonEdit?

          0
          В DevExpress есть контрол с похожим функционалом (если не смущает платность/закрытость).
            0
            Я очень не люблю DevExpress для WPF, потому что они предоставляют массу возможностей для говнокода (и люди любят этим пользоваться). А использовать с ними MVVM во все красе — все равно, что удалять гланды через ухо. Они занимают свойство Tag многих контролов, а что находится в DataContext — ваще приходится догадываться. Ну и оптимизацией там даже не пахнет. Интерфейс на DE тяжелый как танк СТ-1. Зато они красивые :)
            У нас проект с использованием их контролов, так что очень надеюсь, что я просто не умею их правильно готовить. Однако, чем больше я про них читаю, тем меньше понимаю.
            Кстати, скажите название контрола, который позволяет подсвечивать текст. Я как то даже не пытался его искать у них. Глянуть хочу, может какие функции украду у них.
              0

              DevExpress.XtraRichEdit.RichEditControl.
              У него можно задавать цвет текста и фона по указанному смещению и количеству символов.


              DevExpress самому не очень нравится, но весь проект на нём, деваться некуда. Однако не смотря на тяжеловесность, интерфейс даже большим количеством данных ворочает очень живо, заметно лучше, чем стандартные контролы.

                0
                Ах да. RichEditControl.

                Но в любом случае, целью было создать собственный контрол, который я смогу использовать и в личных целях, исходный код которого я могу дорабатывать. Например, стандартная подсветка бэкграундом меня не устраивает своей кривизной (это легко увидеть, если перевести текст в верхний или нижний регистр, тогда часть подсветки так же уйдет за текстом и будет забор из подсветок), поэтому я планирую вместо бэкграунда подсвечивать наложением полупрозрачного прямоугольника на текст.
          0
          Автор, выложите куда-нибудь проект. Анализировать по спойлерам крайне неудобно.
            0
            Держите. Добавил ссылку на гитхаб и скрины
            0

            Без скринов ну совсем как-то тускло.

              0
              Какой смысл читать статью, если не понятно какой результат? Скрины необходимы.
                0
                И скрины выложил и ссылку на гитхаб дал.

              Only users with full accounts can post comments. Log in, please.