Рисование сетки изображений с помощью WPF

я пытаюсь нарисовать сетку изображений / значков с помощью WPF. Размеры сетки будут варьироваться, но обычно будут варьироваться от 10x10 до 200x200. Пользователь должен иметь возможность нажимать на ячейки, а некоторые ячейки нужно будет обновлять (менять изображение) 10-20 раз в секунду. Сетка должна иметь возможность расти и сжиматься во всех четырех направлениях, и она должна иметь возможность переключаться на другой "срез" 3D-структуры, которую она представляет. Моя цель-найти подходящий эффективный метод для рисования сетки, учитывая те требования.

моя текущая реализация использует WPF Grid. Я генерирую определения строк и столбцов во время выполнения и заполняю сетку Line (для сетки) и Border (для ячеек, так как в настоящее время они просто включены/выключены) объекты в соответствующей строке/столбце. (The Line объекты охватывают весь путь поперек.)

Current grid implementation

при расширении сетки (удерживая Num6) я обнаружил, что она рисует слишком медленно, чтобы перерисовывать каждую операцию, поэтому я изменил его, чтобы просто добавить новый ColumnDefinition, Line и Border предметы для каждого столбца роста. Это решило мою проблему роста, и аналогичная тактика может быть использована для быстрого сокращения. Для обновления отдельных ячеек в середине моделирования я мог бы просто хранить ссылки на объекты ячеек и изменять отображаемое изображение. Даже переход на новый Z-уровень может быть улучшен только путем обновления содержимого ячейки вместо перестройки всей сетки.

однако, прежде чем я мог сделать во всех этих оптимизациях я столкнулся с другой проблемой. Всякий раз, когда я наведу курсор мыши на сетку (даже на медленных/нормальных скоростях) всплески использования процессора приложения. Я удалил все обработчики событий из дочерних элементов сетки, но это не имело никакого эффекта. Наконец, единственный способ контролировать использование ЦП - установить IsHitTestVisible = false на Grid. (Настройка для каждого дочернего элемента Grid ничего не сделал!)

я считаю, что использование отдельных элементов управления для создания моей сетки слишком интенсивно и не подходит для этого приложения, и что использование 2D-механизмов рисования WPF может быть более эффективным. Я новичок в WPF, поэтому я ищу совет о том, как лучше всего этого достичь. Из того немногого, что я прочитал, я мог бы использовать DrawingGroup составить изображение каждой клетки совместно на одиночное изображение для дисплея. Тогда я мог бы использовать обработчик события click для всего изображения и вычислить координаты выбранной ячейки расположения мыши. Это кажется грязным, хотя, и я просто не знаю, есть ли лучший способ.

мысли?

обновление 1:

я последовал совету друга и переключился на использование Canvas С Rectangle для каждой ячейки. Когда я впервые рисую сетку, я храню ссылки на все 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;
        }
    }
}

рисование сетки изначально кажется быстрее, а последующие обновления определенно быстрее, но есть еще несколько проблем.

  • независимо от того, насколько мала область, над которой я наведу курсор мыши, использование процессора по-прежнему всплески всякий раз, когда я наведу курсор мыши на сетку, когда она имеет более нескольких сотен ячеек.

  • обновления все еще слишком медленные, поэтому, когда я удерживаю клавишу со стрелкой вверх, чтобы изменить Z-уровень (общий случай использования), программа зависает на несколько секунд за раз, а затем, кажется, прыгает 50 Z-уровней сразу.

  • как только сетка удерживается ~5000 ячеек, обновления занимают порядка одной секунды. Это запредельно медленно, и 5000 клеток приспосабливает внутри типичные случаи пользы.

я еще не пробовал UniformGrid подход, потому что я думаю, что он может проявлять те же проблемы, с которыми я уже сталкивался. Я мог бы попробовать, как только исчерпаю еще несколько вариантов.

7 ответов


Ваш Вопрос

давайте перефразируем ваш вопрос. Это ваши проблемы ограничения:

  1. вы хотите нарисовать сетку динамических размеров
  2. каждая ячейка быстро меняется вкл/выкл
  3. размеры сетки быстро меняться
  4. существует большое количество ячеек (т. е. размеры сетки не тривиально)
  5. вы хотите, чтобы все эти изменения произошли с быстрой частотой кадров (например, 30fps)
  6. расположение и планировка сетка и ячейки детерминированы, просты и не очень интерактивны

судя по этим ограничениям, вы сразу видите, что используете неправильный подход.

Reqruiement: быстрое обновление детерминированных позиций с небольшой интерактивностью

быстрое обновление частоты кадров + много изменений на кадр + большое количество ячеек + один объект WPF на ячейку = dissaster.

Если у вас нет очень быстрого графического оборудования и очень быстрого процессора, ваш частота кадров всегда будет страдать от увеличения размеров сетки.

то, что ваша проблема диктует, больше похоже на видеоигру или программу рисования САПР с динамическим масштабированием. Это lesss как обычное настольное приложение.

немедленный режим против чертежа сохраненного режима

другими словами, вы хотите чертеж "немедленного режима", а не чертеж" сохраненного режима " (WPF сохранен режим). Это потому, что ваши ограничения не требуют много функций предоставляется путем обработки каждой ячейки как отдельного объекта WPF.

например, вам не понадобится поддержка макета, потому что позиция каждой ячейки детерминирована. Вам не понадобится поддержка тестирования попадания, потому что, опять же, позиции детерминированы. Вам не понадобится поддержка контейнера, потому что каждая ячейка представляет собой простой прямоугольник (или изображение). Вам не понадобится сложная поддержка форматирования (например, прозрачность, округленные границы и т. д. потому что ничего не пересекается. Другими словами, нет никакой пользы использовать сетку (или UniformGrid) и один объект WPF на ячейку.

концепция немедленного режима рисования в буфер растрового изображения

чтобы достичь требуемой частоты кадров, по существу, вы будете рисовать на большом растровом изображении (которое охватывает весь экран) - или "буфер экрана". Для ваших ячеек просто нарисуйте это растровое изображение / буфер (возможно, используя GDI). Хит-тестирование легко, так как все позиции ячеек детерминированы.

этот метод будет быстрее, потому что есть только один объект (растровое изображение буфера экрана). Вы можете обновить все растровое изображение для каждого кадра, или обновить только те позиции экрана, которые изменяются, или их интеллектуальную комбинацию.

обратите внимание, что, хотя вы рисуете "сетку" здесь, вы не используете элемент "сетка". Выберите свой алгоритм и структуры данных на основе того, что ваши ограничения проблемы, а не то, что это выглядит хотелось бы быть очевидным решением - другими словами, "сетка" может быть неправильным решением для рисования "сетки".

немедленный режим рисования в WPF

WPF основан на DirectX, поэтому по существу он уже использует растровое изображение буфера экрана (называемое back-buffer) за сценой.

способ использования немедленного режима рисования в WFP заключается в создании ячеек как GeometryDrawing (а не фигуры, которая сохраняется в режиме). GemoetryDrawing обычно очень быстро, потому что объекты GemoetryDrawing сопоставляются непосредственно с примитивами DirectX; они не выложены и отслеживаются индивидуально как элементы каркаса, поэтому они очень легкие - вы можете иметь их большое количество, не влияя на производительность.

выберите THS GeometryDrawing в DrawingImage (это, по сути, ваш задний буфер), и вы получите быстро меняющееся изображение для вашего экрана. За сценой WPF делает именно то, что вы ожидаете, то есть рисует каждый прямоугольник, который изменяется в буфер изображения.

опять же, не используйте Shape - это Элементы фреймворка и будет несут значительные накладные расходы, поскольку они участвуют в макете. Например, не использовать прямоугольник, но и использовать RectangleGeometry вместо.

оптимизация

еще несколько оптимизаций, которые вы можете рассмотреть:

  1. повторное использование объектов GeometryDrawing-просто измените положение и размер
  2. если сетка имеет максимальный размер, предварительно создать объекты
  3. изменить только те объекты GeometryDrawing, которые изменились - поэтому WPF не будет излишне обновлять их
  4. заполните растровое изображение в "этапах" - то есть для разных уровней масштабирования всегда обновляйте сетку, которая намного больше предыдущей, и используйте масштабирование для ее масштабирования. Например, переместитесь из сетки 10x10 непосредственно в сетку 20x20, но уменьшите ее на 55%, чтобы показать квадраты 11x11. Таким образом, при масштабировании с 11x11 до 20x20 ваши объекты GeometryDrawing никогда не изменяются; только масштабирование растрового изображения изменяется, что делает его чрезвычайно быстро обновлять.

EDIT: сделать кадр за кадром рендеринга

переопределить OnRender Как было предложено в ответе, присуждается премия за этот вопрос. Затем вы, по сути, рисуете всю сцену на холсте.

используйте DirectX для абсолютного контроля

кроме того, рассмотрите возможность использования raw DirectX, если вы хотите абсолютный контроль над каждым кадром.


вы можете написать собственный пользовательский элемент управления (на основе холста, панели и т. д...) и переопределить 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;
        }
    }

Я могу успешно рисовать и изменять размер сетки 400x400 с помощью этого на ноутбуке y (а не на соревновательной машине...).

есть более причудливые и лучшие способы сделать это (используя StreamGeometry на DrawingContext), но это, по крайней мере, хороший тестовый верстак.

конечно, вам придется переопределить методы HitTestXXX.


продолжая с 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


Я думаю, вам будет трудно иметь дело с таким количеством элементов, если только небольшое число было видно Виртуализирующее управление Canvas здесь может помочь,но это помогает только при прокрутке. Чтобы иметь столько ячеек, видимых одновременно, вам, вероятно, придется так или иначе рисовать растровое изображение.

вот пример, когда VisualBrush ячейки выложен плиткой, а затем каждая ячейка переключается с помощью OpacityMask. Подход ниже довольно аккуратный, как только один нужен пиксель в клетке; элементы могут быть любого размера и не требует сложного кода блитирование содержимое ячейки в растровое изображение.

образец создает сетку 1000 * 1000, и есть 3 типа ячеек, если вам нужно только два, код может быть упрощен дальше, и многие петли удалены. Обновления были быстрыми (3 мс для 200*200, 100 мс для 1k*1k), прокрутка работает так, как ожидалось, и добавление масштабирования не должно быть слишком сложным.

<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]));
    }
}

Если вы хотите, чтобы ячейки были одинакового размера, я думаю, что UniformGrid может лучше всего подойти. Таким образом, вам не придется беспокоиться о настройке размеров в коде.

Если вы реализуете, я был бы очень заинтересован в результатах.


Я предлагаю вам написать пользовательскую панель для этого, написание этого может быть простым, так как вам просто нужно переопределить методы MeasureOverride и ArrangeOverride. На основе no строк / столбцов вы можете выделить доступный размер для каждой ячейки. Это должно дать вам лучшую производительность, чем сетка, также если вы хотите оптимизировать ее еще больше, вы также можете реализовать виртуализацию на панели.

Я сделал это таким образом, когда мне пришлось создать матрицу рулона, которая должна отображать некоторый текст информация вместо изображений и количество строк/столбцов менялись. Вот пример того, как написать пользовательскую панель

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

Дайте мне знать, если вы хотите, чтобы я поделился кодом, который я написал с вами.


собираюсь сделать несколько предположений:

  1. используйте подход холста.
  2. отключить тестирование хитов на холсте, чтобы держите mouseover CPU от сойти с ума.
  3. отслеживание изменений отдельно от пользовательский интерфейс. Только изменить заливку свойство для элементов, которые имеют изменены с момента последнего обновления. Я предполагая, что медленные обновления из-за обновления тысяч UI элементы и последующие перерисовка всего.