Chapter 10. Custom Elements


Normally a chapter such as this would be titled "Custom Controls," but in the Windows Presentation Foundation the distinction between elements and controls is rather amorphous. Even if you mostly create custom controls rather than elements, you'll probably also be using custom elements in constructing those controls. This chapter and the next two show mostly those techniques for creating custom elements and controls that are best suited for procedural code such as C#. In Part 2 of this book, you'll learn about alternative ways to create custom controls using XAML, and also about styling and template features that can help you customize controls.

When creating a custom element, you'll almost certainly be inheriting from FrameworkElement, just like Image, Panel, TextBlock, and Shape do. (You could alternatively inherit from UIElement, but the process is somewhat different than what I'll be describing.) When creating a custom control, you'll probably inherit from Control or (if you're lucky) from one of the classes that derive from Control such as ContentControl.

When faced with the job of designing a new element, the question poses itself: Should you inherit from FrameworkElement or Control? Object-oriented design philosophy suggests that you should inherit from the lowest class in the hierarchy that provides what you need. For some classes, that will obviously be FrameworkElement. However, the Control class adds several important properties to FrameworkElement that you might want: These properties include Background, Foreground, and all the font-related properties. Certainly if you'll be displaying text, these properties are very handy. But Control also adds some properties that you might prefer to ignore but which you'll probably feel obligated to implement: HorizontalContentAlignment, VerticalContentAlignment, BorderBrush, BorderThickness, and Padding. For example, if you inherit from Control, and if horizontal and vertical content alignment potentially make a difference in how you display the contents of the control, you should probably do something with the HorizontalContentAlignment and VerticalContentAlignment properties.

The property that offers the biggest hint concerning the difference between elements and controls is Focusable. Although FrameworkElement defines this property, the default value is false. The Control class redefines the default to true, strongly suggesting that controls are elements that can receive keyboard input focus. Although you can certainly inherit from FrameworkElement and set Focusable to true, or inherit from Control and set Focusable to false (as several controls do), it's an interesting and convenient way to distinguish elements and controls.

At the end of Chapter 3, I presented a program called RenderTheGraphic that included a class that inherited from FrameworkElement. Here's that class:

SimpleEllipse.cs

[View full width]

//------------------------------------------------- // SimpleEllipse.cs (c) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Media; namespace Petzold.RenderTheGraphic { class SimpleEllipse : FrameworkElement { protected override void OnRender (DrawingContext dc) { dc.DrawEllipse(Brushes.Blue, new Pen (Brushes.Red, 24), new Point(RenderSize.Width / 2, RenderSize.Height / 2), RenderSize.Width / 2, RenderSize .Height / 2); } } }



The virtual OnRender method is defined by UIElement. The single argument is an object of type DrawingContext. (Old-time Windows programmers like me will find it hard to resist naming this object dc in honor of the Device Context used for drawing in Win16 and Win32 programs. Both DCs serve similar purposes, although they are certainly not functionally equivalent.) The DrawingContext class defines a collection of drawing methods that represent the lowest-level drawing you can do and still call your code a "pure WPF application." I'll discuss a couple of these methods in the next two chapters but use others later in this book.

Code in the OnRender method should assume a drawing surface that has an origin of (0,0) with a width and height given by the dimensions of the RenderSize property (also defined by UIElement). In the OnRender method in the SimpleEllipse class, the first argument to DrawEllipse is a blue brush to fill the interior of the ellipse and the second argument is a 24-unit-wide (quarter inch) red pen used to stroke the perimeter of the ellipse. The third argument is a Point object indicating the center of the ellipse, and the final two arguments are the horizontal and vertical radii.

Of course, what a program draws in its OnRender method does not go directly to the screen. The graphical object defined in OnRender is retained by the WPF graphics system and displayed along with other visual objects in a composition. The graphical object is retained until a subsequent call to OnRender replaces it. Calls to OnRender can occur any time the system detects a need to update the visual rendition of the element. This generally happens much less often than WM_PAINT messages in Win32 or Paint events in Windows Forms programs, however, because the graphics are retained. OnRender needn't be called when a visual object is exposed while moving another window, for example. But OnRender is called if the element size changes. An explicit call to the InvalidateVisual method (defined by UIElement) can also force a call to OnRender.

The graphical object that OnRender draws also plays a role in the processing of mouse events. For example, if you draw an ellipse with a null interior brush, the interior will not respond to the mouse.

An OnRender method typically uses the RenderSize property for drawing, or at least to determine the dimensions in which to draw. Where does this property come from? As you'll see in more detail in this chapter and the next, RenderSize is calculated by UIElement based on a number of factors. The dimensions of RenderSize are duplicated in the protected ActualWidth and ActualHeight properties of the element.

If you go back and experiment with the RenderTheGraphic programor recall much of the experimentation with element size in the early chaptersyou'll see that RenderSize is most obviously related to the size of the container in which the element appears. In the RenderTheGraphic program, this container is the client area of the window. But other factors can affect RenderSize. If you set the Margin property for the ellipse, RenderSize is reduced by the margin values.

If a program explicitly sets the Width or Height properties for the element, these properties take priority over the container size in the calculation of RenderSize. If Width is set to a value other than NaN (not a number), RenderSize.Width will equal Width, and that's the width OnRender uses to draw the ellipse. If the container is then made narrower than Width, part of the ellipse is truncated. If a program sets MinWidth, MaxWidth, MinHeight, or MaxHeight, the element is displayed with the MaxWidth or MaxHeight dimensions if the container is larger than those dimensions. If the container is smaller than MaxWidth or MaxHeight, the container size governs the element size. But if the container gets smaller than MinWidth or MinHeight, the ellipse stops decreasing in size and part of it is truncated.

The size of elements is also influenced by their HorizontalAlignment and VerticalAlignment properties. Try setting these properties in RenderTheGraphic:

elips.HorizontalAlignment = HorizontalAlignment.Center; elips.VerticalAlignment = VerticalAlignment.Center; 


You'll see the ellipse collapse into a tiny ball a quarter inch in diameter. What's happened here is that RenderSize now has Width and Height properties of zero, and the only thing visible is part of the quarter-inch-thick perimeter around the ellipse.

Despite zero dimensions, the figure is still somewhat visible because of the logic DrawEllipse uses to render the line drawn as the perimeter around the ellipse. This line is geometrically positioned based on the center and radius values passed to the DrawEllipse method. However, if this perimeter has a width of 24 units, the quarter-inch-wide line around the ellipse actually straddles the geometric circumference of the ellipse. Half the line falls inside the geometric circumference of the ellipse and half falls outside. You can see this in a couple of ways. If you remove all extra code from RenderTheGraphic, you'll see that half the perimeter is cut off on the four sides of the client area. Now try setting the size of the ellipse like this:

elips.Width = 26; elips.Height = 26; 


The ellipse you'll see will have a tiny dot in the center and a 24-unit-wide red perimeter. The actual displayed size of the ellipse is 50 units wide and 50 units highthe 26 specified in the Width and Height properties plus 12 more on each side for the line around the perimeter.

This SimpleEllipse class is perhaps a first step in creating something very much like the Ellipse class from the Shapes library. What more would need to be done in such a class?

Most important, you'd need to define some dependency properties for the brushes used for filling and stroking the ellipse, and for the thickness of the perimeter. You might also consider making some adjustments so that the display of the ellipse more closely mimics the actual Ellipse method. You can analyze that behavior by going back to the ShapeTheEllipse program, also presented in Chapter 3. Here's what you'll find:

  • When the ellipse occupies the full size of the client area, none of the perimeter is clipped.

  • To get the same type of image you saw when you set the Width and Height properties of SimpleEllipse to 26 in RenderTheGraphic, you need to set both the Width and Height properties of the regular Ellipse equal to 50.

  • However, when you set HorizontalAlignment and VerticalAlignment of the ellipse to Center, the regular Ellipse object collapses into a quarter-inch ballexactly the same size as SimpleEllipse.

It is very common for elements to provide some indication of their preferred size, but this is something that SimpleEllipse isn't doing, and that's somewhat unusual. Almost always, an element needs a particular minimum size in which to display itself. TextBlock, for example, needs to be able to display its text. An element's minimum preferred size is sometimes known as a desired size.

A custom element class should not use any of the public properties to define its own desired size. A custom element class should not set its Width, Height, MinWidth, MaxWidth, MinHeight, or MaxHeight properties. These properties are for use by consumers of the classclasses that instantiate your custom element classand you should let those consumers do what they want with these properties.

Instead, a custom element class declares its desired size by overriding the MeasureOverride method defined by FrameworkElement. The MeasureOverride method surely has a peculiar name, and there's a reason for it. MeasureOverride is similar to a method defined by UIElement named MeasureCore. FrameworkElement redefines MeasureCore as sealed, which means that classes that inherit from FrameworkElement cannot override it. FrameworkElement instead defines MeasureOverride as a substitute for MeasureCore. FrameworkElement requires a different approach to element sizing than UIElement because of the inclusion of the Margin property. The element requires space for its margin but doesn't actually use that space for itself.

A class that inherits from FrameworkElement overrides MeasureOverride like so:

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


A call to MeasureOverride always precedes the first call to OnRender. Thereafter, there may be additional calls to OnRender if the element needs to be refreshed but nothing has happened to affect the element's size. A program can force a call to MeasureOverride by calling InvalidateMeasure for the element.

The sizeAvailable argument indicates the size being made available to the element. The Width and Height dimensions of this Size object can range from 0 to positive infinity.

For example, in a window that just sets its Content property to the element, sizeAvailable is simply the client size of the window. However, if the window sets its SizeToContent property equal to SizeToContent.WidthAndHeight, the sizeAvailable argument will have a Width and Height set to Double.PositiveInfinity under the assumption that the window can grow to whatever size the element needs. If a window has SizeToContent set to SizeToContent.Width (for example), sizeAvailable.Width will equal infinity, and sizeAvailable.Height will be the height of the client area. (However, if the user then changes the size of the window by dragging the sizing border, sizeAvailable reverts to the actual client area size.)

Suppose the element is a child of a StackPanel with a default vertical orientation. In that case, sizeAvailable.Width will be the width of the StackPanel (which, of course, will normally be the width of its container) and sizeAvailable.Height will be infinity.

Suppose the element is a child of a Grid, and the element is in a column that has a GridLength of 300 pixels. Then sizeAvailable.Width will equal 300. If the width of the column has been set to GridLength.Auto, sizeAvailable.Width will be infinity because the column will grow in size to accommodate the element. If the width is a GridLength of GridUnitType.Star, sizeAvailable.Width will indicate some portion of the leftover width being made available to this column.

If the Margin property of the element has been set, some space is required for that margin and less size is available to the element itself. The sizeAvailable.Width property is reduced by the sum of the Left and Right properties of the margin, and sizeAvailable.Height is reduced by the sum of the Top and Bottom properties. Of course, dimensions that start out as infinity will remain infinite regardless of the margins. The dimensions of sizeAvailable will always be non-negative.

Everything I've said in the past four paragraphs has to be modified if any of the following properties of the element have been set: Width, MinWidth, MaxWidth, Height, MinHeight, MaxHeight. These are properties being imposed on the element by a consumer of the element. Recall that these properties have default values of NaN. Only if a program sets these properties will they have some effect on the sizing and layout of the element.

If Width is set, the sizeAvailable.Width argument will equal Width, and similarly for Height. Alternatively, if MinWidth is set, sizeAvailable.Width will be greater than or equal to MinWidth (which means that it could still be infinity). If MaxWidth is set, sizeAvailable.Width will be less than or equal to MaxWidth.

The class should use its override of the MeasureOverride method to indicate an appropriate "natural" size of the element. Such a natural size is most obvious in the case of TextBlock or an Image element displaying a bitmap in its metrical size. If there is no natural size for the element, the element should return its minimum acceptable size. This minimum acceptable size could well be zero, or it could be something quite small.

If you don't override MeasureOverride, the base implementation in FrameworkElement returns zero. Returning zero from MeasureOverride does not mean that your element will be rendered at that size! This should be obvious from the RenderTheEllipse program, which displays a satisfactory ellipse in most cases even though SimpleEllipse doesn't override MeasureOverride and lets the base implementation return zero.

What mostly prevents elements from being rendered with a zero size are the default HorizontalAlignment and VerticalAlignment settings of Stretch. The sizeAvailable argument to MeasureOverride does not reflect any alignment settings, but the calculation of the RenderSize property takes account of them.

If you're defining just a simple element without childrenas you'll see in the next chapter, everything changes when you have childrenyour MeasureOverride method quite possibly doesn't even need to examine the sizeAvailable argument. An exception is when your element needs to maintain a particular aspect ratio. For example, the Image class returns a value from MeasureOverride based on the size of the bitmap it needs to display and its Stretch property, which indicates how the bitmap is to be displayed. If Stretch equals Stretch.None, MeasureOverride in Image returns the metrical size of the bitmap. For Stretch.Uniform (the default setting), MeasureOverride uses the sizeAvailable argument to calculate a size that maintains the correct aspect ratio but has one dimension equaling either the Width or Height dimension of sizeAvailable. If one of the dimensions of sizeAvailable is Infinity, it uses the other dimension of sizeAvailable to compute a displayed size of the image. If both are infinity, MeasureOverride returns the metrical size of the bitmap. For Stretch.Fill, MeasureOverride simply returns the sizeAvailable argument unless one or both of the properties are infinity, in which case it falls back on the logic in Stretch.Uniform.

But Image is really an exception. Most elements that don't have a natural size should return zero or a small value. MeasureOverride must not return a Size object with a dimension of infinity even if the argument to MeasureOverride has an infinite dimension or two. (When I tried it, the InvalidOperationException message said exactly that: the element "should not return PositiveInfinity as its DesiredSize, even if Infinity is passed in as available size. Please fix the implementation of this override.")

An element should not attempt to take account of its Width and Height (and related properties) while processing the MeasureOverride method. These properties have already been taken into account when MeasureOverride is called. As I mentioned earlier, an element should not set its own Width and Height properties. These properties are for consumers of the element.

The regular Ellipse class from the Shapes library (or, more accurately, the Shape class from which Ellipse derives) processes the MeasureOverride method by returning a Size object with its dimensions set to the value of its Thickness property, which is usually a small number.

Here's a class that comes much closer to the behavior of Ellipse.

BetterEllipse.cs

[View full width]

//---------------------------------------------- // BetterEllipse.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.RenderTheBetterEllipse { public class BetterEllipse : FrameworkElement { // Dependency properties. public static readonly DependencyProperty FillProperty; public static readonly DependencyProperty StrokeProperty; // Public interfaces to dependency properties. public Brush Fill { set { SetValue(FillProperty, value); } get { return (Brush)GetValue (FillProperty); } } public Pen Stroke { set { SetValue(StrokeProperty, value); } get { return (Pen)GetValue (StrokeProperty); } } // Static constructor. static BetterEllipse() { FillProperty = DependencyProperty.Register("Fill" , typeof(Brush), typeof(BetterEllipse), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender)); StrokeProperty = DependencyProperty.Register ("Stroke", typeof(Pen), typeof(BetterEllipse), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure)); } // Override of MeasureOverride. protected override Size MeasureOverride (Size sizeAvailable) { Size sizeDesired = base .MeasureOverride(sizeAvailable); if (Stroke != null) sizeDesired = new Size(Stroke .Thickness, Stroke.Thickness); return sizeDesired; } // Override of OnRender. protected override void OnRender (DrawingContext dc) { Size size = RenderSize; // Adjust rendering size for width of Pen. if (Stroke != null) { size.Width = Math.Max(0, size .Width - Stroke.Thickness); size.Height = Math.Max(0, size .Height - Stroke.Thickness); } // Draw the ellipse. dc.DrawEllipse(Fill, Stroke, new Point(RenderSize.Width / 2, RenderSize.Height / 2), size.Width / 2, size.Height / 2); } } }



If you examine the regular Shape class, you'll see that it defines a bunch of properties beginning with the word Stroke that govern the appearance of lines such as the Ellipse perimeter. Rather than implement all these various Stroke properties in my class, I decided to define just one property named Stroke of type Pen, because the Pen class basically encapsulates all the properties that Shape explicitly defines. Notice that MeasureOverride returns a size based on the thickness of the Pen object (but only if the Pen actually exists), and OnRender decreases the size of the ellipse radii by the Thickness property.

I decided that Fill and Stroke should be backed with the dependency properties FillProperty and StrokeProperty so that the class would be ready for animation. Notice the definition of the FrameworkPropertyMetadata in the static constructor: The Fill property has a flag of AffectsRender and the Stroke property has a flag of AffectsMeasure. When the Fill property is changed, the InvalidateVisual method is effectively called, which generates a new call to OnRender. But a change to the Stroke property effectively causes InvalidateMeasure to be called, which generates a call to MeasureOverride, which is then followed by a call to OnRender. The difference is that the size of the element as indicated in MeasureOverride is affected by the Pen but not by the Brush.

As you'll see when you run the RenderTheBetterEllipse program, the ellipse perimeter fits entirely in the window's client area.

RenderTheBetterEllipse.cs

[View full width]

//--------- ---------------------------------------------- // RenderTheBetterEllipse.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.RenderTheBetterEllipse { public class RenderTheBetterEllipse : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new RenderTheBetterEllipse()); } public RenderTheBetterEllipse() { Title = "Render the Better Ellipse"; BetterEllipse elips = new BetterEllipse(); elips.Fill = Brushes.AliceBlue; elips.Stroke = new Pen( new LinearGradientBrush(Colors .CadetBlue, Colors.Chocolate, new Point (1, 0), new Point(0, 1)), 24); // 1/4 inch Content = elips; } } }



Now suppose you want to add code to BetterEllipse to display some text centered within the element. Because you've already implemented the MeasureOverride and OnRender methods, you might feel inclined to simply add some code to the OnRender method to call the DrawText method of DrawingContext. By itself, the DrawText method looks fairly simple:

dc.DrawText(formtxt, pt); 


The second argument is a Point where the text is to begin. (By default, the text origin for English and other western languages is the upper-left corner of the text.) However, the first argument to DrawText is an object of type FormattedText, and that one's a real doozy. The simplest of its two constructors has six arguments. The first argument is a text string, and the arguments go on to include information about the display characteristics of this string. One of these arguments is a Typeface object, which you can create like this:

new Typeface(new FontFamily("Times New Roman"), FontStyles.Italic,              FontWeights.Normal, FontStretches.Normal) 


Or like this, which is somewhat easier:

new Typeface("Times New Roman Italic"); 


But FormattedText is more versatile than the constructors imply. The FormattedText object is capable of having different formatting applied to different parts of the text. Methods defined by FormattedText, such as SetFontSize and SetFontStyle, have versions to specify an offset into the text string and a number of characters to apply the formatting.

At any rate, you could add a little text to the BetterEllipse method by inserting the following code at the end of the OnRender method:

FormattedText formtxt =     new FormattedText("Hello, ellipse!", CultureInfo.CurrentCulture, FlowDirection,                       new Typeface("Times New Roman Italic"), 24,                       Brushes.DarkBlue); Point ptText = new Point((RenderSize.Width - formtxt.Width) / 2,                          (RenderSize.Height - formtxt.Height) / 2); dc.DrawText(formtxt, ptText); 


You'll need a using directive for System.Globalization to reference the static CultureInfo.CurrentCulture property. Conveniently, FlowDirection is a property of FrameworkElement. The calculations involved in ptText determine the upper-left corner of the text, assuming that it is positioned in the center of the ellipse.

You'll certainly want to make sure this code goes after the DrawEllipse call. Otherwise the ellipse will be visually on top of the text and at least part of the text will be hidden behind the ellipse. Even if the text is in the foreground, there are problems when you give the ellipse a small size:

elips.Width = 50; 


In this case it's likely the text will exceed the dimensions of the ellipse. As you'll note if you actually add this code and try it out, OnRender will not clip the text to the dimensions of RenderSize. OnRender only performs clipping if either dimension of the sizeDesired return value from MeasureOverride exceeds the corresponding dimension of the sizeAvailable argument to MeasureOverride. Clipping is based on sizeAvailable. If an OnRender method itself wants to prevent graphics from spilling over the dimensions of RenderSize, it can set a clipping region for the DrawingContext:

dc.PushClip(new RectangleGeometry(new Rect(new Point(0, 0), RenderSize))); 


But what the class really needs to be doing is determining FormattedText before or during the MeasureOverride call. MeasureOverride can then take account of the text size in determining the desired size of the element.

Of course, in creating this hypothetical "ellipse with embedded text" class, measuring the string is just the first step. You'll probably want to define properties not only for the text, but also for all the font properties required by FormattedText.

Rather than trying to stuff text into an ellipse, however, let's instead put text inside something a little more conventional, like a button. A class that inherits from Control (as a button probably will) has access to all the font properties defined by Control. These can be passed directly to the FormattedText constructor.

Here's a class named MedievalButton that inherits from Control to define a button that displays text. The class includes a Text property so a program can set the text that the button displays, and the property is backed by the dependency property TextProperty. The class also defines two Click-like routed events named Knock and PreviewKnock. (The code I showed in Chapter 9 for the Knock and PreviewKnock events originated in this code.)

Although this class may seem very modern in its implementation of a dependency property and routed input events, I call this class a medieval button because it draws itself entirely in its OnRender method. As you'll see, better techniques exist for defining custom and controls.

MedievalButton.cs

[View full width]

//----------------------------------------------- // MedievalButton.cs (c) 2006 by Charles Petzold //----------------------------------------------- using System; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace Petzold.GetMedieval { public class MedievalButton : Control { // Just two private fields. FormattedText formtxt; bool isMouseReallyOver; // Static readonly fields. public static readonly DependencyProperty TextProperty; public static readonly RoutedEvent KnockEvent; public static readonly RoutedEvent PreviewKnockEvent; // Static constructor. static MedievalButton() { // Register dependency property. TextProperty = DependencyProperty.Register("Text", typeof(string), typeof (MedievalButton), new FrameworkPropertyMetadata(" ", FrameworkPropertyMetadataOptions.AffectsMeasure)); // Register routed events. KnockEvent = EventManager.RegisterRoutedEvent ("Knock", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(MedievalButton)); PreviewKnockEvent = EventManager.RegisterRoutedEvent ("PreviewKnock", RoutingStrategy.Tunnel, typeof(RoutedEventHandler), typeof(MedievalButton)); } // Public interface to dependency property. public string Text { set { SetValue(TextProperty, value == null ? " " : value); } get { return (string)GetValue(TextProperty); } } // Public interface to routed events. public event RoutedEventHandler Knock { add { AddHandler(KnockEvent, value); } remove { RemoveHandler(KnockEvent, value); } } public event RoutedEventHandler PreviewKnock { add { AddHandler(PreviewKnockEvent, value); } remove { RemoveHandler(PreviewKnockEvent, value); } } // MeasureOverride called whenever the size of the button might change. protected override Size MeasureOverride(Size sizeAvailable) { formtxt = new FormattedText( Text, CultureInfo.CurrentCulture, FlowDirection, new Typeface(FontFamily, FontStyle , FontWeight, FontStretch), FontSize, Foreground); // Take account of Padding when calculating the size. Size sizeDesired = new Size(Math.Max(48, formtxt.Width) + 4, formtxt.Height + 4); sizeDesired.Width += Padding.Left + Padding.Right; sizeDesired.Height += Padding.Top + Padding.Bottom; return sizeDesired; } // OnRender called to redraw the button. protected override void OnRender (DrawingContext dc) { // Determine background color. Brush brushBackground = SystemColors .ControlBrush; if (isMouseReallyOver && IsMouseCaptured) brushBackground = SystemColors .ControlDarkBrush; // Determine pen width. Pen pen = new Pen(Foreground, IsMouseOver ? 2 : 1); // Draw filled rounded rectangle. dc.DrawRoundedRectangle (brushBackground, pen, new Rect(new Point(0, 0), RenderSize), 4, 4); // Determine foreground color. formtxt.SetForegroundBrush( IsEnabled ? Foreground : SystemColors.ControlDarkBrush); // Determine start point of text. Point ptText = new Point(2, 2); switch (HorizontalContentAlignment) { case HorizontalAlignment.Left: ptText.X += Padding.Left; break; case HorizontalAlignment.Right: ptText.X += RenderSize.Width - formtxt.Width - Padding.Right; break; case HorizontalAlignment.Center: case HorizontalAlignment.Stretch: ptText.X += (RenderSize.Width - formtxt.Width - Padding.Left - Padding .Right) / 2; break; } switch (VerticalContentAlignment) { case VerticalAlignment.Top: ptText.Y += Padding.Top; break; case VerticalAlignment.Bottom: ptText.Y += RenderSize.Height - formtxt.Height - Padding.Bottom; break; case VerticalAlignment.Center: case VerticalAlignment.Stretch: ptText.Y += (RenderSize.Height - formtxt.Height - Padding.Top - Padding .Bottom) / 2; break; } // Draw the text. dc.DrawText(formtxt, ptText); } // Mouse events that affect the visual look of the button. protected override void OnMouseEnter (MouseEventArgs args) { base.OnMouseEnter(args); InvalidateVisual(); } protected override void OnMouseLeave (MouseEventArgs args) { base.OnMouseLeave(args); InvalidateVisual(); } protected override void OnMouseMove (MouseEventArgs args) { base.OnMouseMove(args); // Determine if mouse has really moved inside or out. Point pt = args.GetPosition(this); bool isReallyOverNow = (pt.X >= 0 && pt.X < ActualWidth && pt.Y >= 0 && pt.Y < ActualHeight); if (isReallyOverNow != isMouseReallyOver) { isMouseReallyOver = isReallyOverNow; InvalidateVisual(); } } // This is the start of how 'Knock' events are triggered. protected override void OnMouseLeftButtonDown(MouseButtonEventArgs args) { base.OnMouseLeftButtonDown(args); CaptureMouse(); InvalidateVisual(); args.Handled = true; } // This event actually triggers the 'Knock' event. protected override void OnMouseLeftButtonUp(MouseButtonEventArgs args) { base.OnMouseLeftButtonUp(args); if (IsMouseCaptured) { if (isMouseReallyOver) { OnPreviewKnock(); OnKnock(); } args.Handled = true; Mouse.Capture(null); } } // If lose mouse capture (either internally or externally), redraw. protected override void OnLostMouseCapture (MouseEventArgs args) { base.OnLostMouseCapture(args); InvalidateVisual(); } // The keyboard Space key or Enter also triggers the button. protected override void OnKeyDown (KeyEventArgs args) { base.OnKeyDown(args); if (args.Key == Key.Space || args.Key == Key.Enter) args.Handled = true; } protected override void OnKeyUp (KeyEventArgs args) { base.OnKeyUp(args); if (args.Key == Key.Space || args.Key == Key.Enter) { OnPreviewKnock(); OnKnock(); args.Handled = true; } } // OnKnock method raises the 'Knock' event. protected virtual void OnKnock() { RoutedEventArgs argsEvent = new RoutedEventArgs(); argsEvent.RoutedEvent = MedievalButton .PreviewKnockEvent; argsEvent.Source = this; RaiseEvent(argsEvent); } // OnPreviewKnock method raises the 'PreviewKnock' event. protected virtual void OnPreviewKnock() { RoutedEventArgs argsEvent = new RoutedEventArgs(); argsEvent.RoutedEvent = MedievalButton .KnockEvent; argsEvent.Source = this; RaiseEvent(argsEvent); } } }



Although this class surely has many methods (most of which are overrides of methods defined in UIElement or FrameworkElement), not much in this class should be new to you. The static constructor registers the Text dependency property and the Knock and PreviewKnock routed events, and public properties and events are defined immediately following the static constructor.

The MeasureOverride method creates an object of type FormattedText and stores it as a field. Notice that almost every argument to FormattedTextand to the Typeface constructor that is the fourth argument to FormattedTextis a property defined by FrameworkElement, Control, or (in the case of the Text property) MedievalButton itself. The FormattedText constructor may look scary, but it's easy to fill up in a class that inherits from Control.

The MeasureOverride method concludes by calculating a desired size of the button. This size is the width and height of the formatted text, plus 4 units to accommodate a border. To prevent a short text string from resulting in a tiny button, the button width is minimized at a half inch. The MeasureOverride method concludes by taking account of the Padding property defined by Control.

The MeasureOverride method should not try to account for the element's Margin property, HorizontalAlignment, or VerticalAlignment. If the control wishes to implement its BorderBrush and BorderThickness properties, it can do so, but these properties have no effect otherwise. A control that implements these properties would probably treat the BorderThickness dimensions the same way I've treated the border thickness in MedievalButton.

OnRender draws the button. The only two drawing calls are DrawRoundedRectangle to draw the button border and background, and DrawText to display the text inside the button, but much preliminary work needs to be done to determine the actual colors and locations of these items.

The background of the brush should be a little darker when the button has been pressed and the mouse pointer is still positioned over the button. The outline of the button should be a little thicker (I decided) when the mouse pointer is over the button. The text normally has a color based on the Foreground property defined by Control, but it should be different if the button is disabled. The two switch statements calculate a starting position of the text based on HorizontalContentAlignment, VerticalContentAlignment, and Padding. Notice that the Point object named ptText is initialized to the point (2, 2) to allow room for the border drawn by the Rectangle call.

The remainder of the class handles input events. For OnMouseEnter and OnMouseLeave, all that's necessary is a call to InvalidateVisual so that the button is redrawn. I had originally intended for OnRender to use the IsMouseOver property to determine if the mouse was positioned over the button. However, if the mouse is captured, IsMouseOver returns true regardless of the position of the mouse. To allow OnRender to correctly color the button background, the OnMouseMove override calculates the value of a field I called isMouseReallyOver.

During the OnMouseLeftButtonDown call, the class captures the mouse, invalidates the appearance of the button, and sets the Handled property of MouseButtonEventArgs to true. Setting Handled to true prevents the MouseLeftButtonDown event from bubbling back up the visual tree to the window. It's consistent with the way that the regular Button class handles Click events.

The OnMouseLeftButtonUp call effectively fires the PreviewKnock and Knock events in that order by calling OnPreviewKnock and OnKnock. I patterned these two latter methods on the protected virtual parameterless OnClick method implemented by Button. The OnKeyDown and OnKeyUp overrides similarly fire the PreviewKnock and Knock events when the user presses the spacebar or the Enter key.

Here's a program named GetMedieval that creates an instance of MedievalButton.

GetMedieval.cs

[View full width]

//-------------------------------------------- // GetMedieval.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.GetMedieval { public class GetMedieval : Window { [STAThread] public static void Main() { Application app = new Application(); app.Run(new GetMedieval()); } public GetMedieval() { Title = "Get Medieval"; MedievalButton btn = new MedievalButton(); btn.Text = "Click this button"; btn.FontSize = 24; btn.HorizontalAlignment = HorizontalAlignment.Center; btn.VerticalAlignment = VerticalAlignment.Center; btn.Padding = new Thickness(5, 20, 5, 20); btn.Knock += ButtonOnKnock; Content = btn; } void ButtonOnKnock(object sender, RoutedEventArgs args) { MedievalButton btn = args.Source as MedievalButton; MessageBox.Show("The button labeled \"" + btn.Text + "\" has been knocked." , Title); } } }



You can experiment with the button's HorizontalAlignment, VerticalAlignment, HorizontalContentAlignment, VerticalContentAlignment, Margin, and Padding properties to assure yourself that this button behaves pretty much the same as the normal Button. However, if you explicitly make the MedievalButton very narrow

btn.Width = 50; 


you'll see the right side of the button truncated. You may want your button to always display an outline regardless of how small it gets. You can do this by returning a size from MeasureOverride that is always less than or equal to the sizeAvailable argument:

sizeDesired.Width = Math.Min(sizeDesired.Width, sizeAvailable.Width); sizeDesired.Height = Math.Min(sizeDesired.Height, sizeAvailable.Height); 


However, OnRender will not perform clipping unless either dimension of the sizeDesired return value exceeds the corresponding dimension in sizeAvailable. (With the code using Math.Min, the dimensions of sizeDesired will always be less than or equal to the dimensions of sizeAvailable.) Long text that exceeds the bounds of the button will still be displayed. You can force clipping to kick in by adding a small amount to the dimensions of sizeDesired (just 0.1 will work) or by beginning OnRender with code to set a clipping region based on RenderSize:

dc.PushClip(new RectangleGeometry(new Rect(new Point(0, 0), RenderSize))); 


To examine the implementation of routed events in MedievalButton, you can include the MedievalButton.cs file in the ExamineRoutedEvents project from Chapter 9, and you can substitute MedievalButton for Button in the program. You'll need to include a using directive for Petzold.GetMedieval, and make several other changes. Replace the following statement:

btn.Content = text; 


with

btn.Text = text.Text; 


Also, change the statement referring to Button.ClickEvent so that it refers to MedievalButton.KnockEvent. If you want to observe the PreviewKnock events, you can insert another AddHandler statement for that event.

Suppose you rather like using the MedievalButton class rather than Button, but sometimes you prefer that a word or two in the button text be italicized. Fortunately, the FormattedText class includes methods such as SetFontStyle and SetFontWeight that let you apply formatting to a subset of the text. So, you can replace the Text property of MedievalButton with a FormattedText property and just give the button a preformatted FormattedText object to display.

And then, of course, one day you'll need MedievalButton to display a bitmap. Perhaps by this time you've observed that DrawingContext has a DrawImage method that accepts an argument of type ImageSourcethe same type of object you use with the Image elementso you can include a new property in MedievalButton named ImageSource and put a DrawImage call in OnRender. But now you need to decide if you want the bitmap to replace the text or to be displayed along with the text, and possibly include different placement options.

Of course, you know there's a better way because the regular Button class simply has a property named Content, and Content can be any object whatsoever. If you look at the documentation of Button and ButtonBase you'll see that these classes do not themselves override OnRender. These classes are relegating the actual rendering to other classes. In the ExamineRoutedEvents program in the previous chapter, you probably got a sense that a Button was really a ButtonChrome object and whatever element happened to be the content of the button (in that case a TextBlock).

Structurally, the SimpleEllipse, BetterEllipse, and MedievalButton classes illustrate the simplest way in which a class can inherit from FrameworkElement or Control. All that's really necessary is overriding OnRender, possibly accompanied by an override of MeasureOverride. (Of course, the OnRender and MeasureOverride methods might themselves be somewhat complex but the overall structure is simple.) However, it is more common for elements to have children. These children complicate the structure of the element, but make it much more versatile.




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