用 Silverlight 开发围棋在线对弈程序
作者: Neil Chen
第一部分:UI雏形
首先,介绍下围棋的简单规则:黑白双方交替落子,以占据棋盘上交叉点多者为胜。同时,双方为了争夺地盘,可能会发生“对杀”。一个棋子周围接触的空白交叉点数目叫做“气”,如果一个或多个棋子周围的气都被对方封死,气数=0,则这些棋子就称为死棋,需要从棋盘上移去。
在上图中,棋子上的数字一般在棋谱中显示,用于帮助了解棋局进行的次序。
下面我们来尝试用 Silverlight 2.0 开发一个围棋在线对弈程序。
首先,我们来创建围棋程序的 UI 部分。毕竟,这是最直观的东西。而且我喜欢边做边重构的开发方式,这样,不至于因为花了过多的时间做设计,而减慢了实际开发的进度。让我们先从一个小小的原型起步,然后不断的应用设计思维去改进它,最终达到目标。正如一部电影里的台词所说的:
Aim small, miss small.
好了,现在大概分析一下:
1. 我们打算在界面的左侧显示棋盘,而右侧是功能区域。
2. 棋盘是由19道横线,19道竖线,以及9个星位的标志组成的。为了方便查找棋盘上的位置,我们在棋盘的四周可能需要加上坐标。目前我们先只在左侧和上方加上坐标。右边和下面的位置留在那里。
对于棋盘的显示,我们打算用一个 Canvas 来实现。而其中的线条,圆点,棋子等视觉元素,只需往其中添加相应的 Line, Ellipse, Label 即可。
我们假定整个程序的大小为 800 * 600 (以后也许再考虑是否有必要支持任意比例的缩放)。现在,跟随直觉,我写了下面一些代码用于构建 UI:
Page.xaml:
<UserControl
x:Class="WoodFoxWeiQi.UI.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="800"
Height="600">
<Grid
x:Name="LayoutRoot"
Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="0.75*" />
<ColumnDefinition
Width="0.25*" />
Grid.ColumnDefinitions>
<Border
Grid.Column="0">
<Canvas
x:Name="canvasBoard"
Background="LightYellow"
Margin="10">
Canvas>
Border>
<Border
Grid.Column="1">
<StackPanel
Margin="20"
Orientation="Vertical">
<Button
x:Name="btnGo"
Content="Go" />
StackPanel>
Border>
Grid>
UserControl>
Page.xaml.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace WoodFoxWeiQi.UI
{
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
canvasBoard.MouseLeftButtonDown += new MouseButtonEventHandler(canvasBoard_MouseLeftButtonDown);
canvasBoard.SizeChanged += new SizeChangedEventHandler(canvasBoard_SizeChanged);
btnGo.Click += new RoutedEventHandler(btnGo_Click);
}
// 因为 Canvas 的尺寸是根据父控件的尺寸在运行时计算得到的,所以需要在 SizeChanged 方法里
// 才能获得其实际尺寸
void canvasBoard_SizeChanged(object sender, SizeChangedEventArgs e)
{
canvasBoard.Children.Clear();
CreateBoardElements();
}
double boardSize; // 棋盘宽度(不包含坐标)
double cellSize; // 网格宽度
double starSize; // 星位的小圆点的直径
double stoneSize; // 棋子直径
// 创建棋盘上的网格线,星位,坐标等显示元素
private void CreateBoardElements()
{
// 确保使用一个正方形区域作为棋盘显示区域
boardSize = Math.Min(canvasBoard.ActualHeight, canvasBoard.ActualWidth);
// 根据棋盘尺寸计算出相应的其他尺寸
cellSize = boardSize / 20;
starSize = cellSize / 4;
stoneSize = cellSize * 0.8;
for (int i = 1; i <= 19; i++)
{
// 添加水平网格线
var lineHorizontal = new Line();
lineHorizontal.X1 = cellSize;
lineHorizontal.X2 = cellSize * 19;
lineHorizontal.Y1 = lineHorizontal.Y2 = cellSize * i;
lineHorizontal.Stroke = new SolidColorBrush(Colors.Black);
lineHorizontal.StrokeThickness = 1.0;
canvasBoard.Children.Add(lineHorizontal);
// 添加垂直网格线
var lineVertical = new Line();
lineVertical.Y1 = cellSize;
lineVertical.Y2 = cellSize * 19;
lineVertical.X1 = lineVertical.X2 = cellSize * i;
lineVertical.Stroke = new SolidColorBrush(Colors.Black);
lineVertical.StrokeThickness = 1.0;
canvasBoard.Children.Add(lineVertical);
}
// 添加9个星位的标志
for (int i = 4; i <= 16; i += 6)
{
for (int j = 4; j <= 16; j += 6)
{
double x = i * cellSize - starSize / 2;
double y = j * cellSize - starSize / 2;
Ellipse ellipseStar = new Ellipse();
ellipseStar.Stroke = new SolidColorBrush(Colors.Black);
ellipseStar.Fill = new SolidColorBrush(Colors.Black);
ellipseStar.Width = ellipseStar.Height = starSize;
ellipseStar.SetValue(Canvas.LeftProperty, x);
ellipseStar.SetValue(Canvas.TopProperty, y);
canvasBoard.Children.Add(ellipseStar);
}
}
// 画横坐标
for (int i = 1; i <= 19; i++)
{
var txtLabel = new TextBlock();
txtLabel.FontSize = 11.0;
txtLabel.FontWeight = FontWeights.Thin;
txtLabel.Text = i.ToString();
txtLabel.SetValue(Canvas.LeftProperty, i * cellSize - txtLabel.ActualWidth / 2);
txtLabel.SetValue(Canvas.TopProperty, cellSize / 2 - txtLabel.ActualHeight / 2);
txtLabel.Text = i.ToString();
canvasBoard.Children.Add(txtLabel);
}
// 画纵坐标
char c = 'A';
for (int i = 1; i <= 19; i++)
{
var txtLabel = new TextBlock();
txtLabel.FontSize = 11.0;
txtLabel.FontWeight = FontWeights.Thin;
txtLabel.Text = i.ToString();
txtLabel.SetValue(Canvas.LeftProperty, cellSize / 2 - txtLabel.ActualWidth / 2);
txtLabel.SetValue(Canvas.TopProperty, i * cellSize - txtLabel.ActualHeight / 2);
txtLabel.Text = (c++).ToString();
canvasBoard.Children.Add(txtLabel);
}
}
void canvasBoard_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var pos = e.GetPosition(canvasBoard);
MessageBox.Show("Clicked on board, X: " + pos.X + ", Y: " + pos.Y);
}
private void btnGo_Click(object sender, RoutedEventArgs e)
{
// 放置一个测试的棋子(白子)
Ellipse e1 = new Ellipse();
e1.Stroke = new SolidColorBrush(Colors.Black);
e1.Fill = new SolidColorBrush(Colors.White);
e1.Width = e1.Height = stoneSize;
double x = 17 * cellSize - stoneSize / 2;
double y = 4 * cellSize - stoneSize / 2;
e1.SetValue(Canvas.LeftProperty, x);
e1.SetValue(Canvas.TopProperty, y);
canvasBoard.Children.Add(e1);
// 再放一个黑子,带手数显示的
Ellipse e2 = new Ellipse();
e2.Stroke = new SolidColorBrush(Colors.Black);
e2.Fill = new SolidColorBrush(Colors.Black);
e2.Width = e2.Height = stoneSize;
double x2 = 16 * cellSize - stoneSize / 2;
double y2 = 4 * cellSize - stoneSize / 2;
e2.SetValue(Canvas.LeftProperty, x2);
e2.SetValue(Canvas.TopProperty, y2);
canvasBoard.Children.Add(e2);
// 绘制手数显示的 Label
TextBlock lbl2 = new TextBlock();
lbl2.FontSize = 10.0;
lbl2.FontWeight = FontWeights.Thin;
lbl2.Text = "203";
lbl2.Foreground = new SolidColorBrush(Colors.White);
lbl2.SetValue(Canvas.LeftProperty, 16 * cellSize - lbl2.ActualWidth / 2);
lbl2.SetValue(Canvas.TopProperty, 4 * cellSize - lbl2.ActualHeight / 2);
canvasBoard.Children.Add(lbl2);
}
}
}
看起来不赖。在这个界面中,如果点击 ”Go” 按钮,则会在棋盘上摆放两个测试用的棋子,其中黑棋上还标有表示棋步的数字。但是,我们的目标是要做一个能下棋的程序,因此,我们下面要加一些控制代码,比如,在用户点击某个位置的时候,落下棋子(如果该位置是允许落子的),以及控制棋局的开始、结束、认输等操作的按钮以及相关动作处理逻辑。
不过,在开始之前,有必要重构一下上面的 UI 代码,因为它看起来比较乱,一个方法里包含了太多的代码,如果这样继续下去的话,程序很快会变成一堆乱麻而难以为继。
由于很多对象的创建过程是类似的,因此我们可以将它提取到独立的方法中加以重用。另外,因为我们需要能够控制某些界面元素的显示/隐藏(比如坐标),将这些对象保存到当前窗体的字段里是一个不错的主意。
我们还添加了一个 CheckBox,用来控制坐标的显示和隐藏。Xaml 中添加的代码如下:
<CheckBox
x:Name="chkShowAxisLabels"
Content="Show Axis Labels"
Margin="0,10,0,0"
IsChecked="true" />
重构后的代码 Page.xaml.cs (每个方法代码大概5~10行左右):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace WoodFoxWeiQi.UI
{
public partial class Page : UserControl
{
public Page()
{
InitializeComponent();
canvasBoard.MouseLeftButtonDown += canvasBoard_MouseLeftButtonDown;
canvasBoard.SizeChanged += canvasBoard_SizeChanged;
btnGo.Click += btnGo_Click;
chkShowAxisLabels.Checked += chkShowAxisLabels_Checked;
chkShowAxisLabels.Unchecked += chkShowAxisLabels_Checked;
}
#region Fields
private readonly Brush brush_White = new SolidColorBrush(Colors.White);
private readonly Brush brush_Black = new SolidColorBrush(Colors.Black);
private readonly List<TextBlock> yAxisLabels = new List<TextBlock>(20);
private readonly List<TextBlock> xAxisLabels = new List<TextBlock>(20);
double boardSize; // 棋盘宽度(不包含坐标)
double cellSize; // 网格宽度
double starSize; // 星位的小圆点的直径
double stoneSize; // 棋子直径
#endregion
// 因为 Canvas 的尺寸是根据父控件的尺寸在运行时计算得到的,所以需要在 SizeChanged 方法里
// 才能获得其实际尺寸
void canvasBoard_SizeChanged(object sender, SizeChangedEventArgs e)
{
canvasBoard.Children.Clear();
CreateBoardElements();
}
void canvasBoard_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var pos = e.GetPosition(canvasBoard);
MessageBox.Show("Clicked on board, X: " + pos.X + ", Y: " + pos.Y);
}
void btnGo_Click(object sender, RoutedEventArgs e)
{
// 放置一个测试的棋子(白子)
var e1 = BuildCircle(stoneSize, 17 * cellSize, 4 * cellSize, brush_Black, brush_White);
// 再放一个黑子,带手数显示的
var e2 = BuildCircle(stoneSize, 16 * cellSize, 4 * cellSize, brush_Black, brush_Black);
// 绘制手数显示的 Label
var lbl2 = BuildLabel("203", brush_White, 10.0, 16 * cellSize, 4 * cellSize);
}
// 显示或隐藏坐标轴
void chkShowAxisLabels_Checked(object sender, RoutedEventArgs e)
{
var show = chkShowAxisLabels.IsChecked.HasValue && chkShowAxisLabels.IsChecked.Value;
foreach (var label in xAxisLabels.Union(yAxisLabels))
{
label.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
}
}
#region Builder methods for children elements
// 创建棋盘上的网格线,星位,坐标等显示元素
void CreateBoardElements()
{
CalculateSizes();
BuildGridLines();
BuildStarPointMarks();
BuildXAxisLabels();
BuildYAxisLabels();
}
// 计算必要的一些尺寸定义值
void CalculateSizes()
{
// 确保使用一个正方形区域作为棋盘显示区域
boardSize = Math.Min(canvasBoard.ActualHeight, canvasBoard.ActualWidth);
// 根据棋盘尺寸计算出相应的其他尺寸
cellSize = boardSize / 20;
starSize = cellSize / 4;
stoneSize = cellSize * 0.8;
}
// 添加网格线
void BuildGridLines()
{
for (var i = 1; i <= 19; i++)
{
// 添加水平网格线
BuildLine(cellSize, cellSize * i, cellSize * 19, cellSize * i);
// 添加垂直网格线
BuildLine(cellSize * i, cellSize, cellSize * i, cellSize * 19);
}
}
// 添加9个星位的标志
void BuildStarPointMarks()
{
for (var i = 4; i <= 16; i += 6)
{
for (var j = 4; j <= 16; j += 6)
{
BuildCircle(starSize, i * cellSize, j * cellSize, brush_Black, brush_Black);
}
}
}
// 画横坐标
void BuildXAxisLabels()
{
for (var i = 1; i <= 19; i++)
{
var lbl = BuildLabel(i.ToString(), brush_Black, 11.0, i * cellSize, cellSize / 2);
xAxisLabels.Add(lbl);
}
}
// 画纵坐标
void BuildYAxisLabels()
{
var c = 'A';
for (var i = 1; i <= 19; i++)
{
var text = (c++).ToString();
var lbl = BuildLabel(text, brush_Black, 11.0, cellSize / 2, i * cellSize);
yAxisLabels.Add(lbl);
}
}
#endregion
#region Basic builder methods
Line BuildLine(double x1, double y1, double x2, double y2)
{
var line = new Line {
X1 = x1,
X2 = x2,
Y1 = y1,
Y2 = y2,
Stroke = brush_Black,
StrokeThickness = 1.0
};
canvasBoard.Children.Add(line);
return line;
}
Ellipse BuildCircle(double diameter, double centerX, double centerY, Brush stroke, Brush fill)
{
var ellipse = new Ellipse { Stroke = stroke, Fill = fill };
ellipse.Width = ellipse.Height = diameter;
ellipse.SetValue(Canvas.LeftProperty, centerX - diameter / 2);
ellipse.SetValue(Canvas.TopProperty, centerY - diameter / 2);
canvasBoard.Children.Add(ellipse);
return ellipse;
}
// 创建 Label
TextBlock BuildLabel(string text, Brush foreground, double fontSize,
double centerX, double centerY)
{
var lbl = new TextBlock {
FontSize = fontSize,
FontWeight = FontWeights.Thin,
Text = text,
Foreground = foreground
};
lbl.SetValue(Canvas.LeftProperty, centerX - lbl.ActualWidth / 2);
lbl.SetValue(Canvas.TopProperty, centerY - lbl.ActualHeight / 2);
canvasBoard.Children.Add(lbl);
return lbl;
}
#endregion
}
}
[第二部分:MVC]