【问题标题】:Drawing a grid of images with WPF使用 WPF 绘制图像网格
【发布时间】:2011-08-05 19:22:11
【问题描述】:

我正在尝试使用 WPF 绘制图像/图标网格。网格尺寸会有所不同,但通常范围为 10x10 到 200x200。用户应该能够点击单元格,有些单元格需要每秒更新(更改图像)10-20 次。网格应该能够在所有四个方向上增长和缩小,并且应该能够切换到它所代表的 3D 结构的不同“切片”。我的目标是根据这些要求找到一种适当有效的方法来绘制网格。

我当前的实现使用 WPF Grid。我在运行时生成行和列定义,并用Line(用于网格线)和Border(用于单元格,因为它们当前只是开/关)对象在适当的行/列填充网格。 (Line 对象一直跨越。)

在扩展网格时(按住 Num6),我发现它的绘制速度太慢,无法重绘每个操作,因此我对其进行了修改,只为每个操作添加了一个新的 ColumnDefinitionLine 和一组 Border 对象成长的柱子。这解决了我的增长问题,并且可以使用类似的策略来快速收缩。为了在模拟过程中更新单个单元格,我可以简单地存储对单元格对象的引用并更改显示的图像。甚至可以通过仅更新单元格内容而不是重建整个网格来改进到新的 Z 级别。

但是,在进行所有这些优化之前,我遇到了另一个问题。每当我将鼠标悬停在网格上时(即使是低速/正常速度),应用程序的 CPU 使用率都会飙升。我从网格的子元素中删除了所有事件处理程序,但这没有任何效果。最后,控制 CPU 使用率的唯一方法是将IsHitTestVisible = false 设置为Grid。 (为Grid 的每个子元素设置这个没有任何作用!)

我认为使用单独的控件来构建我的网格过于密集且不适合此应用程序,并且使用 WPF 的 2D 绘图机制可能更有效。不过,我是 WPF 的初学者,所以我正在寻求如何最好地实现这一目标的建议。根据我所阅读的内容,我可能会使用DrawingGroup 将每个单元格的图像组合到一个图像中以供显示。然后我可以对整个图像使用单击事件处理程序,并通过鼠标位置计算单击单元格的坐标。不过,这似乎很乱,我只是不知道是否有更好的方法。

想法?

更新 1:

我听取了朋友的建议,转而使用CanvasRectangle 为每个单元格。当我第一次绘制网格时,我将所有Rectangle 的引用存储在一个二维数组中,然后当我更新网格内容时,我只需访问这些引用。

private void UpdateGrid()
{
    for (int x = simGrid.Bounds.Lower.X; x <= simGrid.Bounds.Upper.X; x++)
    {
        for (int y = simGrid.Bounds.Lower.Y; y <= simGrid.Bounds.Upper.Y; y++)
        {
            CellRectangles[x, y].Fill = simGrid[x, y, ZLevel] ? Brushes.Yellow : Brushes.White;
        }
    }
}

最初绘制网格似乎更快,后续更新肯定更快,但仍然存在一些问题。

  • 无论我将鼠标悬停的区域有多小,当我将鼠标悬停在具有数百个单元格的网格上时,CPU 使用率仍然会达到峰值。

  • 更新仍然太慢,所以当我按住向上箭头键更改 Z 级别(常见用例)时,程序一次冻结几秒钟,然后似乎跳了 50 个 Z 级别一次。

  • 一旦网格包含约 5000 个单元格,更新时间大约为一秒。这非常慢,5000 个单元适合典型用例。

我还没有尝试过UniformGrid 方法,因为我认为它可能会出现我已经遇到过的相同问题。不过,一旦我用尽了更多选项,我可能会尝试一下。

【问题讨论】:

  • 您是否在 Canvas 上尝试过 IsHitTestVisible = false 在您的新方法中以控制鼠标悬停时的 CPU 使用?
  • 您是否显示具有动态缩放级别的地图?

标签: c# wpf grid drawing 2d


【解决方案1】:

您的问题

让我们重新表述您的问题。这些是您的问题限制:

  1. 您想要绘制动态大小的网格
  2. 每个单元格快速切换开/关
  3. 网格大小变化很快
  4. 有大量的单元格(即网格尺寸不是微不足道的)
  5. 您希望所有这些更改都以较快的帧速率(例如 30fps)发生
  6. 网格和单元格的定位和布局是确定性的、简单的且交互性不强

从这些限制条件来看,您可以立即看出您使用了错误的方法。

要求:快速刷新确定性位置,几乎没有交互性

快速刷新帧速率 + 每帧许多变化 + 大量单元格 + 每个单元格一个 WPF 对象 = 灾难。

除非您拥有非常快的图形硬件和非常快的 CPU,否则您的帧速率总是会随着网格尺寸的增加而受到影响。

您的问题更像是视频游戏或具有动态缩放功能的 CAD 绘图程序。它不像普通的桌面应用程序。

立即模式与保留模式绘图

换句话说,您想要“立即模式”绘图,而不是“保留模式”绘图(WPF 是保留模式)。这是因为您的约束不需要将每个单元格视为单独的 WPF 对象所提供的大部分功能。

例如,您不需要布局支持,因为每个单元格的位置都是确定的。您不需要命中测试支持,因为位置是确定性的。您不需要容器支持,因为每个单元格都是一个简单的矩形(或图像)。您不需要复杂的格式支持(例如透明度、圆角边框等),因为没有任何重叠。换句话说,使用 Grid(或 UniformGrid)和每个单元格一个 WPF 对象没有任何好处。

立即模式绘制到缓冲区位图的概念

为了达到您所需的帧速率,基本上您将绘制到一个大位图(覆盖整个屏幕)——或“屏幕缓冲区”。对于您的单元格,只需绘制到此位图/缓冲区(可能使用 GDI)。命中测试很容易,因为单元格位置都是确定性的。

此方法会很快,因为只有一个对象(屏幕缓冲区位图)。您可以为每一帧刷新整个位图,或者只更新那些发生变化的屏幕位置,或者智能地组合这些。

请注意,尽管您在此处绘制了“网格”,但并未使用“网格”元素。根据您的问题约束选择算法和数据结构,而不是看起来显而易见的解决方案——换句话说,“网格”可能不是正确的绘图解决方案一个“网格”。

WPF 中的即时模式绘图

WPF 基于 DirectX,因此本质上它已经在后台使用屏幕缓冲区位图(称为后台缓冲区)。

在 WFP 中使用即时模式绘图的方式是将单元格创建为 GeometryDrawing 的(不是 Shape 的,它是保留模式)。 GemoetryDrawing 通常非常快,因为 GemoetryDrawing 对象直接映射到 DirectX 基元;它们没有作为框架元素单独布局和跟踪,因此它们非常轻量级 - 您可以拥有大量它们而不会对性能产生不利影响。

将GeometryDrawing 选择到DrawingImage 中(这实际上是您的后台缓冲区),您将获得一个快速变化的屏幕图像。在幕后,WPF 完全按照您的期望执行 - 即将每个更改的矩形绘制到图像缓冲区上。

同样,不要使用 Shape 的——这些是框架元素,在参与布局时产生大量开销。例如,DO NOT USE Rectangle,而是使用 RectangleGeometry

优化

您可以考虑的更多优化:

  1. 重用 GeometryDrawing 对象 -- 只需更改位置和大小
  2. 如果网格有最大尺寸,请预先创建对象
  3. 仅修改那些已更改的 GeometryDrawing 对象 - 这样 WPF 就不会不必要地刷新它们
  4. 在“阶段”中填充位图——也就是说,对于不同的缩放级别,总是更新到比前一个大得多的网格,并使用缩放来缩小它。例如,从 10x10 网格直接移动到 20x20 网格,但将其缩小 55% 以显示 11x11 方格。这样,当从 11x11 一直缩放到 20x20 时,您的 GeometryDrawing 对象永远不会改变;仅更改位图上的缩放比例,使其更新速度极快。

编辑:逐帧渲染

覆盖OnRender 如答案中的建议授予此问题的赏金。然后,您基本上可以在画布上绘制整个场景。

使用 DirectX 进行绝对控制

或者,如果您想对每一帧进行绝对控制,请考虑使用原始 DirectX。

【讨论】:

  • 我认为几何是这里的路。
  • 好提示!缩放/调整大小策略特别好;我已经在网格的内部表示中使用了这种技巧,所以我也应该考虑将它用于渲染。不过,我对如何使用几何对象加快绘图速度感到有些困惑。绘制数千个GeometryDrawing 对象不会大大减慢速度吗?
  • 嗯,40000 个 RectangleGeometry 比 40000 个 Rectangle(形状)好很多。
  • Geometry 对象通常很轻,您不必担心。但是,拥有超过 40000 个以上的帧仍可能会对帧速率产生不利影响。由于每个矩形实际上只映射到 DirectX 中的两个三角形,如果您有足够快的 CPU 来处理更改通知,那么 80000 个三角形对于现代 GPU 来说应该不是太大的问题。
  • 您能否给出一个代码示例来说明如何实际实现这种绘图?
【解决方案2】:

您可以编写自己的自定义控件(基于 Canvas、Panel 等)并覆盖 OnRender,如下所示:

   public class BigGrid : Canvas
    {
        private const int size = 3; // do something less hardcoded

        public BigGrid()
        {
        }

        protected override void OnRender(DrawingContext dc)
        {
            Pen pen = new Pen(Brushes.Black, 0.1);

            // vertical lines
            double pos = 0;
            int count = 0;
            do
            {
                dc.DrawLine(pen, new Point(pos, 0), new Point(pos, DesiredSize.Height));
                pos += size;
                count++;
            }
            while (pos < DesiredSize.Width);

            string title = count.ToString();

            // horizontal lines
            pos = 0;
            count = 0;
            do
            {
                dc.DrawLine(pen, new Point(0, pos), new Point(DesiredSize.Width, pos));
                pos += size;
                count++;
            }
            while (pos < DesiredSize.Height);

            // display the grid size (debug mode only!)
            title += "x" + count;
            dc.DrawText(new FormattedText(title, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, new Typeface("Arial"), 20, Brushes.White), new Point(0, 0));
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            return availableSize;
        }
    }

我可以在 y 台笔记本电脑(不是竞赛机器...)上成功绘制 400x400 网格并调整其大小。

有更多花哨和更好的方法(在 DrawingContext 上使用 StreamGeometry),但这至少是一个很好的测试工作台。

当然,您必须重写 HitTestXXX 方法。

【讨论】:

    【解决方案3】:

    我认为你会很难处理这么多元素,如果只有一小部分可见,那么 Virtualizing Canvas 控件here 可能会有所帮助,但这只会有助于滚动。要同时显示这么多单元格,您可能必须以一种或另一种方式绘制位图。

    这是一个示例,其中单元格的 VisualBrush 平铺,然后使用 OpacityMask 切换每个单元格。下面的方法非常简洁,因为每个单元只需要一个像素;元素可以是任意大小,您不需要复杂的代码将单元格内容传送到位图。

    该示例创建了一个 1000*1000 的网格,如果您只需要两种,则有 3 种单元格类型,可以进一步简化代码并删除许多循环。更新很快(200*200 为 3 毫秒,1k*1k 为 100 毫秒),滚动按预期工作,添加缩放应该不会太难。

    <Window ... >
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="25*" />
                <RowDefinition Height="286*" />
            </Grid.RowDefinitions>
            <Button Click="Button_Click" Content="Change Cells" />
            <ScrollViewer Grid.Row="1" ScrollViewer.HorizontalScrollBarVisibility="Auto">
            <Grid x:Name="root" MouseDown="root_MouseDown" />
        </ScrollViewer>
        </Grid>
    </Window>
    


    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
    
            Loaded += new RoutedEventHandler(MainWindow_Loaded);
        }
    
        const int size = 1000, elementSize = 20;
        void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            var c = new[] { Brushes.PowderBlue, Brushes.DodgerBlue, Brushes.MediumBlue};
            elements = c.Select((x, i) => new Border
            {
                Background = x,
                Width = elementSize,
                Height = elementSize,
                BorderBrush = Brushes.Black,
                BorderThickness = new Thickness(1),
                Child = new TextBlock
                {
                    Text = i.ToString(),
                    HorizontalAlignment = HorizontalAlignment.Center
                }
            }).ToArray();
    
            grid = new int[size, size];
    
            for(int y = 0; y < size; y++)
            {
                for(int x = 0; x < size; x++)
                {
                    grid[x, y] = rnd.Next(elements.Length);
                }
            }
    
            var layers = elements.Select(x => new Rectangle()).ToArray();
    
            masks = new WriteableBitmap[elements.Length];
            maskDatas = new int[elements.Length][];
    
            for(int i = 0; i < layers.Length; i++)
            {
    
                layers[i].Width = size * elementSize;
                layers[i].Height = size * elementSize;
    
                layers[i].Fill = new VisualBrush(elements[i])
                {
                    Stretch = Stretch.None,
                    TileMode = TileMode.Tile,
                    Viewport = new Rect(0,0,elementSize,elementSize),
                    ViewportUnits = BrushMappingMode.Absolute
    
                };
    
                root.Children.Add(layers[i]);
    
                if(i > 0) //Bottom layer doesn't need a mask
                {
                    masks[i] = new WriteableBitmap(size, size, 96, 96, PixelFormats.Pbgra32, null);
                    maskDatas[i] = new int[size * size];
    
                    layers[i].OpacityMask = new ImageBrush(masks[i]);
                    RenderOptions.SetBitmapScalingMode(layers[i], BitmapScalingMode.NearestNeighbor);
                }
            }
    
            root.Width = root.Height = size * elementSize;
    
            UpdateGrid();
        }
    
        Random rnd = new Random();
    
        private int[,] grid;
        private Visual[] elements;
        private WriteableBitmap[] masks;
        private int[][] maskDatas;
    
        private void UpdateGrid()
        {
            const int black = -16777216, transparent = 0;
            for(int y = 0; y < size; y++)
            {
                for(int x = 0; x < size; x++)
                {
                    grid[x, y] = (grid[x, y] + 1) % elements.Length;
    
                    for(int i = 1; i < maskDatas.Length; i++)
                    {
                        maskDatas[i][y * size + x] = grid[x, y] == i ? black : transparent;
                    }
                }
            }
    
            for(int i = 1; i < masks.Length; i++)
            {
                masks[i].WritePixels(new Int32Rect(0, 0, size, size), maskDatas[i], masks[i].BackBufferStride, 0);
            }
        }
    
    
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var s = Stopwatch.StartNew();
            UpdateGrid();
            Console.WriteLine(s.ElapsedMilliseconds + "ms");
    
        }
    
        private void root_MouseDown(object sender, MouseButtonEventArgs e)
        {
            var p = e.GetPosition(root);
    
            int x = (int)p.X / elementSize;
            int y = (int)p.Y / elementSize;
    
            MessageBox.Show(string.Format("You clicked X:{0},Y:{1} Value:{2}", x, y, grid[x, y]));
        }
    }
    

    【讨论】:

      【解决方案4】:

      继续使用Canvas 方法,看起来如果您可以快速绘制网格线,您可以省略所有空方格并大大减少屏幕上的元素总数,具体取决于您正在做的事情的密度.无论如何,要快速绘制网格线,您可以像这样使用DrawingBrush

      <Grid>
          <Grid.Background>
              <DrawingBrush x:Name="GridBrush" Viewport="0,0,20,20" ViewportUnits="Absolute" TileMode="Tile">
                  <DrawingBrush.Drawing>
                      <DrawingGroup>
                          <GeometryDrawing Brush="#CCCCCC">
                              <GeometryDrawing.Geometry>
                                  <RectangleGeometry Rect="0,0 20,1"/>
                              </GeometryDrawing.Geometry>
                          </GeometryDrawing>
                          <GeometryDrawing Brush="#CCCCCC">
                              <GeometryDrawing.Geometry>
                                  <RectangleGeometry Rect="0,0 1,20"/>
                              </GeometryDrawing.Geometry>
                          </GeometryDrawing>
                      </DrawingGroup>
                  </DrawingBrush.Drawing>
              </DrawingBrush>
          </Grid.Background>
      </Grid>
      

      导致这种效果:

      【讨论】:

        【解决方案5】:

        如果您希望单元格大小相同,我认为 UniformGrid 可能是最合适的。这样您就不必担心在代码中设置大小。

        如果你实施,我会对结果非常感兴趣。

        【讨论】:

        • 在单个单元格上进行命中测试是否还会遇到问题?我还必须维护对单元格控件的引用数组,如果我使用绘图工具,我(可能?)不需要这些引用,但这可能会使某些操作(如调整大小)花费更长的时间。 UniformGrid 是否提供其他性能优势?
        • 我想我会以不同的方式攻击这个,但我不太了解你在做什么。我将创建一个自定义 listView 控件并用 UniformGrid 替换项目容器(通常是堆栈面板)。然后我可以将 ListView 绑定到您希望表示的项目集合。使用 ItemTemplate 指定您希望项目的外观。
        • 我发现虚拟化物品容器在资源上相当保守。如果没有更好地了解您的模型,也没有时间在测试中进行尝试,我不确定在 CPU 资源方面,拥有多达(甚至可能超过)4000 个单元格的网格会做什么。
        【解决方案6】:

        我建议你为此编写一个自定义面板,编写这个应该很简单,因为你只需要重写 MeasureOverride 和 ArrangeOverride 方法。根据行数/列数您可以为每个单元格分配可用大小。这应该会给你比网格更好的性能,如果你想进一步优化它,你也可以在面板中实现虚拟化。

        当我必须创建一个滚动矩阵时,我就是这样做的,该矩阵必须显示一些文本信息而不是图像,并且行/列的数量是不同的。这是一个如何编写自定义面板的示例

        http://blogs.msdn.com/b/dancre/archive/2005/10/02/476328.aspx

        如果您希望我与您分享我编写的代码,请告诉我。

        【讨论】:

          【解决方案7】:

          在这里做一些猜测:

          1. 使用画布方法。
          2. 禁用画布上的命中测试以 防止鼠标悬停 CPU 发疯。
          3. 单独跟踪您的更改 用户界面。只改变填充 元素上的属性 自上次更新以来发生了变化。我是 猜测缓慢的更新是 由于更新了数千个 UI 元素和后续 重新渲染所有内容。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2013-04-25
            • 1970-01-01
            • 1970-01-01
            • 2017-06-02
            • 2018-06-24
            • 2017-10-22
            相关资源
            最近更新 更多