Генерация Canvas через код в WPF

Создаю игру Альтернативные Крестики-нолики (81 игровая ячейка) на WPF, столкнулся с проблемой генерации ячеек через Canvas на игровом поле, генерирую их через код. Чтобы разместить ячейки внутри мини-Canvas наследовал свой класс SmallGameArea от Canvas, и добавил поле Cells, но их заполнить не удалось, т.к. возникает исключение Null Reference при создании ячеек внутри поля

Просьба подсказать решение данной проблемы, потому что вариант у меня только костыльный, т.к. мне нужно отталкиваться от размерности мини-поля, придется добавить еще одну ссылку на SmallGameArea со значениями Width и Height вот так:

this._smallGameArea =
                        new SmallGameArea
                        {                            
                            Width = (this._bigAreaCanvas.Width / 3) - 3.0,
                            Height = (this._bigAreaCanvas.Height / 3) - 3.0                            
                            
                        };
                    this._smallGameArea =
                        new SmallGameArea
                        {
                            Name = "smallField_" + smallArea._coordinateX.ToString() + "_" + smallArea._coordinateY.ToString(),
                            Width = (this._bigAreaCanvas.Width / 3) - 3.0,
                            Height = (this._bigAreaCanvas.Height / 3) - 3.0,
                            Background = (Brush)bc.ConvertFrom("#a4ba8f"),
                            Cells = FillPlayingCells(this._smallGameArea)
                        };

Код MainWindow.xaml

<Grid x:Name="mainGrid" Width="800" Height="800" HorizontalAlignment="Center" VerticalAlignment="Center">
        <Canvas x:Name="_bigAreaCanvas" Width="800" Height="800" Margin="5">
        </Canvas>
    </Grid>

Код MainWindow.cs

public MainWindow()
        {
            InitializeComponent();

            FillPlayingArea();
        }

        private void FillPlayingArea()
        {
            // Задаем размерность игрового поля
            var size = 3;
            // Очищаем игровое поле от элементов
            this._bigAreaCanvas.Children.Clear();
            // Заполняем Backend данными
            BigArea bigArea = new BigArea();

            var bc = new BrushConverter();

            double bigAreaWidth = (this._bigAreaCanvas.Width / 3) - 3.0;
            double bigAreaHeight = (this._bigAreaCanvas.Height / 3) - 3.0;

            for (int i = 0; i < size; i++)
            {
                for (int j = 0; j < size; j++)
                {
                    var smallArea = ((SmallArea)bigArea.playField[i, j]);                   


                    this._smallGameArea =
                        new SmallGameArea
                        {
                            Name = "smallField_" + smallArea._coordinateX.ToString() + "_" + smallArea._coordinateY.ToString(),
                            Width = (this._bigAreaCanvas.Width / 3) - 3.0,
                            Height = (this._bigAreaCanvas.Height / 3) - 3.0,
                            Background = (Brush)bc.ConvertFrom("#a4ba8f"),
                            Cells = FillPlayingCells(this._smallGameArea)
                        };
                    
                    this._bigAreaCanvas.Children.Add(this._smallGameArea);
                    
                    // Размещаем элементы внутри основного Canvas
                    Canvas.SetLeft(this._smallGameArea, i * this._bigAreaCanvas.Width / size - 2.0);
                    Canvas.SetTop(this._smallGameArea, j * this._bigAreaCanvas.Height / size - 2.0);
                }
            }
        }
        
        private List<GameCell> FillPlayingCells (SmallGameArea smallGameArea) // object[,] playCells, double smallCanvasWidth, double smallCanvasHeight
        {
            List<GameCell> gameCells = new List<GameCell>();
            
            for (int i = 0; i < 3; i++)
            {
                for (int j = 0; j < 3; j++)
                {
                    var bc = new BrushConverter();
                    
                    gameCells.Add(
                        new GameCell
                        {
                            Name = "cell_" + i.ToString() + "_" + j.ToString(),
                            Width = smallGameArea.Width / 3 - 2.0,
                            Height = smallGameArea.Height / 3 - 2.0,
                            Background = (Brush)bc.ConvertFrom("#73a9ff"),
                            coordX = i,
                            coordY = j
                        }
                    );
                }
            }

                    return gameCells;
        }

Код класса SmallGameArea.cs

 public class SmallGameArea : Canvas
    {
        private List<GameCell> _cells;
        public List<GameCell> Cells 
        { 
            get { return _cells; } 
            set 
            {                
                _cells = value;                
               
                foreach (var cell in _cells)
                {
                    this.Children.Add(cell);
                    // Размещаем игровые ячейки внутри каждого мини-поля Canvas
                    Canvas.SetLeft(cell, cell.coordX * this.Height / 3 - 5.0);
                    Canvas.SetTop(cell, cell.coordY * this.Width / 3 - 5.0);                
                }

            }
        }

Ответы (1 шт):

Автор решения: aepot

Не так давно я писал Змейку, покажу простой пример реализации игрового поля с кнопками на базе того ответа.

Если нужны детали реализации, что как и почему и что такое MVVM и привязки - почитайте в Змейке, здесь же я приведу только код.

INotifyPropertyChanged и ICommand

Вспомогательные классы. Первый для привязок, второй для команд.

public class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

public class RelayCommand : ICommand
{
    private readonly Action<object> _execute;
    private readonly Func<object, bool> _canExecute;

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
    public void Execute(object parameter) => _execute(parameter);
}

Данные

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

Data.cs

public class Cell : NotifyPropertyChanged
{
    private CellState _state;

    public CellState State
    {
        get => _state;
        set
        {
            _state = value;
            OnPropertyChanged();
        }
    }
}

public enum CellState
{
    Empty,
    Cross,
    Zero
}

Основная логика приложения

Далее, реализую MainViewModel.

MainViewModel.cs

public class MainViewModel : NotifyPropertyChanged
{
    private List<List<Cell>> _arena;
    private ICommand _areaCommand;
    private ICommand _newGameCommand;

    private CellState _turn; // чей ход

    public List<List<Cell>> Arena // игровое поле
    {
        get => _arena;
        set
        {
            _arena = value;
            OnPropertyChanged();
        }
    }

    public ICommand NewGameCommand => _newGameCommand ??= new RelayCommand(parameter =>
    {
        NewGame();
    });

    public ICommand AreaCommand => _areaCommand ??= new RelayCommand(parameter =>
    {
        if (parameter is Cell cell)
        {
            cell.State = _turn;
            _turn = _turn == CellState.Cross ? CellState.Zero : CellState.Cross;
        }
    }, parameter => parameter is Cell cell && cell.State == CellState.Empty);

    private void NewGame()
    {
        _turn = CellState.Cross;
        int width = 8;
        int height = 8;
        List<List<Cell>> arena = new List<List<Cell>>();
        for (int i = 0; i < height; i++)
        {
            List<Cell> row = new List<Cell>();
            for (int j = 0; j < width; j++)
            {
                row.Add(new Cell());
            }
            arena.Add(row);
        }
        Arena = arena;
    }

    public MainViewModel()
    {
        NewGame();
    }
}

Гвоздь программы здесь - я вообще не работаю с координатами на поле, мне это не за чем. А работает это так - вы жмете на кнопку, а кнопка привязана к данным типа Cell, собственно это и есть экземпляр нужной ячейки, где совершен код. Зачем нам искать ее по коодинатам, если ее уже нашел юзер мышкой и прямо буквально ткнул меня носом в эту Cell. Я просто передаю ее в команду как CommandParameter, и готово. :)

Теперь прикручу вьюмодель к окну.

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MainViewModel();
    }
}

Интерфейс

Много общего со змейкой. Обратите внимание, я не использую картинки, а вместо этого использую векторную графику, то есть рисую Крестик или Нолик простыми контолами в шаблонах, а шаблоны переключаю с помощью триггеров, которые реагируют на CellState.

<Window x:Class="TicTacToeWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TicTacToeWPF"
        mc:Ignorable="d"
        Title="MainWindow" d:DataContext="{d:DesignInstance local:MainViewModel, IsDesignTimeCreatable=True}" 
        SizeToContent="WidthAndHeight" ResizeMode="CanMinimize" SnapsToDevicePixels="True">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal">
            <Button Margin="5" Padding="15,0" Command="{Binding NewGameCommand}" Content="New Game"/>
        </StackPanel>
        <ItemsControl Grid.Row="1" Margin="5" ItemsSource="{Binding Arena}" HorizontalAlignment="Center" VerticalAlignment="Center" Focusable="False">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <ItemsControl ItemsSource="{Binding}" Focusable="False">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <Button Width="38" Height="38" BorderThickness="1" Focusable="False" Command="{Binding DataContext.AreaCommand,RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}">
                                    <Button.Style>
                                        <Style TargetType="Button">
                                            <Setter Property="OverridesDefaultStyle" Value="True"/>
                                            <Setter Property="BorderBrush" Value="CadetBlue"/>
                                            <Setter Property="Background" Value="AliceBlue"/>
                                            <Setter Property="Template">
                                                <Setter.Value>
                                                    <ControlTemplate TargetType="Button">
                                                        <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}"/>
                                                    </ControlTemplate>
                                                </Setter.Value>
                                            </Setter>
                                            <Style.Triggers>
                                                <DataTrigger Binding="{Binding State}" Value="Cross">
                                                    <Setter Property="Template">
                                                        <Setter.Value>
                                                            <ControlTemplate TargetType="Button">
                                                                <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}">
                                                                    <Path Data="M5,5 L31,31 M5,31 L31,5" Stroke="Black" StrokeThickness="3"/>
                                                                </Border>
                                                            </ControlTemplate>
                                                        </Setter.Value>
                                                    </Setter>
                                                </DataTrigger>
                                                <DataTrigger Binding="{Binding State}" Value="Zero">
                                                    <Setter Property="Template">
                                                        <Setter.Value>
                                                            <ControlTemplate TargetType="Button">
                                                                <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}">
                                                                    <Ellipse Width="30" Height="30" StrokeThickness="3" Stroke="Black"/>
                                                                </Border>
                                                            </ControlTemplate>
                                                        </Setter.Value>
                                                    </Setter>
                                                </DataTrigger>
                                            </Style.Triggers>
                                        </Style>
                                    </Button.Style>
                                </Button>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <VirtualizingStackPanel Orientation="Horizontal"/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                    </ItemsControl>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</Window>

Суть решения в том, что дано пустое поле 8 на 8, юзер тыкает мышкой в пустую ячейку, и если там ничего нет, появляется крестик, следующий тык в другую ячейку делает нолик, потом снова крестик и т.д. Пока не заполнятся все ячейки. Кнопка New Game просто пересоздает игру.

Готово.

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

Дам подсказку, если вам нужно например "перечеркнуть" линию визуально, чтобы показать причину окончания игры, не перечеркивайте, вы можете в Cell добавить еще одно свойство, например public bool Winner {...}, а в разметке на этот Winner привязать цвет кисти для крестика и нолика, через триггер, например если true, то красный, если нет - черный.

Игровую логику, думаю, теперь вам не составит труда к этому прикрутить.

→ Ссылка