Оптимизация сетки, состоящей из ячеек в WPF для кратчайшего пути


В настоящее время я пытаюсь создать сетку, состоящую из объектов ячеек в WPF. Мне нужно привязать ячейки к объектам, которые должны быть в 2D массиве. - А мне нужно, чтобы он был большим, масштабируемым и менял цвет ячеек и сохранял данные в объектах!

У меня есть реализация, но, кажется, очень медленно рисовать сетку! (Сетка 100x100 занимает >10 секунд!) Вот картина того, что я уже сделал:

Введите описание изображения здесь

Я использую привязку данных в XAML в ItemsControl. Вот мой XAML:

<ItemsControl x:Name="GridArea" ItemsSource="{Binding Cellz}" Grid.Column="1" BorderBrush="Black" BorderThickness="0.1">
        <ItemsControl.Resources>
            <DataTemplate DataType="{x:Type local:Cell}">
                <Border BorderBrush="Black" BorderThickness="0.1">
                    <Grid Background="{Binding CellColor}">
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="MouseMove" >
                                <ei:CallMethodAction TargetObject="{Binding}" MethodName="MouseHoveredOver"/>
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </Grid>
                </Border>

            </DataTemplate>
        </ItemsControl.Resources>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <UniformGrid Rows="{Binding Rows}" Columns="{Binding Columns}" MouseDown="WrapPanelMouseDown" MouseUp="WrapPanelMouseUp" MouseLeave="WrapPanelMouseLeave" >
                    <!--<UniformGrid.Background>
                        <ImageBrush/>
                    </UniformGrid.Background>-->
                </UniformGrid>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>

В своем codebehind я создаю экземпляр объекта класса Grid,который создает 2D-массив (и список, к которому привязывается im) с объектами класса Cell. После некоторой проверки с секундомером, я вижу, что это не занимает много времени. Это фактическая привязка и рисование сеток, поэтому я предполагаю, что моя оптимизация должна происходить в моем XAML, если какая-либо оптимизация доступна.

Но чтобы обеспечить все, вот мой код позади и класс сетки и ячейка класс:

    public MainWindow()
    {
        InitializeComponent();  

        NewGrid = new Grid(75, 75);
        DataContext = NewGrid;

    }

    public class Grid
    {
    public int Columns { get; set; }
    public int Rows { get; set; }

    public ObservableCollection<Cell> Cellz {get;set;}

    public Cell[,] CellArray { get; set; }

    public Grid(int columns, int rows)
    {
        Columns = columns;
        Rows = rows;

        Cellz = new ObservableCollection<Cell>();
        CellArray = new Cell[Rows,Columns];
        InitializeGrid();

    }

    public void InitializeGrid()
    {
        Color col = Colors.Transparent;
        SolidColorBrush Trans = new SolidColorBrush(col);
        for (int i = 0; i < Rows; i++)
        {
            for (int j = 0; j < Columns; j++)
            {
                var brandNewCell = new Cell(i, j) { CellColor = Trans};
                Cellz.Add(brandNewCell);
                CellArray[i, j] = brandNewCell;
            }
        }
    }

    public class Cell : INotifyPropertyChanged
    {
        public int x, y;   // x,y location
        public Boolean IsWall { get; set; }

        private SolidColorBrush _cellcolor;
        public SolidColorBrush CellColor
        {
            get { return _cellcolor; }
            set
            {
                _cellcolor = value;
                OnPropertyChanged();
            }
        }
        public Cell(int tempX, int tempY)
        {
            x = tempX;
            y = tempY;
        }


        public bool IsWalkable(Object unused)
        {
            return !IsWall;
        }
    public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(
            [CallerMemberName] string caller = "")
        {
            if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(caller));
        }
    }
}

Мне нравится довольно простая реализация с привязкой, однако время загрузки действительно неприемлемо - любой совет был бы очень признателен!

2 2

2 ответа:

Ну, я воссоздал ваш пример, с некоторыми изменениями. Я в основном избавился от привязок к DataContext и создал viewmodel специально для вашего случая использования, который привязывается непосредственно к itemscontrol.

Скорость рисования определенно меньше 10 секунд, но я думал, что дал вам как можно больше соответствующего кода, чтобы вы могли сравнить решения...
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using TestSO.model;

namespace TestSO.viewmodel
{
    public class ScreenViewModel : INotifyPropertyChanged, IDisposable
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private IList<Cell> cells;
        public IList<Cell> Cells
        {
            get
            {
                return cells;
            }
            set
            {
                if (object.Equals(cells, value))
                {
                    return;
                }
                UnregisterSource(cells);
                cells = value;
                RegisterSource(cells);
                RaisePropertyChanged("Cells");
            }
        }

        private int rows;
        public int Rows
        {
            get
            {
                return rows;
            }
            set
            {
                if (rows == value)
                {
                    return;
                }
                rows = value;
                RaisePropertyChanged("Rows");
            }
        }

        private int columns;
        public int Columns
        {
            get
            {
                return columns;
            }
            set
            {
                if (columns == value)
                {
                    return;
                }
                columns = value;
                RaisePropertyChanged("Columns");
            }
        }

        private Cell[,] array;
        public Cell[,] Array
        {
            get
            {
                return array;
            }
            protected set
            {
                array = value;
            }
        }

        protected void RaisePropertyChanged(string propertyName)
        {
            var local = PropertyChanged;
            if (local != null)
            {
                App.Current.Dispatcher.BeginInvoke(local, this, new PropertyChangedEventArgs(propertyName));
            }
        }

        protected void RegisterSource(IList<Cell> collection)
        {
            if (collection == null)
            {
                return;
            }
            var colc = collection as INotifyCollectionChanged;
            if (colc != null)
            {
                colc.CollectionChanged += OnCellCollectionChanged;
            }
            OnCellCollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, collection, null));
        }

        protected virtual void OnCellCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Reset)
            {
                Array = null;
            }
            if (e.OldItems != null)
            {
                foreach (var item in e.OldItems)
                {
                    var cell = item as Cell;
                    if (cell == null)
                    {
                        continue;
                    }
                    if (Array == null)
                    {
                        continue;
                    }
                    Array[cell.X, cell.Y] = null;
                }
            }
            if (e.NewItems != null)
            {
                if (Array == null)
                {
                    Array = new Cell[Rows, Columns];
                }
                foreach (var item in e.NewItems)
                {
                    var cell = item as Cell;
                    if (cell == null)
                    {
                        continue;
                    }
                    if (Array == null)
                    {
                        continue;
                    }
                    Array[cell.X, cell.Y] = cell;
                }
            }
        }

        protected void UnregisterSource(IList<Cell> collection)
        {
            if (collection == null)
            {
                return;
            }
            var colc = collection as INotifyCollectionChanged;
            if (colc != null)
            {
                colc.CollectionChanged -= OnCellCollectionChanged;
            }
            OnCellCollectionChanged(collection, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }

        public ScreenViewModel()
        {
        }

        public ScreenViewModel(int row, int col)
            : this()
        {
            this.Rows = row;
            this.Columns = col;
        }

        bool isDisposed = false;
        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (isDisposed)
                {
                    return;
                }
                isDisposed = true;
                Cells = null;
            }
        }

        public void Dispose()
        {
            Dispose(true);
        }
    }
}

И я создал как дополнительный, контроллер, который является владельцем ObservableCollection, main цель состояла бы в том, чтобы не делать никаких изменений на viewModel, а скорее изменить коллекцию внутри контроллера (или добавить add, remove, clear методы к нему), и позволить цепи событий делать работу за меня, сохраняя 2-мерный массив в актуальном состоянии в ScreenViewModel

using System.Collections.Generic;
using System.Collections.ObjectModel;
using TestSO.model;

namespace TestSO.controller
{
    public class GenericController<T>
    {
        private readonly IList<T> collection = new ObservableCollection<T>();
        public IList<T> Collection
        {
            get
            {
                return collection;
            }
        }

        public GenericController()
        {
        }
    }

    public class CellGridController : GenericController<Cell>
    {
        public CellGridController()
        {
        }
    }
}

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

using System.ComponentModel;
using System.Windows;
using System.Windows.Media;

namespace TestSO.model
{
    public class Cell : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void RaisePropertyChanged(string propertyName)
        {
            var local = PropertyChanged;
            if (local != null)
            {
                Application.Current.Dispatcher.BeginInvoke(local, this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private int x;
        public int X
        {
            get
            {
                return x;
            }
            set
            {
                if (x == value)
                {
                    return;
                }
                x = value;
                RaisePropertyChanged("X");
            }
        }

        private int y;
        public int Y
        {
            get
            {
                return y;
            }
            set
            {
                if (y == value)
                {
                    return;
                }
                y = value;
                RaisePropertyChanged("Y");
            }
        }

        private bool isWall;
        public bool IsWall
        {
            get
            {
                return isWall;
            }
            set
            {
                if (isWall == value)
                {
                    return;
                }
                isWall = value;
                RaisePropertyChanged("IsWall");
            }
        }

        private SolidColorBrush _cellColor;
        public SolidColorBrush CellColor
        {
            get
            {
                // either return the _cellColor, or say that it is transparent
                return _cellColor ?? Brushes.Transparent;
            }
            set
            {
                if (SolidColorBrush.Equals(_cellColor, value))
                {
                    return;
                }
                _cellColor = value;
                RaisePropertyChanged("CellColor");
            }
        }

        public Cell()
        {
        }

        public Cell(int x, int y)
            : this()
        {
            this.X = x;
            this.Y = y;
        }
    }
}

Затем я немного изменил xaml (хотя и не взял на себя точки взаимодействия), по создание ресурсов для ScreenViewModel, контроллер, и класс DataTemplate, этот шаблон, затем шаблон данных DataTemplate также напрямую добавляется в элемент управления ItemsControl за свойства itemtemplate, используя вместо функции шаблон данных DataTemplate (не вижу, как выше требование?)

<Window x:Class="TestSO.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:model="clr-namespace:TestSO.model"
        xmlns:viewmodel="clr-namespace:TestSO.viewmodel"
        xmlns:controller="clr-namespace:TestSO.controller"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <controller:CellGridController x:Key="CellController" />
        <viewmodel:ScreenViewModel x:Key="GridViewModel" Rows="75" Columns="75" />
        <DataTemplate x:Key="CellTemplate">
            <Border BorderBrush="Black" BorderThickness="0.5">
                <Grid Background="{Binding CellColor}">
                </Grid>
            </Border>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <ItemsControl ItemsSource="{Binding Cells,Source={StaticResource GridViewModel}}" BorderBrush="Black" BorderThickness="0.1" ItemTemplate="{StaticResource CellTemplate}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <UniformGrid
                        Rows="{Binding Rows,Source={StaticResource GridViewModel}}" 
                        Columns="{Binding Columns,Source={StaticResource GridViewModel}}" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>
    </Grid>
</Window>

И внутри главного.cs страница, которую я загрузил я связал коллекцию контроллера с ScreenViewModel.Свойство Cells, и загружены некоторые данные шаблона. Просто довольно основные фиктивные данные (вы также можете прикрепить screenmodel к DataContext и определить контроллер где-то еще, и изменить привязки в xaml, чтобы вернуться к DataContext, однако через ресурсы, вы также можете получить к уже созданным экземплярам (после initializeComponent)

protected ScreenViewModel ScreenViewModel
{
    get
    {
        return this.Resources["GridViewModel"] as ScreenViewModel;
    }
}

protected CellGridController Controller
{
    get
    {
        return this.Resources["CellController"] as CellGridController;
    }
}

protected void Load()
{
    var controller = Controller;
    controller.Collection.Clear();
    string[] rows = colorToCellSource.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
    string row;
    for (int x = 0; x < rows.Length; x++)
    {
        int length = rows[x].Length;
        ScreenViewModel.Rows = rows.Length;
        ScreenViewModel.Columns = length;
        row = rows[x];
        for (int y = 0; y < length; y++)
        {
            Cell cell = new Cell(x, y);
            cell.CellColor = row[y] == '0' ? Brushes.Transparent : Brushes.Blue;
            controller.Collection.Add(cell);
        }
    }
}

public MainWindow()
{
    InitializeComponent();
    if (Controller != null && ScreenViewModel != null)
    {
        ScreenViewModel.Cells = Controller.Collection;
        Load();
    }
}

Экран занимает перерисовывается здесь менее 1 секунды, изменение размера и максимизация занимает небольшую задержку, но я думаю, что этого можно ожидать... (Мой тестовый шаблон был 105x107)

Отладочный тест

Похоже, что задержка вызвана разрешением шаблона и отрисовкой такого количества элементов.

Некоторые предложения по его улучшению:

  • не храните пустые ячейки. Храните только ячейки с "данными" в них.
  • привязка с использованием статического ItemTemplate вместо разрешения типа динамически
  • попробуйте упростить вашу ItemTemplate

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

XAML будет выглядеть примерно так

<Grid>
    <UniformGrid Rows="{Binding RowCount}" Columns="{Binding ColumnCount}" />
    <ItemsControl ItemsSource="{Binding Cells}" .... />
</Grid>

Где ItemsControl использует Canvas для ItemContainerTemplate и связывает Canvas.Top и Canvas.Left с данными ячейки X, Y в ItemContainerStyle, вероятно, используя какой-то преобразователь,чтобы умножить значение X, Y на размер ячейки сетки.

Чтобы показать линии сетки, вы можете либо согласиться с ShowGridLines=True, чтобы получить слабые пунктирные линии, либо использовать настроенную сетку, такую как эта , что перезаписывает OnRender для рисования линий сетки. Это также поможет уменьшить количество объектов в вашей ItemTemplate для 3-го пункта, так как теперь вам не нужен объект <Border>.

Я также не совсем уверен, как вы хотите взаимодействовать с вашей сеткой, но это может помочь с начальным временем загрузки поместить все обработчики мыши на саму фоновую сетку,а не на каждый отдельный элемент, и просто вычислить элемент под мышью по позиции X, Y. Вероятно, вам придется обеспечить IsHitTestVisible="False" устанавливается в ItemContainerStyle для этого.


Для второго пункта (и как указал Icepickle), использование статического шаблона вместо динамического, основанного на типе объекта, определенно поможет.

Таким образом, вместо использования неявного шаблона, Дайте вашему шаблону свойство x:Key и свяжите его с помощью статической привязки
<DataTemplate x:Key="CellTemplate" DataType="{x:Type local:Cell}">
    <Border BorderBrush="Black" BorderThickness="0.1">
        <Grid Background="{Binding CellColor}">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="MouseMove" >
                    <ei:CallMethodAction TargetObject="{Binding}" MethodName="MouseHoveredOver"/>
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Grid>
    </Border>
</DataTemplate>

<ItemsControl ItemTemplate="{StaticResource CellTemplate}" ... />