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

在 WPF 中绘制棋盘游戏

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (18投票s)

2009年2月11日

CPOL

4分钟阅读

viewsIcon

71048

downloadIcon

2961

本文将向您展示如何使用 DrawingVisual 类绘制围棋游戏。

Go board

引言

在本文中,我将向您展示如何在 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_BoardWidthFactor14m_BoardHeightFactor15

private double getPosX(double value)
{
    return m_BoardWidthFactor * value + m_Border;
}

private double getPosY(double value)
{
    return m_BoardHeightFactor * value + m_Border;
}

Go board description

绘制棋盘和棋子

我将向您展示如何绘制棋盘和棋子。其余的基本上是相同的。让我们看看 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));
}

整合

Sample application

为了向您展示一些功能的应用,我创建了一个小型演示应用程序,它具有以下 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>

如您所见,由于 DependencyPropertyboardsize 已绑定到 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 日 - 添加了第一个版本
© . All rights reserved.