使用WPF绘制图像网格

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

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

当前的网格实现

在扩展网格时(按住Num6),我发现在每次操作时绘制的速度都太慢,所以我修改它只是为每个增长列添加一个新的ColumnDefinitionLineBorder对象集。 这解决了我的增长问题,也可以使用类似的策略来快速缩小。 为了在模拟中更新单个单元格,我可以简单地存储对单元格对象的引用并更改显示的图像。 甚至可以通过仅更新单元格内容而不是重建整个网格来改进到新的Z级别。

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

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

思考?

更新1:

我接受了朋友的建议并转而使用带有RectangleCanvas用于每个单元格。 当我第一次绘制网格时,我将对所有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; } } } 

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

  • 无论我鼠标移动的区域有多小,每当我将鼠标hover在网格上时,CPU使用率仍然会超过几百个单元格。

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

  • 一旦网格保持~5000个单元格,更新大约需要一秒钟。 这非常慢,5000个单元适合典型的使用案例。

我还没有尝试过UniformGrid方法,因为我认为它可能会遇到我遇到的同样问题。 不过,一旦我用尽了一些选项,我可能会尝试一下。

你的问题

让我们改一下你的问题吧。 这些是你的问题限制:

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

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

需求:快速刷新确定性位置,交互性很小

快速刷新帧速率+每帧多次更改+大量单元格+每个单元格一个WPF对象=失败。

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

你的问题所决定的更像是一个动态缩放的video游戏或CAD绘图程序。 它不像普通的桌面应用程序。

立即模式与保留模式绘图

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

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

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

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

这种方法很快,因为只有一个对象(屏幕缓冲区位图)。 您可以刷新每个帧的整个位图,也可以仅更新那些更改的屏幕位置,或者更新这些位置的智能组合。

请注意,虽然您在此处绘制“网格”,但不使用“网格”元素。 根据您的问题约束选择您的算法和数据结构,而不是看起来是明显的解决方案 – 换句话说,“网格”可能不是绘制“网格”的正确解决方案。

WPF中的立即模式绘制

WPF基于DirectX,所以基本上它已经在场景后面使用了一个屏幕缓冲位图(称为后缓冲区)。

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

选择GeometryDrawing进入DrawingImage(这实际上是你的后缓冲区),你可以获得一个快速变化的图像。 在场景后面,WPF完全按照您的预期进行操作 – 即绘制每个更改为图像缓冲区的矩形。

同样,不要使用Shape – 这些是框架元素,并且在参与布局时产生很大的开销。 例如, 不要使用Rectangle ,而是使用RectangleGeometry

优化

您可以考虑多种优化:

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

编辑:逐帧渲染

根据答案中的建议覆盖OnRender ,为此问题授予赏金。 然后你基本上在canvas上绘制整个场景。

使用DirectX进行绝对控制

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

您可以编写自己的自定义控件(基于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方法。

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

                     

这会产生这种效果:

网格线

我认为你将很难处理那么多元素,如果只有少数可见, 这里的Virtualizing Canvas控件可能会有所帮助,但这只会有助于滚动。 要让多个单元格同时可见,您可能需要以某种方式绘制位图。

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

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

             

 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)pX / elementSize; int y = (int)pY / elementSize; MessageBox.Show(string.Format("You clicked X:{0},Y:{1} Value:{2}", x, y, grid[x, y])); } } 

如果你想让细胞大小相同,我认为UniformGrid可能是最合适的。 这样,您就不必担心在代码中设置大小。

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

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

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

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

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

在这里做一些猜测:

  1. 使用Canvas方法。
  2. 禁用Canvas上的命中测试以防止鼠标CPU发疯。
  3. 从UI单独跟踪您的更改。 仅更改自上次更新以来已更改的元素的Fill属性。 我猜测缓慢的更新是由于更新了数千个UI元素以及随后重新渲染所有内容。