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:
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.
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:
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.
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. |