【问题标题】:How do I increase performance of FlowDocumentScrollViewer?如何提高 FlowDocumentScrollViewer 的性能?
【发布时间】:2014-09-22 12:35:54
【问题描述】:

在上一个问题中,我询问了如何在类似 WPF 文本框的元素 (WPF append text blocks UI thread heavily but WinForms doesn't?) 中获取实时日志记录输出。那里的答案使我使用了FlowDocumentScrollViewer,它确实比RichTextBox 快得多。但是,我发现运行具有大量文本输出的命令(如“svn co”)会导致我的 WPF 应用程序明显变慢。在签出 3 或 4 个非常大的 svn 分支后切换选项卡需要 3-4 秒,而且我确信时间会随着我所做的签出次数而增加。滚动也有明显的滞后。

正如我在上面链接的问题中所述,我最近将我的应用程序从 Windows 窗体切换到 WPF。我非常喜欢 WPF - 它提供了许多我在 Forms 中没有的优势。然而,性能在 WPF 中似乎是一个相当大的问题,至少对我来说是这样。在我的应用程序的 Forms 版本中,我可以将大量文本打印到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 中不是问题。


根据 cmets 的要求,以下是完整的代码示例。它非常简单(3 个 FlowDocumentScrollViewer 和简单的打印),但仍然会大大降低大约 20000 行文本的速度,而且比这更糟。

编辑:代码示例已被删除。取而代之的是解决我的性能问题的工作代码。它的工作原理与FlowDocumentScrollViewer 一样,但有一个例外:您不能选择行的子字符串。我正在考虑解决这个问题,虽然这看起来很困难。

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

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

MainWindow.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 的回答中。

【问题讨论】:

  • 那么,你有 3 个FlowDocuments?和3个不同的FlowDocumentScrollViewers?或 3 个显示相同文档的滚动查看器?你能稍微澄清一下吗?
  • @HighCore 我有 3 个单独的 FlowDocument,1 个用于 stdout,1 个用于 stderr,第 3 个用于“日志”选项卡,它结合了两者并添加了一些有用的日志记录文本。每个 FlowDocument 也有自己的 FlowDocumentScrollViewer。
  • 赏金:我现在太懒/太忙了,除了无用的 cmets(比如这个)之外,我太忙了,无法参与 SO。请帮助OP。我确定我不是唯一一个回答 WPF 问题的人......
  • 您能否发布一个可以重现该问题的工作示例?我可以尝试为您找到一些优化。
  • 我没有发现我编写的一个简单示例(大量文本,大量标签)有任何问题。当你有几个例子时,让我们知道!谢谢。

标签: c# wpf performance text


【解决方案1】:

我执行您提供的示例,除了几个线程问题之外,最大的问题是数据量。随着文本的增长,这会减慢文本的渲染速度。

我尝试以不同的方式重写您的代码。我使用 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
    }
}

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

MainWindow.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();,否则日志生成速度太快,因此它会一个接一个地出现在日志选项卡中。

【讨论】:

  • 在我的问题中似乎以某种方式遗漏了一件相当重要的事情,那就是我还需要复制粘贴功能,这在 ListBox 中有很大的限制。我之前根据 HighCore 的建议尝试了 ListBox 方法,虚拟化加快了速度,但使用该解决方案进行复制粘贴绝对是一场噩梦,而且我从未完成过。
  • 我已将我的 PerfTest 应用程序转换为使用您的代码,并且我怀疑复制粘贴是一个大问题。虚拟化 ListBox 非常快并且具有我想要的性能,但在功能方面我确实需要复制/粘贴。
  • 如果您需要复制和粘贴,只需修改 ListBox 的项目模板,改为使用只读文本框。
  • @Clever Neologism 这并没有改变每个 ListBoxItem 与其他项目分开的事实,并且几乎不可能跨多个项目进行选择......我说的是单击并拖动复制/粘贴,不是逐行,这在我的应用程序中没有用。此外(为了 pushpraj 的利益),我使用的是 .NET 4.0,而不是 4.5,所以我必须稍微修改一下您的代码才能使其正常工作。
  • @pushpraj 我已经通过去掉 TextBlock 中的 元素解决了这些问题。 Shift+Arrow 没有像预期的那样上下突出显示,它移动到 ListBox 的顶部和底部,但这对我来说很好。 Shift+Click 现在按预期工作。剩下的唯一问题是 ListBoxItems 内的子字符串选择。我会四处寻找答案,但如果你找到答案,请告诉我!感谢@pushpraj 的帮助!
【解决方案2】:

使用FlowDocumentPageViewer 将有助于提高性能,因为它异步加载文档。

【讨论】:

  • 我误解了这个控件是什么...我不能让输出出现在多个页面上,它必须在一个可滚动页面上。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-09-13
  • 2021-10-13
  • 2018-03-01
  • 2020-04-28
  • 2010-11-04
  • 2021-05-05
  • 2016-01-11
相关资源
最近更新 更多