Pro C#10 CHAPTER 27 WPF Graphics Rendering Services

CHAPTER 27

WPF Graphics Rendering Services

In this chapter, you’ll examine the graphical rendering capabilities of WPF. As you’ll see, WPF provides three separate ways to render graphical data: shapes, drawings, and visuals. After you understand the pros and cons of each approach, you will start learning about the world of interactive 2D graphics using the classes within System.Windows.Shapes. After this, you’ll see how drawings and geometries allow you to render 2D data in a more lightweight manner. Finally, you’ll learn how the visual layer gives you the greatest level of power and performance.
Along the way, you will explore several related topics, such as how to create custom brushes and pens, how to apply graphical transformations to your renderings, and how to perform hit-test operations. You’ll see how the integrated tools of Visual Studio and an additional tool named Inkscape can simplify your graphical coding endeavors.

■Note Graphics are a key aspect of WPF development. Even if you are not building a graphics-heavy application (such as a video game or multimedia application), the topics in this chapter are critical when you work with services such as control templates, animations, and data-binding customization.

Understanding WPF’s Graphical Rendering Services
WPF uses a particular flavor of graphical rendering that goes by the term retained-mode graphics. Simply put, this means that since you are using XAML or procedural code to generate graphical renderings, it is the responsibility of WPF to persist these visual items and ensure that they are correctly redrawn and refreshed in an optimal manner. Thus, when you render graphical data, it is always present, even when the end user hides the image by resizing or minimizing the window, by covering the window with another, and so forth.
In stark contrast, previous Microsoft graphical rendering APIs (including Windows Forms’ GDI+) were immediate-mode graphical systems. In this model, it was up to the programmer to ensure that rendered visuals were correctly “remembered” and updated during the life of the application. For example, in a Windows Forms application, rendering a shape such as a rectangle involved handling the Paint event (or overriding the virtual OnPaint() method), obtaining a Graphics object to draw the rectangle, and, most important, adding the infrastructure to ensure that the image was persisted when the user resized the window (e.g., creating member variables to represent the position of the rectangle and calling Invalidate() throughout your program).
The shift from immediate-mode to retained-mode graphics is indeed a good thing, as programmers have far less grungy graphics code to author and maintain. However, I’m not suggesting that the WPF graphics API is completely different from earlier rendering toolkits. For example, like GDI+, WPF supports various brush types and pen objects, techniques for hit-testing, clipping regions, graphical transformations,

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_27

1197

and so on. So, if you currently have a background in GDI+ (or C/C++-based GDI), you already know a good deal about how to perform basic renderings under WPF.

WPF Graphical Rendering Options
As with other aspects of WPF development, you have a number of choices regarding how to perform your graphical rendering, beyond the decision to do so via XAML or procedural C# code (or perhaps a
combination of both). Specifically, WPF provides the following three distinct ways to render graphical data:
•Shapes: WPF provides the System.Windows.Shapes namespace, which defines a small number of classes for rendering 2D geometric objects (rectangles, ellipses, polygons, etc.). While these types are simple to use and powerful, they do come with a fair amount of memory overhead if used with reckless abandon.
•Drawings and geometries: The WPF API provides a second way to render graphical data, using descendants from the System.Windows.Media.Drawing abstract class. Using classes such as GeometryDrawing or ImageDrawing (in addition to various geometry objects), you can render graphical data in a more lightweight (but less feature-rich) manner.
•Visuals: The fastest and most lightweight way to render graphical data under WPF is using the visual layer, which is accessible only through C# code. Using descendants of System.Windows.Media.Visual, you can speak directly to the WPF graphical subsystem.
The reason for offering different ways to do the same thing (i.e., render graphical data) has to do with memory use and, ultimately, application performance. Because WPF is such a graphically intensive
system, it is not unreasonable for an application to render hundreds or even thousands of different images on a window’s surface, and the choice of implementation (shapes, drawings, or visuals) could have a
huge impact.
Do understand that when you build a WPF application, chances are good you’ll use all three options. As a rule of thumb, if you need a modest amount of interactive graphical data that can be manipulated by the user (receive mouse input, display tooltips, etc.), you’ll want to use members in the System.Windows.Shapes namespace.
In contrast, drawings and geometries are more appropriate when you need to model complex, generally noninteractive, vector-based graphical data using XAML or C#. While drawings and geometries can still respond to mouse events, hit-testing, and drag-and-drop operations, you will typically need to author more code to do so.
Last but not least, if you require the fastest possible way to render massive amounts of graphical data, the visual layer is the way to go. For example, let’s say you are using WPF to build a scientific application that can plot out thousands of points of data. Using the visual layer, you can render the plot points in the most optimal way possible. As you will see later in this chapter, the visual layer is accessible only via C# code and is not XAML-friendly.
No matter which approach you take (shapes, drawings and geometries, or visuals), you will make use of common graphical primitives such as brushes (which fill interiors), pens (which draw exteriors), and transformation objects (which, well, transform the data). To begin the journey, you will start working with the classes of System.Windows.Shapes.

■Note WPF also ships with a full-blown API that can be used to render and manipulate 3D graphics, which is not addressed in this text.

Rendering Graphical Data Using Shapes
Members of the System.Windows.Shapes namespace provide the most straightforward, most interactive, yet most memory-intensive way to render a two-dimensional image. This namespace (defined in the PresentationFramework.dll assembly) is quite small and consists of only six sealed classes that extend the abstract Shape base class: Ellipse, Rectangle, Line, Polygon, Polyline, and Path.
The abstract Shape class inherits from FrameworkElement, which inherits from UIElement. These classes define members to deal with sizing, tooltips, mouse cursors, and whatnot. Given this inheritance chain, when you render graphical data using Shape-derived classes, the objects are just about as functional (as far as user interactivity is concerned) as a WPF control!
For example, determining whether the user has clicked your rendered image is no more complicated than handling the MouseDown event. By way of a simple example, if you authored this XAML of a Rectangle object in the Grid of your initial Window:

you could implement a C# event handler for the MouseDown event that changes the rectangle’s background color when clicked, like so:

private void myRect_MouseDown(object sender, MouseButtonEventArgs e)
{
// Change color of Rectangle when clicked. myRect.Fill = Brushes.Pink;
}

Unlike with other graphical toolkits you may have used, you do not need to author a ton of infrastructure code that manually maps mouse coordinates to the geometry, manually calculates hit-testing, renders to an off-screen buffer, and so forth. The members of System.Windows.Shapes simply respond to the events you register with, just like a typical WPF control (e.g., Button, etc.).
The downside of all this out-of-the-box functionality is that the shapes do take up a fair amount of memory. If you’re building a scientific application that plots thousands of points on the screen, using shapes would be a poor choice (essentially, it would be about as memory intensive as rendering thousands of Button objects!). However, when you need to generate an interactive 2D vector image, shapes are a wonderful choice.
Beyond the functionality inherited from the UIElement and FrameworkElement parent classes, Shape
defines a number of members for each of the children; Table 27-1 shows some of the more useful ones.

Table 27-1. Key Properties of the Shape Base Class

Properties Meaning in Life
DefiningGeometry Returns a Geometry object that represents the overall dimensions of the current shape. This object contains only the plot points that are used to render the data and has no trace of the functionality from UIElement or FrameworkElement.
Fill Allows you to specify a brush object to fill the interior portion of a shape.
GeometryTransform Allows you to apply transformations to a shape before it is rendered on the screen. The inherited RenderTransform property (from UIElement) applies the transformation after it has been rendered on the screen.
(continued)

Table 27-1. (continued)

Properties Meaning in Life
Stretch Describes how to fill a shape within its allocated space, such as its position within a layout manager. This is controlled using the corresponding System. Windows.Media.Stretch enumeration.
Stroke Defines a brush object or, in some cases, a pen object (which is really a brush in disguise) that is used to paint the border of a shape.
StrokeDashArray, StrokeEndLineCap, StrokeStartLineCap, StrokeThickness These (and other) stroke-related properties control how lines are configured when drawing the border of a shape. In a majority of cases, these properties will configure the brush used to draw a border or line.

■Note If you forget to set the Fill and Stroke properties, WPF will give you “invisible” brushes, and, therefore, the shape will not be visible on the screen!

Adding Rectangles, Ellipses, and Lines to a Canvas
You will build a WPF application that can render shapes using XAML and C# and, while doing so, learn a bit about the process of hit-testing. Create a new WPF application named RenderingWithShapes and change the title of MainWindow.xaml to “Fun with Shapes!” Then update the initial XAML of the , replacing the Grid with a containing a (now empty) and a . Note that each contained item has a fitting name via the Name property.





Now, populate the with a set of objects, each of which contains a specific Shape-derived class as content. Notice that each is assigned to the same GroupName (to ensure mutual exclusivity) and is also given a fitting name.











As you can see, declaring Rectangle, Ellipse, and Line objects in XAML is quite straightforward and requires little comment. Recall that the Fill property is used to specify a brush to paint the interior of a shape. When you require a solid-colored brush, just specify a hard-coded string of known values, and the underlying type converter will generate the correct object. One interesting feature of the Rectangle type is that it defines RadiusX and RadiusY properties to allow you to render curved corners.
Line represents its starting and ending points using the X1, X2, Y1, and Y2 properties (given that height and width make little sense when describing a line). Here you are setting up a few additional properties that control how to render the starting and ending points of the Line, as well as how to configure the stroke settings. Figure 27-1 shows the rendered toolbar, as seen through the Visual Studio WPF designer.

Figure 27-1. Using Shapes as content for a set of RadioButtons

Now, using the Properties window of Visual Studio, handle the MouseLeftButtonDown event for the Canvas, and handle the Click event for each RadioButton. In your C# file, your goal is to render the selected shape (a circle, square, or line) when the user clicks within the Canvas. First, define the following nested enum (and corresponding member variable) within your Window-derived class:

public partial class MainWindow : Window
{
private enum SelectedShape
{ Circle, Rectangle, Line }
private SelectedShape _currentShape;
}

Within each Click event handler, set the currentShape member variable to the correct SelectedShape
value, as follows:

private void CircleOption_Click(object sender, RoutedEventArgs e)
{
_currentShape = SelectedShape.Circle;
}

private void RectOption_Click(object sender, RoutedEventArgs e)
{
_currentShape = SelectedShape.Rectangle;
}

private void LineOption_Click(object sender, RoutedEventArgs e)
{
_currentShape = SelectedShape.Line;
}

With the MouseLeftButtonDown event handler of the Canvas, you will render out the correct shape (of a predefined size), using the X,Y position of the mouse cursor as a starting point. Here is the complete implementation, with analysis to follow:

private void CanvasDrawingArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Shape shapeToRender = null;
// Configure the correct shape to draw. switch (_currentShape)
{
case SelectedShape.Circle:
shapeToRender = new Ellipse() { Fill = Brushes.Green, Height = 35, Width = 35 }; break;
case SelectedShape.Rectangle:
shapeToRender = new Rectangle()
{ Fill = Brushes.Red, Height = 35, Width = 35, RadiusX = 10, RadiusY = 10 }; break;
case SelectedShape.Line: shapeToRender = new Line()
{
Stroke = Brushes.Blue, StrokeThickness = 10,
X1 = 0, X2 = 50, Y1 = 0, Y2 = 50,
StrokeStartLineCap= PenLineCap.Triangle, StrokeEndLineCap = PenLineCap.Round
};
break; default:
return;
}
// Set top/left position to draw in the canvas. Canvas.SetLeft(shapeToRender, e.GetPosition(canvasDrawingArea).X); Canvas.SetTop(shapeToRender, e.GetPosition(canvasDrawingArea).Y);
// Draw shape!
canvasDrawingArea.Children.Add(shapeToRender);
}

■Note You might notice that the Ellipse, Rectangle, and Line objects being created in this method have the same property settings as the corresponding XAML definitions! As you might hope, you can streamline this code, but that requires an understanding of the WPF object resources, which you will examine in Chapter 28.

As you can see, you are testing the currentShape member variable to create the correct Shape- derived object. After this point, you set the top-left value within the Canvas using the incoming MouseButtonEventArgs. Last but not least, you add the new Shape-derived type to the collection of UIElement objects maintained by the Canvas. If you run your program now, you should be able to click anywhere in the canvas and see the selected shape rendered at the location of the left mouse-click.

Removing Rectangles, Ellipses, and Lines from a Canvas
With the Canvas maintaining a collection of objects, you might wonder how you can dynamically remove an item, perhaps in response to the user right-clicking a shape. You can certainly do this using a class in the
System.Windows.Media namespace called the VisualTreeHelper. Chapter 28 will explain the roles of “visual trees” and “logical trees” in some detail. Until then, you can handle the MouseRightButtonDown event on your Canvas object and implement the corresponding event handler like so:

private void CanvasDrawingArea_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
// First, get the X,Y location of where the user clicked. Point pt = e.GetPosition((Canvas)sender);
// Use the HitTest() method of VisualTreeHelper to see if the user clicked
// on an item in the canvas.
HitTestResult result = VisualTreeHelper.HitTest(canvasDrawingArea, pt);
// If the result is not null, they DID click on a shape! if (result != null)
{
// Get the underlying shape clicked on, and remove it from
// the canvas. canvasDrawingArea.Children.Remove(result.VisualHit as Shape);
}
}

This method begins by obtaining the exact X,Y location the user clicked in the Canvas and performs a hit-test operation via the static VisualTreeHelper.HitTest() method. The return value, a HitTestResult object, will be set to null if the user does not click a UIElement within the Canvas. If HitTestResult is not null, you can obtain the underlying UIElement that was clicked via the VisualHit property, which you are casting into a Shape-derived object (remember, a Canvas can hold any UIElement, not just shapes!). Again, you’ll get more details on exactly what a “visual tree” is in the next chapter.

■Note By default, VisualTreeHelper.HitTest() returns the topmost UIElement clicked and does not provide information on other objects below that item (e.g., objects overlapping by Z-order).

With this modification, you should be able to add a shape to the canvas with a left mouse-click and delete an item from the canvas with a right mouse-click!
So far, so good. At this point, you have used Shape-derived objects to render content on RadioButtons using XAML and populated a Canvas using C#. You will add a bit more functionality to this example when you examine the role of brushes and graphical transformations. On a related note, a different example in this chapter will illustrate drag-and-drop techniques on UIElement objects. Until then, let’s examine the remaining members of System.Windows.Shapes.

Working with Polylines and Polygons
The current example used only three of the Shape-derived classes. The remaining child classes (Polyline, Polygon, and Path) are extremely tedious to render correctly without tool support (such as Microsoft Blend, the companion tool for Visual Studio designed for WPF developers, or other tools that can create vector graphics) simply because they require a large number of plot points to represent their output. Here is an overview of the remaining Shapes types.

The Polyline type lets you define a collection of (x, y) coordinates (via the Points property) to draw a series of line segments that do not require connecting ends. The Polygon type is similar; however, it is
programmed so that it will always close the starting and ending points and fill the interior with the specified brush. Assume you have authored the following in the Kaxaml editor:




Figure 27-2 shows the rendered output in Kaxaml.

Figure 27-2. Polygons and polylines

Working with Paths
Using the Rectangle, Ellipse, Polygon, Polyline, and Line types alone to draw a detailed 2D vector image would be extremely complex, as these primitives do not allow you to easily capture graphical data such as curves, unions of overlapping data, and so forth. The final Shape-derived class, Path, provides the ability
to define complex 2D graphical data represented as a collection of independent geometries. After you have defined a collection of such geometries, you can assign them to the Data property of the Path class, where this information will be used to render your complex 2D image.
The Data property takes a System.Windows.Media.Geometry-derived class, which contains the key members described in Table 27-2.

Table 27-2. Select Members of the System.Windows.Media.Geometry Type

Member Meaning in Life
Bounds Establishes the current bounding rectangle containing the geometry.
FillContains() Determines whether a given Point (or other Geometry object) is within the bounds of a particular Geometry-derived class. This is useful for hit-testing calculations.
GetArea() Returns the entire area that a Geometry-derived type occupies.
GetRenderBounds() Returns a Rect that contains the smallest possible rectangle that could be used to render the Geometry-derived class.
Transform Assigns a Transform object to the geometry to alter the rendering.

The classes that extend Geometry (see Table 27-3) look very much like their Shape-derived counterparts.
For example, EllipseGeometry has similar members to Ellipse. The big distinction is that Geometry- derived classes do not know how to render themselves directly because they are not UIElements. Rather, Geometry-derived classes represent little more than a collection of plot-point data, which say in effect “If a Path uses my data, this is how I would render myself.”

Table 27-3. Geometry-Derived Classes

Geometry Class Meaning in Life
LineGeometry Represents a straight line
RectangleGeometry Represents a rectangle
EllipseGeometry Represents an ellipse
GeometryGroup Allows you to group several Geometry objects
CombinedGeometry Allows you to merge two different Geometry objects into a single shape
PathGeometry Represents a figure composed of lines and curves

■Note Path is not the only class in WPF that can use a collection of geometries. For example, DoubleAnimationUsingPath, DrawingGroup, GeometryDrawing, and even UIElement can all use geometries for rendering, using the PathGeometry, ClipGeometry, Geometry, and Clip properties, respectively.

The following is a Path that makes use of a few Geometry-derived types. Notice that you are setting the Data property of Path to a GeometryGroup object that contains other Geometry-derived objects such as EllipseGeometry, RectangleGeometry, and LineGeometry. Figure 27-3 shows the output.











Figure 27-3. A Path containing various Geometry objects

The image in Figure 27-3 could have been rendered using the Line, Ellipse, and Rectangle classes shown earlier. However, this would have put various UIElement objects in memory. When you use geometries to model the plot points of what to draw and then place the geometry collection into a container that can render the data (Path, in this case), you reduce the memory overhead.
Now recall that Path has the same inheritance chain as any other member of System.Windows.Shapes and therefore can send the same event notifications as other UIElement objects. Thus, if you were to define this same element in a Visual Studio project, you could determine whether the user clicked
anywhere in the sweeping line simply by handling a mouse event (remember, Kaxaml does not allow you to handle events for the markup you have authored).

The Path Modeling “Mini-Language”
Of all the classes listed in Table 27-3, PathGeometry is the most complex to configure in terms of XAML or code. This has to do with the fact that each segment of the PathGeometry is composed of objects that
contain various segments and figures (e.g., ArcSegment, BezierSegment, LineSegment, PolyBezierSegment, PolyLineSegment, PolyQuadraticBezierSegment, etc.). Here is an example of a Path object whose Data property has been set to a composed of various figures and segments:















Now, to be perfectly honest, few programmers will ever need to manually build complex 2D images by directly describing Geometry- or PathSegment-derived classes. Later in this chapter, you will learn how to convert vector graphics into path statements that can be used in XAML.
Even with the assistance of these tools, the amount of XAML required to define a complex Path object would be ghastly, as the data consists of full descriptions of various Geometry- or PathSegment-derived classes. To produce more concise and compact markup, the Path class has been designed to understand a specialized “mini-language.”
For example, rather than setting the Data property of Path to a collection of Geometry- and
PathSegment-derived types, you can set the Data property to a single string literal containing a number of

known symbols and various values that define the shape to be rendered. Here is a simple example, and the resulting output is shown in Figure 27-4:

Figure 27-4. The Path mini-language allows you to compactly describe a Geometry/PathSegment object model

The M command (short for move) takes an X,Y position that represents the starting point of the drawing.
The C command takes a series of plot points to render a curve (a cubic Bézier curve to be exact), while H
draws a horizontal line.
Now, to be perfectly honest, the chances that you will ever need to manually build or parse a string literal containing Path mini-language instructions are slim to none. However, at the least, you will no longer be surprised when you view XAML-generated dedicated tools.

WPF Brushes and Pens
Each of the WPF graphical rendering options (shape, drawing and geometries, and visuals) makes extensive use of brushes, which allow you to control how the interior of a 2D surface is filled. WPF provides six different brush types, all of which extend System.Windows.Media.Brush. While Brush is abstract, the descendants described in Table 27-4 can be used to fill a region with just about any conceivable option.

Table 27-4. WPF Brush-Derived Types

Brush Type Meaning in Life

DrawingBrush Paints an area with a Drawing-derived object (GeometryDrawing, ImageDrawing, or VideoDrawing)
ImageBrush Paints an area with an image (represented by an ImageSource object)
LinearGradientBrush Paints an area with a linear gradient RadialGradientBrush Paints an area with a radial gradient SolidColorBrush Paints a single color, set with the Color property
VisualBrush Paints an area with a Visual-derived object (DrawingVisual, Viewport3DVisual, and ContainerVisual)

The DrawingBrush and VisualBrush classes allow you to build a brush based on an existing Drawing- or Visual-derived class. These brush classes are used when you are working with the other two graphical options of WPF (drawings or visuals) and will be examined later in this chapter.
ImageBrush, as the name suggests, lets you build a brush that displays image data from an external file or embedded application resource, by setting the ImageSource property. The remaining brush types (LinearGradientBrush and RadialGradientBrush) are quite straightforward to use, though typing in the
required XAML can be a tad verbose. Thankfully, Visual Studio supports integrated brush editors that make it simple to generate stylized brushes.

Configuring Brushes Using Visual Studio
Let’s update your WPF drawing program, RenderingWithShapes, to use some more interesting brushes. The three shapes you have employed so far to render data on your toolbar use simple, solid colors, so you can capture their values using simple string literals. To spice things up a tad, you will now use the integrated brush editor. Ensure that the XAML editor of your initial window is the open window within the IDE and select the Ellipse element. Now, in the Properties window, locate the Brush category and then click Fill property listed on the top (see Figure 27-5).

Figure 27-5. Any property that requires a brush can be configured with the integrated brush editor

At the top of the Brushes editor, you will see a set of properties that are all “brush compatible” for the selected item (i.e., Fill, Stroke, and OpacityMask). Below this, you will see a series of tabs that allow you to configure different types of brushes, including the current solid color brush. You can use the color
selector tool, as well as the ARGB (alpha, red, green, and blue, where “alpha” controls transparency) editors to control the color of the current brush. Using these sliders and the related color selection area, you can create any sort of solid color. Use these tools to change the Fill color of your Ellipse and view the resulting XAML. You will notice the color is stored as a hexadecimal value, as follows:

More interestingly, this same editor allows you to configure gradient brushes, which are used to define a series of colors and transition points. Recall that this Brushes editor provides you with a set of tabs, the first of which lets you set a null brush for no rendered output. The other four allow you to set up a solid color brush (what you just examined), gradient brush, tile brush, or image brush.
Click the gradient brush button, and the editor will display a few new options (see Figure 27-6). The three buttons on the lower left allow you to pick a linear gradient, pick a radial gradient, or reverse the gradient stops. The bottommost strip will show you the current color of each gradient stop, each of which is marked by a “thumb” on the strip. As you drag these thumbs around the gradient strip, you can
control the gradient offset. Furthermore, when you click a given thumb, you can change the color for that particular gradient stop via the color selector. Finally, if you click directly on the gradient strip, you can add gradient stops.

Figure 27-6. The Visual Studio brush editor allows you to build basic gradient brushes

Take a few minutes to play around with this editor to build a radial gradient brush containing three gradient stops, set to your colors of choice. Figure 27-6 shows the brush you just constructed, using three different shades of green.
When you are done, the IDE will update your XAML with a custom brush, set to a brush-compatible property (the Fill property of the Ellipse in this example) using property-element syntax, as follows:








Configuring Brushes in Code
Now that you have built a custom brush for the XAML definition of your Ellipse, the corresponding C# code is out-of-date, in that it will still render a solid green circle. To sync things back up, update the correct case statement to use the same brush you just created. The following is the necessary update, which looks more complex than you might expect, just because you are converting the hexadecimal value to a proper Color object via the System.Windows.Media.ColorConverter class (see Figure 27-7 for the modified output):

case SelectedShape.Circle:
shapeToRender = new Ellipse() { Height = 35, Width = 35 };
// Make a RadialGradientBrush in code! RadialGradientBrush brush = new RadialGradientBrush(); brush.GradientStops.Add(new GradientStop(
(Color)ColorConverter.ConvertFromString(“#FF77F177”), 0)); brush.GradientStops.Add(new GradientStop(
(Color)ColorConverter.ConvertFromString(“#FF11E611”), 1)); brush.GradientStops.Add(new GradientStop(
(Color)ColorConverter.ConvertFromString(“#FF5A8E5A”), 0.545)); shapeToRender.Fill = brush;
break;

Figure 27-7. Drawing circles with a bit more pizzazz

By the way, you can build GradientStop objects by specifying a simple color as the first constructor parameter using the Colors enumeration, which returns a configured Color object.

GradientStop g = new GradientStop(Colors.Aquamarine, 1);

Or, if you require even finer control, you can pass in a configured Color object, like so:

Color myColor = new Color() { R = 200, G = 100, B = 20, A = 40 }; GradientStop g = new GradientStop(myColor, 34);

Of course, the Colors enum and Color class are not limited to gradient brushes. You can use them anytime you need to represent a color value in code.

Configuring Pens
In comparison with brushes, a pen is an object for drawing borders of geometries or, in the case of the Line or PolyLine class, the line geometry itself. Specifically, the Pen class allows you to draw a specified thickness, represented by a double value. In addition, a Pen can be configured with the same sort of properties seen in the Shape class, such as starting and stopping pen caps, dot-dash patterns, and so forth. For example, you can add the following markup to a shape to define the pen attributes:

In many cases, you won’t need to directly create a Pen object because this will be done indirectly when you assign a value to properties, such as StrokeThickness to a Shape-derived type (as well as other UIElements). However, building a custom Pen object is handy when working with Drawing-derived types (described later in the chapter). Visual Studio does not have a pen editor per se, but it does allow you to configure all the stroke-centric properties of a selected item using the Properties window.

Applying Graphical Transformations
To wrap up the discussion of using shapes, let’s address the topic of transformations. WPF ships with numerous classes that extend the System.Windows.Media.Transform abstract base class. Table 27-5 documents many of the key out-of-the-box Transform-derived classes.

Table 27-5. Key Descendants of the System.Windows.Media.Transform Type

Type Meaning in Life
MatrixTransform Creates an arbitrary matrix transformation that is used to manipulate objects or coordinate systems in a 2D plane
RotateTransform Rotates an object clockwise about a specified point in a 2D (x, y) coordinate system
ScaleTransform Scales an object in the 2D (x, y) coordinate system
SkewTransform Skews an object in the 2D (x, y) coordinate system
TranslateTransform Translates (moves) an object in the 2D (x, y) coordinate system
TransformGroup Represents a composite Transform composed of other Transform objects

Transformations can be applied to any UIElement (e.g., descendants of Shape as well as controls such as Button controls, TextBox controls, and the like). Using these transformation classes, you can render graphical data at a given angle, skew the image across a surface, and expand, shrink, or flip the target item in a variety of ways.

■Note While transformation objects can be used anywhere, you will find them most useful when working with WPF animations and custom control templates. As you will see later in the chapter, you can use WPF animations to incorporate visual cues to the end user for a custom control.

Transformations (or a whole set of them) can be assigned to a target object (e.g., Button, Path, etc.) using two common properties, LayoutTransform and RenderTransform.
The LayoutTransform property is helpful, in that the transformation occurs before elements are rendered into a layout manager, and therefore the transformation will not affect Z-ordering operations (in other words, the transformed image data will not overlap).
The RenderTransform property, on the other hand, occurs after the items are in their container, and therefore it is quite possible that elements can be transformed in such a way that they could overlap each other, based on how they were arranged in the container.

A First Look at Transformations
You will add some transformational logic to your RenderingWithShapes project in just a moment. However, to see transformation objects in action, open Kaxaml and define a simple StackPanel in the root Page or Window and set the Orientation property to Horizontal. Now, add the following Rectangle, which will be drawn at a 45-degree angle using a RotateTransform object:






Here is a

And for good measure, here is an Ellipse that is scaled by 20 degrees with a ScaleTransform (note the values set to the initial Height and Width), as well as a TextBox that has a group of transformation objects applied to it:














Note that when a transformation is applied, you are not required to perform any manual calculations to correctly respond to hit-testing, input focus, or whatnot. The WPF graphics engine handles such tasks on your behalf. For example, in Figure 27-8, you can see that the TextBox is still responsive to keyboard input.

Figure 27-8. The results of graphical transformation objects

Transforming Your Canvas Data
Now, let’s incorporate some transformational logic into your RenderingWithShapes example. In addition to applying a transformation object to a single item (e.g., Rectangle, TextBox, etc.), you can also apply transformation objects to a layout manager to transform all of the internal data. You could, for example, render the entire DockPanel of the main window at an angle.






This is a bit extreme for this example, so let’s add a final (less aggressive) feature that allows the user to flip the entire Canvas and all contained graphics. Begin by adding a final ToggleButton to your ToolBar, defined as follows:

Within the Click event handler, create a RotateTransform object and connect it to the Canvas object via the LayoutTransform property if this new ToggleButton is clicked. If the ToggleButton is not clicked, remove the transformation by setting the same property to null.

private void FlipCanvas_Click(object sender, RoutedEventArgs e)
{
if (flipCanvas.IsChecked == true)
{
RotateTransform rotate = new RotateTransform(-180); canvasDrawingArea.LayoutTransform = rotate;
}
else
{
canvasDrawingArea.LayoutTransform = null;
}
}

Run your application and add a bunch of graphics throughout the canvas area, making sure to go edge to edge with them. If you click your new button, you will find that the shape data flows outside of the boundaries of the canvas! This is because you have not defined a clipping region (see Figure 27-9).

Figure 27-9. Oops! Your data is flowing outside of the canvas after the transformation!

Fixing this is trivial. Rather than manually authoring complex clipping-logic code, simply set the ClipToBounds property of the Canvas to true, which prevents child elements from being rendered outside the parent’s boundaries. If you run your program again, you’ll find the data will not bleed off the canvas boundary.

The last tiny modification to make has to do with the fact that when you flip the canvas by pressing your toggle button and then click the canvas to draw a new shape, the point at which you click is not the point where the graphical data is applied. Rather, the data is rendered above the mouse cursor.

To resolve this issue, apply the same transformation object to the shape being drawn before the rendering occurs (via RenderTransform). Here is the crux of the code:

private void CanvasDrawingArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
//omitted for brevity
if (flipCanvas.IsChecked == true)
{
RotateTransform rotate = new RotateTransform(-180); shapeToRender.RenderTransform = rotate;
}
// Set top/left to draw in the canvas. Canvas.SetLeft(shapeToRender,
e.GetPosition(canvasDrawingArea).X); Canvas.SetTop(shapeToRender,
e.GetPosition(canvasDrawingArea).Y);

// Draw shape! canvasDrawingArea.Children.Add(shapeToRender);
}

This wraps up your examination of System.Windows.Shapes, brushes, and transformations. Before looking at the role of rendering graphics using drawings and geometries, let’s see how Visual Studio can be used to simplify how you work with primitive graphics.

Working with the Visual Studio Transform Editor
In the previous example, you applied various transformations by manually entering markup and authoring some C# code. While this is certainly useful, you will be happy to know that the latest version of Visual Studio ships with an integrated transformation editor. Recall that any UI element can be the recipient of transformational services, including a layout system containing various UI elements. To illustrate the use of Visual Studio’s transform editor, create a new WPF application named FunWithTransforms.

Building the Initial Layout
First, split your initial Grid into two columns using the integrated grid editor (the exact size does not matter). Now, locate the StackPanel control within your Toolbox and add it to take up the entire space of the first column of the Grid; then add three Button controls to the StackPanel, like so:








No matter which brush-compatible property you set with your custom , the bottom line is you are rendering a 2D vector image with much less overhead than the same 2D image rendered with shapes.

Containing Drawing Types in a DrawingImage
The DrawingImage type allows you to plug your drawing geometry into a WPF control. Consider the following:









In this case, your has been placed into a , rather than a
. Using this , you can set the Source property of the Image control.

Working with Vector Images
As you might agree, it would be quite challenging for a graphic artist to create a complex vector-based image using the tools and techniques provided by Visual Studio. Graphic artists have their own set of tools that
can produce amazing vector graphics. Neither Visual Studio nor its companion Expression Blend for Visual

Studio has that type of design power. Before you can import vector images into WPF application, they must be converted into Path expressions. At that point, you can program against the generated object model using Visual Studio.

■Note You can find the image being used (LaserSign.svg) as well as the exported path (LaserSign. xaml) data in the Chapter 27 folder of the download files. the image is originally from Wikipedia, located at https://en.wikipedia.org/wiki/Hazard_symbol.

Converting a Sample Vector Graphic File into XAML
Before you can import complex graphical data (such as vector graphics) into a WPF application, you need to convert the graphics into path data. As an example of how to do this, start with a sample .svg image file, such as the laser sign referenced in the preceding note. Then download and install an open source tool called Inkscape (located at www.inkscape.org). Using Inkscape, open the LaserSign.svg file from the chapter download. You might be prompted to upgrade the format. Fill in the selections as shown in Figure 27-12.

Figure 27-12. Upgrading the SVG file to the latest format in Inkscape

The next steps will seem a bit odd at first, but once you get over the oddity, it is a simple way to convert vector images to the correct XAML. When you have the image the way you want it, select the File ➤ Print menu option. Next, select the Microsoft XPS Document Writer as the printer target and then click Print. On the next screen, enter a filename and select where the file should be saved; then click Save. Now you have a complete *.xps (or *.oxps) file.

■Note Depending on a number of variables with your system configuration, the generated file will have either the .xps or .oxps extension. Either way, the process works the same.

The *.xps and *.oxps formats are actually .zip files. Rename the extension of the file to .zip, and you can open the file in File Explorer (or 7-Zip, or your favorite archive tool). You will see that it contains the hierarchy shown in Figure 27-13.

Figure 27-13. The folder hierarchy of the printed XPS file

The file that you need is in the Pages directory (Documents/1/Pages) and is named 1.fpage. Open the file with a text editor and copy everything except the open and closing tags. The path data can then be copied into the Kaxaml and placed inside a Canvas in the main Window. Your image will show in the XAML window.

■Note the latest version of Inkscape has an option to save the file as Microsoft XAML. Unfortunately, at the time of this writing, it is not compatible with WPF.

Importing the Graphical Data into a WPF Project
At this point, create a new WPF application named InteractiveLaserSign. Resize the Window to a Height of 600 and Width of 650, and replace the Grid with a Canvas.




Copy the entire XAML from the 1.fpage file (excluding the outer FixedPage tag) and paste it into the
Canvas control. View the Window in design mode, and you will see the sign reproduced in your application. If you view the Document Outline, you will see that each part of the image is represented as a XAML
Path element. If you resize your Window, the image quality stays the same, regardless of how big you make the window. This is because images represented by Path elements are rendered using the drawing engine and math instead of flipping pixels.

Interacting with the Sign
Recall that routed event tunnel and bubble, so any Path clicked inside the Canvas can be handled by a click event handler on the canvas. Update the Canvas markup to the following:

Add the event handler with the following code:

private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is Path p)
{
p.Fill = new SolidColorBrush(Colors.Red);
}
}

Now, run your application. Click the lines to see the effects.
You now understand the process of generating Path data for complex graphics and how to interact with the graphical data in code. As you might agree, the ability for professional graphic artists to generate complex graphical data and export the data as XAML is extremely powerful. Once the graphical data has been generated, developers can import the markup and program against the object model.

Rendering Graphical Data Using the Visual Layer
The final option for rendering graphical data with WPF is termed the visual layer. As mentioned, you can gain access to this layer only through code (it is not XAML-friendly). While a vast majority of your WPF applications will work just fine using shapes, drawings, and geometries, the visual layer does provide the fastest possible way to render huge amounts of graphical data. This low-level graphical layer can also
be useful when you need to render a single image over a large area. For example, if you need to fill the background of a window with a plain, static image, the visual layer is the fastest way to do so. It can also be useful if you need to change between window backgrounds quickly, based on user input or whatnot.
I won’t spend too much time delving into the details of this aspect of WPF programming, but let’s build a small sample program to illustrate the basics.

The Visual Base Class and Derived Child Classes
The abstract System.Windows.Media.Visual class type supplies a minimal set of services (rendering, hit-testing, transformations) to render graphics, but it does not provide support for additional nonvisual
services, which can lead to code bloat (input events, layout services, styles, and data binding). The Visual class is an abstract base class. You need to use one of the derived types to perform actual rendering operations. WPF provides a handful of subclasses, including DrawingVisual, Viewport3DVisual, and ContainerVisual.
In this example, you will focus only on DrawingVisual, a lightweight drawing class that is used to render shapes, images, or text.

A First Look at Using the DrawingVisual Class
To render data onto a surface using DrawingVisual, you need to take the following basic steps:
1.Obtain a DrawingContext object from the DrawingVisual class.
2.Use the DrawingContext to render the graphical data.
These two steps represent the bare minimum necessary for rendering some data to a surface. However, if you want the graphical data you’ve rendered to be responsive to hit-testing calculations (which would be important for adding user interactivity), you will also need to perform these additional steps:
1.Update the logical and visual trees maintained by the container upon which you are rendering.
2.Override two virtual methods from the FrameworkElement class, allowing the container to obtain the visual data you have created.
You will examine these final two steps in a bit. First, to illustrate how you can use the DrawingVisual class to render 2D data, create a new WPF application named RenderingWithVisuals. Your first goal is to use a DrawingVisual to dynamically assign data to a WPF Image control. Begin by updating the XAML of your window to handle the Loaded event, like so:


Title=”Fun With Visual Layer” Height=”450″ Width=”800″ Loaded=”MainWindow_Loaded”>

Next, replace the Grid with a StackPanel and add an Image in the StackPanel, like this:



Your control does not yet have a Source value because that will happen at runtime. The Loaded event will do the work of building the in-memory graphical data, using a DrawingBrush object. Make sure the following namespaces are at the top of MainWindow.cs:

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

Here is the implementation of the Loaded event handler:

private void MainWindow_Loaded( object sender, RoutedEventArgs e)
{
const int TextFontSize = 30;
// Make a System.Windows.Media.FormattedText object.
FormattedText text = new FormattedText( “Hello Visual Layer!”,
new System.Globalization.CultureInfo(“en-us”), FlowDirection.LeftToRight,

new Typeface(this.FontFamily, FontStyles.Italic, FontWeights.DemiBold, FontStretches.UltraExpanded),
TextFontSize, Brushes.Green, null,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
// Create a DrawingVisual, and obtain the DrawingContext. DrawingVisual drawingVisual = new DrawingVisual(); using(DrawingContext drawingContext =
drawingVisual.RenderOpen())
{
// Now, call any of the methods of DrawingContext to render data.
drawingContext.DrawRoundedRectangle( Brushes.Yellow, new Pen(Brushes.Black, 5), new Rect(5, 5, 450, 100), 20, 20);
drawingContext.DrawText(text, new Point(20, 20));
}
// Dynamically make a bitmap, using the data in the DrawingVisual.
RenderTargetBitmap bmp = new RenderTargetBitmap( 500, 100, 100, 90, PixelFormats.Pbgra32);
bmp.Render(drawingVisual);
// Set the source of the Image control!
myImage.Source = bmp;
}

This code introduces a number of new WPF classes, which I will briefly comment on here. The method begins by creating a new FormattedText object that represents the textual portion of the in-memory image you are constructing. As you can see, the constructor allows you to specify numerous attributes such as font size, font family, foreground color, and the text itself.
Next, you obtain the necessary DrawingContext object via a call to RenderOpen() on the DrawingVisual instance. Here, you are rendering a colored, rounded rectangle into the DrawingVisual, followed by your formatted text. In both cases, you are placing the graphical data into the DrawingVisual using hard-coded values, which is not necessarily a great idea for production but is fine for this simple test.
The last few statements map the DrawingVisual into a RenderTargetBitmap object, which is a member of the System.Windows.Media.Imaging namespace. This class will take a visual object and transform it into an in-memory bitmap image. After this point, you set the Source property of the Image control, and sure enough, you will see the output in Figure 27-14.

Figure 27-14. Using the visual layer to render an in-memory bitmap

■Note the System.Windows.Media.Imaging namespace contains a number of additional encoding classes that let you save the in-memory RenderTargetBitmap object to a physical file in a variety of formats. Check out the JpegBitmapEncoder class (and friends) for more information.

Rendering Visual Data to a Custom Layout Manager
While it is interesting to use DrawingVisual to paint onto the background of a WPF control, it is perhaps more common to build a custom layout manager (Grid, StackPanel, Canvas, etc.) that uses the visual layer internally to render its content. After you have created such a custom layout manager, you can plug it into a normal Window (or Page or UserControl) and have a part of the UI using a highly optimized rendering agent while the noncritical aspects of the hosting Window are using shapes and drawings for the remainder of the graphical data.
If you don’t require the extra functionality provided by a dedicated layout manager, you could opt to simply extend FrameworkElement, which does have the necessary infrastructure to also contain visual items. To illustrate how this could be done, insert a new class to your project named CustomVisualFrameworkElement. Extend this class from FrameworkElement and import the System,
System.Windows, System.Windows.Input, System.Windows.Media, and System.Windows.Media.Imaging
namespaces.
This class will maintain a member variable of type VisualCollection, which contains two fixed DrawingVisual objects (of course, you could add new members to this collection via a mouse operation, but this example will keep it simple). Update your class with the following new functionality:

public class CustomVisualFrameworkElement : FrameworkElement
{
// A collection of all the visuals we are building.
VisualCollection theVisuals;
public CustomVisualFrameworkElement()
{
// Fill the VisualCollection with a few DrawingVisual objects.
// The ctor arg represents the owner of the visuals.
theVisuals = new VisualCollection(this)
{AddRect(),AddCircle()};
}
private Visual AddCircle()
{
DrawingVisual drawingVisual = new DrawingVisual();
// Retrieve the DrawingContext in order to create new drawing content.
using DrawingContext drawingContext = drawingVisual.RenderOpen()
// Create a circle and draw it in the DrawingContext.
drawingContext.DrawEllipse(Brushes.DarkBlue, null, new Point(70, 90), 40, 50);
return drawingVisual;
}
private Visual AddRect()
{
DrawingVisual drawingVisual = new DrawingVisual();

using DrawingContext drawingContext = drawingVisual.RenderOpen()
Rect rect =
new Rect(new Point(160, 100), new Size(320, 80)); drawingContext.DrawRectangle(Brushes.Tomato, null, rect); return drawingVisual;
}
}

Now, before you can use this custom FrameworkElement in your Window, you must override two key virtual methods mentioned previously, both of which are called internally by WPF during the rendering process. The GetVisualChild() method returns a child at the specified index from the collection of child elements. The read-only VisualChildrenCount property returns the number of visual child elements within this visual collection. Both methods are easy to implement because you can delegate the real work to the VisualCollection member variable.

protected override int VisualChildrenCount
=> theVisuals.Count;

protected override Visual GetVisualChild(int index)
{
// Value must be greater than zero, so do a sanity check. if (index < 0 || index >= theVisuals.Count)
{
throw new ArgumentOutOfRangeException();
}
return theVisuals[index];
}

You now have just enough functionality to test your custom class. Update the XAML description of the Window to add one of your CustomVisualFrameworkElement objects to the existing StackPanel. Doing so will require you to add a custom XML namespace that maps to your .NET namespace.






When you run the program, you will see the result shown in Figure 27-15.

Figure 27-15. Using the visual layer to render data to a custom FrameworkElement

Responding to Hit-Test Operations
Because DrawingVisual does not have any of the infrastructures of UIElement or FrameworkElement, you will need to programmatically add the ability to calculate hit-test operations. Thankfully, this is fairly easy to do in the visual layer because of the concept of logical and visual trees. As it turns out, when you author a blob of XAML, you are essentially building a logical tree of elements. However, behind every logical tree is a much richer description known as the visual tree, which contains lower-level rendering instructions.
Chapter 28 will delve into these trees in more detail, but for now, just understand that until you register your custom visuals with these data structures, you will not be able to perform hit-testing operations.
Luckily, the VisualCollection container does this on your behalf (which explains why you needed to pass in a reference to the custom FrameworkElement as a constructor argument).
First, update the CustomVisualFrameworkElement class to handle the MouseDown event in the class constructor using standard C# syntax, like so:
this.MouseDown += CustomVisualFrameworkElement_MouseDown;
The implementation of this handler will call the VisualTreeHelper.HitTest() method to see whether the mouse is within the boundaries of one of the rendered visuals. To do this, you specify as a parameter to HitTest() a HitTestResultCallback delegate that will perform the calculations. If you click a visual, you will toggle between a skewed rendering of the visual and the original rendering. Add the following methods to your CustomVisualFrameworkElement class:
void CustomVisualFrameworkElement_MouseDown(object sender, MouseButtonEventArgs e)
{
// Figure out where the user clicked.
Point pt = e.GetPosition((UIElement)sender);
// Call helper function via delegate to see if we clicked on a visual.
VisualTreeHelper.HitTest(this, null,
new HitTestResultCallback(myCallback), new PointHitTestParameters(pt));
}

public HitTestResultBehavior myCallback(HitTestResult result)
{
// Toggle between a skewed rendering and normal rendering,
// if a visual was clicked.
if (result.VisualHit.GetType() == typeof(DrawingVisual))
{
if (((DrawingVisual)result.VisualHit).Transform == null)
{
((DrawingVisual)result.VisualHit).Transform = new SkewTransform(7, 7);
}
else
{
((DrawingVisual)result.VisualHit).Transform = null;
}
}
// Tell HitTest() to stop drilling into the visual tree.
return HitTestResultBehavior.Stop;
}

Now, run your program once again. You should now be able to click either rendered visual and see the transformation in action! While this is just a simple example of working with the visual layer of WPF,
remember that you make use of the same brushes, transformations, pens, and layout managers as you would when working with XAML. As a result, you already know quite a bit about working with this Visual-derived classes.
That wraps up your investigation of the graphical rendering services of Windows Presentation Foundation. While you learned a number of interesting topics, the reality is that you have only scratched the surface of WPF’s graphical capabilities. I will leave it in your hands to dig deeper into the topics of shapes, drawings, brushes, transformations, and visuals (and, to be sure, you will see some additional details of these topics in the remaining WPF chapters).

Summary
Because Windows Presentation Foundation is such a graphically intensive GUI API, it comes as no surprise that we are given a number of ways to render graphical output. This chapter began by examining each of three ways a WPF application can do so (shapes, drawings, and visuals) and discussed various rendering primitives such as brushes, pens, and transformations.
Remember that when you need to build interactive 2D renderings, shapes make the process very simple. However, static, noninteractive renderings can be rendered in a more optimal manner by using drawings and geometries, while the visual layer (accessible only in code) gives you maximum control and performance.

发表评论