【问题标题】:10000's+ UI elements, bind or draw?10000 多个 UI 元素,绑定还是绘制?
【发布时间】:2012-12-05 01:00:22
【问题描述】:

我正在为时间线控件绘制标题。 它看起来像这样:

我每行 0.01 毫秒,所以对于 10 分钟的时间线,我正在查看绘制 60000 行 + 6000 个标签。 这需要一段时间,大约 10 秒。 我想从 UI 线程中卸载它。 我的代码目前是:

private void drawHeader()
{
  Header.Children.Clear();
  switch (viewLevel)
  {
    case ViewLevel.MilliSeconds100:
        double hWidth = Header.Width;
        this.drawHeaderLines(new TimeSpan(0, 0, 0, 0, 10), 100, 5, hWidth);

        //Was looking into background worker to off load UI

        //backgroundWorker = new BackgroundWorker();

        //backgroundWorker.DoWork += delegate(object sender, DoWorkEventArgs args)
        //                               {
        //                                   this.drawHeaderLines(new TimeSpan(0, 0, 0, 0, 10), 100, 5, hWidth);
        //                               };
        //backgroundWorker.RunWorkerAsync();
        break;
    }
}

private void drawHeaderLines(TimeSpan timeStep, int majorEveryXLine, int distanceBetweenLines, double headerWidth)
{
var currentTime = new TimeSpan(0, 0, 0, 0, 0);
const int everyXLine100 = 10;
double currentX = 0;
var currentLine = 0;
while (currentX < headerWidth)
{
    var l = new Line
                {
                    ToolTip = currentTime.ToString(@"hh\:mm\:ss\.fff"),
                    StrokeThickness = 1,
                    X1 = 0,
                    X2 = 0,
                    Y1 = 30,
                    Y2 = 25
                };
    if (((currentLine % majorEveryXLine) == 0) && currentLine != 0)
    {
        l.StrokeThickness = 2;
        l.Y2 = 15;
        var textBlock = new TextBlock
                            {
                                Text = l.ToolTip.ToString(),
                                FontSize = 8,
                                FontFamily = new FontFamily("Tahoma"),
                                Foreground = new SolidColorBrush(Color.FromRgb(255, 255, 255))
                            };

        Canvas.SetLeft(textBlock, (currentX - 22));
        Canvas.SetTop(textBlock, 0);
        Header.Children.Add(textBlock);
    }

    if ((((currentLine % everyXLine100) == 0) && currentLine != 0)
        && (currentLine % majorEveryXLine) != 0)
    {
        l.Y2 = 20;
        var textBlock = new TextBlock
                            {
                                Text = string.Format(".{0}", TimeSpan.Parse(l.ToolTip.ToString()).Milliseconds),
                                                            FontSize = 8,
                                                            FontFamily = new FontFamily("Tahoma"),
                                                            Foreground = new SolidColorBrush(Color.FromRgb(192, 192, 192))
                            };

        Canvas.SetLeft(textBlock, (currentX - 8));
        Canvas.SetTop(textBlock, 8);
        Header.Children.Add(textBlock);
    }
    l.Stroke = new SolidColorBrush(Color.FromRgb(255, 255, 255));
    Header.Children.Add(l);
    Canvas.SetLeft(l, currentX);

    currentX += distanceBetweenLines;
    currentLine++;
    currentTime += timeStep;
}
}

我研究过 BackgroundWorker,但你不能在非 UI 线程上创建 UI 元素。

是否可以在非 UI 线程中执行 drawHeaderLines?

我可以使用数据绑定来绘制线条吗? 这对 UI 响应有帮助吗?

我想我可以使用数据绑定,但样式可能超出了我当前的 WPF 能力(来自 winforms 并试图了解所有这些样式对象是什么并绑定它们)。

谁能提供一个引诱这个的起点?或者谷歌一个可以帮助我入门的教程?

【问题讨论】:

  • 我已经编辑了你的标题。请参阅“Should questions include “tags” in their titles?”,其中的共识是“不,他们不应该”。
  • 这是用于屏幕绘画的吗?在 1980 屏幕列上画 60000 条平行线有意义吗?相当多的线可能会被绘制在另一条之上或者将是不可见的。我的意思是,您可以调整绘制线条的数量,以使效果在视觉上仍然可以接受。即使是 1980 行在高清显示器上看起来也像一个块,所以你需要的远比这要少。
  • 如果性能是你所追求的(即你正在构建一个序列器或其他东西),你可能想要坚持使用 GDI+,因为这样的东西不完全是 WPF 的之一强项。您将覆盖OnPaint,使用事件参数的ClipRectangle 来获取需要绘制的时间线部分,然后手动计算并绘制该区域内的每个实体。不想强迫您远离 WPF,这只是一个建议。
  • @HighCore:我不敢苟同,因为我是在一台非常慢的笔记本电脑上同时使用 GDI+ 和 WPF 制作塔防游戏的人:D 你的里程可能会有所不同。
  • @HighCore 这是一个误解。 WPF 并不意味着一切都被卸载到 GPU。 XNA 和 WPF 仍然大量使用 CPU。使用 ItemsControl 处理 60k 个项目的重量非常重,而不是画几条线,这对于现代 CPU 来说绝对算不上什么。你用过 CUDA 或 OpenCL 吗?如果是,您无疑会注意到,由于开销,GPU 计算变得比 CPU 计算更高效的边界高得离谱。我创建了两个比较程序。 GDI+ 流畅,WPF 启动时间为 11 秒,窗口大小调整闪烁。

标签: c# wpf data-binding


【解决方案1】:

所以,正如您所说,所有这些工作都需要在 UI 线程中完成;您不能只在后台线程中执行此操作。

但是,运行一个很长的循环在 UI 线程中进行大量 UI 修改会阻塞 UI 线程,所以显然我们不能这样做。

这里的关键是您需要将您正在做的事情分解为许多较小的工作单元,然后在 UI 线程中完成所有这些小工作单元。 UI 线程所做的工作与以前一样多(可能甚至更多,因为管理所有这些的开销),但它允许其他事情(例如鼠标移动/单击事件、按键等)发生在这些任务之间。

这是一个简单的例子:

private void button1_Click(object sender, EventArgs e)
{
    TaskScheduler uiContext = TaskScheduler.FromCurrentSynchronizationContext();
    Task.Run(async () =>
    {
        for (int i = 0; i < 1000; i++)
        {
            await Task.Factory.StartNew(() =>
            {
                Controls.Add(new Label() { Text = i.ToString() });
            }, CancellationToken.None, TaskCreationOptions.None, uiContext);
        }
    });
}

首先,我们在 UI 线程中获取 UI 上下文,然后在后台启动一个新线程,负责启动我们所有的小任务。在这里,您将开始循环。 (您可能希望将其提取到另一种方法,因为您的方法并不那么简单。)然后,立即在循环内部启动一个新任务,并在 UI 的上下文中启动该任务。然后,您可以在该任务内部放置循环中的整个内容。通过await对该任务进行@ing,您可以确保每个任务都被安排为前一个任务的延续,因此它们都按顺序运行。如果订单无关紧要(这不太可能,但可能),那么您根本不需要等待。

如果您需要 C# 4.0 版本,您可以保留一个 Task 退出循环,并在每次迭代中连接一个新任务作为前一个任务的延续,然后将“自身”设置为该任务。不过会比较麻烦。

【讨论】:

    【解决方案2】:

    这只是概念验证,但我认为您可以使用 ListView 虚拟化。而不是文本只有一组图像(每 10 行一个图像)。通过虚拟化,它只呈现显示中的内容。

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <ListView Grid.Row="0" Grid.Column="0" ItemsSource="{Binding Path=Numbers}" Width="100" Height="500">
            <ListView.RenderTransform>
                <RotateTransform Angle="90" CenterX="150" CenterY="150"/>
            </ListView.RenderTransform> 
        </ListView>
    </Grid>
    
    
    
    
    public List<string> Numbers
    {
        get
        {
            List<string> numbers = new List<string>();
            for (UInt16 i=1; i < 60000; i++)
            {
                numbers.Add(i.ToString());
            }
            return numbers;
        }
    }
    

    【讨论】:

      【解决方案3】:

      好吧,我已经把它放在次要位置了,但是。

      但我想我已经想出了一个解决方案。

      使用 DrawingVisual 和 DrawingContext。

      这篇博文对我帮助很大:Simple WPF 2D Graphics: DrawingVisual

      首先我们需要获取我们的 Visual 类(注意这段代码已经被拍打在一起了,清理是个好主意):

      public class MyVisualHost : FrameworkElement
      {
      private readonly VisualCollection children;
      
      public MyVisualHost(int width)
      {
          children = new VisualCollection(this);
      
          var visual = new DrawingVisual();
          children.Add(visual);
      
          var currentTime = new TimeSpan(0, 0, 0, 0, 0);
          const int everyXLine100 = 10;
          double currentX = 0;
          var currentLine = 0;
          double distanceBetweenLines = 5;
          TimeSpan timeStep = new TimeSpan(0, 0, 0, 0, 10);
          int majorEveryXLine = 100;
      
          var grayBrush = new SolidColorBrush(Color.FromRgb(192, 192, 192));
          grayBrush.Freeze();
          var grayPen = new Pen(grayBrush, 2);
          var whitePen = new Pen(Brushes.White, 2);
          grayPen.Freeze();
          whitePen.Freeze();
      
          using (var dc = visual.RenderOpen())
          {
              while (currentX < width)
              {
                  if (((currentLine % majorEveryXLine) == 0) && currentLine != 0)
                  {
                      dc.DrawLine(whitePen, new Point(currentX, 30), new Point(currentX, 15));
      
                      var text = new FormattedText(
                          currentTime.ToString(@"hh\:mm\:ss\.fff"),
                          CultureInfo.CurrentCulture,
                          FlowDirection.LeftToRight,
                          new Typeface("Tahoma"),
                          8,
                          grayBrush);
      
                      dc.DrawText(text, new Point((currentX - 22), 0));
                  }
                  else if ((((currentLine % everyXLine100) == 0) && currentLine != 0)
                              && (currentLine % majorEveryXLine) != 0)
                  {
                      dc.DrawLine(grayPen, new Point(currentX, 30), new Point(currentX, 20));
      
                      var text = new FormattedText(
                          string.Format(".{0}", currentTime.Milliseconds),
                          CultureInfo.CurrentCulture,
                          FlowDirection.LeftToRight,
                          new Typeface("Tahoma"),
                          8,
                          grayBrush);
                      dc.DrawText(text, new Point((currentX - 8), 8));
                  }
                  else
                  {
                      dc.DrawLine(grayPen, new Point(currentX, 30), new Point(currentX, 25));
                  }
      
                  currentX += distanceBetweenLines;
                  currentLine++;
                  currentTime += timeStep;
              }
          }
      }
      
      // Provide a required override for the VisualChildrenCount property.
      protected override int VisualChildrenCount { get { return children.Count; } }
      
      // Provide a required override for the GetVisualChild method.
      protected override Visual GetVisualChild(int index)
      {
          if (index < 0 || index >= children.Count)
          {
              throw new ArgumentOutOfRangeException();
          }
      
          return children[index];
      }
      }
      

      接下来我们需要绑定:

      public static readonly DependencyProperty HeaderDrawingVisualProperty = DependencyProperty.Register("HeaderDrawingVisual", typeof(MyVisualHost), typeof(MainWindow));
      
      public MyVisualHost VisualHost
      {
          get { return (MyVisualHost)GetValue(HeaderDrawingVisualProperty); }
          set { SetValue(HeaderDrawingVisualProperty, value); }
      }
      

      XAML:

      <Canvas x:Name="Header" Background="#FF2D2D30" Grid.Row="0">
          <ContentPresenter Content="{Binding HeaderDrawingVisual}" />
      </Canvas>
      

      然后只需在代码中设置:

      Header.Width = 50000;
      VisualHost = new MyVisualHost(50000);
      

      我的测试表明,使用这种新方法使用 50000 的宽度,我看到了很大的增加!

      Old way Total Milliseconds: 277.061
      New way Total Milliseconds: 13.9982
      Old way Total Milliseconds: 518.4632
      New way Total Milliseconds: 12.9423
      Old way Total Milliseconds: 479.1846
      New way Total Milliseconds: 23.4987
      Old way Total Milliseconds: 477.1366
      New way Total Milliseconds: 12.6469
      Old way Total Milliseconds: 481.3118
      New way Total Milliseconds: 12.9678
      

      第一次时间少,因为重建时必须清除项目。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-03-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-12-21
        • 2013-05-04
        • 1970-01-01
        相关资源
        最近更新 更多