Генерация 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 шт):
Не так давно я писал Змейку, покажу простой пример реализации игрового поля с кнопками на базе того ответа.
Если нужны детали реализации, что как и почему и что такое 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, то красный, если нет - черный.
Игровую логику, думаю, теперь вам не составит труда к этому прикрутить.
