.Net Framework3.0 实践纪实(3)
图形和背景
任务1.3画出棋盘上的星。要完成这个任务,一个关键的地方就是确定星在不同大小的棋盘上的数量和位置。其实TopGo对棋盘的做了限制,那就是小于9*9或者大于19*19的棋盘不被支持。在星的数量确定上,我们考虑到如果是偶数的棋盘,那么没有唯一的中心点(像19*19的中央的那个叫做“天元”的星),在这种情况下,我们仅仅设置星的数量为4(即每个角部一个)。下面的代码显示了这一过程:
protected override void OnRender(DrawingContext dc)
{
… …
if (BoardSize > 8 && BoardSize < 20)
{
Point[] stars = GetDemarkations();
for (int i = 0; i < stars.Length; i++)
{
dc.DrawEllipse(Brushes.Black, null, stars[i], 3 * scale, 3 * scale);
}
}
}
代码首先通过调用GetDemarkations来获取星的坐标位置,然后通过一个循环,调用DrawingContext对象的DrawEllipse来画出没一个星。星是一个半径3倍于直线宽度圆点。GetDemarkations的方法代码如下:
readonly
int[,] demarkCount ={ { 2, 3 }, { 3, 3 }, { 3, 4 }, { 3, 5 }, { 3, 6 } };
private Point[] GetDemarkations()
{
Point[] demarks;
if (BoardSize == 9)
{
demarks = new Point[1];
demarks[0] = new Point(3, 3);
return demarks;
}
if (BoardSize % 2 == 0)
{
demarks = new Point[4];
demarks[0] = new Point(3, 3);
demarks[1] = new Point(3, BoardSize - 4);
demarks[2] = new Point(BoardSize - 4, 3);
demarks[3] = new Point(BoardSize - 4, BoardSize - 4);
return demarks;
}
demarks =new Point[9];
int index = ((int)BoardSize - 11) / 2;
int i = 0;
for (int x = demarkCount[index, 0]; x < BoardSize - 1; x += demarkCount[index, 1])
{
for (int y = demarkCount[index, 0]; y < BoardSize - 1; y += demarkCount[index, 1])
demarks[i++] =new Point(x, y);
}
return demarks;
}
代码使用了一个预先定义的数组来保存星的数量和星之间的间距,其他部分我想应该很清晰,所以就不作解释了。
虽然我们已经可以显示不同大小的棋盘,但是有一个问题,必须提供一个接口让用户来设置BoardSize,我们把这个需求加入到任务表,同时给任务1.3做上标记:
1、TopGo必须能够显示一个棋盘;
1.1 棋盘在界面上的位置
1.2 画棋盘的纵横线(标准为19*19 ),棋盘的大小必须可以动态设置比如说(10*10 )
1.3 画出棋盘上的星(星的数量应该和棋盘大小一致)
1.4 提供用户修改棋盘大小的接口
在我们做这项工作之前,让我们给目前为止的程序界面美美容,所以我们继续添加一些任务:
1.5 设置棋盘背景
…
4、设置窗体背景
棋盘背景的颜色,很自然想到的是黄色调的,这是因为棋盘大部分都是木质的,黄色调比较接近。当定下了棋盘的主色调,我们就要考虑窗体的背景颜色必须和棋盘协调。首先我想到的就是暗红色,因为暗红色可以让我想到红木家具,这仿佛棋盘置于高贵的红木桌面。
给窗体添加背景,我们可以使用渐变笔刷,WPF中有两种渐变笔刷,一种是辐射渐变笔刷,另外一种是线性渐变笔刷。我们这里使用线性渐变笔刷,xaml的代码如下:
<
Window
x:Class
=
"TopGo.MainWindow"
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uc
=
"clr-namespace:TopGo"
Title
=
"TopGo" MinHeight="600"MinWidth="800"WindowState="Maximized"
>
<
Window.Background
>
<
LinearGradientBrush
StartPoint
=
"0,0"EndPoint="0,1">
<
LinearGradientBrush.GradientStops
>
<
GradientStop
Offset
=
"0"Color="DarkRed" />
<
GradientStop
Offset
=
"0.8"Color="Chocolate" />
<
GradientStop
Offset
=
"1"Color="DarkRed" />
</
LinearGradientBrush.GradientStops
>
</
LinearGradientBrush
>
</
Window.Background
>
StartPoint设置颜色渐变的起点坐标,EndPoint设置结束点坐标,两个点的坐标决定了渐变的方向,从我们的设置看这是一个从上至下的垂直方向。
GradientStops定义了一组颜色的变化点,Offset是相对于起点沿着渐变方向的偏移值。编译运行,看看效果如何。你可以根据自己的理解来设置颜色和偏移。
给任务表任务4做上标记。接着我们设计棋盘的背景,这一次我们采用不同的方式,实际上你可以在BoardControl类的OnRender方法中通过代码来画出棋盘的背景。这里我们采用在棋盘控件的后面画一个矩形,然后给这个矩形填充颜色来作为棋盘的背景色。为了在棋盘后面放置画一个矩形,首先我们需要在Viewbox元属中插入一个Canvas(画布)元素,xaml代码如下:
…
<
Viewbox
Grid.Row
=
"1"Grid.Column="1">
<
Canvas
Width
=
"19"Height="19">
<
Rectangle
Width
=
"19"Height="19">
<
Rectangle.Fill
>
<
LinearGradientBrush
StartPoint
=
"0,0"EndPoint="1,1">
<
LinearGradientBrush.GradientStops
>
<
GradientStop
Offset
=
"0"Color="Gold" />
<
GradientStop
Offset
=
"1"Color="Goldenrod" />
</
LinearGradientBrush.GradientStops
>
</
LinearGradientBrush
>
</
Rectangle.Fill
>
</
Rectangle
>
<
uc:BoardControl
BoardSize
=
"19"Width="19"Height="19" />
</
Canvas
>
</
Viewbox
>
…
编译运行,是不是发现棋盘的线条偏了?这是因为我们的线条是从画布的0,0点还是画的,解决这个问题,很简单,我们只要设置棋盘控件的Margin属性即可:
<
uc:BoardControl
BoardSize
=
"19"Width="19"Height="19"Margin="0.5" />
现在再运行看看。
为什么不在代码中实现棋盘的背景呢?事实是xaml的出现就想让桌面应用程序实现asp.net那样的代码和表现分离的效果,这种分离是为了更好的让界面设计人员(如美工)和程序开发人员彼此同步工作而不相互的干扰。比如假如你是一个美工,你决定给棋盘加上阴影,那么你不用懂的编程语言,你可以很容易的做到这一点,只要在棋盘背景的那个矩形下面再画一个表示阴影的矩形就可以了,代码如下:
<
Viewbox
Grid.Row
=
"1"Grid.Column="1">
<
Canvas
Width
=
"19"Height="19">
<
Rectangle
Width
=
"19"Height="19"Fill="Black"Opacity="0.3">
<
Rectangle.RenderTransform
>
<
TranslateTransform
X
=
"0.2"Y="0.2" />
</
Rectangle.RenderTransform
>
</
Rectangle
>
……
WPF也有一个叫做BitmapEffect的属性可以实现各种特殊的效果,阴影、浮雕等。不过我发现使用这个属性后,程序运行变得很慢,它们占用更多的CPU资源,也许在最终的版本会解决这个问题。
OK, 将任务1.5做上标记。
数据绑定
任务1.4 为用户提供设置棋盘大小的接口。这个任务的实现看上去很简单,我们在窗体的某一个位置放置一个组合框,用户可以从中选择棋盘的大小,然后我们通过程序更新有关控件的属性。
那么,就动手吧!
在<Viewbox>元素标签的前面一行插入xaml代码如下:
<
StackPanel
Grid.Row
=
"1"Grid.Column="0"Orientation="Vertical"Margin="10">
<
TextBlock
Foreground
=
"White"FontWeight="Bold"FontSize="14">Game Board Size</TextBlock>
<
ComboBox
Name
=
"boardSizeComboBox" >
<
ComboBoxItem
>
9</ComboBoxItem>
<
ComboBoxItem
>
10</ComboBoxItem>
<
ComboBoxItem
>
11</ComboBoxItem>
<
ComboBoxItem
>
12</ComboBoxItem>
<
ComboBoxItem
>
13</ComboBoxItem>
<
ComboBoxItem
>
14</ComboBoxItem>
<
ComboBoxItem
>
15</ComboBoxItem>
<
ComboBoxItem
>
16</ComboBoxItem>
<
ComboBoxItem
>
17</ComboBoxItem>
<
ComboBoxItem
>
18</ComboBoxItem>
<
ComboBoxItem
>
19</ComboBoxItem>
</
ComboBox
>
</
StackPanel
>
我们在Grid控件的第2行第1列放置一个StackPanel面板做为容器,设置它的布局方向为垂直,然后我们放入一个文本控件和一个组合框。如果你还不明白的话,可以运行一下看看效果。
当用户选择了某一个ComboBoxItem的时候,会触发组合框的SelectionChanged事件,所以我们只要注册这个事件就可以接收到用户选择的值。
添加SelectionChanged属性到ComboBox控件:
<
ComboBox
Name
=
"boardSizeComboBox"electionChanged="BoardSizeSelectionChanged" >
为了能够更新棋盘的属性,我们需要围棋控件设置一个名称:
<
uc:BoardControl
x:Name
=
"boardControl"
BoardSize
=
"19"Width="19"Height="19"Margin="0.5" />
切换到Source方式,在MainWindow类中,添加一个方法:
private
void BoardSizeSelectionChanged(object sender, SelectionChangedEventArgs e)
{
int boardSize = int.Parse(((ComboBoxItem)boardSizeComboBox.SelectedItem).Content.ToString());
boardControl.BoardSize = boardSize;
boardControl.Height = boardControl.Width = boardSize;
boardControl.InvalidateVisual();
}
运行程序,然后用鼠标在新添加的组合框中选择棋盘的大小。发生什么了?你重新看到了前面我们遇到的问题,也就是棋盘的并不是像我们希望的那样显示,问题的原因是我们仅仅改变了棋盘控件的属性,我们没有相应地对棋盘背景、阴影和Viewbox这些控件的尺寸做更新,当然我们可以这么做,为需要更新的控件命名,然后在BoardSizeSelectionChanged中设置它们的Width和Height的值。但是我们有更好的方法,设想如果要设置的属性不只是一个BoardSize, 我们要写许多更新的代码,很郁闷不是吗?这个更好的方法就是使用数据绑定。
要使用数据绑定,首先,我们必须设计一个数据类,然后让这个类实现INotifyPropertyChanged接口。在TopGo项目中添加一个新类:GameInfo。打开GameInfo.cs文件,修改和插入代码如下:
using
System;
using
System.Collections.Generic;
using
System.Text;
using
System.ComponentModel;
namespace
TopGo
{
public class GameInfo : INotifyPropertyChanged
{
int boardSize=19;
public int BoardSize
{
get { return boardSize; }
set
{
if (boardSize != value)
{
boardSize = value;
OnPropertyChanged("BoardSize");
}
}
}
#region
INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
void OnPropertyChanged(string info)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
回到MainWindow的Xaml方式,修改代码如下:
<
Window
x:Class
=
"TopGo.MainWindow"
xmlns
=
"http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x
=
"http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uc
=
"clr-namespace:TopGo"
Title
=
"TopGo" MinHeight="600"MinWidth="800"WindowState="Maximized"
Loaded
=
"WindowLoaded"
>
……
<
ComboBox
Name
=
"boardSizeComboBox"Text="{Binding Path=BoardSize, Mode=TwoWay}" SelectionChanged="BoardSizeSelectionChanged" >
......
<
Viewbox
Grid.Row
=
"1"Grid.Column="1">
<
Canvas
Width
=
"{Binding Path=BoardSize}"Height="{Binding Path=BoardSize}"
>
<
Rectangle
Width
=
"{Binding Path=BoardSize}"Height="{Binding Path=BoardSize}"
Fill
=
"Black"Opacity="0.3">
<
Rectangle.RenderTransform
>
<
TranslateTransform
X
=
"0.2"Y="0.2" />
</
Rectangle.RenderTransform
>
</
Rectangle
>
<
Rectangle
Width
=
"{Binding Path=BoardSize}"Height="{Binding Path=BoardSize}"
>
<
Rectangle.Fill
>
<
LinearGradientBrush
StartPoint
=
"0,0"EndPoint="1,1">
<
LinearGradientBrush.GradientStops
>
<
GradientStop
Offset
=
"0"Color="Gold" />
<
GradientStop
Offset
=
"1"Color="Goldenrod" />
</
LinearGradientBrush.GradientStops
>
</
LinearGradientBrush
>
</
Rectangle.Fill
>
</
Rectangle
>
<
uc:BoardControl
x:Name
=
"boardControl" BoardSize="{Binding Path=BoardSize}"Margin="0.5" />
</
Canvas
>
</
Viewbox
>
切换到Source方式:
修改MainWindow类的代码如下:
public
partial class MainWindow : System.Windows.Window
{
GameInfo gameInfo = new GameInfo();
……
private void WindowLoaded(object sender, RoutedEventArgs e)
{
this.DataContext = gameInfo;
boardControl.Height = boardControl.Width = boardControl.BoardSize - 1;
}
private void BoardSizeSelectionChanged(object sender, SelectionChangedEventArgs e)
{
boardControl.Height = boardControl.Width = boardControl.BoardSize - 1;
boardControl.InvalidateVisual();
}
}
在WindowLoaded方法中,我们设置GameInfo实例对象到MainWindow的DataContext属性,这样Xaml中数据绑定的路径的根就是GameInfo。同时注意到我们显式的对棋盘控件的高度和宽度进行赋值,这是因为我们不能xaml中方便的对它们进行赋值,这里它们的值比BoardSize小1(想想为什么?)。
另外,我们对组合框绑定的是Text属性,同时设置绑定的模式为双向,这样当组合框的内容改变的时候,改变的内容直接更新到数据源,也就是GameInfo中的BoardSize属性。
编译运行,然后试着选择不同的棋盘大小,看看棋盘的显示是不是我们希望的那样。
Ok, 给任务1.4做上标记。如果你是用户,你对现在这个棋盘还满意吗?
我听到你在嘀咕:好像少了什么?
是的,少了什么呢?如果你在网络上下过围棋,你会发现那些棋盘旁边都显示有坐标,纵坐标从上到下是阿拉伯数字,横坐标是从左到右是英文字母。
给我们的任务表添上新的任务:
1.6 显示棋盘坐标(提供隐藏棋盘坐标的功能);
然后,休息。我们下一次再继续。
(待续)