在上一个问题中,我询问了如何在类似 WPF 文本框的元素 ( WPF append text blocks UI thread heavily but WinForms doesn't?) 中获取实时日志记录输出。那里的答案让我使用了 FlowDocumentScrollViewer
,它确实比 RichTextBox
快得多。但是,我发现运行具有大量文本输出的命令(如“svn co”)会导致我的 WPF 应用程序明显变慢。在 checkout 3 或 4 个非常大的 svn 分支后切换选项卡需要 3-4 秒,而且我确信时间会随着我执行的 checkout 次数而增加。滚动也有明显的延迟。
正如我在上面链接的问题中所述,我最近将我的应用程序从 Windows 窗体切换到 WPF。我非常喜欢 WPF - 它提供了许多我在 Forms 中没有的优势。然而,性能在 WPF 中似乎是一个相当大的问题,至少对我来说是这样。在我的应用程序的表单版本中,我可以将大量文本打印到 RichTextBox
控件中,并且我的应用程序丝毫没有减速。切换标签是即时的,滚动是无缝的。这就是我想要在我的 WPF 应用程序中获得的体验。
所以我的问题是:如何提高我的 FlowDocumentScrollViewer
的性能以匹配 Windows 窗体 RichTextBox
的性能,同时不丢失粗体和斜体等格式化功能,并且不会丢失复制/粘贴功能?我愿意切换 WPF 控件,只要它们提供我正在寻找的格式化功能。
这是我的打印代码,供引用:
public void PrintOutput(String s)
{
if (outputParagraph.FontSize != defaultFontSize)
{
outputParagraph = new Paragraph();
outputParagraph.Margin = new Thickness(0);
outputParagraph.FontFamily = font;
outputParagraph.FontSize = defaultFontSize;
outputParagraph.TextAlignment = TextAlignment.Left;
OutputBox.Document.Blocks.Add(outputParagraph);
}
outputParagraph.Inlines.Add(s);
if (!clearOutputButton.IsEnabled) clearOutputButton.IsEnabled = true;
}
public void PrintImportantOutput(String s)
{
if (outputParagraph.FontSize != importantFontSize)
{
outputParagraph = new Paragraph();
outputParagraph.Margin = new Thickness(0);
outputParagraph.FontFamily = font;
outputParagraph.FontSize = importantFontSize;
outputParagraph.TextAlignment = TextAlignment.Left;
OutputBox.Document.Blocks.Add(outputParagraph);
}
String timestamp = DateTime.Now.ToString("[hh:mm.ss] ");
String toPrint = timestamp + s;
outputParagraph.Inlines.Add(new Bold(new Run(toPrint)));
if (!clearOutputButton.IsEnabled) clearOutputButton.IsEnabled = true;
}
我在打印“重要”文本时切换字体大小并使文本加粗。这段代码有这么多行的原因是因为我试图对所有文本重复使用同一段落,直到我点击“重要”文本;我添加了一个包含所有“重要”文本的新段落,然后在我切换回非重要文本后添加另一个段落并附加到该段落,直到我找到更多“重要”文本。我希望重复使用同一段落可以提高性能。
此外,应该注意的是,我将 stdout 打印到一个 FlowDocumentScrollViewer
,将 stderr 打印到另一个 FlowDocumentScrollViewer
,然后同时打印到第三个 FlowDocumentScrollViewer
。因此,从技术上讲,stdout 和 stderr 的每一行都会打印两次,从而使我的应用程序的负载加倍。同样,这在 WinForms 中不是问题。
根据评论中的要求,下面是完整的代码示例。它非常简单(3 个 FlowDocumentScrollViewer 和简单的打印),但仍然减慢了大约 20000 行文本的时间,并且更糟。
编辑:代码示例已被删除。取而代之的是解决我的性能问题的工作代码。它的工作方式与 FlowDocumentScrollViewer
一样,但有一个异常(exception):您不能选择行的子字符串。我正在考虑解决这个问题,尽管这看起来很困难。
Bridge.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Documents;
using System.Threading;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace PerformanceTest
{
public class Bridge
{
int counterLimit;
public BlockingCollection<PrintInfo> output;
public BlockingCollection<PrintInfo> errors;
public BlockingCollection<PrintInfo> logs;
protected static Bridge controller = new Bridge();
public static Bridge Controller
{
get
{
return controller;
}
}
public MainWindow Window
{
set
{
if (value != null)
{
output = value.outputProducer;
errors = value.errorProducer;
logs = value.logsProducer;
}
}
}
public bool Running
{
get;
set;
}
private Bridge()
{
//20000 lines seems to slow down tabbing enough to prove my point.
//increase this number to get even worse results.
counterLimit = 40000;
}
public void PrintLotsOfText()
{
new Thread(new ThreadStart(GenerateOutput)).Start();
new Thread(new ThreadStart(GenerateError)).Start();
}
private void GenerateOutput()
{
//There is tons of output text, so print super fast if possible.
int counter = 1;
while (Running && counter < counterLimit)
{
if (counter % 10 == 0)
PrintImportantOutput("I will never say this horrible word again as long I live. This is confession #" + counter++ + ".");
else
PrintOutput("I will never say this horrible word again as long I live. This is confession #" + counter++ + ".");
//Task.Delay(1).Wait();
}
Console.WriteLine("GenerateOutput thread should end now...");
}
private void GenerateError()
{
int counter = 1;
while (Running && counter < counterLimit)
{
PrintError("I will never forgive your errors as long I live. This is confession #" + counter++ + ".");
//Task.Delay(1).Wait();
}
Console.WriteLine("GenerateError thread should end now...");
}
#region Printing
delegate void StringArgDelegate(String s);
delegate void InlineArgDelegate(Inline inline);
public void PrintOutput(String s)
{
output.TryAdd(new PrintInfo(s, false));
PrintLog("d " + s);
}
public void PrintImportantOutput(String s)
{
output.TryAdd(new PrintInfo(s, true));
PrintLog("D " + s);
}
public void PrintError(String s)
{
errors.TryAdd(new PrintInfo(s, false));
PrintLog("e " + s);
}
public void PrintImportantError(String s)
{
errors.TryAdd(new PrintInfo(s, true));
PrintLog("E " + s);
}
public void PrintLog(String s)
{
logs.TryAdd(new PrintInfo(s, false));
}
#endregion
}
public class PrintInfo
{
public String Text { get; set; }
public bool IsImportant { get; set; }
public PrintInfo() { }
public PrintInfo(String text, bool important)
{
Text = text;
IsImportant = important;
}
}
}
主窗口.xaml
<Window x:Class="PerformanceTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
xmlns:l="clr-namespace:PerformanceTest"
WindowStartupLocation="CenterScreen">
<Grid>
<TabControl>
<TabControl.Resources>
<Style TargetType="ListBox">
<Setter Property="TextElement.FontFamily" Value="Consolas" />
<Setter Property="TextElement.FontSize" Value="12" />
<Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingStackPanel.VirtualizationMode" Value="Recycling" />
<Setter Property="l:ListBoxSelector.Enabled" Value="True" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Command="Copy" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="Margin" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<TextBlock Text="{Binding Text}" TextWrapping="Wrap"
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
FontWeight="{TemplateBinding FontWeight}" />
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding IsImportant}" Value="true">
<Setter Property="TextElement.FontWeight" Value="SemiBold" />
<Setter Property="TextElement.FontSize" Value="14" />
</DataTrigger>
<Trigger Property="IsSelected" Value="true">
<Setter Property="Background" Value="{StaticResource {x:Static SystemColors.HighlightBrushKey}}" />
<Setter Property="Foreground" Value="{StaticResource {x:Static SystemColors.HighlightTextBrushKey}}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</TabControl.Resources>
<TabItem Header="Bridge">
<StackPanel Orientation="Vertical" HorizontalAlignment="Left">
<Button Content="Start Test" Click="StartButton_Click" />
<Button Content="End Test" Click="EndButton_Click" />
</StackPanel>
</TabItem>
<TabItem Header="Output">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!--<RichTextBox x:Name="OutputBox" ScrollViewer.VerticalScrollBarVisibility="Auto"/>-->
<ListBox Grid.Column="0" ItemsSource="{Binding Output,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.CommandBindings>
<CommandBinding Command="Copy" Executed="CopyExecuted" />
</ListBox.CommandBindings>
</ListBox>
<GridSplitter Grid.Column="1" Width="5" ResizeBehavior="PreviousAndNext" />
<ListBox Grid.Column="2" ItemsSource="{Binding Errors,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.CommandBindings>
<CommandBinding Command="Copy" Executed="CopyExecuted" />
</ListBox.CommandBindings>
</ListBox>
</Grid>
</TabItem>
<TabItem Header="Log">
<Grid>
<ListBox Grid.Column="0" ItemsSource="{Binding Logs,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.CommandBindings>
<CommandBinding Command="Copy" Executed="CopyExecuted" />
</ListBox.CommandBindings>
</ListBox>
</Grid>
</TabItem>
</TabControl>
</Grid>
</Window>
主窗口.xaml.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Threading;
namespace PerformanceTest
{
///
/// Interaction logic for MainWindow.xaml
///
public partial class MainWindow : Window
{
public BlockingCollection<PrintInfo> outputProducer = new BlockingCollection<PrintInfo>();
public BlockingCollection<PrintInfo> errorProducer = new BlockingCollection<PrintInfo>();
public BlockingCollection<PrintInfo> logsProducer = new BlockingCollection<PrintInfo>();
public ObservableCollection<PrintInfo> Output { get; set; }
public ObservableCollection<PrintInfo> Errors { get; set; }
public ObservableCollection<PrintInfo> Logs { get; set; }
protected FontFamily font = new FontFamily("Consolas");
protected int defaultFontSize = 12;
protected int importantFontSize = 14;
Dispatcher dispatcher;
public MainWindow()
{
Bridge.Controller.Window = this;
try
{
InitializeComponent();
}
catch (Exception ex)
{
Console.WriteLine(ex.InnerException.ToString());
Console.WriteLine(ex.StackTrace);
}
dispatcher = Dispatcher;
Output = new ObservableCollection<PrintInfo>();
Errors = new ObservableCollection<PrintInfo>();
Logs = new ObservableCollection<PrintInfo>();
new Thread(new ThreadStart(() => Print(outputProducer, Output))).Start();
new Thread(new ThreadStart(() => Print(errorProducer, Errors))).Start();
new Thread(new ThreadStart(() => Print(logsProducer, Logs))).Start();
}
public delegate void EmptyDelegate();
public void Print(BlockingCollection<PrintInfo> producer, ObservableCollection<PrintInfo> target)
{
try
{
foreach (var info in producer.GetConsumingEnumerable())
{
dispatcher.Invoke(new EmptyDelegate(() =>
{
if (info.IsImportant)
{
String timestamp = DateTime.Now.ToString("[hh:mm.ss] ");
String toPrint = timestamp + info.Text;
info.Text = toPrint;
}
target.Add(info);
}), DispatcherPriority.Background);
}
}
catch (TaskCanceledException)
{
//window closing before print finish
}
}
private void StartButton_Click(object sender, RoutedEventArgs e)
{
if (!Bridge.Controller.Running)
{
Bridge.Controller.Running = true;
Bridge.Controller.PrintLotsOfText();
}
}
private void EndButton_Click(object sender, RoutedEventArgs e)
{
Bridge.Controller.Running = false;
}
private void CopyExecuted(object sender, ExecutedRoutedEventArgs e)
{
ListBox box = sender as ListBox;
HashSet<PrintInfo> allItems = new HashSet<PrintInfo>(box.Items.OfType<PrintInfo>());
HashSet<PrintInfo> selectedItems = new HashSet<PrintInfo>(box.SelectedItems.OfType<PrintInfo>());
IEnumerable<PrintInfo> sortedItems = allItems.Where(i => selectedItems.Contains(i));
IEnumerable<String> copyItems = from i in sortedItems select i.Text;
string log = string.Join("\r\n", copyItems);
Clipboard.SetText(log);
}
}
}
ListBoxSelector.cs 在@pushpraj 的回答中。
最佳答案
我执行了您提供的示例,除了几个线程问题外,最大的问题是数据量。随着文本的增长,它会减慢文本呈现速度。
我试图以不同的方式重写您的代码。我使用 Tasks、BlockingCollection 和 Virtualization 来提高性能,假设应用程序的主要兴趣是记录速度。
Bridge.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Documents;
using System.Threading;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace PerformanceTest
{
public class Bridge
{
int counterLimit;
public BlockingCollection<string> output;
public BlockingCollection<string> impOutput;
public BlockingCollection<string> errors;
public BlockingCollection<string> impErrors;
public BlockingCollection<string> logs;
protected static Bridge controller = new Bridge();
public static Bridge Controller
{
get
{
return controller;
}
}
public MainWindow Window
{
set
{
if (value != null)
{
output = value.outputProducer;
impOutput = value.impOutputProducer;
errors = value.errorProducer;
impErrors = value.impErrorProducer;
logs = value.logsProducer;
}
}
}
public bool Running
{
get;
set;
}
private Bridge()
{
//20000 lines seems to slow down tabbing enough to prove my point.
//increase this number to get even worse results.
counterLimit = 40000;
}
public void PrintLotsOfText()
{
Task.Run(() => GenerateOutput());
Task.Run(() => GenerateError());
}
private void GenerateOutput()
{
//There is tons of output text, so print super fast if possible.
int counter = 1;
while (Running && counter < counterLimit)
{
PrintOutput("I will never say this horrible word again as long I live. This is confession #" + counter++ + ".");
//Task.Delay(1).Wait();
}
}
private void GenerateError()
{
int counter = 1;
while (Running && counter < counterLimit)
{
PrintError("I will never forgive your errors as long I live. This is confession #" + counter++ + ".");
//Task.Delay(1).Wait();
}
}
#region Printing
delegate void StringArgDelegate(String s);
delegate void InlineArgDelegate(Inline inline);
public void PrintOutput(String s)
{
output.TryAdd(s);
PrintLog("d " + s);
}
public void PrintImportantOutput(String s)
{
impOutput.TryAdd(s);
PrintLog("D " + s);
}
public void PrintError(String s)
{
errors.TryAdd(s);
PrintLog("e " + s);
}
public void PrintImportantError(String s)
{
impErrors.TryAdd(s);
PrintLog("E " + s);
}
public void PrintLog(String s)
{
String text = s;
logs.TryAdd(text);
}
#endregion
}
}
主窗口.xaml
<Window x:Class="PerformanceTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
WindowStartupLocation="CenterScreen">
<Grid>
<TabControl>
<TabControl.Resources>
<Style TargetType="ListBox">
<Setter Property="TextElement.FontFamily"
Value="Consolas" />
<Setter Property="TextElement.FontSize"
Value="12" />
<Setter Property="VirtualizingPanel.IsVirtualizing"
Value="True" />
<Setter Property="VirtualizingPanel.VirtualizationMode"
Value="Recycling" />
</Style>
</TabControl.Resources>
<TabItem Header="Bridge">
<StackPanel Orientation="Vertical"
HorizontalAlignment="Left">
<Button Content="Start Test"
Click="StartButton_Click" />
<Button Content="End Test"
Click="EndButton_Click" />
</StackPanel>
</TabItem>
<TabItem Header="Output">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0"
ItemsSource="{Binding Output,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" />
<GridSplitter Grid.Column="1"
Width="5"
ResizeBehavior="PreviousAndNext" />
<ListBox Grid.Column="2"
ItemsSource="{Binding Errors,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" />
</Grid>
</TabItem>
<TabItem Header="Log">
<Grid>
<ListBox Grid.Column="0"
ItemsSource="{Binding Logs,RelativeSource={RelativeSource FindAncestor,AncestorType=Window}}" />
</Grid>
</TabItem>
</TabControl>
</Grid>
</Window>
主窗口.xaml.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace PerformanceTest
{
///
/// Interaction logic for MainWindow.xaml
///
public partial class MainWindow : Window
{
public BlockingCollection<string> outputProducer = new BlockingCollection<string>();
public BlockingCollection<string> impOutputProducer = new BlockingCollection<string>();
public BlockingCollection<string> errorProducer = new BlockingCollection<string>();
public BlockingCollection<string> impErrorProducer = new BlockingCollection<string>();
public BlockingCollection<string> logsProducer = new BlockingCollection<string>();
public ObservableCollection<object> Output { get; set; }
public ObservableCollection<object> Errors { get; set; }
public ObservableCollection<object> Logs { get; set; }
Dispatcher dispatcher;
public MainWindow()
{
Bridge.Controller.Window = this;
try
{
InitializeComponent();
}
catch (Exception ex)
{
Console.WriteLine(ex.InnerException.ToString());
Console.WriteLine(ex.StackTrace);
}
dispatcher = Dispatcher;
Output = new ObservableCollection<object>();
Errors = new ObservableCollection<object>();
Logs = new ObservableCollection<object>();
Task.Run(() => Print(outputProducer, Output));
Task.Run(() => Print(errorProducer, Errors));
Task.Run(() => Print(logsProducer, Logs));
}
public void Print(BlockingCollection<string> producer, ObservableCollection<object> target)
{
try
{
foreach (var str in producer.GetConsumingEnumerable())
{
dispatcher.Invoke(() =>
{
target.Insert(0, str);
}, DispatcherPriority.Background);
}
}
catch (TaskCanceledException)
{
//window closing before print finish
}
}
private void StartButton_Click(object sender, RoutedEventArgs e)
{
if (!Bridge.Controller.Running)
{
Bridge.Controller.Running = true;
Bridge.Controller.PrintLotsOfText();
}
}
private void EndButton_Click(object sender, RoutedEventArgs e)
{
Bridge.Controller.Running = false;
}
}
}
完整的工作示例下载 PerformanceTest.zip看看这是否接近您的需要。我只重写了一部分。如果此示例正朝着预期的方向发展,我们就可以实现其余功能。取消注释 Bridge.cs 中的 Task.Delay(1).Wait();
如果您可能想减慢生产速度以查看混合日志,否则日志生成速度太快,因此它显示为一个在日志选项卡中的其他之后。
关于c# - 如何提高 FlowDocumentScrollViewer 的性能?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25044107/