Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Draw a Boardgame in WPF

0.00/5 (No votes)
11 Feb 2009 1  
This article will show you how to use the DrawingVisual class to draw the game of GO
Go board

Introduction

In this article, I will show you how to draw the game of GO in WPF. Because you need a lot of shapes, for performance reasons, I use the System.Windows.Media.DrawingVisual class for drawing instead of the System.Windows.Shapes classes. Please be aware that this is my first article on The Code Project and my very first WPF project. So I think I made a lot of mistakes. Any suggestions on how to improve my code will be appreciated.

This article only shows the drawing methods. I will cover the board logic in a later article.

Background

I just started to learn WPF and I'm very excited about it! So to learn more about WPF, I thought it would be cool to draw the best game in the world in WPF. GO is about 4000 years old and is very well known especially in Asia. If you are interested, you can learn more about it at: Wikipedia Go game.

The GoBoardPainter

As I said before, I'm using the DrawingVisual class here. This way you only need one FrameworkElement which saves you a lot of overhead. The actual drawings are derived from the Visual class which is much smaller. So let's start with our BoardPainter like this:

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];
    }
}

As you can see, we have a List with Visuals. This list will be drawn to the FrameworkElement. In this example, I only add one DrawingVisual to the list which is the m_BoardVisual. To also draw the Stones, Annotations, etc. I will add these DrawingVisuals to the m_BoardVisual. The overridden methods are necessary, because we use our own visual drawing mechanics. Now let's look at the InitializeBoard() method:

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
}

Because we are able to reinitialize the board any time, we have to first clear our Lists and remove all Visuals from the FrameworkElement. After this, we create our Board Rectangle. A Go board has a 14 to 15 size ratio so the board length is (boardSize * 14) and the board height is (boardSize * 15).

The Hit Box Rectangle is slightly bigger than the normal Goboard and our complete FrameworkElement also gets a border where we can draw some coordinates.
Now we draw our Visuals in the appropriate order. Each Drawing method adds its DrawingVisual to the m_BoardVisual. After we add the Visual List to the FrameworkElement, the initialization is complete.

Now we need two helper methods, which translate a Goboard coordinate to a DrawingVisual coordinate. m_BoardWidthFactor is 14 and m_BoardHeightFactor is 15.

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

Draw the Board and Stones

I will show you how to draw the board and the stones. The rest is pretty much the same. Let's look at the DrawBoard method:

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));
            }
        }
    }
}

After creating the DrawingVisual, we can now draw to the DrawingContext which is created by the RenderOpen() method. DrawingContext implements the IDisposeable interface and uses the Close() method to flush the content.

After drawing two rectangles (one for the complete board with borders, one for the actual board without borders), we draw boardSize * boardSize rectangles. That's all we have to do. The cool thing about WPF is that it automatically scales all visuals. We only work with our 14 to 15 ratio and that's it!

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);
}

In DrawStones(), you can see something that I used for every subvisual of m_BoardVisual. First the visual removes itself and after the drawing is finished, it adds itself again. While iterating through the stoneList Dictionary, we translate the stoneposition (which is from Type GoBoardPoint and has an X and Y coordinate) to the visual position. Then we draw a small shadow and then the actual stone. The Brush for a black stone looks like this:

<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>

Adding Some WPF Functionality

Ok. Now we have drawn the board, but we need some functionality like binding to the boardsize or a click event. The boardsize is implemented via a DependencyProperty. The MovePlayed event is implemented via a RoutedEvent.

BoardSize Implementation

The standard size is 19. Only values between 2 and 19 are valid. After the boardsize changes, the board is reinitialized.

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 Event Implementation

MovePlayedEvent is a Bubble Event. In OnMouseLeftButtonDown, we check if the click is inside the HitBox and then raise the event.

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));
}

Putting It All Together

Sample application

To show you some of the functionality in action, I created a small demo application which has the following XAML code:

<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>

As you can see, the boardsize is binded to a Slider value, thanks to the DependencyProperty. Also the MovePlayed event is registered in the code behind file. In this event handler, we check the radio buttons and add a stone or an annotation. The code looks like this:

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();
}

Conclusion

I hope you enjoyed my first article and also hope you learned something! I will cover a Go board with functional Go logic in a later article.

PS: My first language is not English.
If you want to play GO online, visit www.gokgs.com.
My Go rank is 2D.

History

  • 11th February 2009 - Added first version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here