Chapter 7. Canvas


The Canvas panel is the layout option closest to traditional graphical environments. You specify where elements go using coordinate positions. As with the rest of the Windows Presentation Foundation, these coordinates are device-independent units of 1/96 inch relative to the upper-left corner.

You may have noticed that elements themselves have no X or Y or Left or Top property. When using a Canvas panel, you specify the location of child elements with the static methods Canvas.SetLeft and Canvas.SetTop. Like the SetDock method defined by DockPaneland the SetRow, SetColumn, SetRowSpan, and SetColumnSpan methods defined by GridSetLeft and SetTop are associated with attached properties defined by the Canvas class. If you'd like, you can alternatively use Canvas.SetRight or Canvas.SetBottom, to specify the location of the right or bottom of the child element relative to the right or bottom of the Canvas.

Some of the Shapes classesspecifically, Line, Path, Polygon, and Polylinealready contain coordinate data. If you add these elements to the Children collection of a Canvas panel and don't set any coordinates, they will be positioned based on the coordinate data of the element. Any explicit coordinate position that you set with SetLeft or SetTop is added to the coordinate data of the element.

Many elements, such as controls, will properly size themselves on a Canvas. However, some elements will not (for example, the Rectangle and Ellipse classes), and for those you must assign explicit Width and Height values. It is also common to assign the Width and Height properties of the Canvas panel itself.

It's also possibleand often desirableto overlap elements on a Canvas panel. As you've seen, you can put multiple elements into the cells of a Grid, but the effect is often difficult to control. With Canvas, the layering of elements is easy to control and predict. The elements added to the Children collection earlier are covered by those added later.

For example, suppose you want a button to display a blue 1.5-inch-square background with rounded corners and a yellow star one inch in diameter centered within the square. Here's the code:

PaintTheButton.cs

[View full width]

//----------------------------------------------- // 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); } } }



The program creates a Button and makes that the content of the window. It then creates a Canvas 1.5 inches square and makes that the content of the button. Because the HorizontalAlignment and VerticalAlignment properties of the button have been set to Center, the button will size itself to the size of the Canvas panel.

The program then creates a Rectangle shape and assigns its Width and Height properties to be the same as the Canvas. The Rectangle is added to the Canvas children collection in the same way as other panels:

canv.Children.Add(rect); 


The following code ensures that the Rectangle is positioned at the top-left corner of the Canvas:

Canvas.SetLeft(rect, 0); Canvas.SetRight(rect, 0); 


Strictly speaking, these two statements are not required because the default settings are 0.

The next step involves a Polygon shape. The Polygon class defines a property named Points of type PointCollection to store the points of the polygon. However, the Points property in a newly created Polygon object is null. You must explicitly create an object of type PointCollection and assign it to the Points property. The PaintTheButton program shows one way of doing this that involves the parameterless constructor of PointCollection. Each point is then added to the collection with the Add method.

PointCollection also defines a constructor that requires an argument of type IEnumerable<Point>. An array of Point objects is acceptable here, as well as a List<Point> collection.

The code in the for loop calculates the points of the star. These points will have X and Y coordinates that range from 48 to 48, with the center of the ellipse at the point (0, 0). To center the ellipse within the Canvas, the following code offsets all points in the polygon by half the width and height of the Canvas:

Canvas.SetLeft(poly, canv.Width / 2); Canvas.SetTop(poly, canv.Height / 2); 


You'll notice that the interior pentagon of the star is not filled. That's a result of the default setting of the FillRule property defined by Polygon. The enumeration value FillRule.EvenOdd implements an algorithm based on the lines of the polygon that separate a particular enclosed area from infinity. An area is filled only if the number of lines going in one direction minus the number of lines going in the opposite direction is odd. Set FillRule to FillRule.NonZero to fill the center of the star as well.

Although you can certainly use Canvas for laying out controls in your window, you'll probably find it more of a hindrance than a help in that job. Canvas is good for displaying graphics (as you'll see in Part 2 of this book), and doing mouse-driven drawing (shown in Chapter 9, coming up soon) but avoid it for general layout jobs.

The following class actually derives from Canvas to implement a tile used in a game. The class defines a constant named SIZE that defines a size of 2/3-inch square for the tile, and another constant named BORD that defines a 1/16-inch-wide border. This border is shaded to give the appearance of three-dimensionality. By convention, objects on the computer screen are shaded to mimic a light source at the upper-left corner. The border consists of two Polygon objects colored with SystemColors.ControlLightLightBrush (for the top and left edges) and SystemColors.ControlDarkBrush for the shadow at the right and bottom edges. The interior of the tile thus appears to be raised.

Tile.cs

[View full width]

//------------------------------------- // 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; } } } }



Notice the way in which the program sets the Points collection of the Polygon objects: The entire Point array is defined in the PointCollection constructor!

The Tile class must also display some text in the center of the tile. Rather than trying to figure out the actual size of the TextBlock element and centering it on the Canvas, the class takes an easy way out. After creating its own border from two polygons, the Tile class then creates an actual object of type Border, which is a class that derives from FrameworkElement by way of Decorator. This Border element is positioned in the center of the tile. Decorator defines (and Border inherits) a property named Child that can hold a single instance of UIElement, and that's the TextBlock. The TextBlock sets its HorizontalAlignment and VerticalAlignment properties to Center to sit in the center of the Border object. The Border object can display a border of a solid color (and with rounded edges), but Tile doesn't require that feature.

The Tile class is for a puzzle called Jeu de Tacquinthe teasing gameor in English, the 14-15 Puzzle. The puzzle was probably invented in the 1870s by American puzzle-maker Sam Loyd (18411911). In its classic form, the game consists of 15 numbered squares in a 4 x 4 grid, leaving one blank cell so that you can move the squares around. Here's the class for the blank cell:

Empty.cs

//-------------------------------------- // Empty.cs (c) 2006 by Charles Petzold //-------------------------------------- namespace Petzold.PlayJeuDeTacquin {     class Empty : System.Windows.FrameworkElement     {     } } 



In its original form, the numbered squares were arranged in numeric order except with the 14 and 15 swapped. Sam Loyd offered $1000 to anyone who could find a way to shift the squares to correct the numeric order. A solution is impossible, as was first disclosed in an 1879 article in the American Journal of Mathematics. An analysis appears in volume 4 of The World Of Mathematics (Simon and Schuster, 1956).

In computer form, Jeu de Tacquin was one of the first game programs created for the Apple Macintosh, where it was called PUZZLE, and it also made an appearance in early versions of the Microsoft Windows Software Development Kit (renamed MUZZLE) as the only sample program coded in Microsoft Pascal rather than C.

Both the Macintosh and Windows versions of the puzzle displayed the cells in correct order with a menu option to scramble the cells. My version has a Button for scrambling the cells, and it creates a DispatcherTimer for the job so that you can watch the scrambling action.

The layout begins with a StackPanel with two children: the scramble button and another Border object. The Border object here is strictly for aesthetic purposes: It draws a thin line to separate the Button from the content of the Border. The Child property of the Border is a UniformGrid panel, which holds the 15 Tile objects and the one Empty.

PlayJeuDeTacquin.cs

[View full width]

//------------------------------------------------- // 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; } } }



The program handles all movement of the Tile objects by manipulating the Children collection of the UniformGrid, and that happens in the MoveTile method at the very bottom of the file. The two arguments to MoveTile are the horizontal and vertical grid coordinates of the tile to be moved. There's no ambiguity about where that tile is to be moved because it can only be moved into the cell currently occupied by the Empty object. Notice that the MoveTile code first obtains the two elements being swapped by indexing the Children collection, and then performs a pair of RemoveAt and Insert calls to exchange their places. Watch out for the order of statements like this: Any time you remove or insert a child element, the indices of all the child elements after it change.

The program has both a keyboard and a mouse interface. The mouse interface is in the form of an event handler for the MouseLeftButtonDown event of the Tile object. The particular Tile object being clicked is easily obtainable by casting the first argument of the event handler. The user can move multiple tiles with a single mouse click, and that's the purpose of the while loop.

The keyboard interface is in the form of an override to the window's OnKeyDown method. The cursor up, down, left, and right keys move a tile adjoining the empty cell into the empty cell.

The number of rows and columns in the PlayJeuDeTacquin program are indicated by two fields. You can change those to whatever you want, although the display of numbers in the tiles doesn't quite work when it gets into the 4-digit range, and the scrambling operation may go on for awhile.




Applications = Code + Markup. A Guide to the Microsoft Windows Presentation Foundation
Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation (Pro - Developer)
ISBN: 0735619573
EAN: 2147483647
Year: 2006
Pages: 72

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net