Гексагональная сетка

ParanoidPanda

Вопрос. Как можно реализовать визуализацию гексагональной сетки на WPF, при том что каждая клеточка этой сетки является отдельным объектом, допустим может иметь другой цвет.

На ум приходят только канвасы. Описать поле на котором будут закрепляться по координатам ячейки. Как нарисовать саму ячейку даже не знаю.

1 ответ

ParanoidPanda

Проще всего применить 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;
}
</t></t>

Несколько вспомогательных структур:

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 CellVM(i, j)); Cells = list; }
}
</cellvm></cellvm></cellvm>

С VM-частью всё, обратимся к View. Тут будут несколько трюков.

Для начала, нам нужны конвертеры, которые превращают координаты в позиции на экране. В них немного разные вычисления для чётных и нечётных строк. Размер клетки передаём как параметр.

class FieldPositionToCoordinateXConverter : IValueConverter
{ public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { FieldPosition position = (FieldPosition)value; ****** cellSize = (******)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 ****** diag = Math.Sqrt(3) / 2; public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { FieldPosition position = (FieldPosition)value; ****** cellSize = (******)parameter; return position.Y * cellSize * diag; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotSupportedException(); }
}

Теперь XAML. В ресурсы окна подключим размер клетки и конвертеры:

<window.resources> <sys:****** x:key="CellSize">25</sys:******> <view:fieldpositiontocoordinatexconverter x:key="XConv"> </view:fieldpositiontocoordinatexconverter></window.resources>

Далее, сам контент. Я не сделал автоподстройку размеров окна, для этого понадобится ещё конвертер.

Клетки подключаем из коллекции через ItemsControl:

Поскольку мы будем расставлять координаты «вручную», нам понадобится Canvas в качестве носителя:

<itemscontrol.itemspanel> <itemspaneltemplate> <canvas isitemshost="True"> </canvas></itemspaneltemplate> </itemscontrol.itemspanel>

Теперь, одна клетка.

<itemscontrol.itemtemplate> </itemscontrol.itemtemplate>

Положим её в квадратный Grid нужного размера:

Внутреннюю часть зададим через Path, вычислив координаты как здесь:

Далее, смена цвета в зависимости от значения IsActive. Писать ещё один конвертер некрасиво, воспользуемся триггером. DataTrigger доступен только в стиле, так что установкой фона займётся стиль:

<path.style> </path.style>

Теперь, нам нужно организовать клик. Поскольку Path сам по себе не умеет такого, есть три пути: либо воспользоваться вместо него стилизованным Button'ом (в котором положить тот же Path в ControlTemplate), либо определить InputBinding, либо подключить System.Windows.Interactivity.dll (из Expression Blend SDK, устанавливаемого через Visual Studio Installer, или из nuget System.Windows.Interactivity.WPF), и навесить команду на событие от мыши. Проще всего второй путь (его подсказал @АндрейNOP):

<path.inputbindings> <mousebinding gesture="LeftClick" command="{Binding Activate}"> </mousebinding></path.inputbindings>

Хотя я пошёл третьим путём (i: — префикс, определённый как xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity").

<i:interaction.triggers> <i:eventtrigger eventname="MouseLeftButtonUp"> <i:invokecommandaction command="{Binding Activate}"> </i:invokecommandaction></i:eventtrigger> </i:interaction.triggers>

С клеткой всё.

Что ещё нужно? Нужно разместить клетку по нужным координатам. Сам Grid в DataTemplate упаковывается в контейнер, так что выставлять для него координаты бесполезно. Поэтому надо двигать сам контейнер. Это делается так:

<itemscontrol.itemcontainerstyle> </itemscontrol.itemcontainerstyle>

Приведу ещё раз полный код окна:

<window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:class="HexGrid.View.MainWindow" xmlns:view="clr-namespace:HexGrid.View" xmlns:sys="clr-namespace:System;assembly=mscorlib" title="Test" height="290" width="300"> <window.resources> <sys:****** x:key="CellSize">25</sys:******> <view:fieldpositiontocoordinatexconverter x:key="XConv"> <view:fieldpositiontocoordinateyconverter x:key="YConv"> </view:fieldpositiontocoordinateyconverter></view:fieldpositiontocoordinatexconverter></window.resources> <grid margin="10"> <itemscontrol itemssource="{Binding Cells}"> <itemscontrol.itemspanel> <itemspaneltemplate> <canvas isitemshost="True"> </canvas></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> </path.style> <path.inputbindings> <mousebinding gesture="LeftClick" command="{Binding Activate}"> </mousebinding></path.inputbindings> </path> </grid> </datatemplate> </itemscontrol.itemtemplate> <itemscontrol.itemcontainerstyle> </itemscontrol.itemcontainerstyle> </itemscontrol> </grid>
</window>

Ну и стандартный App.xaml/App.xaml.cs:

public partial class App : Application
{ protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); new MainWindow() { DataContext = new BoardVM() { FieldSize = new FieldSize(10, 10) } }.Show(); }
}

Результат:

Всё!

licensed under cc by-sa 3.0 with attribution.