Вопрос. Как можно реализовать визуализацию гексагональной сетки на WPF
, при том что каждая клеточка этой сетки является отдельным объектом, допустим может иметь другой цвет.
На ум приходят только канвасы. Описать поле на котором будут закрепляться по координатам ячейки. Как нарисовать саму ячейку даже не знаю.
Проще всего применить ItemsControl
/Canvas
, и задавать форму через Path
.
Я взял за основу вот этот код: Игра на WPF, переносим код WinForms в MVVM.
Для начала, VM-часть. Базовый класс VM стандартный:
class VM : INotifyPropertyChanged
{
protected bool Set<T>(ref T field, T value, [CallerMemberName]string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
NotifyPropertyChanged(propertyName);
return true;
}
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public event PropertyChangedEventHandler PropertyChanged;
}
Несколько вспомогательных структур:
struct FieldSize
{
public int Width { get; }
public int Height { get; }
public FieldSize(int w, int h) { Width = w; Height = h; }
}
struct FieldPosition
{
public int X { get; }
public int Y { get; }
public FieldPosition(int x, int y) { X = x; Y = y; }
}
Они представляют собой, понятно, размер поля и позицию на поле.
Теперь VM одной клетки. Тут всё очевидно:
class CellVM : VM
{
public CellVM(int row, int column)
{
Position = new FieldPosition(row, column);
Activate = new RelayCommand(OnActivate);
}
public FieldPosition Position { get; }
public ICommand Activate { get; }
bool isActive = false;
public bool IsActive
{
get { return isActive; }
private set { Set(ref isActive, value); }
}
void OnActivate()
{
IsActive = !IsActive;
}
}
Единственное поле, которое может меняться — IsActive
, поэтому его изменение отправляет NotifyPropertyChanged
. Команда Activate
будет вызываться на клике.
Теперь всё поле. На изменении размера поля генерируем клетки. Покамест клетки представлены в виде списка, если вам они нужны как двумерный массив, добавьте сохранение в массив в GenerateCells
.
class BoardVM : VM
{
FieldSize fieldSize;
public FieldSize FieldSize
{
get { return fieldSize; }
set { if (Set(ref fieldSize, value)) GenerateCells(); }
}
IEnumerable<CellVM> cells;
public IEnumerable<CellVM> Cells
{
get { return cells; }
private set { Set(ref cells, value); }
}
void GenerateCells()
{
var list = new List<CellVM>(FieldSize.Width * FieldSize.Height);
for (int j = 0; j < FieldSize.Height; j++)
for (int i = 0; i < FieldSize.Width; i++)
list.Add(new HexCellVM(i, j));
Cells = list;
}
}
С VM-частью всё, обратимся к View. Тут будут несколько трюков.
Для начала, нам нужны конвертеры, которые превращают координаты в позиции на экране. В них немного разные вычисления для чётных и нечётных строк. Размер клетки передаём как параметр.
class FieldPositionToCoordinateXConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
FieldPosition position = (FieldPosition)value;
double cellSize = (double)parameter;
if (position.Y % 2 == 0)
return position.X * cellSize;
else
return (position.X + 0.5) * cellSize;
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
class FieldPositionToCoordinateYConverter : IValueConverter
{
static double diag = Math.Sqrt(3) / 2;
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
FieldPosition position = (FieldPosition)value;
double cellSize = (double)parameter;
return position.Y * cellSize * diag;
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
throw new NotSupportedException();
}
}
Теперь XAML. В ресурсы окна подключим размер клетки и конвертеры:
<Window.Resources>
<sys:Double x:Key="CellSize">25</sys:Double>
<view:FieldPositionToCoordinateXConverter x:Key="XConv"/>
<view:FieldPositionToCoordinateYConverter x:Key="YConv"/>
</Window.Resources>
Далее, сам контент. Я не сделал автоподстройку размеров окна, для этого понадобится ещё конвертер.
Клетки подключаем из коллекции через ItemsControl
:
<ItemsControl ItemsSource="{Binding Cells}">
Поскольку мы будем расставлять координаты «вручную», нам понадобится Canvas
в качестве носителя:
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
Теперь, одна клетка.
<ItemsControl.ItemTemplate>
<DataTemplate>
Положим её в квадратный Grid
нужного размера:
<Grid Width="{StaticResource CellSize}" Height="{StaticResource CellSize}">
Внутреннюю часть зададим через Path
, вычислив координаты как здесь:
<Path Data="M -1,-1
M 0,-1
L 0.86602540378443864676372317075294,-0.5
L 0.86602540378443864676372317075294,0.5
L 0,1
L -0.86602540378443864676372317075294,0.5
L -0.86602540378443864676372317075294,-0.5
L 0,-1
M 1,1"
Stretch="Uniform"
Stroke="Black"
StrokeThickness="0.5">
Далее, смена цвета в зависимости от значения IsActive
. Писать ещё один конвертер некрасиво, воспользуемся триггером. DataTrigger
доступен только в стиле, так что установкой фона займётся стиль:
<Path.Style>
<Style TargetType="Path">
<Setter Property="Fill" Value="LightGreen"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive}" Value="True">
<Setter Property="Fill" Value="DarkGreen"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
Теперь, нам нужно организовать клик. Поскольку Path
сам по себе не умеет такого, есть два пути: либо воспользоваться вместо него стилизованным Button
'ом (в котором положить тот же Path
в ControlTemplate
), либо подключить через nuget System.Windows.Interactivity.WPF
, и навесить команду на событие от мыши. Я пошёл вторым путём (i:
— префикс, определённый как xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
).
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonUp">
<i:InvokeCommandAction Command="{Binding Activate}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
С клеткой всё.
</Path>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
Что ещё нужно? Нужно разместить клетку по нужным координатам. Это делается так:
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left"
Value="{Binding Position,
Converter={StaticResource XConv},
ConverterParameter={StaticResource CellSize}}"/>
<Setter Property="Canvas.Top"
Value="{Binding Position,
Converter={StaticResource YConv},
ConverterParameter={StaticResource CellSize}}"/>
</Style>
</ItemsControl.ItemContainerStyle>
Приведу ещё раз полный код окна:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="HexGrid.MainWindow"
xmlns:view="clr-namespace:HexGrid.View"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Title="Test" Height="290" Width="300">
<Window.Resources>
<sys:Double x:Key="CellSize">25</sys:Double>
<view:FieldPositionToCoordinateXConverter x:Key="XConv"/>
<view:FieldPositionToCoordinateYConverter x:Key="YConv"/>
</Window.Resources>
<Grid Margin="10">
<ItemsControl ItemsSource="{Binding Cells}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas IsItemsHost="True"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Width="{StaticResource CellSize}"
Height="{StaticResource CellSize}">
<Path Data="M -1,-1
M 0,-1
L 0.86602540378443864676372317075294,-0.5
L 0.86602540378443864676372317075294,0.5
L 0,1
L -0.86602540378443864676372317075294,0.5
L -0.86602540378443864676372317075294,-0.5
L 0,-1
M 1,1"
Stretch="Uniform"
Stroke="Black"
StrokeThickness="0.5">
<Path.Style>
<Style TargetType="Path">
<Setter Property="Fill" Value="LightGreen"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive}" Value="True">
<Setter Property="Fill" Value="DarkGreen"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonUp">
<i:InvokeCommandAction Command="{Binding Activate}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Path>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Canvas.Left"
Value="{Binding Position,
Converter={StaticResource XConv},
ConverterParameter={StaticResource CellSize}}"/>
<Setter Property="Canvas.Top"
Value="{Binding Position,
Converter={StaticResource YConv},
ConverterParameter={StaticResource CellSize}}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Grid>
</Window>
Результат:
Всё!
Айфон мало держит заряд, разбираемся с проблемой вместе с AppLab
Перевод документов на английский язык: Важность и ключевые аспекты
Имеется две моделиUser (получена в результате make:auth) и Ad