65.9K
CodeProject 正在变化。 阅读更多。
Home

HexGrid

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (39投票s)

2017 年 7 月 8 日

CPOL

6分钟阅读

viewsIcon

32077

downloadIcon

551

WPF HexGrid 面板。

引言

WPF 面板在矩形区域内排列元素,并且大多数 WPF 元素都是矩形。HexGrid 项目最初是作为创建一个自定义形状控件的尝试,后来发展成了一个六边形控件和一个可以排列它们的面板。

HexGrid board

HexItem

HexItem 是一个简单的 ContentControl,具有六边形形状。它只有一个额外的属性 OrientationHorizontalVertical),它决定了六边形的形状。

HexItem

    /// <summary>
    /// Hexagonal content control
    /// </summary>
    public class HexItem : ListBoxItem
    {
        static HexItem()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(HexItem), 
                   new FrameworkPropertyMetadata(typeof(HexItem)));
        }

        public static readonly DependencyProperty OrientationProperty = 
                      HexGrid.OrientationProperty.AddOwner(typeof(HexItem));
        
        public Orientation Orientation
        {
            get { return (Orientation) GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }
    }

HexItem 派生自 ListBoxItem,因为它开箱即用地提供了选择支持(IsSelected 属性)。

Orientation 属性在 HexGrid 类中声明,并且 HexItem 共享该属性定义。这是 Orientation DP 代码。

        public static readonly DependencyProperty OrientationProperty =
            DependencyProperty.RegisterAttached
            ("Orientation", typeof(Orientation), typeof(HexGrid),
                new FrameworkPropertyMetadata(Orientation.Horizontal,
                    FrameworkPropertyMetadataOptions.AffectsMeasure | 
                    FrameworkPropertyMetadataOptions.AffectsArrange |
                    FrameworkPropertyMetadataOptions.Inherits));

        public static void SetOrientation(DependencyObject element, Orientation value)
        {
            element.SetValue(OrientationProperty, value);
        }

        public static Orientation GetOrientation(DependencyObject element)
        {
            return (Orientation)element.GetValue(OrientationProperty);
        }

如您所见,它具有 FrameworkPropertyMetadataOptions.Inherits 属性,这意味着用户可以为 HexGrid 设置 Orientation 值,并且该 HexGrid 内的所有 HexItems 都将通过 DP 值继承获得相同的值(HexGrid 和嵌套的 HexItems 应该具有相同的 Orientation,否则它们将无法形成漂亮的蜂窝状图案)。

HexItem 的六边形形状在 Generic.xaml 的模板中配置。模板由两个具有六边形 Clip 几何形状的 Grid 组成(一个(“hexBorder”)代表边框,另一个(“hexContent”)是背景覆盖),其中包含一个 ContentPresenter

    <converters:HexClipConverter x:Key="ClipConverter"/>

    <!--HexItem-->
    <Style TargetType="{x:Type local:HexItem}">        
        <Setter Property="Background" Value="CornflowerBlue"/>
        <Setter Property="BorderBrush" Value="Black"/>
        <Setter Property="BorderThickness" Value="4"/>
        <Setter Property="HorizontalContentAlignment" Value="Center"/>
        <Setter Property="VerticalContentAlignment" Value="Center"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:HexItem">
                    <Grid Name="hexBorder" Background="{TemplateBinding BorderBrush}">
                        <Grid.Clip>
                            <MultiBinding Converter="{StaticResource ClipConverter}">
                                <Binding Path="ActualWidth" ElementName="hexBorder"/>
                                <Binding Path="ActualHeight" ElementName="hexBorder"/>
                                <Binding Path="Orientation" 
                                 RelativeSource="{RelativeSource TemplatedParent}"/>
                            </MultiBinding>
                        </Grid.Clip>

                        <Grid Name="hexContent"                              
                              Background="{TemplateBinding Background}"
                              Margin="{TemplateBinding BorderThickness}" 
                              VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
                            <Grid.Clip>
                                <MultiBinding Converter="{StaticResource ClipConverter}">
                                    <Binding Path="ActualWidth" ElementName="hexContent"/>
                                    <Binding Path="ActualHeight" ElementName="hexContent"/>
                                    <Binding Path="Orientation" 
                                     RelativeSource="{RelativeSource TemplatedParent}"/>
                                </MultiBinding>
                            </Grid.Clip>

                            <ContentPresenter VerticalAlignment=
                                    "{TemplateBinding VerticalContentAlignment}"
                                     HorizontalAlignment="{TemplateBinding 
                                                          HorizontalContentAlignment}"
                                     ClipToBounds="True"

                                     Margin="{TemplateBinding Padding}"
                                     Content="{TemplateBinding Content}"
                                     ContentTemplate="{TemplateBinding ContentTemplate}"/>
                        </Grid>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsSelected" Value="True">
                            <Setter Property="BorderBrush" Value="Gold"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

六边形几何形状由 HexClipConverter 根据元素尺寸和方向创建。

    public class HexClipConverter: IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, 
                                       object parameter, CultureInfo culture)
        {
            double w = (double)values[0];
            double h = (double)values[1];
            Orientation o = (Orientation) values[2];

            if (w <= 0 || h <= 0)
                return null;

            PathFigure figure = o == Orientation.Horizontal
                ? new PathFigure
                  {
                      StartPoint = new Point(0, h*0.5),
                      Segments =
                      {
                          new LineSegment {Point = new Point(w*0.25, 0)},
                          new LineSegment {Point = new Point(w*0.75, 0)},
                          new LineSegment {Point = new Point(w, h*0.5)},
                          new LineSegment {Point = new Point(w*0.75, h)},
                          new LineSegment {Point = new Point(w*0.25, h)},
                      }
                  }
                : new PathFigure
                  {
                      StartPoint = new Point(w*0.5, 0),
                      Segments =
                      {
                          new LineSegment {Point = new Point(w, h*0.25)},
                          new LineSegment {Point = new Point(w, h*0.75)},
                          new LineSegment {Point = new Point(w*0.5, h)},
                          new LineSegment {Point = new Point(0, h*0.75)},
                          new LineSegment {Point = new Point(0, h*0.25)},
                      }
                  };
            return new PathGeometry { Figures = { figure } };
        }

        public object[] ConvertBack(object value, Type[] targetTypes, 
                                    object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }  

HexList

HexList 是一个 Selector ItemsControl,派生自 ListBox(开箱即用地提供选择支持)。它重写了项容器类型,并创建 HexItems 而不是 ListBoxItems

        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return (item is HexItem);
        }

        protected override DependencyObject GetContainerForItemOverride()
        {
            return new HexItem();
        }

HexList 使用 HexGrid 作为默认的 ItemsPanel(在 Generic.xamlHexList 样式中设置)。

        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <local:HexGrid ColumnCount="{Binding Path=ColumnCount, 
                           RelativeSource={RelativeSource AncestorType=ListBox}}"
                           RowCount="{Binding Path=RowCount, 
                           RelativeSource={RelativeSource AncestorType=ListBox}}"
                           Background="{Binding Path=Background, 
                           RelativeSource={RelativeSource AncestorType=ListBox}}"/>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter> 

HexItem 类似,HexListHexGrid 共享 OrientationRowCountColumnCount 依赖属性的定义。ItemsPanel RowCountColumnCount 属性绑定到 HexList 属性,用户可以仅为 HexList 设置它们,而无需重复 ItemsPanelTemplate

HexGrid

HexGrid 是一个 WPF Panel,旨在以蜂窝状图案排列元素(主要是六边形 HexItems)。根据 Orientation,图案看起来不同。

HexGrid 声明了三个影响排列的依赖属性。

        #region Orientation
        public static readonly DependencyProperty OrientationProperty =
            DependencyProperty.RegisterAttached
                ("Orientation", typeof(Orientation), typeof(HexGrid),
                new FrameworkPropertyMetadata(Orientation.Horizontal,
                    FrameworkPropertyMetadataOptions.AffectsMeasure | 
                    FrameworkPropertyMetadataOptions.AffectsArrange |
                    FrameworkPropertyMetadataOptions.Inherits));

        public static void SetOrientation(DependencyObject element, Orientation value)
        {
            element.SetValue(OrientationProperty, value);
        }

        public static Orientation GetOrientation(DependencyObject element)
        {
            return (Orientation)element.GetValue(OrientationProperty);
        }

        public Orientation Orientation
        {
            get { return (Orientation) GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }
        #endregion

        public static readonly DependencyProperty RowCountProperty = 
            DependencyProperty.Register("RowCount", typeof (int), typeof (HexGrid),
            new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsMeasure | 
                                          FrameworkPropertyMetadataOptions.AffectsArrange), 
            ValidateCountCallback);

        public int RowCount
        {
            get { return (int) GetValue(RowCountProperty); }
            set { SetValue(RowCountProperty, value); }
        }

        public static readonly DependencyProperty ColumnCountProperty = 
            DependencyProperty.Register("ColumnCount", typeof (int), typeof (HexGrid),
            new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsMeasure | 
                                          FrameworkPropertyMetadataOptions.AffectsArrange),
            ValidateCountCallback);
        
        public int ColumnCount
        {
            get { return (int) GetValue(ColumnCountProperty); }
            set { SetValue(ColumnCountProperty, value); }
        }

        private static bool ValidateCountCallback(object value)
        {
            if (value is int)
            {
                int count = (int)value;
                return count > 0;
            }

            return false;
        }

HexGrid 类似于 UniformGrid。它将可用空间分割为预定义的行数(RowCount)和列数(ColumnCount),并且每个子元素在排列时获得相同的区域大小。与 UniformGrid 不同,它重用了 Grid.RowGrid.Column 附加属性来将子元素定位在适当的单元格中(类似于 Grid)。

HexGrid 测量

        protected override Size MeasureOverride(Size availableSize)
        {
            double w = availableSize.Width;
            double h = availableSize.Height;            

            // if there is Infinity size dimension
            if (Double.IsInfinity(w) || Double.IsInfinity(h))
            {
                // determine maximum desired size
                h = 0;
                w = 0;
                foreach (UIElement e in InternalChildren)
                {
                    e.Measure(availableSize);
                    var s = e.DesiredSize;
                    if (s.Height > h)
                        h = s.Height;
                    if (s.Width > w)
                        w = s.Width;
                }                

                // multiply maximum size to RowCount and ColumnCount to get total size
                if (Orientation == Orientation.Horizontal)
                    return new Size(w*(ColumnCount * 3 + 1)/4, h*(RowCount * 2 + 1)/2);

                return new Size(w*(ColumnCount * 2 + 1)/2, h*(RowCount * 3 + 1)/4);
            }            

            return availableSize;
        }

如果至少一个维度是 InfinityHexGrid 将获取子元素的最大高度和最大宽度,并将其乘以 RowCountColumnCount 以获得总大小。否则,HexGrid 将使用可用大小。

HexGrid 排列

        protected override Size ArrangeOverride(Size finalSize)
        {            
            // determine if there is empty space at grid borders
            bool first, last;
            HasShift(out first, out last);

            // compute final hex size
            Size hexSize = GetHexSize(finalSize);
            
            // compute arrange line sizes
            double columnWidth, rowHeight;
            if (Orientation == Orientation.Horizontal)
            {
                rowHeight   = 0.50 * hexSize.Height;
                columnWidth = 0.25 * hexSize.Width;
            }
            else
            {
                rowHeight   = 0.25 * hexSize.Height;
                columnWidth = 0.50 * hexSize.Width;
            }            

            // arrange elements
            UIElementCollection elements = base.InternalChildren;
            for (int i = 0; i < elements.Count; i++)
            {
                if (elements[i].Visibility == Visibility.Collapsed)
                    continue;
                ArrangeElement(elements[i], hexSize, columnWidth, rowHeight, first);
            }
                        
            return finalSize;
        }

我将以 Vertical 方向为例解释 HexGrid 的排列。为简化解释,我制作了一个带有可见排列线的示例。

VerticalArrange

如您所见,垂直 HexGrid 空间被分割成等宽(W)的列和等高的行(H)。每个六边形占据 2 列和 4 行,并且不同行中的相邻六边形在 1 列和 1 行中重叠。总排列列数为 ColumnCount * 2 + 1。总排列行为 RowCount * 3 + 1

如果我隐藏带有文本“First”的六边形,左侧将出现空白的灰色空间。为了避免这种情况,黄色六边形应该更靠近左侧。六边形“Last”是右侧的类似情况。在排列过程中,void HasShift(out bool first, out bool last) 方法确定第一个或最后一个排列列是否可以被忽略。HasShift 方法用于 Vertical 方向。

        private void HasShift(out bool first, out bool last)
        {
            if (Orientation == Orientation.Horizontal)
                HasRowShift(out first, out last);
            else
                HasColumnShift(out first, out last);
        }

        private void HasColumnShift(out bool firstColumn, out bool lastColumn)
        {
            firstColumn = lastColumn = true;

            UIElementCollection elements = base.InternalChildren;
            for (int i = 0; i < elements.Count && (firstColumn || lastColumn); i++)
            {
                var e = elements[i];
                if (e.Visibility == Visibility.Collapsed)
                    continue;

                int row = GetRow(e);
                int column = GetColumn(e);

                int mod = row % 2;

                if (column == 0 && mod == 0)
                    firstColumn = false;

                if (column == ColumnCount - 1 && mod == 1)
                    lastColumn = false;
            }
        }

GetHexSize 方法计算 HexGrid 中的最终六边形大小。GetHexSize 检查是否可以移动,然后将可用空间分割成适当数量的排列行和列。每个子元素将获得 2 个排列行的行高和 4 个排列列的列宽。但是,如果该大小小于任何子元素的 MinHeightMinWidth,六边形大小将增加到 MinHeight/MinWidth 以适应它们,即使这意味着超出排列边界。当 HexGrid 用作 HexList ItemsPanel 时,这将导致 HexList 激活滚动条。

        private Size GetHexSize(Size gridSize)
        {
            double minH = 0;
            double minW = 0;

            foreach (UIElement e in InternalChildren)
            {
                var f = e as FrameworkElement;
                if (f != null)
                {
                    if (f.MinHeight > minH)
                        minH = f.MinHeight;
                    if (f.MinWidth > minW)
                        minW = f.MinWidth;
                }
            }

            bool first, last;
            HasShift(out first, out last);

            var possibleSize = GetPossibleSize(gridSize);
            double possibleW = possibleSize.Width;
            double possibleH = possibleSize.Height;

            var w = Math.Max(minW, possibleW);
            var h = Math.Max(minH, possibleH);

            return new Size(w, h);
        }

        private Size GetPossibleSizeVertical(Size gridSize, bool first, bool last)
        {
            int columns = ((first ? 0 : 1) + 2*ColumnCount - (last ? 1 : 0));
            double w = 2 * (gridSize.Width / columns);

            int rows = 1 + 3*RowCount;
            double h = 4 * (gridSize.Height / rows);

            return new Size(w, h);
        }

Horizontal 方向的 HexGrid 的排列是对称的(在公式中,行与列互换,宽度与高度互换)。

HorizontalArrange

HexGrid 示例

圆形

HexGrid 是为六边形设计的,但其他元素也可以很好地适应(尽管它们会重叠而没有边距)。

                <hx:HexGrid Margin="20" 
                            Orientation="Vertical"
                            RowCount="3" ColumnCount="3">

                    <Ellipse Grid.Row="0" Grid.Column="1" Fill="Purple"/>
                    <Ellipse Grid.Row="0" Grid.Column="2" Fill="DarkOrange"/>

                    <Ellipse Grid.Row="1" Grid.Column="0" Fill="Blue"/>
                    <Ellipse Grid.Row="1" Grid.Column="1" Fill="Red"/>
                    <Ellipse Grid.Row="1" Grid.Column="2" Fill="Yellow"/>

                    <Ellipse Grid.Row="2" Grid.Column="1" Fill="Cyan"/>
                    <Ellipse Grid.Row="2" Grid.Column="2" Fill="Green"/>
                </hx:HexGrid>

Circles

Office 颜色选择器

此颜色列表类似于 MS Word 中用于选择文本颜色的列表。单击颜色六边形,列表背景将获得相同的颜色。

                <hx:HexList Name="HexColors" Orientation="Vertical" 
                            Grid.Row="1"
                            Padding="10"
                            SelectedIndex="0"
                            Background="{Binding Path=SelectedItem.Background, 
                                         RelativeSource={RelativeSource Self}}"
                            RowCount="5" ColumnCount="5">
                    <hx:HexItem Grid.Row="0" Grid.Column="1" Background="#006699"/>
                    <hx:HexItem Grid.Row="0" Grid.Column="2" Background="#0033CC"/>
                    <hx:HexItem Grid.Row="0" Grid.Column="3" Background="#3333FF"/>
                    <!--...-->
                    <hx:HexItem Grid.Row="4" Grid.Column="1" Background="#CC9900"/>
                    <hx:HexItem Grid.Row="4" Grid.Column="2" Background="#FF3300"/>
                    <hx:HexItem Grid.Row="4" Grid.Column="3" Background="#CC0000"/>
                </hx:HexList>

HexColorSelector

一个问题:我应该添加所有颜色并创建一个 HexColorSelector 控件吗?您怎么看?

六边形菜单

一组按钮(共 7 个)以六边形形式排列。可能是水平/垂直工具栏按钮的有趣替代方案。

            <hx:HexGrid Grid.Row="1" Grid.Column="1"
                        RowCount="3" ColumnCount="3" Orientation="Horizontal">
                <hx:HexItem Grid.Row="0" Grid.Column="1" Content="2"/>

                <hx:HexItem Grid.Row="1" Grid.Column="0" Content="1"/>
                <hx:HexItem Grid.Row="1" Grid.Column="1" Content="0" Background="Gold"/>
                <hx:HexItem Grid.Row="1" Grid.Column="2" Content="3"/>

                <hx:HexItem Grid.Row="2" Grid.Column="0" Content="6"/>
                <hx:HexItem Grid.Row="2" Grid.Column="1" Content="5"/>
                <hx:HexItem Grid.Row="2" Grid.Column="2" Content="4"/>
            </hx:HexGrid>

HexMenu

HexGrid 辅助方法

六边形控件可用于创建游戏(事实上,我正在制作几个简单的游戏作为概念证明)。六边形棋盘通常比矩形棋盘提供更多选项。但是,它会使控制器逻辑更加复杂。

在矩形棋盘上,相邻单元格的 X Y 坐标有 +1/-1 的差值(对角线单元格的 X Y 坐标有 +1/-1 的差值)。在六边形棋盘上,两个相邻六边形有 8 种可能的相对位置(见截图)。每个网格方向只有 6 种是有效的。

HexArrayHelper 类定义了应该帮助处理六边形棋盘的方法。HexArrayHelper 假设用户有一个尺寸为 size(具有 int Widthint Height 属性的 IntSize 结构)的棋盘,并且当前六边形位于位置 origin(具有 int Xint Y 属性的 IntPoint 结构)。HexArrayHelper 处理六边形的坐标,并且实际表示棋盘和瓦片的真实数据结构可能不同(例如,最简单的是任何类型的二维数组)。

基本的 HexArrayHelper 方法是 GetNextHex。该方法返回相邻六边形的坐标,或者在请求方向上没有六边形时返回 null。结果取决于棋盘方向(如果 IsHorizontal=true 则为 Horizontal,否则为 Vertical)。实际工作由 GetNextHexHorizontal/GetNextHexVertical 方法完成,这些方法会检查有效方向、请求方向可能超出边界的边界情况,然后计算相邻六边形的坐标。

        /// <summary>
        /// Returns adjacent hex 
        /// </summary>
        /// <param name="size">Board dimensions</param>
        /// <param name="origin">Current hex coordinates</param>
        /// <param name="dir">Direction</param>
        public IntPoint? GetNextHex(IntSize size, IntPoint origin, HexDirection dir)
        {
            if (IsHorizontal)
                return GetNextHexHorizontal(size, origin, dir);

            return GetNextHexVertical(size, origin, dir);
        } 

GetNeighbours 方法返回所有相邻的六边形。

        /// <summary>
        /// Returns all adjacent hexes
        /// </summary>
        /// <param name="size">Board dimensions</param>
        /// <param name="origin">Current hex coordinates</param>        
        public IEnumerable<IntPoint> GetNeighbours(IntSize size, IntPoint origin)
        {
            for (int index = 0; index < _directions.Length; index++)
            {
                HexDirection dir = _directions[index];
                var point = GetNextHex(size, origin, dir);
                if (point.HasValue)
                    yield return point.Value;
            }
        }

GetArea 方法返回当前六边形周围所有满足提供标准的六边形。

        /// <summary>
        /// Returns all hexes around current hex which meet provided criteria
        /// </summary>
        /// <param name="size">Board dimensions</param>
        /// <param name="origin">Current hex coordinates</param> 
        /// <param name="predicate">Search criteria</param>
        /// <returns></returns>
        public IEnumerable<IntPoint> GetArea(IntSize size, IntPoint origin, 
                                             Func<IntPoint, bool> predicate)
        {
            if (false == predicate(origin))
                yield break;
            int idx = 0;

            var points = new List<IntPoint>();
            points.Add(origin);
            do
            {
                IntPoint p = points[idx];
                yield return p;
                foreach (var point in GetNeighbours(size, p).Where(predicate))
                {
                    if (points.IndexOf(point) < 0)
                        points.Add(point);
                }
                idx++;
            }
            while (idx < points.Count);
        }

GetRay 方法返回从当前六边形到棋盘边界的请求方向上的所有六边形。

        /// <summary>
        /// Returns all hexes in the requested direction from current hex to board border 
        /// </summary>
        /// <param name="size">Board dimensions</param>
        /// <param name="origin">Current hex coordinates</param>
        /// <param name="dir">Direction</param>        
        public IEnumerable<IntPoint> GetRay(IntSize size, IntPoint origin, HexDirection dir)
        {
            IntPoint? next;
            do
            {
                next = GetNextHex(size, origin, dir);
                if (next != null)
                {
                    yield return next.Value;
                    origin = next.Value;
                }
            }
            while (next != null);
        }

结论

在本文中,我试图给出总体概述,解释最重要的设计决策和实现细节。有关更多详细信息,请浏览 CodeProject 或 GitHub 上的项目源代码。

HexGrid 是我第一次尝试创建自定义 WPF Panel。可能的 HexGrid 用途包括不寻常的控件布局、图形图案以及当然还有六边形游戏棋盘。如果您在自己的项目中使用了 HexGrid 并分享了您的结果,那就太好了(欢迎您评论并附上截图/源代码链接)。

历史

  • 2017 年 7 月 8 日:初始版本
© . All rights reserved.