Chapter 11. Single-Child Elements


Many classes that inherit from FrameworkElement or Control have children, and to accommodate these children the class usually overrides one property and four methods. These five overrides are:

  1. VisualChildrenCount. This read-only property is defined by the Visual class from which UIElement inherits. A class that derives from FrameworkElement overrides this property so that the element can indicate the number of children that the element maintains. The override of VisualChildrenCount in your class will probably look something like this:

    protected override int VisualChildrenCount {     get     {         ...     } } 

  2. GetVisualChild. This method is also defined by Visual. The parameter is an index from 0 to one less than the value returned from VisualChildrenCount. The class must override this method so that the element can return the child corresponding to that index:

    protected override Visual GetVisualChild(int index) {     ... } 

    The documentation states that this method should never return null. If the index is incorrect, the method should raise an exception.

  3. MeasureOverride. You've seen this one before. An element calculates its desired size during this method and returns that size:

    protected override Size MeasureOverride(Size sizeAvailable) {     ...     return sizeDesired; } 

    But an element with children must also take into account the sizes required by the children. It does this by calling the Measure method for each child, and then examining the DesiredSize property of that child. Measure is a public method defined by UIElement.

  4. ArrangeOverride. This method is defined by FrameworkElement to replace the ArrangeCore method defined by UIElement. The method receives a Size object indicating the final layout size for the element. During the ArrangeOverride call the element arranges its children on its surface by calling Arrange for each child. Arrange is a public method defined by UIElement. The single argument to Arrange is a Rect object that indicates the location and size of the child relative to the parent. The ArrangeOverride method generally returns the same Size object it received:

    protected override Size ArrangeOverride(Size sizeFinal) {     ...     return sizeFinal; } 

  5. OnRender. This method allows an element to draw itself. An element's children draw themselves in their own OnRender methods. The children will appear on top of whatever the element draws during the element's OnRender method:

    protected override void OnRender(DrawingContext dc) {     ... } 

The calls to MeasureOverride, ArrangeOverride, and OnRender occur in sequence. A call to MeasureOverride is always followed by a call to ArrangeOverride, which is always followed by a call to OnRender. However, subsequent OnRender calls might occur without being preceded by a call to ArrangeOverride, and subsequent ArrangeOverride calls might occur without being preceded by a call to MeasureOverride.

In this chapter, I will focus on elements that have just one child. Elements with multiple children are commonly categorized as panels, and I'll discuss those in the next chapter.

Here's a class named EllipseWithChild that inherits from BetterEllipse from Chapter 10 (just so it doesn't have to duplicate the Brush and Fill properties) but includes another property named Child of type UIElement.

EllipseWithChild.cs

[View full width]

//------------------------------------------------- // EllipseWithChild.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.EncloseElementInEllipse { public class EllipseWithChild : Petzold .RenderTheBetterEllipse.BetterEllipse { UIElement child; // Public Child property. public UIElement Child { set { if (child != null) { RemoveVisualChild(child); RemoveLogicalChild(child); } if ((child = value) != null) { AddVisualChild(child); AddLogicalChild(child); } } get { return child; } } // Override of VisualChildrenCount returns 1 if Child is non-null. protected override int VisualChildrenCount { get { return Child != null ? 1 : 0; } } // Override of GetVisualChildren returns Child. protected override Visual GetVisualChild (int index) { if (index > 0 || Child == null) throw new ArgumentOutOfRangeException("index"); return Child; } // Override of MeasureOverride calls child's Measure method. protected override Size MeasureOverride (Size sizeAvailable) { Size sizeDesired = new Size(0, 0); if (Stroke != null) { sizeDesired.Width += 2 * Stroke .Thickness; sizeDesired.Height += 2 * Stroke .Thickness; sizeAvailable.Width = Math.Max(0, sizeAvailable .Width - 2 * Stroke.Thickness); sizeAvailable.Height = Math.Max(0, sizeAvailable .Height - 2 * Stroke.Thickness); } if (Child != null) { Child.Measure(sizeAvailable); sizeDesired.Width += Child .DesiredSize.Width; sizeDesired.Height += Child .DesiredSize.Height; } return sizeDesired; } // Override of ArrangeOverride calls child's Arrange method. protected override Size ArrangeOverride (Size sizeFinal) { if (Child != null) { Rect rect = new Rect( new Point((sizeFinal.Width - Child.DesiredSize.Width) / 2, (sizeFinal.Height - Child.DesiredSize.Height) / 2), Child.DesiredSize); Child.Arrange(rect); } return sizeFinal; } } }



The private child field is accessible through the public Child property. Although it's unlikely that a child of this element will derive from UIElement but not from FrameworkElement, it's common to assume that element children are UIElement objects, and that's the type of this field and property.

Notice that the set accessor of the Child property calls the methods AddVisualChild and AddLogicalChild when setting the child field to a non-null element, and RemoveVisualChild and RemoveLogicalChild if child has previously been set to a non-null element. It is the responsibility of a class that maintains child elements to call these methods to maintain a proper visual and logical element tree. The visual and logical trees are necessary for the proper operation of property inheritance and event routing.

The override of VisualChildrenCount in this class returns 1 if Child is non-null and 0 otherwise. The override of GetVisualChild returns the Child property, and raises an exception if it's not available.

You may wonder about the necessity of overriding GetVisualChild. Because the set accessor of the Child property calls AddVisualChild, isn't it reasonable to assume that the default implementation of GetVisualChild returns that element? Whether it's reasonable or not, it doesn't work that way. Implementing VisualChildrenCount and GetVisualChild is your responsibility in any class that derives from FrameworkElement and maintains its own child collection.

As in any class that derives from FrameworkElement, the argument to MeasureOverride is an available size that can range from 0 to infinity. The method calculates a desired size and returns it the method. In EllipseWithChild the method begins with a sizeDesired object of zero dimensions and then adds to sizeDesired twice the width of the perimeter of the ellipse. The method is indicating that it wants to display at least the entire perimeter. These widths and heights are then subtracted from the sizeAvailable argument, but with logic that prevents zero values.

If the Child property is non-null, MeasureOverride continues by calling the Measure method of the child with the adjusted sizeAvailable. This Measure method of the child eventually calls the child's MeasureOverride method, and that method potentially calls the Measure methods of the child's children. This is how an entire tree of child elements is measured. The Measure method is basically responsible for updating the element's DesiredSize property.

The Measure method of the child does not return a value, but MeasureOverride can now examine the DesiredSize property of the child and take that size into account when determining its own sizeDesired. This is what the EllipseWithChild class returns from MeasureOverride.

What is this DesiredSize property? Of course, the immediate impulse is to identify it with the return value from MeasureOverride. It's similar, but it's not quite the same. The big difference is that DesiredSize includes any Margin set on the element. I'll discuss the calculations that go on behind the scenes shortly.

The ArrangeOverride method gives an element the opportunity to arrange its child elements on its surface. The argument to ArrangeOverride is a final layout size. The ArrangeOverride method is responsible for calling the Arrange method of all its children. Arrange has a Rect argument indicating the location and size of the child: The Left and Top properties must be relative to the upper-left corner of the parent. The Width and Height properties are generally extracted from the DesiredSize property of the child. The Arrange method assumes that the Width and Height properties of the Rect parameter include the child's Margin, which is why DesiredSize (which includes the child's Margin) is the proper choice for setting these properties.

The Arrange method in the child eventually calls the child's ArrangeOverride method with an argument that excludes the Margin, which potentially calls its children's Arrange methods, and so forth.

Here's a program that creates an EllipseWithChild object and assigns the Child property a TextBlock object. It could just as well assign the Child property an Image object or any other element.

EncloseElementInEllipse.cs

[View full width]

//--------- ----------------------------------------------- // EncloseElementInEllipse.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.EncloseElementInEllipse { public class EncloseElementInEllipse : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new EncloseElementInEllipse()); } public EncloseElementInEllipse() { Title = "Enclose Element in Ellipse"; EllipseWithChild elips = new EllipseWithChild(); elips.Fill = Brushes.ForestGreen; elips.Stroke = new Pen(Brushes.Magenta , 48); Content = elips; TextBlock text = new TextBlock(); text.FontFamily = new FontFamily ("Times New Roman"); text.FontSize = 48; text.Text = "Text inside ellipse"; // Set Child property of EllipseWithChild to TextBlock. elips.Child = text; } } }



As you can see from experimenting with this program, the TextBlock element is always enclosed within the ellipse perimeter except when the ellipse is so small that the curvature of the perimeter causes the MeasureOverride logic to break down. If not enough space is available for the entire text, the text is clipped. The TextBlock element probably uses one of the techniques discussed in Chapter 10 to make sure its text doesn't spill out beyond the boundaries of the element.

It should now be fairly clear that layout in the Windows Presentation Foundation is a two-pass process that begins with a root element and works downward through the element tree. The root element is an object of type Window. This object has a single visual child of type Border. (That's the sizing border around the window.) The border has a visual child of type Grid. The grid has two children: an AdornerDecorator and a ResizeGrip (the latter of which only optionally appears in the window). The AdornerDecorator has one visual child of type ContentPresenter, which is responsible for hosting the Content property of the window. In the preceding program, the single visual child of the ContentPresenter is an EllipseWithChild, and its single visual child is a TextBlock. (I obtained information about the visual tree of this program through static methods of the VisualTreeHelper class.)

Each of these elements processes the MeasureOverride method by calling Measure on each of its children. (In most cases, that's just one child.) The Measure method of the child element calls the child element's MeasureOverride method, which then calls Measure on each of the child's children, and so on. A similar process occurs with ArrangeOverride and Arrange.

Here's the general logic behind these methods:

The argument to MeasureOverride (which is commonly called sizeAvailable) is generally the size of the element's container minus the element's Margin property. This argument can range from 0 to positive infinity. It is infinite if the container can size itself to the size of its children. However, if the element's Height property has been set to something other than NaN, sizeAvailable.Height will equal that property. If MinHeight or MaxHeight has been set, sizeAvailable.Height will reflect those constraints, and similarly for width.

MeasureOverride is responsible for calling Measure for each of its children. If an element has one child and the child is to occupy the entire surface of the element, the argument to Measure can be the same sizeAvailable parameter to MeasureOverride. Most commonly, the argument to Measure is a size based on sizeAvailable but somewhat less than that size. For example, EllipseWithChild offers its child a size from which the ellipse's own border has been subtracted.

The Measure method in the child calls the child's MeasureOverride method and uses the return value of MeasureOverride to calculate its own DesiredSize property. This calculation can be conceived as occurring in several steps. Here's how DesiredSize.Width is calculated. (The other dimension is calculated identically.)

  1. If the element's Width property is NaN, DesiredSize.Width is the return value from MeasureOverride. Otherwise, DesiredSize.Width equals the element's Width.

  2. DesiredSize.Width is adjusted by adding the Left and Right properties of the Margin.

  3. DesiredSize.Width is adjusted so that it is not larger than the width of the container.

So, DesiredSize is basically the layout size required of the element including the element's margin, but not larger than the container. It is not very useful for an element to examine its own DesiredSize, but the DesiredSize properties of the element's children are very useful. After calling Measure on each of its children, an element can examine the children's DesiredSize properties to determine how much space each child needs and then calculate its own return value from the MeasureOverride method.

The parameter to ArrangeOverride (commonly called sizeFinal) is calculated from the original parameter to MeasureOverride and the return value from MeasureOverride, with a couple of other factors. Here's how sizeFinal.Width is calculated:

If Width, MinWidth, and MaxWidth are all NaN and the HorizontalAlignment property of the element is set to Center, Left, or Right, sizeFinal.Width equals the return value from MeasureOverride. If HorizontalAlignment is set to Stretch, sizeFinal.Width equals the maximum of the sizeAvailable argument to MeasureOverride and the return value from MeasureOverride. If the Width property has been set to something other than NaN, sizeFinal.Width is the maximum of the Width property and the return value from MeasureOverride.

The job of the ArrangeOverride method is to lay out its children. Conceptually, the parent element needs to arrange its children in a rectangle whose upper-left corner is the point (0, 0) and whose size is the sizeFinal argument to ArrangeOverride. Commonly, ArrangeOverride uses the sizeFinal argument and the DesiredSize property of the child to calculate a Rect object for that child that indicates the upper-left corner and size of the child. The parent passes this Rect object to the Arrange method of each child. This is how the children get positioned on the surface of the parent.

After calling Arrange on its children, ArrangeOverride generally returns its sizeFinal parameter. (The base implementation of ArrangeOverride does precisely that.) However, it could return something different. Whatever ArrangeOverride returns becomes RenderSize.

In general, controls and elements are built up of other elements. Often, a control begins with some kind of Decorator object. Decorator inherits from FrameworkElement and defines a Child property of type UIElement. For example, the Border class inherits from Decorator to define properties Background, BorderBrush, BorderThickness, CornerRadius (for rounded corners), and Padding. The ButtonChrome class (in the Microsoft.Windows.Themes namespace) also inherits from Decorator and provides the look of the standard Button. The Button is basically a ButtonChrome object and a ContentPresenter object that is a child of the ButtonChrome object.

Here is a decorator for something I call a rounded button. The class implements its OnRender method by calling DrawRoundedRectangle. The horizontal and vertical radii of the corners are set to half the button's height so that the left and right sides of the button are circular.

RoundedButtonDecorator.cs

[View full width]

//--------- ---------------------------------------------- // RoundedButtonDecorator.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.CalculateInHex { public class RoundedButtonDecorator : Decorator { // Public dependency property. public static readonly DependencyProperty IsPressedProperty; // Static constructor. static RoundedButtonDecorator() { IsPressedProperty = DependencyProperty.Register ("IsPressed", typeof(bool), typeof (RoundedButtonDecorator), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)); } // Public property. public bool IsPressed { set { SetValue(IsPressedProperty, value); } get { return (bool)GetValue (IsPressedProperty); } } // Override of MeasureOverride. protected override Size MeasureOverride (Size sizeAvailable) { Size szDesired = new Size(2, 2); sizeAvailable.Width -= 2; sizeAvailable.Height -= 2; if (Child != null) { Child.Measure(sizeAvailable); szDesired.Width += Child .DesiredSize.Width; szDesired.Height += Child .DesiredSize.Height; } return szDesired; } // Override of ArrangeOverride. protected override Size ArrangeOverride (Size sizeArrange) { if (Child != null) { Point ptChild = new Point(Math.Max(1, (sizeArrange.Width - Child .DesiredSize.Width) / 2), Math.Max(1, (sizeArrange.Height - Child .DesiredSize.Height) / 2)); Child.Arrange(new Rect(ptChild, Child.DesiredSize)); } return sizeArrange; } // Override of OnRender. protected override void OnRender (DrawingContext dc) { RadialGradientBrush brush = new RadialGradientBrush( IsPressed ? SystemColors .ControlDarkColor : SystemColors .ControlLightLightColor, SystemColors.ControlColor); brush.GradientOrigin = IsPressed ? new Point(0.75, 0.75) : new Point(0.25, 0.25); dc.DrawRoundedRectangle(brush, new Pen(SystemColors .ControlDarkDarkBrush, 1), new Rect(new Point(0, 0), RenderSize), RenderSize.Height / 2, RenderSize.Height / 2); } } }



The class doesn't explicitly define a Child property because it inherits that property from Decorator. The class defines one additional property named IsPressed that it uses for the radial gradient brush created in the OnRender method. To indicate a pressed button, OnRender uses a darker center and shifts the gradient origin.

The RoundedButton class shown next uses its constructor to create an object of type RoundedButtonDecorator and then calls AddVisualChild and AddLogicalChild to make that decorator a child. RoundedButton defines its own Child property but simply transfers any child set from this property to the decorator. The class returns 1 from its override of the VisualChildCount property and returns the RoundedButtonDecorator object from GetVisualChild.

RoundedButton thus doesn't need to do any drawing and is solely responsible for processing user input. The input logic is very similar to MedievalButton in Chapter 10, except that RoundedButton defines a Click event to signal that the button has been pressed. The IsPressed property in RoundedButton provides direct access to the same property in RoundedButtonDecorator. A program using RoundedButton can simulate a button being pressed and released by setting this property.

RoundedButton.cs

[View full width]

//---------------------------------------------- // RoundedButton.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.CalculateInHex { public class RoundedButton : Control { // Private field. RoundedButtonDecorator decorator; // Public static ClickEvent. public static readonly RoutedEvent ClickEvent; // Static Constructor. static RoundedButton() { ClickEvent = EventManager.RegisterRoutedEvent ("Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler) , typeof(RoundedButton)); } // Constructor. public RoundedButton() { decorator = new RoundedButtonDecorator(); AddVisualChild(decorator); AddLogicalChild(decorator); } // Public properties. public UIElement Child { set { decorator.Child = value; } get { return decorator.Child; } } public bool IsPressed { set { decorator.IsPressed = value; } get { return decorator.IsPressed; } } // Public event. public event RoutedEventHandler Click { add { AddHandler(ClickEvent, value); } remove { RemoveHandler(ClickEvent, value); } } // Overridden property and methods. protected override int VisualChildrenCount { get { return 1; } } protected override Visual GetVisualChild (int index) { if (index > 0) throw new ArgumentOutOfRangeException("index"); return decorator; } protected override Size MeasureOverride (Size sizeAvailable) { decorator.Measure(sizeAvailable); return decorator.DesiredSize; } protected override Size ArrangeOverride (Size sizeArrange) { decorator.Arrange(new Rect(new Point(0 , 0), sizeArrange)); return sizeArrange; } protected override void OnMouseMove (MouseEventArgs args) { base.OnMouseMove(args); if (IsMouseCaptured) IsPressed = IsMouseReallyOver; } protected override void OnMouseLeftButtonDown(MouseButtonEventArgs args) { base.OnMouseLeftButtonDown(args); CaptureMouse(); IsPressed = true; args.Handled = true; } protected override void OnMouseLeftButtonUp(MouseButtonEventArgs args) { base.OnMouseRightButtonUp(args); if (IsMouseCaptured) { if (IsMouseReallyOver) OnClick(); Mouse.Capture(null); IsPressed = false; args.Handled = true; } } bool IsMouseReallyOver { get { Point pt = Mouse.GetPosition(this); return (pt.X >= 0 && pt.X < ActualWidth && pt.Y >= 0 && pt.Y < ActualHeight); } } // Method to fire Click event. protected virtual void OnClick() { RoutedEventArgs argsEvent = new RoutedEventArgs(); argsEvent.RoutedEvent = RoundedButton .ClickEvent; argsEvent.Source = this; RaiseEvent(argsEvent); } } }



The CalculateInHex program creates 29 instances of RoundedButton to make a hexadecimal calculator. The buttons are laid out in a Grid panel, and three of the buttons require some special handling in the window's constructor to make them span multiple columns.

CalculateInHex.cs

[View full width]

//----------------------------------------------- // CalculateInHex.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.Threading; namespace Petzold.CalculateInHex { public class CalculateInHex : Window { // Private fields. RoundedButton btnDisplay; ulong numDisplay; ulong numFirst; bool bNewNumber = true; char chOperation = '='; [STAThread] public static void Main() { Application app = new Application(); app.Run(new CalculateInHex()); } // Constructor. public CalculateInHex() { Title = "Calculate in Hex"; SizeToContent = SizeToContent .WidthAndHeight; ResizeMode = ResizeMode.CanMinimize; // Create Grid as content of window. Grid grid = new Grid(); grid.Margin = new Thickness(4); Content = grid; // Create five columns. for (int i = 0; i < 5; i++) { ColumnDefinition col = new ColumnDefinition(); col.Width = GridLength.Auto; grid.ColumnDefinitions.Add(col); } // Create seven rows. for (int i = 0; i < 7; i++) { RowDefinition row = new RowDefinition(); row.Height = GridLength.Auto; grid.RowDefinitions.Add(row); } // Text to appear in buttons. string[] strButtons = { "0", "D", "E", "F" , "+", "&", "A", "B", "C" , "-", "|", "7", "8", "9" , "*", "^", "4", "5", "6" , "/", "<<", "1", "2", "3" , "%", ">>", "0", "Back", "Equals" }; int iRow = 0, iCol = 0; // Create the buttons. foreach (string str in strButtons) { // Create RoundedButton. RoundedButton btn = new RoundedButton(); btn.Focusable = false; btn.Height = 32; btn.Margin = new Thickness(4); btn.Click += ButtonOnClick; // Create TextBlock for Child of RoundedButton. TextBlock txt = new TextBlock(); txt.Text = str; btn.Child = txt; // Add RoundedButton to Grid. grid.Children.Add(btn); Grid.SetRow(btn, iRow); Grid.SetColumn(btn, iCol); // Make an exception for the Display button. if (iRow == 0 && iCol == 0) { btnDisplay = btn; btn.Margin = new Thickness(4, 4, 4, 6); Grid.SetColumnSpan(btn, 5); iRow = 1; } // Also for Back and Equals. else if (iRow == 6 && iCol > 0) { Grid.SetColumnSpan(btn, 2); iCol += 2; } // For all other buttons. else { btn.Width = 32; if (0 == (iCol = (iCol + 1) % 5)) iRow++; } } } // Click event handler. void ButtonOnClick(object sender, RoutedEventArgs args) { // Get the clicked button. RoundedButton btn = args.Source as RoundedButton; if (btn == null) return; // Get the button text and the first character. string strButton = (btn.Child as TextBlock).Text; char chButton = strButton[0]; // Some special cases. if (strButton == "Equals") chButton = '='; if (btn == btnDisplay) numDisplay = 0; else if (strButton == "Back") numDisplay /= 16; // Hexadecimal digits. else if (Char.IsLetterOrDigit(chButton)) { if (bNewNumber) { numFirst = numDisplay; numDisplay = 0; bNewNumber = false; } if (numDisplay <= ulong.MaxValue >> 4) numDisplay = 16 * numDisplay + (ulong)(chButton - (Char.IsDigit (chButton) ? '0' : 'A' - 10)); } // Operation. else { if (!bNewNumber) { switch (chOperation) { case '=': break; case '+': numDisplay = numFirst + numDisplay; break; case '-': numDisplay = numFirst - numDisplay; break; case '*': numDisplay = numFirst * numDisplay; break; case '&': numDisplay = numFirst & numDisplay; break; case '|': numDisplay = numFirst | numDisplay; break; case '^': numDisplay = numFirst ^ numDisplay; break; case '<': numDisplay = numFirst << (int)numDisplay; break; case '>': numDisplay = numFirst >> (int)numDisplay; break; case '/': numDisplay = numDisplay != 0 ? numFirst / numDisplay : ulong.MaxValue; break; case '%': numDisplay = numDisplay != 0 ? numFirst % numDisplay : ulong.MaxValue; break; default: numDisplay = 0; break; } } bNewNumber = true; chOperation = chButton; } // Format display. TextBlock text = new TextBlock(); text.Text = String.Format("{0:X}", numDisplay); btnDisplay.Child = text; } protected override void OnTextInput (TextCompositionEventArgs args) { base.OnTextInput(args); if (args.Text.Length == 0) return; // Get character input. char chKey = Char.ToUpper(args.Text[0]); // Loop through buttons. foreach (UIElement child in (Content as Grid).Children) { RoundedButton btn = child as RoundedButton; string strButton = (btn.Child as TextBlock).Text; // Messy logic to check for matching button. if ((chKey == strButton[0] && btn != btnDisplay && strButton != "Equals" && strButton != "Back") || (chKey == '=' && strButton == "Equals") || (chKey == '\r' && strButton == "Equals") || (chKey == '\b' && strButton == "Back") || (chKey == '\x1B' && btn == btnDisplay)) { // Simulate Click event to process keystroke. RoutedEventArgs argsClick = new RoutedEventArgs (RoundedButton.ClickEvent, btn); btn.RaiseEvent(argsClick); // Make the button appear as if it's pressed. btn.IsPressed = true; // Set timer to unpress button. DispatcherTimer tmr = new DispatcherTimer(); tmr.Interval = TimeSpan .FromMilliseconds(100); tmr.Tag = btn; tmr.Tick += TimerOnTick; tmr.Start(); args.Handled = true; } } } void TimerOnTick(object sender, EventArgs args) { // Unpress button. DispatcherTimer tmr = sender as DispatcherTimer; RoundedButton btn = tmr.Tag as RoundedButton; btn.IsPressed = false; // Turn off time and remove event handler. tmr.Stop(); tmr.Tick -= TimerOnTick; } } }



Most of the logic for the calculator is concentrated in the ButtonOnClick method used for handling Click events from all the buttons. However, the button also has a keyboard interface, and that's handled in the OnTextInput override. The method identifies the button that corresponds to the key being pressed and then raises the Click event on that button by calling the RaiseEvent method of a RoutedEventArgs object. The OnTextInput method concludes by making the button appear as if it has been pressed by setting the IsPressed property to true. Of course, this property must be reset to false at some point, so a DispatcherTimer is created for that express purpose.

Besides overriding FrameworkElement and Control, there is another approach to creating custom controls that is used much more in XAML than in procedural code. This is through the definition of a template. I'll be discussing this technique in more detail in Chapter 25, but you might like to see how it's done in procedural code first.

The key property of Control that allows this approach is Template, which is of type ControlTemplate. This Template property essentially defines the look and feel of the control. As you know by now, a normal Button is basically a ButtonChrome object and a ContentPresenter object. The ButtonChrome object gives the button the look of its background and border, while the ContentPresenter is responsible for hosting whatever you've set to the button's Content property. The template defines the links between these elements as well as "triggers" that cause the control to react to certain changes in element properties.

To create a custom template for a control, first create an object of type ControlTemplate:

ControlTemplate template = new ControlTemplate(); 


The ControlTemplate class inherits a property named VisualTree from FrameworkTemplate. This is the property that defines which elements make up the control. The syntax of the code that goes into constructing the template is likely to look rather strange because it's necessary to define certain elements and their properties without actually creating those elements. This is accomplished through a class named FrameworkElementFactory. You create a FrameworkElementFactory object for each element in the visual tree. For example:

FrameworkElementFactory factoryBorder =                 new FrameworkElementFactory(typeof(Border)); 


You can also specify properties and attach event handlers for that element. You refer to properties and events using DependencyProperty and RoutedEvent fields defined or inherited by the element. Here's an example of setting a property:

factoryBorder.SetValue(Border.BorderBrushProperty, Brushes.Red); 


You can also establish a parent-child relationship with multiple FactoryElementFactory objects:

factoryBorder.AppendChild(factoryContent); 


But keep in mind that the elements are not created until the control is ready to be rendered.

The ControlTemplate also defines a property named Triggers, which is a collection of Trigger objects that define how the control changes as a result of certain changes in properties. The Trigger class has a property named Property of type DependencyProperty that indicates the property to monitor, and a Value property for the value of that property. For example:

Trigger trig = new Trigger(); trig.Property = UIElement.IsMouseOverProperty; trig.Value = true; 


When the IsMouseOver property becomes true, something happens, and that something is indicated by one or more Setter objects. For example, you might want the FontStyle property to change to italics:

Setter set = new Setter(); set.Property = Control.FontStyleProperty; set.Value = FontStyles.Italic; 


The Setter is associated with the Trigger by becoming part of the trigger's Setters collection:

trig.Setters.Add(set); 


And the Trigger becomes part of the ControlTemplate:

template.Triggers.Add(trig); 


Now you're ready to create a button and give it this template:

Button btn = new Button(); btn.Template = template; 


And now the button has a whole new look, but it otherwise generates Click events as before. Here's a program that assembles a ControlTemplate for a Button in code:

BuildButtonFactory.cs

[View full width]

//--------------------------------------------------- // BuildButtonFactory.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.BuildButtonFactory { public class BuildButtonFactory : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new BuildButtonFactory()); } public BuildButtonFactory() { Title = "Build Button Factory"; // Create a ControlTemplate intended for a Button object. ControlTemplate template = new ControlTemplate(typeof(Button)); // Create a FrameworkElementFactory for the Border class. FrameworkElementFactory factoryBorder = new FrameworkElementFactory(typeof (Border)); // Give it a name to refer to it later. factoryBorder.Name = "border"; // Set certain default properties. factoryBorder.SetValue(Border .BorderBrushProperty, Brushes.Red); factoryBorder.SetValue(Border .BorderThicknessProperty, new Thickness(3)); factoryBorder.SetValue(Border .BackgroundProperty, SystemColors .ControlLightBrush); // Create a FrameworkElementFactory for the // ContentPresenter class. FrameworkElementFactory factoryContent = new FrameworkElementFactory(typeof (ContentPresenter)); // Give it a name to refer to it later. factoryContent.Name = "content"; // Bind some ContentPresenter properties to Button properties. factoryContent.SetValue (ContentPresenter.ContentProperty, new TemplateBindingExtension (Button.ContentProperty)); // Notice that the button's Padding is the content's Margin! factoryContent.SetValue (ContentPresenter.MarginProperty, new TemplateBindingExtension (Button.PaddingProperty)); // Make the ContentPresenter a child of the Border. factoryBorder.AppendChild(factoryContent); // Make the Border the root element of the visual tree. template.VisualTree = factoryBorder; // Define a new Trigger when IsMouseOver is true. Trigger trig = new Trigger(); trig.Property = UIElement .IsMouseOverProperty; trig.Value = true; // Associate a Setter with that Trigger to change the // CornerRadius property of the "border" element. Setter set = new Setter(); set.Property = Border .CornerRadiusProperty; set.Value = new CornerRadius(24); set.TargetName = "border"; // Add the Setter to the Setters collection of the Trigger. trig.Setters.Add(set); // Similarly, define a Setter to change the FontStyle. // (No TargetName is needed because it's the button's property.) set = new Setter(); set.Property = Control.FontStyleProperty; set.Value = FontStyles.Italic; // Add it to the same trigger's Setters collection as before. trig.Setters.Add(set); // Add the Trigger to the template. template.Triggers.Add(trig); // Similarly, define a Trigger for IsPressed. trig = new Trigger(); trig.Property = Button.IsPressedProperty; trig.Value = true; set = new Setter(); set.Property = Border.BackgroundProperty; set.Value = SystemColors.ControlDarkBrush; set.TargetName = "border"; // Add the Setter to the trigger's Setters collection. trig.Setters.Add(set); // Add the Trigger to the template. template.Triggers.Add(trig); // Finally, create a Button. Button btn = new Button(); // Give it the template. btn.Template = template; // Define other properties normally. btn.Content = "Button with Custom Template"; btn.Padding = new Thickness(20); btn.FontSize = 48; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Click += ButtonOnClick; Content = btn; } void ButtonOnClick(object sender, RoutedEventArgs args) { MessageBox.Show("You clicked the button", Title); } } }



The Template property is defined by the Control class, so you can use this technique with your own custom controls. In fact, you can entirely define custom controls using this approach, and you'll see XAML examples in Part II of this book.

You may have noticed that the Measure and Arrange methods are defined by the UIElement class, but that VisualChildrenCount and GetVisualChild are defined by the Visual class. Let's look at the class hierarchy from Object to Control once again:

Object

    DispatcherObject (abstract)

          DependencyObject

                Visual (abstract)

                      UIElement

                            FrameworkElement

                                 Control

The children of an element are generally other elements, but some or all of the children may actually be visualsan instance of any class that descends from Visual. These visuals are what constitute the visual tree.

The following class hierarchy shows all the descendants of Visual except for the descendents of FrameworkElement:

Object

    DispatcherObject (abstract)

          DependencyObject

                Visual (abstract)

                      ContainerVisual

                            DrawingVisual

                            HostVisual

                            Viewport3DVisual

                            UIElement

                                  FrameworkElement

Of particular interest here is DrawingVisual. As you've seen, a class that derives from UIElement can override the OnRender method and obtain a DrawingContext to draw graphical objects on the screen. The only other place you can get a DrawingContent object is from DrawingVisual. Here's how:

DrawingVisual drawvis = new DrawingVisual(); DrawingContext dc = drawvis.RenderOpen(); // draw on dc ... dc.Close(); 


Following the execution of this code you are now a proud owner of a DrawingVisual objectin other words, a "visual" that stores a particular image. The parameters to the drawing functions of the DrawingContext that you called gave this visual a specific location and size. That location is relative to some parent element that may not yet exist. To display this visual on the screen, a particular element must indicate the existence of the child visual with the return value of VisualChildrenCount and GetVisualChild. That's it. You can't call Measure or Arrange on this visual because those methods are defined by UIElement. The visual is displayed relative to its parent element and visibly on top of anything the element draws during OnRender. The order with respect to other children of the element depends on the order established by GetVisualChild: Later visuals will appear in the foreground of earlier visuals.

If you want this visual to participate in event routing (which probably means that you want mouse events to be routed to the parent element of the visual), you should also call AddVisualChild to add the visual to the visual tree of the element.

Here's a class called ColorCell that derives from FrameworkElement. This class is responsible for rendering an element that is always 20 device-independent units square. In the center of this element is a 12-unit-square color. That little rectangle is an object of type DrawingVisual created during the class's constructornotice that the constructor has a Color parameterand stored as a field.

ColorCell.cs

[View full width]

//------------------------------------------ // ColorCell.cs (c) 2006 by Charles Petzold //------------------------------------------ using System; using System.Windows; using System.Windows.Media; namespace Petzold.SelectColor { class ColorCell : FrameworkElement { // Private fields. static readonly Size sizeCell = new Size (20, 20); DrawingVisual visColor; Brush brush; // Dependency properties. public static readonly DependencyProperty IsSelectedProperty; public static readonly DependencyProperty IsHighlightedProperty; static ColorCell() { IsSelectedProperty = DependencyProperty.Register ("IsSelected", typeof(bool), typeof(ColorCell), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)); IsHighlightedProperty = DependencyProperty.Register ("IsHighlighted", typeof(bool), typeof(ColorCell), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender)); } // Properties. public bool IsSelected { set { SetValue(IsSelectedProperty, value); } get { return (bool)GetValue (IsSelectedProperty); } } public bool IsHighlighted { set { SetValue(IsHighlightedProperty, value); } get { return (bool)GetValue (IsHighlightedProperty); } } public Brush Brush { get { return brush; } } // Constructor requires Color argument. public ColorCell(Color clr) { // Create a new DrawingVisual and store as field. visColor = new DrawingVisual(); DrawingContext dc = visColor.RenderOpen(); // Draw a rectangle with the color argument. Rect rect = new Rect(new Point(0, 0), sizeCell); rect.Inflate(-4, -4); Pen pen = new Pen(SystemColors .ControlTextBrush, 1); brush = new SolidColorBrush(clr); dc.DrawRectangle(brush, pen, rect); dc.Close(); // AddVisualChild is necessary for event routing! AddVisualChild(visColor); AddLogicalChild(visColor); } // Override protected properties and methods for visual child. protected override int VisualChildrenCount { get { return 1; } } protected override Visual GetVisualChild (int index) { if (index > 0) throw new ArgumentOutOfRangeException("index"); return visColor; } // Override protected methods for size and rendering of element. protected override Size MeasureOverride (Size sizeAvailable) { return sizeCell; } protected override void OnRender (DrawingContext dc) { Rect rect = new Rect(new Point(0, 0), RenderSize); rect.Inflate(-1, -1); Pen pen = new Pen(SystemColors .HighlightBrush, 1); if (IsHighlighted) dc.DrawRectangle(SystemColors .ControlDarkBrush, pen, rect); else if (IsSelected) dc.DrawRectangle(SystemColors .ControlLightBrush, pen, rect); else dc.DrawRectangle(Brushes .Transparent, null, rect); } } }



The VisualChildrenCount property returns 1 to indicate the presence of this visual, and GetVisualChild returns the visual itself. MeasureOverride simply returns the size of the total element. OnRender displays a rectangle that appears underneath the visual. The brush that DrawRectangle uses is based on two properties named IsSelected and IsHighlighted that are backed with dependency properties.

Forty ColorCell objects are part of a control I've called ColorGrid. ColorGrid overrides VisualChildrenCount to return 1 and GetVisualChild to return an object of type Border. By that time, the Child property of this Border object has been assigned a UniformGrid panel, and the UniformGrid panel has been filled with 40 instances of ColorCell, each with a different color.

ColorGrid.cs

[View full width]

//------------------------------------------ // ColorGrid.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.Shapes; namespace Petzold.SelectColor { class ColorGrid : Control { // Number of rows and columns. const int yNum = 5; const int xNum = 8; // The colors to be displayed. string[,] strColors = new string[yNum, xNum] { { "Black", "Brown", "DarkGreen", "MidnightBlue", "Navy", "DarkBlue", "Indigo", "DimGray" }, { "DarkRed", "OrangeRed", "Olive", "Green", "Teal", "Blue", "SlateGray", "Gray" }, { "Red", "Orange", "YellowGreen", "SeaGreen", "Aqua", "LightBlue", "Violet", "DarkGray" }, { "Pink", "Gold", "Yellow", "Lime", "Turquoise", "SkyBlue", "Plum", "LightGray" }, { "LightPink", "Tan", "LightYellow", "LightGreen", "LightCyan", "LightSkyBlue", "Lavender", "White" } }; // The ColorCell objects to be created. ColorCell[,] cells = new ColorCell[yNum, xNum]; ColorCell cellSelected; ColorCell cellHighlighted; // Elements that comprise this control. Border bord; UniformGrid unigrid; // Currently selected color. Color clrSelected = Colors.Black; // Public "Changed" event. public event EventHandler SelectedColorChanged; // Public constructor. public ColorGrid() { // Create a Border for the control. bord = new Border(); bord.BorderBrush = SystemColors .ControlDarkDarkBrush; bord.BorderThickness = new Thickness(1); AddVisualChild(bord); // necessary for event routing. AddLogicalChild(bord); // Create a UniformGrid as a child of the Border. unigrid = new UniformGrid(); unigrid.Background = SystemColors .WindowBrush; unigrid.Columns = xNum; bord.Child = unigrid; // Fill up the UniformGrid with ColorCell objects. for (int y = 0; y < yNum; y++) for (int x = 0; x < xNum; x++) { Color clr = (Color) typeof(Colors). GetProperty(strColors[y, x]) .GetValue(null, null); cells[y, x] = new ColorCell(clr); unigrid.Children.Add(cells[y, x]); if (clr == SelectedColor) { cellSelected = cells[y, x]; cells[y, x].IsSelected = true; } ToolTip tip = new ToolTip(); tip.Content = strColors[y, x]; cells[y, x].ToolTip = tip; } } // Public get-only SelectedColor property. public Color SelectedColor { get { return clrSelected; } } // Override of VisualChildrenCount. protected override int VisualChildrenCount { get { return 1; } } // Override of GetVisualChild. protected override Visual GetVisualChild (int index) { if (index > 0) throw new ArgumentOutOfRangeException("index"); return bord; } // Override of MeasureOverride. protected override Size MeasureOverride (Size sizeAvailable) { bord.Measure(sizeAvailable); return bord.DesiredSize; } // Override of ArrangeOverride. protected override Size ArrangeOverride (Size sizeFinal) { bord.Arrange(new Rect(new Point(0, 0), sizeFinal)); return sizeFinal; } // Mouse event handling. protected override void OnMouseEnter (MouseEventArgs args) { base.OnMouseEnter(args); if (cellHighlighted != null) { cellHighlighted.IsHighlighted = false; cellHighlighted = null; } } protected override void OnMouseMove (MouseEventArgs args) { base.OnMouseMove(args); ColorCell cell = args.Source as ColorCell; if (cell != null) { if (cellHighlighted != null) cellHighlighted.IsHighlighted = false; cellHighlighted = cell; cellHighlighted.IsHighlighted = true; } } protected override void OnMouseLeave (MouseEventArgs args) { base.OnMouseLeave(args); if (cellHighlighted != null) { cellHighlighted.IsHighlighted = false; cellHighlighted = null; } } protected override void OnMouseDown (MouseButtonEventArgs args) { base.OnMouseDown(args); ColorCell cell = args.Source as ColorCell; if (cell != null) { if (cellHighlighted != null) cellHighlighted.IsSelected = false; cellHighlighted = cell; cellHighlighted.IsSelected = true; } Focus(); } protected override void OnMouseUp (MouseButtonEventArgs args) { base.OnMouseUp(args); ColorCell cell = args.Source as ColorCell; if (cell != null) { if (cellSelected != null) cellSelected.IsSelected = false; cellSelected = cell; cellSelected.IsSelected = true; clrSelected = (cellSelected.Brush as SolidColorBrush).Color; OnSelectedColorChanged(EventArgs .Empty); } } // Keyboard event handling. protected override void OnGotKeyboardFocus( KeyboardFocusChangedEventArgs args) { base.OnGotKeyboardFocus(args); if (cellHighlighted == null) { if (cellSelected != null) cellHighlighted = cellSelected; else cellHighlighted = cells[0, 0]; cellHighlighted.IsHighlighted = true; } } protected override void OnLostKeyboardFocus( KeyboardFocusChangedEventArgs args) { base.OnGotKeyboardFocus(args); if (cellHighlighted != null) { cellHighlighted.IsHighlighted = false; cellHighlighted = null; } } protected override void OnKeyDown (KeyEventArgs args) { base.OnKeyDown(args); int index = unigrid.Children.IndexOf (cellHighlighted); int y = index / xNum; int x = index % xNum; switch (args.Key) { case Key.Home: y = 0; x = 0; break; case Key.End: y = yNum - 1; x = xNum + 1; break; case Key.Down: if ((y = (y + 1) % yNum) == 0) x++; break; case Key.Up: if ((y = (y + yNum - 1) % yNum) == yNum - 1) x--; break; case Key.Right: if ((x = (x + 1) % xNum) == 0) y++; break; case Key.Left: if ((x = (x + xNum - 1) % xNum) == xNum - 1) y--; break; case Key.Enter: case Key.Space: if (cellSelected != null) cellSelected.IsSelected = false; cellSelected = cellHighlighted; cellSelected.IsSelected = true; clrSelected = (cellSelected .Brush as SolidColorBrush).Color; OnSelectedColorChanged (EventArgs.Empty); break; default: return; } if (x >= xNum || y >= yNum) MoveFocus(new TraversalRequest( FocusNavigationDirection.Next)); else if (x < 0 || y < 0) MoveFocus(new TraversalRequest( FocusNavigationDirection.Previous)); else { cellHighlighted.IsHighlighted = false; cellHighlighted = cells[y, x]; cellHighlighted.IsHighlighted = true; } args.Handled = true; } // Protected method to fire SelectedColorChanged event. protected virtual void OnSelectedColorChanged(EventArgs args) { if (SelectedColorChanged != null) SelectedColorChanged(this, args); } } }



I patterned ColorGrid after a control in Microsoft Word. You may have seen it on the Format Background menu or emerging from the Font Color button on the Formatting toolbar. There is always one cell that is "selected." This corresponds to the color obtained through the read-only SelectedColor property. (I made this property read-only because the control is not able to accept arbitrary colors not in its grid.) The selected color is indicated by a lighter background rectangle. You can change the selected color using the mouse.

When ColorGrid has input focus, there is also a "highlighted" cell, indicated by a darker background rectangle. You can change which cell is highlighted using the cursor movement keys. When you get to the beginning or end of the grid, the MoveFocus method shifts input focus to the previous or next control. The highlighted cell can become the selected cell with a tap of the spacebar or the Enter key.

Whenever the selected color changes, the control makes a call to its OnSelectedColorChanged method, which then fires its SelectedColorChanged event.

Here's a program that creates an object of type ColorGrid and puts it between two do-nothing buttons so that you can see how the input focus works. The window installs an event handler for the SelectedColorChanged event and changes the window background based on the SelectedColor property of the control.

SelectColor.cs

[View full width]

//-------------------------------------------- // SelectColor.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.SelectColor { public class SelectColor : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new SelectColor()); } public SelectColor() { Title = "Select Color"; SizeToContent = SizeToContent .WidthAndHeight; // Create StackPanel as content of window. StackPanel stack = new StackPanel(); stack.Orientation = Orientation .Horizontal; Content = stack; // Create do-nothing button to test tabbing. Button btn = new Button(); btn.Content = "Do-nothing button\nto test tabbing"; btn.Margin = new Thickness(24); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; stack.Children.Add(btn); // Create ColorGrid control. ColorGrid clrgrid = new ColorGrid(); clrgrid.Margin = new Thickness(24); clrgrid.HorizontalAlignment = HorizontalAlignment.Center; clrgrid.VerticalAlignment = VerticalAlignment.Center; clrgrid.SelectedColorChanged += ColorGridOnSelectedColorChanged; stack.Children.Add(clrgrid); // Create another do-nothing button. btn = new Button(); btn.Content = "Do-nothing button\nto test tabbing"; btn.Margin = new Thickness(24); btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; stack.Children.Add(btn); } void ColorGridOnSelectedColorChanged (object sender, EventArgs args) { ColorGrid clrgrid = sender as ColorGrid; Background = new SolidColorBrush (clrgrid.SelectedColor); } } }



I assume you're familiar with the ListBox control, which is a control that displays multiple items (generally text items) in a vertical list and let the user select one. Does ColorGrid look anything like a ListBox to you? It doesn't really look like one to me, but consider the two controls more abstractly. Both let you choose an item from a collection of multiple items.

In Chapter 13, you'll see how to create a control very similar to ColorGrid by using most of the keyboard, mouse, and selection logic already built into ListBox.




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