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

333
27 февраля 2017, 10:36

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

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

Answer 1

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

Результат:

Всё!

READ ALSO
C# Наследование

C# Наследование

Решал задачу по наследованию на: http://skillsitvdn

294
Many To Many laravel отношения

Many To Many laravel отношения

Имеются 2 таблицыproducts и regions с таким содержимым

297
Как настроить отношения User Model в Laravel 5.4?

Как настроить отношения User Model в Laravel 5.4?

Имеется две моделиUser (получена в результате make:auth) и Ad

286
Не подключается PHP скрипт

Не подключается PHP скрипт

Есть файл Functionsphp, в нем прописаны строки:

228