HexGrid






4.93/5 (39投票s)
WPF HexGrid 面板。
引言
WPF 面板在矩形区域内排列元素,并且大多数 WPF 元素都是矩形。HexGrid
项目最初是作为创建一个自定义形状控件的尝试,后来发展成了一个六边形控件和一个可以排列它们的面板。
HexItem
HexItem
是一个简单的 ContentControl
,具有六边形形状。它只有一个额外的属性 Orientation
(Horizontal
或 Vertical
),它决定了六边形的形状。
/// <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
和嵌套的 HexItem
s 应该具有相同的 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.xaml 的 HexList
样式中设置)。
<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
类似,HexList
与 HexGrid
共享 Orientation
、RowCount
和 ColumnCount
依赖属性的定义。ItemsPanel RowCount
和 ColumnCount
属性绑定到 HexList
属性,用户可以仅为 HexList
设置它们,而无需重复 ItemsPanelTemplate
。
HexGrid
HexGrid
是一个 WPF Panel
,旨在以蜂窝状图案排列元素(主要是六边形 HexItem
s)。根据 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.Row
和 Grid.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;
}
如果至少一个维度是 Infinity
,HexGrid
将获取子元素的最大高度和最大宽度,并将其乘以 RowCount
和 ColumnCount
以获得总大小。否则,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
的排列。为简化解释,我制作了一个带有可见排列线的示例。
如您所见,垂直 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 个排列列的列宽。但是,如果该大小小于任何子元素的 MinHeight
或 MinWidth
,六边形大小将增加到 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
的排列是对称的(在公式中,行与列互换,宽度与高度互换)。
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>
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
控件吗?您怎么看?
六边形菜单
一组按钮(共 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>
HexGrid 辅助方法
六边形控件可用于创建游戏(事实上,我正在制作几个简单的游戏作为概念证明)。六边形棋盘通常比矩形棋盘提供更多选项。但是,它会使控制器逻辑更加复杂。
在矩形棋盘上,相邻单元格的 X 或 Y 坐标有 +1/-1 的差值(对角线单元格的 X 和 Y 坐标有 +1/-1 的差值)。在六边形棋盘上,两个相邻六边形有 8 种可能的相对位置(见截图)。每个网格方向只有 6 种是有效的。
HexArrayHelper
类定义了应该帮助处理六边形棋盘的方法。HexArrayHelper
假设用户有一个尺寸为 size
(具有 int Width
和 int Height
属性的 IntSize
结构)的棋盘,并且当前六边形位于位置 origin
(具有 int X
和 int 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 日:初始版本