7.5. Visual-Layer Programming
The shape elements can provide a convenient way to work with
graphics. However, in some situations, adding elements representing a drawing
to the UI tree may be more trouble than it's worth. Your data may be structured
in such a way that it's easier to write code that simply performs a series of
drawing operations based on the data, rather than constructing a tree of
objects.
WPF provides a "visual layer" API as a lower-level alternative
to shape elements. (In fact the shape elements are all implemented on top of
this visual layer.) This API lets us write code that renders content on demand.
 |
A visual is a visible object. A WPF
application's appearance is formed by composing all of its visuals onto the
screen. Since WPF builds on top of the visual layer, every element is a
visualthe FrameworkElement base class derives indirectly from Visual.
Programming at the visual layer simply involves creating a visual and writing
code that tells WPF what we'd like to appear in that visual.
|
|
Even at this low level, WPF behaves very differently from Win32.
The way in which graphics acceleration is managed means that your on-demand
rendering code is called much less often than it would be in a classic Windows
application.
7.5.1. Rendering On Demand
The key to custom on-demand rendering is the OnRender method.
This method is called by WPF when it needs your component to generate its
appearance. (This is how the built-in shape classes render themselves.)
 |
The virtual OnRender method is defined by
the OnDemandVisual class. Most elements derive from this indirectly
via FrameworkElement, which adds core features such as layout and
input handling.
|
|
Example 7-47 shows
a custom element that overrides OnRender.
Example 7-47. A custom OnRender implementation
public class CustomRender : FrameworkElement
{
protected override void OnRender(DrawingContext drawingContext)
{
Debug.WriteLine("OnRender");
base.OnRender(drawingContext);
drawingContext.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 50));
FormattedText text = new FormattedText("Hello, world",
CultureInfo.CurrentUICulture, FlowDirection.LeftToRightThenTopToBottom,
new Typeface("Verdana"), 24, Brushes.Black);
drawingContext.DrawText(text, new Point(3, 3));
}
}
The OnRender method is passed a single parameter of
type DrawingContext . This is the
low-level drawing API in WPF. It provides a set of primitive drawing
operations, which are listed in Table
7-4. Example 7-47 uses
the DrawRectangle and DrawText methods.
Note that the DrawingContext uses the Brush and
Pen classes to indicate how shapes should be filled and outlined, just
like the higher-level shape objects we saw earlier. We can also pass in the
same Geometry and Drawing objects we saw earlier in the
chapter.
Table 7-4. DrawingContext drawing operations
|
Operation
|
Usage
|
DrawDrawing
|
Draws a Drawing object.
|
DrawEllipse
|
Draws an ellipse.
|
DrawGeometry
|
Draws any Geometry object.
|
DrawGlyphRun
|
Draws a series of glyphs
(i.e., text elements) offering detailed control over typography.
|
DrawImage
|
Draws a bitmap image.
|
DrawLine
|
Draws a line (a single
segment).
|
DrawRectangle
|
Draws a rectangle.
|
DrawRoundedRectangle
|
Draws a rectangle with rounded
corners.
|
DrawText
|
Draws text.
|
DrawVideo
|
Draws a rectangular region
that can display video.
|
PushTransform
|
Sets a transform that will be
applied to all subsequent drawing operations until Pop is called; if a
transform is already in place, the net effect will be the combination of all
the transforms currently pushed.
|
PushClip
|
Sets a clip region that will
be applied to all subsequent drawing operations until Pop is called;
as with PushTransform, multiple active clip regions will combine.
|
PushOpacity
|
Sets a level of opacity that
will be applied to all subsequent drawing operations until Pop is
called; as with transforms and clips, multiple opacities are combined.
|
Pop
|
Removes the transform, clip
region, or opacity added most recently by PushTransform, PushClip,
or PushOpacity. If those methods have been called multiple times,
calls to Pop remove their effects in reverse order. (The transforms,
clip regions, and opacities behave like a stack.)
|
Because our custom element derives from FrameworkElement,
it integrates naturally into any WPF application.
Example 7-48 shows markup for a window that uses this custom elementwe
can use it just like we'd use any custom element. This window is shown in
Figure 7-56.
Example 7-48. Loading a custom visual into a window
<?Mapping XmlNamespace="controls" ClrNamespace="VisualRender" ?>
<Window x:Class="VisualRender.Window1"
xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
xmlns:cc="controls"
Text="Visual Layer Rendering"
>
<Canvas>
<cc:CustomRender Canvas.Top="10" Canvas.Left="10" x:Name="customRender" />
</Canvas>
</Window>
Notice that the OnRender function in
Example 7-47 calls Debug.WriteLine
. If the program is run inside the VS 2005 debugger, this will print a message
to the Output window each time OnRender is called. This enables us to
see how often WPF asks our custom visual to render itself. If you are used to
how the standard on-demand painting in Win32 and Windows Forms works, you might
expect to see this called regularly whenever the window is resized or partially
obscured and uncovered. In fact it is called just once!
It turns out that on-demand rendering is not as similar to
old-style Win32 rendering as you might think. WPF will call your OnRender
function when it needs to know what content your visual displays, but the way
graphics acceleration works in WPF means that this happens far less often than
the equivalent repaints in Win32. WPF caches the rendering instructions. The
extent and form of this caching is not documented, but it clearly occurs.
Moreover, it is more subtle than simple bitmap-based caching. We can add this
code to the host window in Example
7-48 (this would go in the code-behind file):
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
VisualOperations.SetTransform(customRender, new ScaleTransform(6, 6));
}
The preceding snippet applies a transform to our element,
scaling it up by a factor of 6. When clicking on the user interface, the custom
visual expands as you would expect, and yet OnRender is not called.
Moreover, the enlarged visual does not show any of the pixelation or blurring
artifacts you would see with a simple bitmap scaleit continues to be sharp, as
you can see in Figure 7-57.
This indicates that WPF is retaining scalable information about
the contents of the visual. It is able to redraw our visual's onscreen
appearance without bothering our OnRender method, even when the
transformation has changed. This is in part due to the acceleration
architecture, but also because transformation support is built into WPF at the
most fundamental levels.
WPF's ability to redraw without calling OnRender allows
the user interface to remain intact onscreen even if our application is busy.
It also enables the animation system to work without much intervention from the
applicationbecause all of the primitive drawing operations are retained, WPF
can rebuild any part of the UI that it needs to even when individual elements
change.
If the state of our object should change in a way that needs the
appearance to be updated, we can call the InvalidateVisual method.
This will cause WPF to call our OnRender method, allowing us to
rebuild the appearance.
Note that when you override OnRender, you should
typically also override the MeasureOverride and ArrangeOverride
methods. Otherwise, WPF's layout system will have no idea how large your
element is. The only reason we got away without doing this here is that we used
the element on a Canvas, which doesn't care how large its children
are. To work in other panels, it is essential to let the layout system know
your size. Chapter 2
described the MeasureOverride and ArrangeOverride methods.
 |