.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 顯示棋盤座標(提供隱藏棋盤座標的功能);
然後,休息。我們下一次再繼續。
(待續)