.Net Framework3.0 实践纪实(3)

来源:互联网 发布:win和linux双系统引导 编辑:程序博客网 时间:2024/05/16 23:59

  

.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的代码如下:
<Windowx: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>
    <LinearGradientBrushStartPoint="0,0"EndPoint="0,1">
      <LinearGradientBrush.GradientStops>
        <GradientStopOffset="0"Color="DarkRed" />
        <GradientStopOffset="0.8"Color="Chocolate" />
        <GradientStopOffset="1"Color="DarkRed" />
      </LinearGradientBrush.GradientStops>
    </LinearGradientBrush>
 </Window.Background>
StartPoint设置颜色渐变的起点坐标,EndPoint设置结束点坐标,两个点的坐标决定了渐变的方向,从我们的设置看这是一个从上至下的垂直方向。
GradientStops定义了一组颜色的变化点,Offset是相对于起点沿着渐变方向的偏移值。编译运行,看看效果如何。你可以根据自己的理解来设置颜色和偏移。
给任务表任务4做上标记。接着我们设计棋盘的背景,这一次我们采用不同的方式,实际上你可以在BoardControl类的OnRender方法中通过代码来画出棋盘的背景。这里我们采用在棋盘控件的后面画一个矩形,然后给这个矩形填充颜色来作为棋盘的背景色。为了在棋盘后面放置画一个矩形,首先我们需要在Viewbox元属中插入一个Canvas(画布)元素,xaml代码如下:
      <ViewboxGrid.Row="1"Grid.Column="1">
        <CanvasWidth="19"Height="19">
        <RectangleWidth="19"Height="19">
          <Rectangle.Fill>
            <LinearGradientBrushStartPoint="0,0"EndPoint="1,1">
              <LinearGradientBrush.GradientStops>
                <GradientStopOffset="0"Color="Gold" />
                <GradientStopOffset="1"Color="Goldenrod" />
              </LinearGradientBrush.GradientStops>
            </LinearGradientBrush>
          </Rectangle.Fill>
        </Rectangle>
       
          <uc:BoardControlBoardSize="19"Width="19"Height="19" />
        </Canvas>
      </Viewbox>
编译运行,是不是发现棋盘的线条偏了?这是因为我们的线条是从画布的0,0点还是画的,解决这个问题,很简单,我们只要设置棋盘控件的Margin属性即可:
<uc:BoardControlBoardSize="19"Width="19"Height="19"Margin="0.5" />
现在再运行看看。
为什么不在代码中实现棋盘的背景呢?事实是xaml的出现就想让桌面应用程序实现asp.net那样的代码和表现分离的效果,这种分离是为了更好的让界面设计人员(如美工)和程序开发人员彼此同步工作而不相互的干扰。比如假如你是一个美工,你决定给棋盘加上阴影,那么你不用懂的编程语言,你可以很容易的做到这一点,只要在棋盘背景的那个矩形下面再画一个表示阴影的矩形就可以了,代码如下:
      <ViewboxGrid.Row="1"Grid.Column="1">
        <CanvasWidth="19"Height="19">
         
          <RectangleWidth="19"Height="19"Fill="Black"Opacity="0.3">
            <Rectangle.RenderTransform>
              <TranslateTransformX="0.2"Y="0.2" />
            </Rectangle.RenderTransform>
          </Rectangle>
        ……
WPF也有一个叫做BitmapEffect的属性可以实现各种特殊的效果,阴影、浮雕等。不过我发现使用这个属性后,程序运行变得很慢,它们占用更多的CPU资源,也许在最终的版本会解决这个问题。
OK, 将任务1.5做上标记。
数据绑定

任务1.4 为用户提供设置棋盘大小的接口。这个任务的实现看上去很简单,我们在窗体的某一个位置放置一个组合框,用户可以从中选择棋盘的大小,然后我们通过程序更新有关控件的属性。
那么,就动手吧!
在<Viewbox>元素标签的前面一行插入xaml代码如下:
      <StackPanelGrid.Row="1"Grid.Column="0"Orientation="Vertical"Margin="10">
        <TextBlockForeground="White"FontWeight="Bold"FontSize="14">Game Board Size</TextBlock>
         <ComboBoxName="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控件:
<ComboBoxName="boardSizeComboBox"electionChanged="BoardSizeSelectionChanged" >
为了能够更新棋盘的属性,我们需要围棋控件设置一个名称:
<uc:BoardControlx: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方式,修改代码如下:
<Windowx: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"
    >
……
         <ComboBoxName="boardSizeComboBox"Text="{Binding Path=BoardSize, Mode=TwoWay}" SelectionChanged="BoardSizeSelectionChanged" >
......
     <ViewboxGrid.Row="1"Grid.Column="1">
        <CanvasWidth="{Binding Path=BoardSize}"Height="{Binding Path=BoardSize}">
         
          <RectangleWidth="{Binding Path=BoardSize}"Height="{Binding Path=BoardSize}"Fill="Black"Opacity="0.3">
            <Rectangle.RenderTransform>
              <TranslateTransformX="0.2"Y="0.2" />
            </Rectangle.RenderTransform>
          </Rectangle>
         
        <RectangleWidth="{Binding Path=BoardSize}"Height="{Binding Path=BoardSize}">
          <Rectangle.Fill>
            <LinearGradientBrushStartPoint="0,0"EndPoint="1,1">
              <LinearGradientBrush.GradientStops>
                <GradientStopOffset="0"Color="Gold" />
                <GradientStopOffset="1"Color="Goldenrod" />
              </LinearGradientBrush.GradientStops>
            </LinearGradientBrush>
          </Rectangle.Fill>
        </Rectangle>
       
          <uc:BoardControlx: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 显示棋盘坐标(提供隐藏棋盘坐标的功能);
然后,休息。我们下一次再继续。
(待续)
原创粉丝点击