在 WPF 中绘制棋盘游戏






4.64/5 (18投票s)
本文将向您展示如何使用 DrawingVisual 类绘制围棋游戏。

引言
在本文中,我将向您展示如何在 WPF 中绘制围棋。由于需要大量的形状,为了性能考虑,我使用了 System.Windows.Media.DrawingVisual
类进行绘制,而不是 System.Windows.Shapes
类。请注意,这是我写在 The Code Project 的第一篇文章,也是我的第一个 WPF 项目。所以我认为我犯了很多错误。任何关于改进我的代码的建议都将不胜感激。
本文仅展示了绘制方法。我将在后续文章中介绍棋盘逻辑。
背景
我刚开始学习 WPF,并且对此感到非常兴奋!为了更多地了解 WPF,我认为在 WPF 中绘制世界上最好的游戏会很酷。围棋已有约 4000 年的历史,并且非常出名,尤其是在亚洲。如果您有兴趣,可以在以下网站了解更多信息:维基百科围棋。
GoBoardPainter
正如我之前所说,我在这里使用的是 DrawingVisual
类。这样您只需要一个 FrameworkElement
,可以节省大量的开销。实际的绘图继承自 Visual
类,它更小。那么,让我们从我们的 BoardPainter
开始吧。
public class GoBoardPainter : FrameworkElement
{
private List<Visual> m_Visuals = new List<Visual>();
private DrawingVisual m_BoardVisual, m_StonesVisual, m_StarPointVisual,
m_CoordinatesVisual, m_AnnotationVisual, m_MouseHoverVisual;
private Dictionary<GoBoardPoint, Stone> m_StoneList =
new Dictionary<GoBoardPoint, Stone>();
private ObservableCollection<GoBoardAnnotation> m_AnnotationsList =
new ObservableCollection<GoBoardAnnotation>();
... more private variables ...
public GoBoardPainter()
{
Resources.Source = new Uri(
"pack://application:,,,/GoBoard;component/GoBoardPainterResources.xaml");
m_BlackStoneBrush = (Brush)TryFindResource("blackStoneBrush");
... assign some resources...
InitializeBoard(this.BoardSize - 1);
}
protected override int VisualChildrenCount
{
get { return m_Visuals.Count; }
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= m_Visuals.Count)
{
throw new ArgumentOutOfRangeException("index");
}
return m_Visuals[index];
}
}
正如您所见,我们有一个 Visuals 列表。这个列表将被绘制到 FrameworkElement
上。在这个例子中,我只将一个 DrawingVisual
添加到列表中,那就是 m_BoardVisual
。为了绘制棋子、注解等,我将这些 DrawingVisuals 添加到 m_BoardVisual
中。重写方法是必需的,因为我们使用了自己的视觉绘制机制。现在让我们看看 InitializeBoard()
方法。
private void InitializeBoard(int boardSize)
{
// Remove all Visuals
m_Visuals.ForEach(delegate(Visual v) { RemoveVisualChild(v); });
m_Visuals.Clear();
m_StoneList.Clear();
m_AnnotationsList.Clear();
m_AnnotationsList.CollectionChanged += new NotifyCollectionChangedEventHandler(
m_AnnotationsList_CollectionChanged);
m_BoardSize = boardSize;
m_GoBoardRect = new Rect(new Size(m_BoardSize * m_BoardWidthFactor,
m_BoardSize * m_BoardHeightFactor));
m_GoBoardHitBox = m_GoBoardRect;
m_GoBoardHitBox.Inflate((m_BoardWidthFactor / 2), (m_BoardHeightFactor / 2));
this.Width = m_GoBoardRect.Width + m_Border * 2;
this.Height = m_GoBoardRect.Height + m_Border * 2;
DrawBoard();
DrawCoordinates();
DrawStarPoints();
DrawStones();
DrawMouseHoverVisual();
m_Visuals.Add(m_BoardVisual);
m_Visuals.ForEach(delegate(Visual v) { AddVisualChild(v); }); // Add all Visuals
}
由于我们能够随时重新初始化棋盘,所以我们必须先清除我们的列表,并从 FrameworkElement
中移除所有 Visuals。在此之后,我们创建棋盘矩形。围棋棋盘的比例是 14 比 15,所以棋盘的长度是 (boardSize * 14)
,棋盘的高度是 (boardSize * 15)
。
热区矩形比普通围棋棋盘稍大,并且我们的整个 FrameworkElement
也会有一个边框,我们可以在其中绘制一些坐标。
现在我们按照适当的顺序绘制我们的 Visuals。每个绘图方法都将其 DrawingVisual
添加到 m_BoardVisual
。将 Visual 列表添加到 FrameworkElement
后,初始化就完成了。
现在我们需要两个辅助方法,它们可以将围棋棋盘坐标转换为 DrawingVisual
坐标。m_BoardWidthFactor
是 14
,m_BoardHeightFactor
是 15
。
private double getPosX(double value)
{
return m_BoardWidthFactor * value + m_Border;
}
private double getPosY(double value)
{
return m_BoardHeightFactor * value + m_Border;
}
绘制棋盘和棋子
我将向您展示如何绘制棋盘和棋子。其余的基本上是相同的。让我们看看 DrawBoard
方法。
private void DrawBoard()
{
m_BoardVisual = new DrawingVisual();
using (DrawingContext dc = m_BoardVisual.RenderOpen())
{
dc.DrawRectangle(m_BoardBrush, new Pen(Brushes.Black, 0.2), new Rect(0, 0,
m_BoardSize * m_BoardWidthFactor + m_Border * 2,
m_BoardSize * m_BoardHeightFactor + m_Border * 2));
dc.DrawRectangle(m_BoardBrush, new Pen(Brushes.Black, 0.2),
new Rect(m_Border, m_Border, m_BoardSize * m_BoardWidthFactor,
m_BoardSize * m_BoardHeightFactor));
for (int x = 0; x < m_BoardSize; x++)
{
for (int y = 0; y < m_BoardSize; y++)
{
dc.DrawRectangle(null, m_BlackPen, new Rect(getPosX(x), getPosY(y),
m_BoardWidthFactor, m_BoardHeightFactor));
}
}
}
}
创建 DrawingVisual
后,我们现在可以绘制到由 RenderOpen()
方法创建的 DrawingContext
中。DrawingContext
实现 IDisposeable
接口,并使用 Close()
方法刷新内容。
在绘制两个矩形(一个用于带有边框的完整棋盘,一个用于没有边框的实际棋盘)之后,我们绘制 boardSize * boardSize
个矩形。这就是我们所要做的。WPF 的优点在于它会自动缩放所有 Visuals。我们只需处理我们的 14 比 15 的比例,仅此而已!
public void DrawStones()
{
m_BoardVisual.Children.Remove(m_StonesVisual);
m_StonesVisual = new DrawingVisual();
using (DrawingContext dc = m_StonesVisual.RenderOpen())
{
foreach (var item in m_StoneList)
{
double posX = getPosX(item.Key.X);
double posY = getPosY(item.Key.Y);
dc.DrawEllipse(m_StoneShadowBrush, null, new Point(posX + 1, posY + 1),
6.7, 6.7);
dc.DrawEllipse(((
item.Value == Stone.White) ? m_WhiteStoneBrush : m_BlackStoneBrush),
m_BlackPen, new Point(posX, posY), m_BoardWidthFactor / 2 - 0.5,
m_BoardWidthFactor / 2 - 0.5);
}
}
m_BoardVisual.Children.Add(m_StonesVisual);
}
在 DrawStones()
中,您可以看到我用于 m_BoardVisual
的每个子 Visual 的一些东西。首先,Visual 会自行移除,绘制完成后,它会再次添加自身。在遍历 stoneList
字典时,我们将 stoneposition
(类型为 GoBoardPoint
,具有 X 和 Y 坐标)转换为 Visual 位置。然后我们绘制一个小的阴影,然后绘制实际的棋子。黑色棋子的 Brush
如下所示:
<RadialGradientBrush Center="0.3,0.3" GradientOrigin="0.3,0.3" Opacity="1"
x:Key="blackStoneBrush">
<RadialGradientBrush.GradientStops>
<GradientStop Color="Gray" Offset="0"/>
<GradientStop Color="Black" Offset="1"/>
</RadialGradientBrush.GradientStop>
</RadialGradientBrush>
添加一些 WPF 功能
好的。现在我们已经绘制了棋盘,但我们需要一些功能,例如绑定到棋盘大小或点击事件。棋盘大小通过 DependencyProperty
实现。MovePlayed
事件通过 RoutedEvent
实现。
BoardSize 实现
标准大小是 19。只有 2 到 19 之间的值是有效的。棋盘大小更改后,将重新初始化棋盘。
public static readonly DependencyProperty BoardSizeProperty =
DependencyProperty.Register
("BoardSize", typeof(int), typeof(GoBoardPainter),
new FrameworkPropertyMetadata
(19, new PropertyChangedCallback(OnBoardSizeChanged)),
new ValidateValueCallback(BoardSizeValidateCallback));
public int BoardSize
{
get { return (int)GetValue(BoardSizeProperty); }
set { SetValue(BoardSizeProperty, value); }
}
private static bool BoardSizeValidateCallback(object target)
{
if ((int)target < 2 || (int)target > 19)
return false;
return true;
}
private static void OnBoardSizeChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs args)
{
(sender as GoBoardPainter).InitializeBoard
((sender as GoBoardPainter).BoardSize - 1);
}
MovePlayed 事件实现
MovePlayedEvent
是一个冒泡事件。在 OnMouseLeftButtonDown
中,我们检查点击是否在 HitBox
内,然后引发事件。
public static readonly RoutedEvent MovePlayedEvent =
EventManager.RegisterRoutedEvent("MovePlayed", RoutingStrategy.Bubble,
typeof(MovePlayedEventHandler), typeof(GoBoardPainter));
public delegate void MovePlayedEventHandler(object sender,
RoutedMovePlayedEventArgs args);
public event MovePlayedEventHandler MovePlayed
{
add { AddHandler(MovePlayedEvent, value); }
remove { RemoveHandler(MovePlayedEvent, value); }
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
Point pos = e.GetPosition(this);
if (!m_GoBoardHitBox.Contains(new Point
(pos.X - m_Border, pos.Y - m_Border))) return;
int x = (int)Math.Round((pos.X - m_Border) /
(m_GoBoardRect.Width / m_BoardSize));
int y = (int)Math.Round((pos.Y - m_Border) /
(m_GoBoardRect.Height / m_BoardSize));
RaiseEvent(new RoutedMovePlayedEventArgs
(MovePlayedEvent, this, new Point(x, y),
m_ToPlay));
}
整合

为了向您展示一些功能的应用,我创建了一个小型演示应用程序,它具有以下 XAML 代码:
<Window x:Class="GoBoard.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:goBoard="clr-namespace:GoBoard.UI"
Title="Window1" Height="500" Width="400">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Slider Maximum="19" Minimum="2" Value="19" Name="slBoardSize"
VerticalAlignment="Center" Margin="5" ></Slider>
<StackPanel Grid.Column="1" Margin="5">
<RadioButton Name="rdStone" IsChecked="True"
Checked="rdStone_Checked">Stone</RadioButton>
<RadioButton Name="rdRectangle"
Checked="rdRectangle_Checked">Rectangle</RadioButton>
<RadioButton Name="rdCircle"
Checked="rdRectangle_Checked">Circle</RadioButton>
</StackPanel>
</Grid>
<Viewbox Grid.Row="1">
<goBoard:GoBoardPainter
BoardSize="{Binding ElementName=slBoardSize, Path=Value}"
MouseHoverType="Stone"
x:Name="goBoardPainter"
MovePlayed="goBoardPainter_MovePlayed">
</goBoard:GoBoardPainter>
</Viewbox>
</Grid>
</Window>
如您所见,由于 DependencyProperty
,boardsize
已绑定到 Slider
的值。此外,MovePlayed
事件在代码隐藏文件中注册。在此事件处理程序中,我们检查单选按钮,并添加棋子或注解。代码如下:
private void goBoardPainter_MovePlayed(object sender,
RoutedMovePlayedEventArgs e)
{
if (rdStone.IsChecked.Value &&
!goBoardPainter.StoneList.ContainsKey(e.Position))
{
goBoardPainter.StoneList.Add(new GoBoardPoint
(e.Position.X, e.Position.Y),
e.StoneColor);
goBoardPainter.ToPlay = e.StoneColor ^ Stone.White;
}
else if (rdRectangle.IsChecked.Value)
{
goBoardPainter.AnnotationList.Add(new GoBoardAnnotation(
GoBoardAnnotationType.Rectangle, e.Position));
}
else if (rdCircle.IsChecked.Value)
{
goBoardPainter.AnnotationList.Add(new GoBoardAnnotation(
GoBoardAnnotationType.Circle, e.Position));
}
goBoardPainter.Redraw();
}
结论
我希望您喜欢我的第一篇文章,也希望您学到了一些东西!我将在以后的文章中介绍一个具有功能性围棋逻辑的围棋棋盘。
附注:我的母语不是英语。
如果您想在线玩围棋,请访问 www.gokgs.com。
我的围棋段位是 2D。
历史
- 2009 年 2 月 11 日 - 添加了第一个版本