使用WPF绘制图像网格

时间:2011-04-14 18:54:08

标签: c# wpf grid drawing 2d

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

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

Current grid implementation

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

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

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

思想?

更新1:

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

Rectangle

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

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

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

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

我还没有尝试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; } } } 方法,因为我认为它可能会遇到我遇到的同样问题。不过,如果我已经用尽了一些选择,我可能会尝试一下。

7 个答案:

答案 0 :(得分:17)

答案 1 :(得分:3)

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

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

该示例创建一个1000 * 1000网格,如果您只需要两个,则有3个单元格类型,可以进一步简化代码并删除大量循环。更新很快(200 * 200毫秒,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]));
    }
}

答案 2 :(得分:3)

您可以编写自己的自定义控件(基于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 :(得分:2)

继续使用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>

会产生这种效果:

Grid Lines

答案 4 :(得分:1)

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

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

答案 5 :(得分:0)

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

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

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

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

答案 6 :(得分:0)

在这里做一些猜测:

  1. 使用Canvas方法。
  2. 禁用“画布”上的点击测试 让鼠标停止CPU变得疯狂。
  3. 分别跟踪您的更改 用户界面。只更改填充 有元素的属性 自上次更新后更改。我 猜测缓慢的更新是 由于更新了数千个UI 元素和随后的 重新渲染一切。