#c# #wpf #grid #drawing #2d
#c# #wpf #сетка #рисование #2d
Вопрос:
Я пытаюсь нарисовать сетку изображений / значков с помощью WPF. Размеры сетки могут варьироваться, но обычно они варьируются от 10×10 до 200×200. Пользователь должен иметь возможность нажимать на ячейки, и некоторым ячейкам потребуется обновлять (менять изображение) 10-20 раз в секунду. Сетка должна иметь возможность увеличиваться и уменьшаться во всех четырех направлениях, и она должна иметь возможность переключаться на другой «срез» 3D-структуры, которую она представляет. Моя цель — найти подходящий эффективный метод для рисования сетки с учетом этих требований.
Моя текущая реализация использует WPF Grid
. Я генерирую определения строк и столбцов во время выполнения и заполняю сетку Line
(для линий сетки) и Border
(для ячеек, поскольку в данный момент они просто включены / выключены) объектами в соответствующей строке / столбце. ( Line
Объекты простираются по всей длине.)
При расширении сетки (удерживая нажатой цифру 6) Я обнаружил, что он рисует слишком медленно, чтобы перерисовывать при каждой операции, поэтому я изменил его, чтобы просто добавить новый ColumnDefinition
, Line
и набор Border
объектов для каждого столбца роста. Это решило мою проблему с ростом, и аналогичную тактику можно было бы использовать и для ускорения сокращения. Для обновления отдельных ячеек в середине моделирования я мог бы просто сохранить ссылки на объекты cell и изменить отображаемое изображение. Даже переход на новый 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
подход, потому что думаю, что он может вызвать те же проблемы, с которыми я уже сталкивался. Я мог бы попробовать, как только исчерпаю еще несколько вариантов.
Комментарии:
1. Вы пробовали использовать IsHitTestVisible = false на холсте в вашем новом подходе, чтобы контролировать использование процессора при наведении курсора мыши?
2. Отображаете ли вы карту с динамическими уровнями масштабирования?
Ответ №1:
Ваш вопрос
Давайте перефразируем ваш вопрос. Это ограничения вашей проблемы:
- Вы хотите нарисовать сетку динамических размеров
- Каждая ячейка быстро включается / выключается
- Размеры сетки быстро меняются
- Имеется большое количество ячеек (т. е. размеры сетки не являются тривиальными)
- Вы хотите, чтобы все эти изменения происходили с высокой частотой кадров (например, 30 кадров в секунду)
- Расположение и компоновка сетки и ячеек детерминированы, просты и не очень интерактивны
Судя по этим ограничениям, вы можете сразу увидеть, что используете неправильный подход.
Требование: Быстрое обновление детерминированных позиций с небольшой интерактивностью
Высокая частота обновления кадров много изменений за кадр большое количество ячеек один объект WPF на ячейку = разрушитель.
Если у вас нет очень быстрого графического оборудования и очень быстрого процессора, ваша частота кадров всегда будет снижаться с увеличением размеров сетки.
То, что диктует ваша проблема, больше похоже на видеоигру или программу для рисования CAD с динамическим масштабированием. Это меньше похоже на обычное настольное приложение.
Немедленный режим против Рисование в сохраненном режиме
Другими словами, вы хотите рисовать в «немедленном режиме», а не в «сохраненном режиме» (WPF — это сохраненный режим). Это потому, что ваши ограничения не требуют больших функциональных возможностей, предоставляемых обработкой каждой ячейки как отдельного объекта WPF.
Например, вам не понадобится поддержка макета, потому что положение каждой ячейки является детерминированным. Вам не понадобится поддержка hit-тестирования, потому что, опять же, позиции детерминированы. Вам не понадобится поддержка контейнеров, потому что каждая ячейка представляет собой простой прямоугольник (или изображение). Вам не понадобится сложная поддержка форматирования (например, прозрачность, закругленные границы и т.д.), Потому что ничего не перекрывается. Другими словами, нет никакой пользы в использовании сетки (или UniformGrid) и одного объекта WPF на ячейку.
Концепция немедленного рисования в режиме буферного растрового изображения
Чтобы достичь требуемой частоты кадров, по сути, вы будете рисовать на большом растровом изображении (которое покрывает весь экран) — или «буфере экрана». Для ваших ячеек просто нарисуйте это растровое изображение / буфер (возможно, используя GDI). Проверка попадания проста, поскольку все позиции ячеек детерминированы.
Этот метод будет быстрым, потому что существует только один объект (растровое изображение буфера экрана). Вы можете либо обновить все растровое изображение для каждого кадра, либо обновить только те положения экрана, которые изменяются, или разумную комбинацию этих параметров.
Обратите внимание, что, хотя вы рисуете здесь «сетку», вы не используете элемент «Grid». Выбирайте свой алгоритм и свои структуры данных, основываясь на том, каковы ограничения вашей проблемы, а не на том, что это выглядит как очевидное решение — другими словами, «сетка» может быть неподходящим решением для рисования «сетки».
Рисование в немедленном режиме в WPF
WPF основан на DirectX, поэтому, по сути, он уже использует растровое изображение экранного буфера (называемое обратным буфером) за сценой.
Способ использования немедленного рисования в WFP заключается в создании ячеек в формате GeometryDrawing (не Shape, который сохраняется в режиме). GemoetryDrawing обычно выполняется чрезвычайно быстро, потому что объекты GemoetryDrawing отображаются непосредственно в примитивы DirectX; они не размещаются и не отслеживаются по отдельности как элементы фреймворка, поэтому они очень легкие — у вас может быть большое их количество без негативного влияния на производительность.
Выберите геометрические рисунки в DrawingImage (по сути, это ваш задний буфер), и вы получите быстро меняющееся изображение для вашего экрана. За сценой WPF делает именно то, что вы ожидаете от него — т. е. рисует каждый прямоугольник, который изменяется, в буфере изображения.
Опять же, не используйте формы — это элементы фреймворка, и они будут сопряжены со значительными накладными расходами при их участии в макете. Например, НЕ ИСПОЛЬЗУЙТЕ Rectangle, а используйте вместо этого RectangleGeometry.
Оптимизация
Вы можете рассмотреть еще несколько вариантов оптимизации:
- Повторно используйте объекты GeometryDrawing — просто измените положение и размер
- Если сетка имеет максимальный размер, предварительно создайте объекты
- Изменяйте только те объекты GeometryDrawing, которые изменились, чтобы WPF не обновлял их без необходимости
- Заполняйте растровое изображение «поэтапно», то есть для разных уровней масштабирования всегда обновляйте сетку, которая намного больше предыдущей, и используйте масштабирование, чтобы уменьшить ее. Например, перейдите от сетки 10×10 непосредственно к сетке 20×20, но уменьшите ее на 55%, чтобы отобразить квадраты 11×11. Таким образом, при масштабировании с 11×11 вплоть до 20×20 ваши объекты GeometryDrawing никогда не изменяются; изменяется только масштабирование растрового изображения, что делает его чрезвычайно быстрым для обновления.
РЕДАКТИРОВАТЬ: Выполнять покадровый рендеринг
Переопределение OnRender
, как предложено в ответе, присуждает награду за этот вопрос. Затем вы, по сути, рисуете всю сцену на холсте.
Используйте DirectX для абсолютного контроля
В качестве альтернативы рассмотрите возможность использования raw DirectX, если вы хотите получить абсолютный контроль над каждым кадром.
Комментарии:
1. Я думаю, что геометрия — это правильный путь здесь.
2. Хорошие советы! Стратегия масштабирования / изменения размера особенно хороша; я уже использую такой трюк для внутреннего представления сетки, поэтому мне следовало подумать использовать его и для рендеринга. Однако я немного запутался в том, как использовать геометрические объекты для ускорения рисования. Не приведет ли рисование нескольких тысяч
GeometryDrawing
объектов к значительному замедлению процесса?3. Ну, 40000 RectangleGeometry — это куча лучше, чем 40000 Rectangular (форма).
4. Геометрические объекты обычно достаточно легкие, чтобы вы не беспокоились. Однако наличие более чем 40000 из них все еще может отрицательно повлиять на частоту кадров. Поскольку в DirectX каждый прямоугольник действительно сопоставляется только двум треугольникам, если у вас достаточно быстрый процессор для обработки уведомлений об изменениях, то 80000 треугольников не должны быть слишком большой проблемой для современных графических процессоров.
5. Не могли бы вы привести пример кода для того, как на самом деле реализовать такой вид рисования?
Ответ №2:
Вы можете написать свой собственный пользовательский элемент управления (на основе 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;
}
}
Я могу успешно нарисовать и изменить размер сетки 400×400 с помощью этого на ноутбуке y (не на соревновательной машине …).
Есть более причудливые и лучшие способы сделать это (используя StreamGeometry в DrawingContext), но это, по крайней мере, хороший тестовый верстак.
Конечно, вам придется переопределить методы HitTestXXX.
Ответ №3:
Я думаю, вам будет сложно справиться с таким количеством элементов, если бы было видно только небольшое их количество, здесь мог бы помочь элемент управления виртуализацией Canvas, но это помогает только при прокрутке. Чтобы одновременно было видно столько ячеек, вам, вероятно, придется так или иначе рисовать на растровом изображении.
Вот пример, в котором VisualBrush ячейки разбивается на плитки, а затем каждая ячейка переключается с помощью маски непрозрачности. Приведенный ниже подход довольно прост, поскольку требуется всего один пиксель на ячейку; элементы могут быть любого размера, и вам не нужен сложный код, превращающий содержимое ячейки в растровое изображение.
В примере создается сетка размером 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]));
}
}
Ответ №4:
Продолжая 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>
что приводит к такому эффекту:
Ответ №5:
Если вы хотите, чтобы ячейки были одинакового размера, я думаю, что лучше всего подойдет UniformGrid. Таким образом, вам не придется беспокоиться о настройке размеров в коде.
Если вы реализуете, я был бы очень заинтересован в результатах.
Комментарии:
1. Не буду ли я по-прежнему сталкиваться с проблемами при тестировании попадания на отдельные ячейки? Мне также пришлось бы поддерживать массив ссылок на элементы управления ячейками, которые я (вероятно?) не понадобилось бы, если бы я использовал инструменты рисования, но это могло бы сделать некоторые операции (например, изменение размера) более длительными.
UniformGrid
Предлагает ли другие преимущества в производительности?2. Я думаю, что я бы подошел к этому по-другому, но я мало что знаю о том, что вы делаете. Я бы создал пользовательский элемент управления ListView и заменил контейнер элементов (обычно stackpanel) на UniformGrid. Тогда я мог бы привязать ListView к коллекции элементов, которые вы хотите представить. Используйте ItemTemplate, чтобы указать, как вы хотите, чтобы элементы выглядели.
3. Я обнаружил, что виртуализация контейнеров элементов довольно экономична в ресурсах. Без лучшего понимания вашей модели и времени, чтобы опробовать это в тесте, я не уверен, что будет делать сетка, содержащая до (и, возможно, более) 4000 ячеек с чем-то происходящим в них, с точки зрения ресурсов процессора.
Ответ №6:
Я предлагаю вам написать пользовательскую панель для этого, написание этого должно быть простым, поскольку вам просто нужно переопределить методы MeasureOverride и ArrangeOverride. На основе количества строк / столбцов Вы можете выделить доступный размер для каждой ячейки. Это должно обеспечить вам лучшую производительность, чем сетка, также, если вы хотите оптимизировать ее еще больше, вы также можете реализовать виртуализацию на панели.
Я сделал это таким образом, когда мне нужно было создать матрицу прокрутки, которая должна отображать некоторую текстовую информацию вместо изображений, и количество строк / столбцов менялось. Вот пример того, как написать пользовательскую панель
http://blogs.msdn.com/b/dancre/archive/2005/10/02/476328.aspx
Дайте мне знать, если хотите, чтобы я поделился с вами написанным мной кодом.
Ответ №7:
Собираюсь сделать несколько предположений здесь:
- Используйте подход Canvas.
- Отключите тестирование попадания на холст, чтобы процессор при наведении курсора мыши не сходил с ума.
- Отслеживайте свои изменения отдельно от пользовательского интерфейса. Изменяйте свойство Fill только для элементов, которые изменились с момента последнего обновления. Я предполагаю, что медленные обновления связаны с обновлением тысяч элементов пользовательского интерфейса и последующим повторным отображением всего.