Canvas panel是一種跟傳統畫面環境很像的排版選項。你可以特別指定元件的位置!
和其他WPF部分一樣,這些協調運作都是用與裝置無關的單位:1/96吋,從左上角開始算!
你可能有注意到元件並沒有X或是Y或是Top或是Left屬性。當使用Canvas panel,你可以用Canvas.SetLeft和Canvas.Set來定位元件的位置!就像DockPanel所定義的SetDock方法,以及Grid所定義的SetRow,SetColumn,SetSpan,SetColumnSpen一樣,這兩種方法是和attached 屬性有關。假如你可以的話,你可以改用CanVas.SetButtom或是CanVas.SetRight來指定子元件的位置!
有一些Shapes類別,特別是Line,Path,Polygon和Polyline,都有coodinate資料。假如你把這些元件加到Canvas的Children中,並且沒有設定任何的coodinate,他們將會依照元件的coodinate資料來排列。任何你設定的SetLeft/SetTop 而比較精確的coodinate位置會加到元件的coodinate資料。
許多元件,力如控制項,在Canvas中會正確的顯示大小。但是有一些卻不會!(例如:Rectangle/Ellipse類別)且這些你必須要指定精確的Width和Height。而設定Canvas本身的Width和Height其實也是滿常見的!
有一個可能也大家都希望的:元件可以在Canvas panel中重疊。如同你所看過的,你可以把許多元件放入一個Grid的cell中,但是這種情形就會變的很難控制!
使用Canvas,元件的版型就會很容易控制且可預期!先加到Children屬性的元件會被後來加入的元件覆蓋!
舉例來說,假設你想要一個按鈕顯示一個藍色的1.5方寸如下:

//-----------------------------------------------
// PaintTheButton.cs (c) 2006 by Charles Petzold
//-----------------------------------------------
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Petzold.PaintTheButton
{
public class PaintTheButton : Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new PaintTheButton());
}
public PaintTheButton()
{
Title = "Paint the Button";
// Create the Button as content of the window.
Button btn = new Button();
btn.HorizontalAlignment = HorizontalAlignment.Center;
btn.VerticalAlignment = VerticalAlignment.Center;
Content = btn;
// Create the Canvas as content of the button.
Canvas canv = new Canvas();
canv.Width = 144;
canv.Height = 144;
btn.Content = canv;
// Create Rectangle as child of canvas.
Rectangle rect = new Rectangle();
rect.Width = canv.Width;
rect.Height = canv.Height;
rect.RadiusX = 24;
rect.RadiusY = 24;
rect.Fill = Brushes.Blue;
canv.Children.Add(rect);
Canvas.SetLeft(rect, 0);
Canvas.SetRight(rect, 0);
// Create Polygon as child of canvas.
Polygon poly = new Polygon();
poly.Fill = Brushes.Yellow;
poly.Points = new PointCollection();
for (int i = 0; i < 5; i++)
{
double angle = i * 4 * Math.PI / 5;
Point pt = new Point(48 * Math.Sin(angle),
-48 * Math.Cos(angle));
poly.Points.Add(pt);
}
canv.Children.Add(poly);
Canvas.SetLeft(poly, canv.Width / 2);
Canvas.SetTop(poly, canv.Height / 2);
}
}
}
這個程式會建立一個按鈕,並且讓視窗的Content等於他!接下來會建立一個Canvas 1.5吋方正,並且讓其等於按鈕的Content! 因為這按鈕的Horizontal-Alignment和VerticalAlignment屬性會設成Center,所以這個按鈕會隨著Canvas的內容而改變大小!
這個程式會建立一個Rectangle shape,並且指定他的width和height,並且和Canvas一樣大小。Rectangle會被加入Canvas的children集合如下:
canv.Children.Add(rect);
下面的程式會確保Rectangle是在Canvas中靠左上角:
Canvas.SetLeft(rect, 0);
Canvas.SetRight(rect, 0);
嚴格來說,這兩個指令是不需要的,因為本來預設就是0
下一步會提到Polygon shape。這個Polygon 類別定義了一個屬性叫做Point,是一種PointCollection的型態,來存放polygon的point。然而,在Polygon物件新建立起來的時後,points屬性是null。你必須要明確地建立一種PointCollection以及指定他的Points屬性!
上面這個程式會用這種方式來顯示,會在建構子中指定。每個point之後會用Add方法來加入集合中!
PointCollection也會定義一個需要參數是Ienumerable<Point>的建構子,會需要一個Point的陣列物件,也可以是一個List<Point>的物件。
在for回圈中的程式會計算星星的points,這些points將會有X,Y在範圍-48~48間,會以(0,0)為中心,要取Canvas的中間,可以用下面來取得:
Canvas.SetLeft(poly, canv.Width / 2);
Canvas.SetTop(poly, canv.Height / 2);
你可以注意到五星的內部並沒有塗滿,這就是Polygon所定義的FillRule屬性的預設設定的結果。而FillRule.EvenOdd將會基於多邊形的邊來計算演算法來切割一塊特定的區域!一個區域會被填滿只會在某一方向的線的數量減去相反方向線的數量會是偶數。設定FillRule.NonZero就會被填滿中間!
預設:poly.FillRule = FillRule.EvenOdd;
改成: poly.FillRule = FillRule.Nonzero;

雖然你現在已經可以用Canvas來排版你的視窗,你將會發現還有許多礙角石,Cavas用來顯示圖片是很用好,且可以做一些用滑鼠畫畫圖,但是卻要盡量避免在一般的場合使用他!
下面的類別就是繼承自Cavas,在遊戲中實做一個拼塊!這類別定義一個參數叫做SIZE,2/3"方塊,且另外一個BORD的參數定義一個1/16"寬的版子,用來顯示3D樣貌,
依照慣例,物件在這個電腦螢幕上通常都是比較明亮的顯示,這個小版子會有兩個Polygon物件,會用SystemColors.ControlDarkBrush/SystemColors.ControlLightLightBrush 來圖色,呈現明暗不同的樣子!
// Tile.cs (c) 2006 by Charles Petzold
//-------------------------------------
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Petzold.PlayJeuDeTacquin
{
public class Tile : Canvas
{
const int SIZE = 64; // 2/3 inch
const int BORD = 6; // 1/16 inch
TextBlock txtblk;
public Tile()
{
Width = SIZE;
Height = SIZE;
// Upper-left shadowed border.
Polygon poly = new Polygon();
poly.Points = new PointCollection(new Point[]
{
new Point(0, 0), new Point(SIZE, 0),
new Point(SIZE-BORD, BORD),
new Point(BORD, BORD),
new Point(BORD, SIZE-BORD), new Point(0, SIZE)
});
poly.Fill = SystemColors.ControlLightLightBrush;
Children.Add(poly);
// Lower-right shadowed border.
poly = new Polygon();
poly.Points = new PointCollection(new Point[]
{
new Point(SIZE, SIZE), new Point(SIZE, 0),
new Point(SIZE-BORD, BORD),
new Point(SIZE-BORD, SIZE-BORD),
new Point(BORD, SIZE-BORD), new Point(0, SIZE)
});
poly.Fill = SystemColors.ControlDarkBrush;
Children.Add(poly);
// Host for centered text.
Border bord = new Border();
bord.Width = SIZE - 2 * BORD;
bord.Height = SIZE - 2 * BORD;
bord.Background = SystemColors.ControlBrush;
Children.Add(bord);
SetLeft(bord, BORD);
SetTop(bord, BORD);
// Display of text.
txtblk = new TextBlock();
txtblk.FontSize = 32;
txtblk.Foreground = SystemColors.ControlTextBrush;
txtblk.HorizontalAlignment = HorizontalAlignment.Center;
txtblk.VerticalAlignment = VerticalAlignment.Center;
bord.Child = txtblk;
}
// Public property to set text.
public string Text
{
set { txtblk.Text = value; }
get { return txtblk.Text; }
}
}
}

注意到這個程式設定Polygon物件的Points集合的方法,整個Point陣列會被定義在PointCollection建構子中。
Tile類別也必須要在方塊中間顯示一些字!不要嘗試去想出TextBlock的尺寸然後把他放在Canvas中,還有更簡單的方法!
再用兩個border建立他這個border後,Tile類別接下來會建立一個真的Border型態的物件,會由Decorator繼承FrameworkElement下來,這個Border元件會被放在方塊的正中間,Decorator會定義一個屬性叫做Child,可以控制一個UIElement,也就是TextBlock,這個TextBlock會設定他的Horizontal和VeriticalAlignment,來放在Border的中間,Border物件會顯示一個有實體顏色的border,但是這個Tile程式目前不需要這功能!
這個排版是會用一個StackPanel包含了兩個children,一個是scramble按鈕另外一個是border物件。Border物件在此只是為了美學的觀點存在,他會用細線畫出來切割這些按鈕,而Border的Child會是一個UniformGrid 的panel,會有15個拼塊和一個空白的地方!
這個程式會藉由操控UniformGrid的children 來控制所有的方塊的移動!且這會發生在按鈕的MoveTile方法,MoveTile的兩個參數, void MoveTile(int xTile, int yTile) :也就是看哪一個方塊被移動!而要移動到哪裡這是毫無疑問的,因為只能移往空白的地方!注意到MoveTile的程式會先從Children集合中取得被這兩個index所包含的兩個元件,並且互換,且用了兩次的remove和insert來改變位置,當你移除或是塞入一個child元件,就會重新改變順序!
這個程式有一個keyboard和滑鼠的介面,滑鼠的介面滑鼠介面就是用MouseLeftButtonDown event handler 來處理,我們要找到特定的Tile物件被點選,可以輕易的從取得第一個event handle的參數中去判斷。使用者也可以一次移動多個,這就是他有寫for loop的原因。
這個鍵盤介面就是用OnKeyDown方法,上下左右按鍵的游標移動可以移動方塊到空白處!
這個列欄的數量的指定是由兩個欄位來指定!你可以依據你想要的去改變!但可能會在四位數的列數行數會有問題,且答案鍵也會跑很久!︿︿
//--------------------------------------
// Empty.cs (c) 2006 by Charles Petzold
//--------------------------------------
namespace Petzold.PlayJeuDeTacquin
{
class Empty : System.Windows.FrameworkElement
{
}
}
//-------------------------------------------------
// PlayJeuDeTacquin.cs (c) 2006 by Charles Petzold
//-------------------------------------------------
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace Petzold.PlayJeuDeTacquin
{
public class PlayJeuDeTacquin : Window
{
const int NumberRows = 4;
const int NumberCols = 4;
UniformGrid unigrid;
int xEmpty, yEmpty, iCounter;
Key[] keys = { Key.Left, Key.Right, Key.Up, Key.Down };
Random rand;
UIElement elEmptySpare = new Empty();
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new PlayJeuDeTacquin());
}
public PlayJeuDeTacquin()
{
Title = "Jeu de Tacquin";
SizeToContent = SizeToContent.WidthAndHeight;
ResizeMode = ResizeMode.CanMinimize;
Background = SystemColors.ControlBrush;
// Create StackPanel as content of window.
StackPanel stack = new StackPanel();
Content = stack;
// Create Button at top of window.
Button btn = new Button();
btn.Content = "_Scramble";
btn.Margin = new Thickness(10);
btn.HorizontalAlignment = HorizontalAlignment.Center;
btn.Click += ScrambleOnClick;
stack.Children.Add(btn);
// Create Border for aesthetic purposes.
Border bord = new Border();
bord.BorderBrush = SystemColors.ControlDarkDarkBrush;
bord.BorderThickness = new Thickness(1);
stack.Children.Add(bord);
// Create Unigrid as Child of Border.
unigrid = new UniformGrid();
unigrid.Rows = NumberRows;
unigrid.Columns = NumberCols;
bord.Child = unigrid;
// Create Tile objects to fill all but one cell.
for (int i = 0; i < NumberRows * NumberCols - 1; i++)
{
Tile tile = new Tile();
tile.Text = (i + 1).ToString();
tile.MouseLeftButtonDown += TileOnMouseLeftButtonDown;
unigrid.Children.Add(tile);
}
// Create Empty object to fill the last cell.
unigrid.Children.Add(new Empty());
xEmpty = NumberCols - 1;
yEmpty = NumberRows - 1;
}
void TileOnMouseLeftButtonDown(object sender, MouseButtonEventArgs args)
{
Tile tile = sender as Tile;
int iMove = unigrid.Children.IndexOf(tile);
int xMove = iMove % NumberCols;
int yMove = iMove / NumberCols;
if (xMove == xEmpty)
while (yMove != yEmpty)
MoveTile(xMove, yEmpty + (yMove - yEmpty) /
Math.Abs(yMove - yEmpty));
if (yMove == yEmpty)
while (xMove != xEmpty)
MoveTile(xEmpty + (xMove - xEmpty) /
Math.Abs(xMove - xEmpty), yMove);
}
protected override void OnKeyDown(KeyEventArgs args)
{
base.OnKeyDown(args);
switch (args.Key)
{
case Key.Right: MoveTile(xEmpty - 1, yEmpty); break;
case Key.Left: MoveTile(xEmpty + 1, yEmpty); break;
case Key.Down: MoveTile(xEmpty, yEmpty - 1); break;
case Key.Up: MoveTile(xEmpty, yEmpty + 1); break;
}
}
void ScrambleOnClick(object sender, RoutedEventArgs args)
{
rand = new Random();
iCounter = 16 * NumberCols * NumberRows;
DispatcherTimer tmr = new DispatcherTimer();
tmr.Interval = TimeSpan.FromMilliseconds(10);
tmr.Tick += TimerOnTick;
tmr.Start();
}
void TimerOnTick(object sender, EventArgs args)
{
for (int i = 0; i < 5; i++)
{
MoveTile(xEmpty, yEmpty + rand.Next(3) - 1);
MoveTile(xEmpty + rand.Next(3) - 1, yEmpty);
}
if (0 == iCounter--)
(sender as DispatcherTimer).Stop();
}
void MoveTile(int xTile, int yTile)
{
if ((xTile == xEmpty && yTile == yEmpty) ||
xTile < 0 || xTile >= NumberCols ||
yTile < 0 || yTile >= NumberRows)
return;
int iTile = NumberCols * yTile + xTile;
int iEmpty = NumberCols * yEmpty + xEmpty;
UIElement elTile = unigrid.Children[iTile];
UIElement elEmpty = unigrid.Children[iEmpty];
unigrid.Children.RemoveAt(iTile);
unigrid.Children.Insert(iTile, elEmptySpare);
unigrid.Children.RemoveAt(iEmpty);
unigrid.Children.Insert(iEmpty, elTile);
xEmpty = xTile;
yEmpty = yTile;
elEmptySpare = elEmpty;
}
}
}

