用 Silverlight 开发围棋在线对弈程序 作者: Neil Chen 第一部分:UI雏形 首先,介绍下围棋的简单规则:黑白双方交替落子,以占据棋盘上交叉点多者为胜。同时,双方为了争夺地盘,可能会发生“对杀”。一个棋子周围接触的空白交叉点数目叫做“气”,如果一个或多个棋子周围的气都被对方封死,气数=0,则这些棋子就称为死棋,需要从棋盘上移去。 一个围棋棋谱大致如下图所示(截图自Tom围棋网站): |
在上图中,棋子上的数字一般在棋谱中显示,用于帮助了解棋局进行的次序。 下面我们来尝试用 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 的尺寸是根据父控件的尺寸在运行时计算得到的, // 才能获得其实际尺寸 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 } } |