RichTextBox для WPF шустрее, чем родной?

301
25 августа 2017, 22:28

В окне отображаю лог довольно длительного процесса (5000 операций в среднем по 10 строчек каждая, потом будет и больше вплоть до 500k строчек)

Как известно, plain text убог :) например, на MacOS в стандартном блокноте только rich text.
вот и у меня для удобства чтения и для красоты часть строчек имеет свой цвет, часть кусков текста выделяется желтым маркером, возможно даже будут ссылки или и того круче - спойлеры из UIElement.

Но после того, как строк накопится много, лог тормозит и на выделение его кусков мышью (причем кусков совсем маленьких) и - что самое неприятное - на само добавление строк.

Есть ли что-то шустрее?

Решил попробовать три варианта - Syncfusion, Telerik и ComponentOne - и замерить. И ни один не заработал вообще.
Видно, не мой день.
(С первым разобрался, сделал все по аналогии с оригиналом, все работает, но строки добавленной не видно.
Второй вылетает при простом добавлении самого rtb в пустое окно свежесозданного проекта точь-в-точь по мануалу Getting started.
Со третьим разобрался, сделал все по аналогии с оригиналом, все работает, но строки добавленной не видно.
Пишу в саппорты...)

Единственным заработал AvalonEdit, но он заточен под подсветку синтаксиса, и я не понял, как в нем просто взять и задать шрифт для такого-то участка. И примитивный он какой-то по виду.

Интересно имеет ли вообще смысл что-то искать?

Answer 1

Вы не должны держать в памяти громадные массивы UI-контролов. Это неправильно и не нужно. Для отображения логов прекрасно подходит виртуализированный список. А для отображения — легковесный TextBlock. Я обойдусь стоковыми средствами.

Дополнительные преимущества списка — вы легко можете устроить фильтрацию, сортировку и прочие плюшки, чего не так-то просто добиться в чисто текстовом формате.

Давайте засучим рукава и напишем немного кода.

Создадим простейший класс для одного элемента лога.

class LogEntry
{
    public DateTime Time { get; }
    public int Severity { get; }
    public string ModuleName { get; }
    public string Text { get; }
    public LogEntry(DateTime time, int severity, string moduleName, string text)
    {
        Time = time;
        Severity = severity;
        ModuleName = moduleName;
        Text = text;
    }
}

Создадим 500K таких элементов, и положим их список в DataContext. Отображение — в XAML:

<ListView ItemsSource="{Binding}" ScrollViewer.CanContentScroll="True">
    <ListView.Resources>
        <local:SeverityConverter x:Key="SevConv"/>
    </ListView.Resources>
    <ListView.ItemTemplate>
        <DataTemplate DataType="{x:Type local:LogEntry}">
            <TextBlock>
                <Run Text="{Binding Time, Mode=OneWay}"/>
                <Run Text="{Binding ModuleName, Mode=OneWay}"
                     Background="{Binding Severity, Converter={StaticResource SevConv}}"/>
                <Run Text="{Binding Text, Mode=OneWay}"/>
            </TextBlock>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Я использую самописный конвертер Severity в цвет:

class SeverityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object p, CultureInfo ci)
    {
        switch ((int)value)
        {
        case 1: return Brushes.Red;
        case 2: return Brushes.Yellow;
        case 3: return Brushes.Green;
        default: return Brushes.Black;
        }
    }
    public object ConvertBack(object value, Type targetType, object p, CultureInfo ci)
    {
        throw new NotSupportedException();
    }
}

Да, давайте создадим случайные данные.

static class LogEntryFactory
{
    static Random r = new Random();
    static string[] Modules = { "Main", "VM", "Model", "DataBase", "Connectivity" };
    public static LogEntry CreateRandom()
    {
        var time = DateTime.Now.AddDays(r.NextDouble() * 10);
        var severity = r.Next(1, 4);
        var moduleName = Modules[r.Next(Modules.Length)];
        var text = string.Join(" ", Enumerable.Range(0, r.Next(2, 10))
                                              .Select(_ => CreateRandomWord()));
        return new LogEntry(time, severity, moduleName, text);
    }
    static char[] allowedChars = Enumerable.Range('a', 26).Select(Convert.ToChar)
                         .Concat(Enumerable.Range('A', 26).Select(Convert.ToChar)).ToArray();
    private static string CreateRandomWord() =>
        new string(Enumerable.Range(0, r.Next(3, 12))
                             .Select(_ => allowedChars[r.Next(allowedChars.Length)])
                             .ToArray());
}

Запускаем. Выводим надпись «Creating» на время создания в фоновом потоке 500K элементов.

Если вы хотите, чтобы можно было выделять текст, придётся использовать RichTextBox в каждой строке. Получается как-то так:

<ListView ItemsSource="{Binding}" ScrollViewer.CanContentScroll="True"
          HorizontalContentAlignment="Stretch" 
          ScrollViewer.HorizontalScrollBarVisibility="Disabled">
    <ListView.Resources>
        <local:SeverityConverter x:Key="SevConv"/>
    </ListView.Resources>
    <ListView.ItemTemplate>
        <DataTemplate DataType="{x:Type local:LogEntry}">
            <RichTextBox IsReadOnly="True" HorizontalContentAlignment="Stretch" >
                <FlowDocument>
                    <Paragraph>
                        <Run Text="{Binding Time, Mode=OneWay}"/>
                        <Run Text="{Binding ModuleName, Mode=OneWay}"
                             Background="{Binding Severity,
                                                  Converter={StaticResource SevConv}}"/>
                        <Run Text="{Binding Text, Mode=OneWay}"/>
                    </Paragraph>
                </FlowDocument>
            </RichTextBox>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Получается так:

Выделять текст через границу ячеек нельзя, можно только в одной.

Дополнение. Наверняка вы хотите выделять не внутри одной строки, а несколько строк, используя Ctrl-C и контекстное меню. Это тоже можно сделать, мы ж программисты!

Возвращаемся к TextBlock'ам, добавляем Multiselect, привязку команды ApplicationCommands.Copy и контекстное меню:

<ListView ItemsSource="{Binding Entries}" ScrollViewer.CanContentScroll="True"
          SelectionMode="Extended">
    <ListView.Resources>
        <local:SeverityConverter x:Key="SevConv"/>
    </ListView.Resources>
    <ListView.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Copy" Command="Copy"/>
        </ContextMenu>
    </ListView.ContextMenu>
    <ListView.CommandBindings>
        <CommandBinding Command="Copy" Executed="OnListCopy"/>
    </ListView.CommandBindings>
    <ListView.ItemTemplate>
        <DataTemplate DataType="{x:Type local:LogEntry}">
            <TextBlock>
                <Run Text="{Binding Time, Mode=OneWay}"/>
                <Run Text="{Binding ModuleName, Mode=OneWay}"
                        Background="{Binding Severity, Converter={StaticResource SevConv}}"/>
                <Run Text="{Binding Text, Mode=OneWay}"/>
            </TextBlock>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

В code-behind:

void OnListCopy(object sender, ExecutedRoutedEventArgs e)
{
    var lv = (ListView)sender;
    var text = string.Join(Environment.NewLine, lv.SelectedItems.Cast<LogEntry>());
    Clipboard.SetText(text);
}

В LogEntry добавляем

public override string ToString() => $"{Time} {ModuleName} {Text}";

Пробуем:

READ ALSO
Как настроить запуск windows service?

Как настроить запуск windows service?

Всем приветНедавно написал windows службу

246
Перемещение ImageBrush (маски прозрачности) для Image

Перемещение ImageBrush (маски прозрачности) для Image

Пошерстил интернет на данную тему и нашел примерно такую реализацию:

315
Помогите понять как работает код

Помогите понять как работает код

Мне нужно чтобы в массив urls добавлялись строки из hrefvalue, но вместо этого все строки добавляются в 0 элемент, метод ToArray() не помог

297
Незнакомый синтаксис в WPF проекте

Незнакомый синтаксис в WPF проекте

До сего момента не сильно сталкивался с WCFСегодня, разбирая чужой код, наткнулся на объявление свойства и присвоение ему пустого делегата:

253