Рисование сетки изображений с помощью WPF - PullRequest
7 голосов
/ 14 апреля 2011

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

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

Current grid implementation

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

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

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

Мысли?

Обновление 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 ]

17 голосов
/ 26 апреля 2011

Ваш вопрос

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

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

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

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

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

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

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

Немедленный режим против рисования в режиме удержания

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

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

Концепция рисования в непосредственном режиме в буферную битовую карту

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

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

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

Рисование в непосредственном режиме в WPF

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

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

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

Опять же, не используйте Shape's - это элементы Framework, и будет накладывать значительные накладные расходы, так как они участвуют в макете.Например, НЕ ИСПОЛЬЗУЙТЕ Rectangle , но вместо этого используйте RectangleGeometry .

Оптимизации

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

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

РЕДАКТИРОВАТЬ: делать покадровый рендеринг

Переопределить OnRender, как указано в ответе, присуждаемом за вознаграждениена этот вопрос.Затем вы фактически рисуете всю сцену на холсте.

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

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

3 голосов
/ 27 апреля 2011

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

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

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

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

3 голосов
/ 26 апреля 2011

Я думаю, вам будет трудно справиться с таким количеством элементов. Если бы было видно только небольшое число, элемент управления Virtualization 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]));
    }
}
2 голосов
/ 24 апреля 2011

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

1 голос
/ 14 апреля 2011

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

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

0 голосов
/ 25 апреля 2011

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

  1. Использовать подход Canvas.
  2. Отключить проверку попаданий на Canvas, чтобы не дать сойти с ума при наведении курсора на процессор.* Отслеживайте свои изменения отдельно от пользовательского интерфейса.Изменяйте свойство Fill только для элементов, которые изменились с момента последнего обновления.Я предполагаю, что медленные обновления происходят из-за обновления тысяч элементов пользовательского интерфейса и последующего повторного рендеринга всего.
0 голосов
/ 25 апреля 2011

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

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

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

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

...