用WPF绘制图像网格

| 我正在尝试使用WPF绘制图像/图标网格。网格尺寸会有所不同,但通常会在10x10到​​200x200之间。用户应该能够单击单元格,并且某些单元格每秒需要更新(更改图像)10-20次。网格应该能够在所有四个方向上增长和收缩,并且应该能够切换到它所表示的3D结构的不同“切片”。我的目标是在给定这些要求的情况下,找到一种合适的有效方法来绘制网格。 我当前的实现使用WPF
Grid
。我在运行时生成行和列定义,并在适当的行/列中用
Line
(对于网格线)和
Border
(对于单元格,因为它们当前只是打开/关闭)对象填充网格。 (“ 1”个对象贯穿整个对象。) 在扩展网格时(按住Num6键),我发现它绘制得太慢而无法在每个操作上重画,因此我对其进行了修改,只为增长的每一列简单地添加了新的
ColumnDefinition
Line
和set2ѭ对象集。这解决了我的成长问题,并且可以使用类似的策略来快速缩小。为了在仿真过程中更新单个单元格,我可以简单地存储对单元格对象的引用并更改显示的图像。通过仅更新单元格内容而不是重建整个网格,甚至可以更改为新的Z级别。 但是,在进行所有这些优化之前,我遇到了另一个问题。每当我将鼠标悬停在网格上时(即使是以低速/正常速度运行),应用程序的CPU使用率都会飙升。我从网格的子元素中删除了所有事件处理程序,但这没有任何效果。最后,唯一可以控制CPU使用率的方法是将ѭ0设置为ѭ7。 (为
Grid
的每个子元素设置此设置均无济于事!) 我相信使用单独的控件来构建我的网格对于此应用程序来说过于繁琐且不合适,并且使用WPF的2D绘制机制可能会更高效。我是WPF的初学者,所以我正在寻求有关如何最好地实现这一目标的建议。从我所读的内容中,我可能会使用ѭ10来将每个单元格的图像组合到一个图像上进行显示。然后,我可以对整个图像使用click事件处理程序,并通过鼠标位置计算单击的单元格的坐标。不过,这似乎很混乱,我只是不知道是否有更好的方法。 有什么想法吗? 更新1: 我接受了朋友的建议,并切换为每个单元格使用
Canvas
Rectangle
。当我第一次绘制网格时,我将所有ѭ12的引用存储在一个二维数组中,然后在更新网格内容时,我只需访问这些引用即可。
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个单元。 我尚未尝试过“ 15”法,因为我认为它可能会遇到与我已经遇到的问题相同的问题。不过,一旦我用尽了更多的选择,就可以尝试一下。     
已邀请:
你的问题 让我们改一下您的问题。这些是您的问题约束: 您想绘制一个动态尺寸的网格 每个单元快速切换开/关 网格大小快速变化 单元格很多(即网格尺寸不小) 您希望所有这些更改都以快速的帧速率(例如30fps)发生 网格和单元的位置和布局是确定性的,简单的并且不是很互动 从这些约束条件来看,您可以立即看到您使用的方法错误。 要求:几乎没有交互性的快速刷新确定性职位 快速刷新帧速率+每帧许多更改+大量单元+每个单元一个WPF对象=灾难。 除非您拥有非常快的图形硬件和非常快的CPU,否则,随着网格尺寸的增加,帧速始终会受到影响。 您的问题所指示的更像是具有动态缩放功能的视频游戏或CAD绘图程序。它不像普通的桌面应用程序。 立即模式与保留模式图 换句话说,您需要的是“立即模式”图形,而不是“保留模式”图形(WPF是保留模式)。这是因为您的约束不需要通过将每个单元格视为单独的WPF对象来提供许多功能。 例如,您将不需要布局支持,因为每个单元格的位置都是确定的。同样,您也不需要命中测试支持,因为职位是确定性的。您不需要容器支持,因为每个单元格都是一个简单的矩形(或图像)。您将不需要复杂的格式支持(例如透明度,圆角边框等),因为不会重叠。换句话说,每个单元格使用一个Grid(或UniformGrid)和一个WPF对象没有好处。 立即模式绘制到缓冲区位图的概念 为了获得所需的帧速率,实际上,您将绘制一个大的位图(覆盖整个屏幕)或一个“屏幕缓冲区”。对于您的单元,只需绘制到该位图/缓冲区(也许使用GDI)即可。命中测试很容易,因为单元格的位置都是确定性的。 因为只有一个对象(屏幕缓冲区位图),所以此方法很快。您可以刷新每一帧的整个位图,或者仅更新那些已更改的屏幕位置,或者这些位置的智能组合。 请注意,尽管您在此处绘制一个\“ grid \”,但是您并未使用\“ Grid \”元素。根据问题的约束条件而不是显而易见的解决方案来选择算法和数据结构,换句话说,“ Grid”可能不是绘制“ grid”的正确解决方案”。 WPF中的即时模式绘图 WPF基于DirectX,因此本质上它已经在后台使用了屏幕缓冲区位图(称为后缓冲区)。 在WFP中使用即时模式绘制的方式是将单元格创建为GeometryDrawing \(而不是Shape \),而是保留模式。 GemoetryDrawing通常非常快,因为GemoetryDrawing对象直接映射到DirectX基元。它们没有作为Framework Elements单独进行布局和跟踪,因此它们非常轻便-您可以在不影响性能的前提下拥有大量的元素。 选择GeometryDrawing \到DrawingImage中(本质上这是您的后缓冲区),您将获得屏幕快速变化的图像。在幕后,WPF完全按照您的期望进行操作-即将每个更改的矩形绘制到图像缓冲区上。 同样,不要使用Shape的-这些是Framework元素,因为它们参与布局会产生大量开销。例如,请勿使用Rectangle,而应使用RectangleGeometry。 最佳化 您可能会考虑其他几种优化: 重复使用GeometryDrawing对象-只需更改位置和大小 如果网格具有最大尺寸,请预先创建对象 仅修改那些已更改的GeometryDrawing对象-因此WPF不会不必要地刷新它们 在“舞台”中填充位图-也就是说,对于不同的缩放级别,请始终将其更新为比上一个更大的网格,并使用缩放比例将其缩放。例如,从10x10网格直接移动到20x20网格,但将其缩小55%以显示11x11正方形。这样,当从11x11一直缩放到20x20时,您的GeometryDrawing对象永远不会改变。仅更改位图上的缩放比例,使其更新速度非常快。 编辑:逐帧渲染 答案中建议的覆盖
OnRender
奖励了该问题。然后,您基本上将整个场景绘制在画布上。 使用DirectX进行绝对控制 另外,如果您想对每个帧进行绝对控制,请考虑使用原始DirectX。     
我认为您将很难处理这么多元素,如果仅能看到少量元素,此处的Virtualizing Canvas控件可能会有所帮助,但这只会有助于滚动。要同时显示这么多的单元格,您可能必须以一种或另一种方式绘制到位图。 这是一个示例,其中平铺单元格的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]));
    }
}
    
您可以编写自己的自定义控件(基于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方法。     
通过继续使用“ 11”方法,似乎可以快速绘制网格线,就可以忽略所有空的正方形,并根据操作的密度来大幅度减少屏幕上的元素总数。无论如何,要快速绘制网格线,您可以使用
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>
结果如下:     
如果您希望像元大小相同,我认为UniformGrid可能是最合适的。这样,您就不必担心在代码中设置大小。 如果实施,我会对结果非常感兴趣。     
我建议您为此编写一个自定义面板,编写此方法很简单,因为您只需要重写MeasureOverride和ArrangeOverride方法即可。基于行数/列数您可以为每个单元格分配可用大小。这应该比网格提供更好的性能,如果您想进一步优化它,也可以在面板中实现虚拟化。 当我不得不创建一个必须显示一些文本信息而不是图像并且行数/列数有所变化的滚动矩阵时,我就是这样做的。这是一个如何编写自定义面板的示例 http://blogs.msdn.com/b/dancre/archive/2005/10/02/476328.aspx 让我知道您是否要与您分享我编写的代码。     
在这里做一些猜测: 使用画布方法。 在画布上禁用点击测试以 防止鼠标悬停的CPU发疯。 分别跟踪您的更改 用户界面。仅更改填充 具有以下元素的属性 自上次更新以来已更改。我是 猜测更新缓慢 由于更新了数千个用户界面 元素及后续 重新渲染一切。     

要回复问题请先登录注册