Chapter 12. Custom Panels


Some descendants of FrameworkElement have children and some do not. Those that don't include Image and all the Shape descendants. Other descendants of FrameworkElementsuch as everything that derives from ContentControl and Decoratorhave the capability to support a single child, although often the child can also have a nested child. A class that inherits from FrameworkElement can also support multiple children, and the descendants of Panel are one important category of such elements. But it's not necessary to inherit from Panel to host multiple children. For example, InkCanvas inherits directly from FrameworkElement and maintains a collection of multiple children.

In this chapter I will show you how to inherit from Panel and how to support multiple children without inheriting from Panel, and you'll probably understand why inheriting from Panel is easier. The big gift of Panel is the definition of the Children property for storing the children. This property is of type UIElementCollection, and that collection itself handles the calling of AddVisualChild, AddLogicalChild, RemoveVisualChild, and RemoveLogicalChild when children are added to or removed from the collection. UIElementCollection is able to perform this feat because it has knowledge of the parent element. The sole constructor of UIElementCollection requires two arguments: a visual parent of type UIElement and a logical parent of type FrameworkElement. The two arguments can be identical, and usually are.

As you can determine from examining the documentation of Panel, the Panel class overrides VisualChildrenCount and GetVisualChild and handles these for you. When inheriting from Panel it is usually not necessary to override OnRender, either. The Panel class defines a Background property and undoubtedly simply calls DrawRectangle with the background brush during its OnRender override.

That leaves MeasureOverride and ArrangeOverridethe two essential methods you must implement in your panel class. The Panel documentation gives you some advice on implementing these methods: It recommends that you use InternalChildren rather than Children to obtain the collection of children. The InternalChildren property (also an object of type UIElementCollection) includes everything in the normal Children collection plus children added through data binding.

Perhaps the simplest type of panel is the UniformGrid. This grid contains a number of rows that have the same height, and columns that have the same width. To illustrate what's involved in implementing a panel, the following UniformGridAlmost class attempts to duplicate the functionality of UniformGrid. The class defines a property named Columns backed by a dependency property that indicates a default value of 1. UniformGridAlmost does not attempt to figure out the number of columns and rows based on the number of children. It requires that the Columns property be set explicitly, and then determines the number of rows based on the number of children. This calculated Rows value is available as a read-only property. UniformGridAlmost doesn't include a FirstColumn property, either. (That's why I named it Almost.)

UniformGridAlmost.cs

[View full width]

// UniformGridAlmost.cs (c) 2006 by Charles Petzold using System; using System.Windows; using System.Windows.Input; using System.Windows.Controls; using System.Windows.Media; namespace Petzold.DuplicateUniformGrid { class UniformGridAlmost : Panel { // Public static readonly dependency properties. public static readonly DependencyProperty ColumnsProperty; // Static constructor to create dependency property. static UniformGridAlmost() { ColumnsProperty = DependencyProperty.Register( "Columns", typeof(int), typeof (UniformGridAlmost), new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsMeasure)); } // Columns property. public int Columns { set { SetValue(ColumnsProperty, value); } get { return (int)GetValue (ColumnsProperty); } } // Read-Only Rows property. public int Rows { get { return (InternalChildren.Count + Columns - 1) / Columns; } } // Override of MeasureOverride apportions space. protected override Size MeasureOverride (Size sizeAvailable) { // Calculate a child size based on uniform rows and columns. Size sizeChild = new Size (sizeAvailable.Width / Columns, sizeAvailable.Height / Rows); // Variables to accumulate maximum widths and heights. double maxwidth = 0; double maxheight = 0; foreach (UIElement child in InternalChildren) { // Call Measure for each child ... child.Measure(sizeChild); // ... and then examine DesiredSize property of child. maxwidth = Math.Max(maxwidth, child.DesiredSize.Width); maxheight = Math.Max(maxheight, child.DesiredSize.Height); } // Now calculate a desired size for the grid itself. return new Size(Columns * maxwidth, Rows * maxheight); } // Override of ArrangeOverride positions children. protected override Size ArrangeOverride (Size sizeFinal) { // Calculate a child size based on uniform rows and columns. Size sizeChild = new Size(sizeFinal .Width / Columns, sizeFinal .Height / Rows); for (int index = 0; index < InternalChildren.Count; index++) { int row = index / Columns; int col = index % Columns; // Calculate a rectangle for each child within sizeFinal ... Rect rectChild = new Rect(new Point(col * sizeChild.Width, row * sizeChild.Height), sizeChild); // ... and call Arrange for that child. InternalChildren[index].Arrange (rectChild); } return sizeFinal; } } }



For MeasureOverride, the class first calculates an available size for each child, assuming that the area is divided by equal rows and columns. (Keep in mind that sizeAvailable could be infinite in one or both dimensions, in which case sizeChild will also have an infinite dimension or two.) The method calls Measure for each child and then examines DesiredSize, keeping a running accumulation of the width of the widest element and the height of the tallest element in the grid. Preferably, the grid should be large enough so that every cell is this width and height.

The ArrangeOverride method gives the panel the opportunity to arrange all the children in a grid. From the argument to the method, it's easy to calculate the width and height of each cell and the position of each child. The method calls Arrange for each child and then returns.

The UniformGridAlmost class and the following DuplicateUniformGrid class comprise the DuplicateUniformGrid project.

DuplicateUniformGrid.cs

[View full width]

//--------- -------------------------------------------- // DuplicateUniformGrid.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.DuplicateUniformGrid { public class DuplicateUniformGrid : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new DuplicateUniformGrid()); } public DuplicateUniformGrid() { Title = "Duplicate Uniform Grid"; // Create UniformGridAlmost as content of window. UniformGridAlmost unigrid = new UniformGridAlmost(); unigrid.Columns = 5; Content = unigrid; // Fill UniformGridAlmost with randomly-sized buttons. Random rand = new Random(); for (int index = 0; index < 48; index++) { Button btn = new Button(); btn.Name = "Button" + index; btn.Content = btn.Name; btn.FontSize += rand.Next(10); unigrid.Children.Add(btn); } AddHandler(Button.ClickEvent, new RoutedEventHandler(ButtonOnClick)); } void ButtonOnClick(object sender, RoutedEventArgs args) { Button btn = args.Source as Button; MessageBox.Show(btn.Name + " has been clicked", Title); } } }



The program creates a UniformGridAlmost object, sets 5 columns, and then fills it with 48 buttons of various sizes. Here are several experiments you'll want to try:

  • Set SizeToContent of the window to SizeToContent.WidthAndHeight. The window and the grid now collapse so that all cells are the size of the largest button.

  • Set HorizontalAlignment and VerticalAlignment of the grid to Center. Now the window remains a default size but the grid collapses so that all cells are the size of the largest button.

  • Set HorizontalAlignment and VerticalAlignment of each button to Center. Now the grid remains the same size as the client area, and each cell of the grid is a uniform size, but the buttons are all sized to their content.

  • Set Height and Width properties of the grid to values inadequate for all the buttons. Now the grid tries to fit all the buttons in the grid, but part of the grid is truncated.

In other words, the grid is behaving as we might hope and expect. To further test it, dig out the ColorGrid control from the last chapter and replace UniformGrid with UniformGridAlmost.

Usually, you will first encounter the concept of attached properties with panels. DockPanel, Grid, and Canvas all have attached properties. As you recall, when you add an element to a Canvas, you also set two attached properties that apply to the element:

canv.Children.Add(el); Canvas.SetLeft(el, 100); Canvas.SetTop(el, 150); 


When I first introduced attached properties, I mentioned that the panels used these attached properties when laying out their children. It is now time to examine how this is done.

The CanvasClone class shown here is very much like Canvas except that it implements only the Left and Top attached properties and doesn't bother with Right and Bottom.

CanvasClone.cs

[View full width]

//-------------------------------------------- // CanvasClone.cs (c) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.PaintOnCanvasClone { public class CanvasClone : Panel { // Define two dependency properties. public static readonly DependencyProperty LeftProperty; public static readonly DependencyProperty TopProperty; static CanvasClone() { // Register the dependency properties as attached properties. // Default value is 0 and any change invalidates parent's arrange. LeftProperty = DependencyProperty .RegisterAttached("Left", typeof(double), typeof (CanvasClone), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions .AffectsParentArrange)); TopProperty = DependencyProperty .RegisterAttached("Top", typeof(double), typeof (CanvasClone), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions .AffectsParentArrange)); } // Static methods to set and get attached properties. public static void SetLeft (DependencyObject obj, double value) { obj.SetValue(LeftProperty, value); } public static double GetLeft (DependencyObject obj) { return (double)obj.GetValue(LeftProperty); } public static void SetTop(DependencyObject obj, double value) { obj.SetValue(TopProperty, value); } public static double GetTop (DependencyObject obj) { return (double)obj.GetValue(TopProperty); } // Override of MeasureOverride just calls Measure on children. protected override Size MeasureOverride (Size sizeAvailable) { foreach (UIElement child in InternalChildren) child.Measure(new Size(Double .PositiveInfinity, Double .PositiveInfinity)); // Return default value (0, 0). return base.MeasureOverride (sizeAvailable); } // Override of ArrangeOverride positions children. protected override Size ArrangeOverride (Size sizeFinal) { foreach (UIElement child in InternalChildren) child.Arrange(new Rect( new Point(GetLeft(child), GetTop(child)), child.DesiredSize)); return sizeFinal; } } }



There is actually nothing in the class that is named Left or Top. The static read-only fields LeftProperty and TopProperty are defined exactly like regular dependency properties. The static constructor, however, registers these properties with the static method DependencyProperty.RegisterAttached. Take careful note of the metadata option FrameworkPropertyMetadataOptions.AffectsParentArrange. These attached properties are always attached to a child of the canvas, so whenever the property changes, it affects the parent's arrangementthat is, the canvas's arrangementbecause the canvas is responsible for positioning its children. There is a similar flag named AffectsParentMeasure to use when a change in an attached property affects the size of the parent and not just the layout.

The class next defines four static methods for setting and getting these attached properties. When these methods are called, the first argument will probably be a child of the canvas, or an element that is soon to be a child of the canvas. Notice that the Set methods call the child's SetValue method so that the property is stored with the child.

All MeasureOverride does is call Measure for each of its children with an infinite size. This is consistent with Canvas. Generally, elements placed on a canvas are given an explicit size, or otherwise have a size that is fairly fixed (such as a bitmap image or a polyline). The call to Measure is necessary to allow the child to calculate its DesiredSize property, which is used during ArrangeOverride. MeasureOverride returns the value from the base class implementation of the method, which is the size (0, 0).

The ArrangeOverride method simply calls Arrange on each of its children, positioning the child at the location obtained from the GetLeft and GetTop methods. These methods call GetValue on the child. (At this point, an interesting distinction between classes that define dependency properties and those that define only attached properties becomes evident. A class that defines dependency properties must derive from DependencyObject because the class calls the SetValue and GetValue methods that it inherits from DependencyObject. However, a class that defines attached properties need not derive from DependencyObject, because it doesn't need to call its own SetValue and GetValue methods.)

Here's a simple program that demonstrates that CanvasClone can actually position elements on its surface:

PaintOnCanvasClone.cs

[View full width]

//--------------------------------------------------- // PaintOnCanvasClone.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.PaintOnCanvasClone { public class PaintOnCanvasClone : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new PaintOnCanvasClone()); } public PaintOnCanvasClone() { Title = "Paint on Canvas Clone"; Canvas canv = new Canvas(); Content = canv; SolidColorBrush[] brushes = { Brushes.Red, Brushes.Green, Brushes.Blue }; for (int i = 0; i < brushes.Length; i++) { Rectangle rect = new Rectangle(); rect.Fill = brushes[i]; rect.Width = 200; rect.Height = 200; canv.Children.Add(rect); Canvas.SetLeft(rect, 100 * (i + 1)); Canvas.SetTop(rect, 100 * (i + 1)); } } } }



To really give CanvasClone a workout, however, you'll want to include it in the DrawCircles program from Chapter 9. (It was only after I tried CanvasClone in DrawCircles that I realized the importance of the AffectsParentArrange flag on the attached property metadata.) Be sure to change every mention of Canvas in that program to Petzold.PaintOnCanvasClone.CanvasClone. Several methods in the program deal with the Left and Top attached properties, and those must all be changed to use CanvasClone. Unfortunately, you won't get any kind of error if the program sets or gets an attached property for a nonexistent Canvas class. But the program will only work if you set and get the attached properties for CanvasClone.

If you create a class that supports multiple children but doesn't inherit from Panel, you'll probably want to define a property named Children of type UIElementCollection, and also a Background property of type Brush. That approach comes closest to Panel itself.

On the other hand, it's also instructive to create a panel-like element by inheriting directly from FrameworkElement and implementing a custom child collection that is not an object of type UIElementCollection (if only to see what the problems are).

It is tempting to implement this child collection by first defining a private field like so:

List<UIElement> children = new List<UIElement>(); 


And then to expose this collection through a public property:

public List<UIElement> Children {     get { return children; } } 


That way, you could add children to this collection in the same way you add children to one of the regular panels:

pnl.Children.Add(btn); 


But something is missing. The class that defines this children field and Children property has no way of knowing when something is added to or removed from the collection. The class needs to know exactly what's going into the collection and what's being removed so that it can properly call AddVisualChild, AddLogicalChild, RemoveVisualChild, and RemoveLogicalChild.

One possible solution is to write a class somewhat similar to UIElementCollection that either performs these jobs itself, or notifies the panel class when a child has entered or left the collection.

Or, you could keep the simple private children field and instead define methods like this:

public void Add(UIElement el) {     children.Add(el);     AddVisualChild(el);     AddLogicalChild(el); } 


and like this:

public void Remove(UIElement el) {     children.Remove(el);     RemoveVisualChild(el);     RemoveLogicalChild(el); } 


So instead of adding a child element to the panel like this:

pnl.Children.Add(btn); 


you do it like this:

pnl.Add(btn); 


How many of these methods you want to write is up to you. The Add is essential, of course. Remove, IndexOf, Count, and perhaps an indexer are optional, depending on the application.

Here's a class that inherits from FrameworkElement, maintains its own collection of children, and arranges the children in a diagonal pattern from upper left to lower right.

DiagonalPanel.cs

[View full width]

//----------------------------------------------- // DiagonalButton.cs (c) 2006 by Charles Petzold //----------------------------------------------- using System; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.DiagonalizeTheButtons { class DiagonalPanel : FrameworkElement { // Private children collection. List<UIElement> children = new List<UIElement>(); // Private field. Size sizeChildrenTotal; // Dependency Property. public static readonly DependencyProperty BackgroundProperty; // Static constructor to create Background dependency property. static DiagonalPanel() { BackgroundProperty = DependencyProperty.Register( "Background", typeof(Brush), typeof(DiagonalPanel), new FrameworkPropertyMetadata (null, FrameworkPropertyMetadataOptions.AffectsRender)); } // Background property. public Brush Background { set { SetValue(BackgroundProperty, value); } get { return (Brush)GetValue (BackgroundProperty); } } // Methods to access child collection. public void Add(UIElement el) { children.Add(el); AddVisualChild(el); AddLogicalChild(el); InvalidateMeasure(); } public void Remove(UIElement el) { children.Remove(el); RemoveVisualChild(el); RemoveLogicalChild(el); InvalidateMeasure(); } public int IndexOf(UIElement el) { return children.IndexOf(el); } // Overridden properties and methods protected override int VisualChildrenCount { get { return children.Count; } } protected override Visual GetVisualChild (int index) { if (index >= children.Count) throw new ArgumentOutOfRangeException("index"); return children[index]; } protected override Size MeasureOverride (Size sizeAvailable) { sizeChildrenTotal = new Size(0, 0); foreach (UIElement child in children) { // Call Measure for each child ... child.Measure(new Size(Double .PositiveInfinity, Double .PositiveInfinity)); // ... and then examine DesiredSize property of child. sizeChildrenTotal.Width += child .DesiredSize.Width; sizeChildrenTotal.Height += child .DesiredSize.Height; } return sizeChildrenTotal; } protected override Size ArrangeOverride (Size sizeFinal) { Point ptChild = new Point(0, 0); foreach (UIElement child in children) { Size sizeChild = new Size(0, 0); sizeChild.Width = child .DesiredSize.Width * (sizeFinal .Width / sizeChildrenTotal.Width); sizeChild.Height = child .DesiredSize.Height * (sizeFinal .Height / sizeChildrenTotal.Height); child.Arrange(new Rect(ptChild, sizeChild)); ptChild.X += sizeChild.Width; ptChild.Y += sizeChild.Height; } return sizeFinal; } protected override void OnRender (DrawingContext dc) { dc.DrawRectangle(Background, null, new Rect(new Point(0, 0), RenderSize)); } } }



The class defines a Background property (backed by a dependency property) and implements OnRender simply by drawing this background brush over its entire surface. The class implements the Add and Remove methods I showed earlier, as well as an IndexOf method. The class also has very simple overrides of VisualChildrenCount and GetVisualChild.

It wasn't immediately obvious to me what Size values I should pass to the children's Measure methods during MeasureOverride. I first thought it should be similar to UniformGridAlmost: If the panel had seven children, the argument to Measure should be 1/7 of the sizeAvailable parameter to MeasureOverride. But that's not right: If one child is much larger than the other, that child should get more space. I then decided that the entire sizeAvailable parameter should be passed to the Measure method of each child, and then convinced myself that wasn't right either. It would be wrong if a child assumed that it had exclusive use of all that space.

The only alternative was to give Measure a Size argument of infinite dimensions, and let the child determine its size without any prompting. It's exactly the same as if you created a Grid and gave each of its rows and columns a GridLength of Auto, and then put children in the diagonal cells.

In ArrangeOverride, however, I decided to do something a little different. The class retains the sizeChildrenTotal value calculated during MeasureOverride as a field, and then uses thattogether with the sizeFinal parameter to ArrangeOverride and the DesiredSize property of each childto scale the children up to the final size of the panel. Remember: A call to MeasureOverride is always followed by a call to ArrangeOverride, so that anything you calculate during MeasureOverride (such as sizeChildrenTotal) you can later use in ArrangeOverride.

The DiagonalizeTheButtons program creates a panel of type DiagonalPanel and puts five buttons in it of various sizes. The panel is always at least the size necessary to fit all five buttons, but the buttons become bigger if the panel is larger than that size.

DiagonalizeTheButtons.cs

[View full width]

//--------- --------------------------------------------- // DiagonalizeTheButtons.cs (c) 2006 by Charles Petzold //------------------------------------------------ ------ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.DiagonalizeTheButtons { public class DiagonalizeTheButtons : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new DiagonalizeTheButtons()); } public DiagonalizeTheButtons() { Title = "Diagonalize the Buttons"; DiagonalPanel pnl = new DiagonalPanel(); Content = pnl; Random rand = new Random(); for (int i = 0; i < 5; i++) { Button btn = new Button(); btn.Content = "Button Number " + (i + 1); btn.FontSize += rand.Next(20); pnl.Add(btn); } } } }



The final example in this chapter is a radial panelthat is, a panel that arranges its children in a circle. But before you start coding such a panel, you really need to nail down some conceptual issues.

A circle can be divided into pie slice wedges, and probably the most straightforward approach to arranging elements in a circle is to position each element in its own wedge. To take maximum advantage of the space available in the wedge, the element must snugly fit within the circumference of the circle at the outer part of the wedge, as shown here:

This particular wedge happens to be located at the far right part of the total circle, so the element is oriented normally. That's certainly convenient, but for other wedges, the element must be rotated so that it fits in the wedge in the same way.

The panel width and height equals twice the radius R, and all the angles a total 360 degrees. For a particular child element size, the larger the radius, the smaller the angle. There are two general ways to go about apportioning space for the children: If the size of the panel is fixed, angles can be apportioned based on the size of each element. This approach is best if you expect the children to vary widely in size.

However, if you expect all the children to be about the same size, it makes more sense to give each element the same angle, so α equals 360 degrees divided by the number of children. The radius R can then be calculated based on the largest child. This is the approach that I took.

It is fairly straightforward to calculate a radius R based on a fixed angle α and a width (W) and height (H) of a child element. First, construct a line A that bisects angle α and extends to the inner edge of the child:

You can calculate the length of A like so:


In the code coming up for the radial panel, this length A is called innerEdgeFromCenter, referring to the inner edge of the element and the center of the circle. The program also calculates outerEdgeFromCenter, which is innerEdgeFromCenter plus the width W of the element, as shown here:

Notice the other line drawn from the center to the top-right corner of the element. That too is a radius. It is now possible to use the Pythagorean theorem to calculate the length of that line:


The diagrams of the pie wedge show an element that is nearly square, but many elements (such as buttons or text blocks) are wider than they are high. Both the width and the height play a role in determining the radius, but the orientation shown in the diagrams suggests that the element's height dominates, and that a collection of elements will be arranged on the panel like so:

It is also possible to orient the buttons so that the width dominates the calculation:

I call these two possibilities ByHeight and ByWidth, and they are represented as two members of an enumeration:

RadialPanelOrientation.cs

[View full width]

//--------- ---------------------------------------------- // RadialPanelOrientation.cs (c) 2006 by Charles Petzold //------------------------------------------------ ------- namespace Petzold.CircleTheButtons { public enum RadialPanelOrientation { ByWidth, ByHeight } }



The RadialPanel class defines a property named Orientation of type RadialPanelOrientation backed by a dependency property that indicates a default value of ByWidth (corresponding to the second of the two diagrams of buttons arranged in a circle). The class inherits from Panel, so it can take advantage of the Children and InternalChildren collections as well as the Background property. (It has enough to do without worrying about the routine stuff!)

RadialPanel also defines another property named ShowPieLines that is similar to the ShowGridLines property of Grid. It is intended solely for experimentation.

RadialPanel.cs

[View full width]

//-------------------------------------------- // RadialPanel.cs (c) 2006 by Charles Petzold //------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.CircleTheButtons { public class RadialPanel : Panel { // Dependency Property. public static readonly DependencyProperty OrientationProperty; // Private fields. bool showPieLines; double angleEach; // angle for each child Size sizeLargest; // size of largest child double radius; // radius of circle double outerEdgeFromCenter; double innerEdgeFromCenter; // Static constructor to create Orientation dependency property. static RadialPanel() { OrientationProperty = DependencyProperty.Register ("Orientation", typeof(RadialPanelOrientation) , typeof(RadialPanel), new FrameworkPropertyMetadata (RadialPanelOrientation.ByWidth, FrameworkPropertyMetadataOptions.AffectsMeasure)); } // Orientation property. public RadialPanelOrientation Orientation { set { SetValue(OrientationProperty, value); } get { return (RadialPanelOrientation)GetValue (OrientationProperty); } } // ShowPieLines property. public bool ShowPieLines { set { if (value != showPieLines) InvalidateVisual(); showPieLines = value; } get { return showPieLines; } } // Override of MeasureOverride. protected override Size MeasureOverride (Size sizeAvailable) { if(InternalChildren.Count == 0) return new Size(0,0); angleEach = 360.0 / InternalChildren .Count; sizeLargest = new Size(0, 0); foreach (UIElement child in InternalChildren) { // Call Measure for each child ... child.Measure(new Size(Double .PositiveInfinity, Double .PositiveInfinity)); // ... and then examine DesiredSize property of child. sizeLargest.Width = Math.Max (sizeLargest.Width, child .DesiredSize.Width); sizeLargest.Height = Math.Max (sizeLargest.Height, child.DesiredSize.Height); } if (Orientation == RadialPanelOrientation.ByWidth) { // Calculate the distance from the center to element edges. innerEdgeFromCenter = sizeLargest .Width / 2 / Math.Tan (Math.PI * angleEach / 360); outerEdgeFromCenter = innerEdgeFromCenter + sizeLargest.Height; // Calculate the radius of the circle based on the largest child. radius = Math.Sqrt(Math.Pow (sizeLargest.Width / 2, 2) + Math.Pow (outerEdgeFromCenter, 2)); } else { // Calculate the distance from the center to element edges. innerEdgeFromCenter = sizeLargest .Height / 2 / Math.Tan (Math.PI * angleEach / 360); outerEdgeFromCenter = innerEdgeFromCenter + sizeLargest.Width; // Calculate the radius of the circle based on the largest child. radius = Math.Sqrt(Math.Pow (sizeLargest.Height / 2, 2) + Math.Pow (outerEdgeFromCenter, 2)); } // Return the size of that circle. return new Size(2 * radius, 2 * radius); } // Override of ArrangeOverride. protected override Size ArrangeOverride (Size sizeFinal) { double angleChild = 0; Point ptCenter = new Point(sizeFinal .Width / 2, sizeFinal.Height / 2); double multiplier = Math.Min(sizeFinal .Width / (2 * radius), sizeFinal .Height / (2 * radius)); foreach (UIElement child in InternalChildren) { // Reset RenderTransform. child.RenderTransform = Transform .Identity; if (Orientation == RadialPanelOrientation.ByWidth) { // Position the child at the top. child.Arrange( new Rect(ptCenter.X - multiplier * sizeLargest.Width / 2, ptCenter.Y - multiplier * outerEdgeFromCenter, multiplier * sizeLargest.Width, multiplier * sizeLargest.Height)); } else { // Position the child at the right. child.Arrange( new Rect(ptCenter.X + multiplier * innerEdgeFromCenter, ptCenter.Y - multiplier * sizeLargest.Height / 2, multiplier * sizeLargest.Width, multiplier * sizeLargest.Height)); } // Rotate the child around the center (relative to the child). Point pt = TranslatePoint(ptCenter , child); child.RenderTransform = new RotateTransform(angleChild, pt.X, pt.Y); // Increment the angle. angleChild += angleEach; } return sizeFinal; } // Override OnRender to display optional pie lines. protected override void OnRender (DrawingContext dc) { base.OnRender(dc); if (ShowPieLines) { Point ptCenter = new Point(RenderSize.Width / 2 , RenderSize.Height / 2); double multiplier = Math.Min (RenderSize.Width / (2 * radius), RenderSize.Height / (2 * radius)); Pen pen = new Pen(SystemColors .WindowTextBrush, 1); pen.DashStyle = DashStyles.Dash; // Display circle. dc.DrawEllipse(null, pen, ptCenter , multiplier * radius, multiplier * radius); // Initialize angle. double angleChild = -angleEach / 2; if (Orientation == RadialPanelOrientation.ByWidth) angleChild += 90; // Loop through each child to draw radial lines from center. foreach (UIElement child in InternalChildren) { dc.DrawLine(pen, ptCenter, new Point(ptCenter.X + multiplier * radius * Math.Cos(2 * Math.PI * angleChild / 360), ptCenter.Y + multiplier * radius * Math.Sin(2 * Math.PI * angleChild / 360))); angleChild += angleEach; } } } } }



The MeasureOverride method calculates an angleEach by simply dividing 360 by the total number of children. It then loops through all the children and calls Measure with an infinite size. The foreach loop examines the size of each child and finds the largest width and height of all the children. It stores this in the variable sizeLargestChild.

The remainder of the MeasureOverride method depending on the orientation. The method calculates the two fields innerEdgeFromCenter and outerEdgeFromCenter, and then the radius of the circle. The size that MeasureOverride returns is a square with dimensions that are double the calculated radius.

ArrangeOverride begins by calculating a center point of sizeFinal and a factor I call multiplier. This is a multiplicative factor that can be applied to items calculated during MeasureOverride (specifically, sizeLargest, innerEdgeFromCenter, and outerEdgeFromCenter) to expand the circle to the dimensions of sizeFinal. For RadialPanelOrientation.ByWidth, the Arrange method arranges every child at the top of the circle. For ByHeight, the Arrange method puts each child at the position on the far right. The RenderTransform method then rotates the child around the center of sizeFinal by angleChild degrees. (Notice that angleChild begins at 0 and increases by angleEach for each child.) The second and third arguments to RenderTransform indicate the center of sizeFinal relative to the child.

RenderTransform is one of two different graphics transforms supported by FrameworkElement. I demonstrated LayoutTransform in Chapter 3. As the name implies, LayoutTransform affects layout because it causes different DesiredSize values to be calculated by the Measure method. For example, suppose an element normally calculates a DesiredSize of 100 units wide by 25 units high. If LayoutTransform is set for a 90-degree rotation, DesiredSize will be 25 units wide and 100 units high.

RenderTransform, however, is intended mostly for transforms that should not affect layout. As the documentation suggests, RenderTransform is "typically intended for animating or applying a temporary effect to an element." Obviously, the RadialPanel class is using RenderTransform for a far more profound effect, but it's also very clear that something that affects element size (such as LayoutTransform) shouldn't be messed with during layout.

And here's a program to experiment with RadialPanel. It's set up to arrange 10 buttons of somewhat varying size with a ByWidth orientation.

CircleTheButtons.cs

[View full width]

//------------------------------------------------- // CircleTheButtons.cs (c) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.CircleTheButtons { public class CircleTheButtons : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new CircleTheButtons()); } public CircleTheButtons() { Title = "Circle the Buttons"; RadialPanel pnl = new RadialPanel(); pnl.Orientation = RadialPanelOrientation.ByWidth; pnl.ShowPieLines = true; Content = pnl; Random rand = new Random(); for (int i = 0; i < 10; i++) { Button btn = new Button(); btn.Content = "Button Number " + (i + 1); btn.FontSize += rand.Next(10); pnl.Children.Add(btn); } } } }



As a result of the multiplier factor in the ArrangeOverride method in RadialPanel, as the panel expands to the size of its container, the buttons expand proportionally. (You can set multiplier to 1 to prevent this expansion of buttons.) You might want to try the typical experiments: If you set SizeToContent of the window to WidthAndHeight, or if you set the HorizontalAlignment and VerticalAlignment properties of the panel to something other than Stretch, every button will be the size of the largest button, and the panel will be just as large as it needs to be. You can also try setting a non-zero Margin for each button, or setting the HorizontalAlignment or VerticalAlignment of the buttons to something besides Stretch.

And, of course, you can set the RadialPanel orientation to ByWidth and change the number of buttons to 40 or so.

Although panels are used most obviously for laying out windows and dialog boxes, they can also play roles in layout within controls. You saw an example of this in the ColorGrid control from the previous chapter. But consider a more general-purpose control, such as a ListBox. A ListBox basically begins with a Border, and the Border has a child of ScrollViewer, and the ScrollViewer has a child of a StackPanel, and the items listed by the ListBox are children of the StackPanel.

What do you suppose might happen if it was possible to substitute the StackPanel in a ListBox with some other kind of panel, such as the RadialPanel? The ListBox would look completely different, but it would still work in much the same way. The next chapter will take on this challenge and conclude with a radial list box.




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