Graphics, Animation & Multimedia
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
WPF draws everything itself, with a retained-mode, vector-based renderer that sits on top of DirectX. That is why a
Button, a Path, a video and a rotating 3-D cube can all live in one visual tree, be transformed, animated and
data bound in the same way. This page covers the drawing and media features of that renderer, from the highest
level (Shape elements) to the lowest (DrawingVisual), then animation, media playback, a short look at 3-D, and
the rendering tiers and RenderOptions that control how the GPU is used.
The page was written against these versions, current in October 2026:
-
.NET 10 LTS, latest patch 10.0.13, with C# 14. .NET 11 is at release candidate 2 (general availability is expected 10 to 13 November 2026). The graphics, animation and media pages of Microsoft Learn cited below are, for the most part, written for .NET Framework and dated 2017. They list no .NET 10 specific change, and the
System.Windows.Media,System.Windows.Media.AnimationandSystem.Windows.Media.Media3DAPIs shown here are documented as available for .NET Framework 4.8 and for .NET up to 11. -
Visual Studio 2026 18.11.1.
-
CommunityToolkit.Mvvm 8.4.2, used for the view model in the examples.
-
Windows App SDK 2.5.1 (stable channel) for the WinUI 3 comparison.
-
Windows 10 and 11. The WPF rendering-tier definitions are expressed in DirectX 9 level terms.
What Microsoft Learn Provides, and What It Does Not
The WPF documentation has a "Graphics and Multimedia" section with overviews for shapes, geometry, brushes, transforms, animation, storyboards, easing functions, multimedia and 3-D graphics, plus the rendering-tier page under the advanced topics. These are conceptual overviews with isolated snippets, mostly in XAML with C# and Visual Basic versions.
Microsoft Learn does not provide:
-
A single end-to-end sample that combines shapes, animation and media in an MVVM application. The examples below that tie the features to the Bookshelf Desktop scenario are original; each one is followed by the official page its APIs come from.
-
Guidance for animating from a view model. Storyboards live in XAML, in styles and templates (see the Styles, Templates & Triggers page of this section once published), or in code-behind; keeping animation in the view and driving it with state, as shown later, is a convention of this page, not an official pattern.
-
Current numbers for the rendering tiers on modern GPUs. The tier definitions refer to DirectX 9 level hardware and to video memory sizes of 60 MB and 120 MB, which any current GPU exceeds.
The Layers of WPF Graphics
WPF offers several ways to put pixels on screen. They are layers over the same Visual class, so the choice is a
trade between convenience and cost.
| Layer | What it gives you | What it costs |
|---|---|---|
|
A |
The most memory per item, because every shape is a full element |
|
A freezable description of a region: drawing, clipping, hit testing, animation paths |
Cannot draw itself; needs a |
|
A light-weight, freezable drawing that you can use as an image source or a brush |
No layout or events of its own |
|
The lightest drawable object: no layout, no events, drawn through a |
You write the host element, the child bookkeeping and the hit testing |
|
Draw straight into an element you own, keeping layout and events |
You manage invalidation yourself |
Microsoft Learn summarises the trade this way: drawings are simpler constructs than shapes and perform better, and
shapes derive from FrameworkElement, which can add significantly more memory consumption, so use Drawing objects
when the FrameworkElement features are not needed. Every Visual stores its output as a serialised list of
drawing instructions rather than pixels; the DrawingContext you receive from DrawingVisual.RenderOpen records
instructions, it does not paint to the screen immediately. This retained-mode design is what lets WPF redraw only
what changed and scale vector content without distortion.
Source: Graphics rendering overview
Shapes
All shapes derive from Shape (namespace System.Windows.Shapes): Ellipse, Line, Path, Polygon,
Polyline and Rectangle. They share these properties.
| Property | Meaning |
|---|---|
|
The |
|
The |
|
Outline thickness in device-independent pixels |
|
How the shape fills its layout slot: |
Data properties |
|
A shape is a layout participant, so it takes part in a Grid or StackPanel like any other element. A Canvas
is the usual host when you want absolute coordinates. The Stretch value matters when the points are small numbers:
a Polygon with points 0,0 0,1 1,1 and Stretch="Fill" fills its whole slot, which is how a resolution-free icon
is made. The outline is drawn after stretching, so the stroke keeps its thickness.
The Bookshelf Desktop details pane shows a five-star rating as a row of Path shapes whose Fill depends on the
rating, using path markup for the star:
<UserControl x:Class="Bookshelf.Desktop.Controls.StarRating"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Resources>
<!-- A frozen geometry shared by all five stars. -->
<PathGeometry x:Key="StarGeometry"
Figures="M 10,0 L 12.6,7 L 20,7.6 L 14.2,12.4 L 16,20 L 10,15.8 L 4,20 L 5.8,12.4 L 0,7.6 L 7.4,7 Z" />
<SolidColorBrush x:Key="StarOn" Color="#F5A623" />
<SolidColorBrush x:Key="StarOff" Color="#D9D9D9" />
</UserControl.Resources>
<StackPanel Orientation="Horizontal">
<Path Data="{StaticResource StarGeometry}" Fill="{StaticResource StarOn}"
Stroke="#B9790F" StrokeThickness="0.75" Margin="0,0,2,0" />
<Path Data="{StaticResource StarGeometry}" Fill="{StaticResource StarOn}"
Stroke="#B9790F" StrokeThickness="0.75" Margin="0,0,2,0" />
<Path Data="{StaticResource StarGeometry}" Fill="{StaticResource StarOn}"
Stroke="#B9790F" StrokeThickness="0.75" Margin="0,0,2,0" />
<Path Data="{StaticResource StarGeometry}" Fill="{StaticResource StarOff}"
Stroke="#9A9A9A" StrokeThickness="0.75" Margin="0,0,2,0" />
<Path Data="{StaticResource StarGeometry}" Fill="{StaticResource StarOff}"
Stroke="#9A9A9A" StrokeThickness="0.75" />
</StackPanel>
</UserControl>
The star control above is static on purpose. In the real application, a rating is better modelled as a data
template that repeats one Path per star; the Converters, Collection Views & Data Templates page of this section
shows the rating-to-stars converter that feeds it.
Geometries
A Geometry describes a 2-D region. It derives from Freezable, not from FrameworkElement, so it cannot render or
lay itself out, but it can be declared as a resource, shared, frozen, cloned and used for clipping, hit testing and
animation paths.
| Class | Use |
|---|---|
|
Simple regions; |
|
One or more |
|
A lightweight alternative to |
|
Several geometries as one, without combining their areas (cheaper than combining) |
|
A Boolean operation on exactly two geometries, selected by |
Each simple geometry is defined by a start and end point (LineGeometry), a Rect (RectangleGeometry) or a centre
and two radii (EllipseGeometry).
Most XAML paths are written in the path markup syntax, a string of move and draw commands. Commands are case-sensitive: an uppercase letter uses absolute coordinates and a lowercase letter relative ones.
| Command | Meaning |
|---|---|
|
Move to a new start point (begins a figure) |
|
Line to a point, horizontal line (an x value), vertical line (a y value) |
|
Cubic Bezier, quadratic Bezier, smooth cubic Bezier |
|
Elliptical arc |
|
Close the figure |
|
A prefix that selects the fill rule, even-odd or non-zero |
The Bookshelf cover placeholder is built from three geometries: a rounded rectangle for the book, a Union for the
ribbon bookmark notch and a clip so that the cover image has rounded corners.
<Grid Width="120" Height="170" Margin="8">
<Grid.Resources>
<!-- Rounded corners for the cover; used both to fill and to clip. -->
<RectangleGeometry x:Key="CoverShape" Rect="0,0,120,170" RadiusX="8" RadiusY="8" />
</Grid.Resources>
<!-- Filled shape built from a combined geometry: the cover minus a bookmark notch. -->
<Path Fill="#3F5F8A" Stroke="#243A57" StrokeThickness="1">
<Path.Data>
<CombinedGeometry GeometryCombineMode="Exclude">
<CombinedGeometry.Geometry1>
<RectangleGeometry Rect="0,0,120,170" RadiusX="8" RadiusY="8" />
</CombinedGeometry.Geometry1>
<CombinedGeometry.Geometry2>
<PathGeometry Figures="M 96,-1 L 108,14 L 120,-1 Z" />
</CombinedGeometry.Geometry2>
</CombinedGeometry>
</Path.Data>
</Path>
<!-- A cover image clipped to the same rounded rectangle. -->
<Image Source="pack://application:,,,/Assets/DefaultCover.png"
Stretch="UniformToFill"
Clip="{StaticResource CoverShape}" />
</Grid>
Source: Geometry overview
Brushes
Everything visible is painted by a Brush. Brushes are Freezable and Animatable; all of them except
VisualBrush can be frozen.
| Brush | Paints with |
|---|---|
|
One |
|
A gradient along an axis defined by |
|
A gradient across a circle, with a movable |
|
An |
|
A |
|
A live |
|
A cached bitmap of part of the visual tree (WPF 4 and later) |
Properties common to all brushes: Opacity (multiplied with the opacity of colours inside the brush; changing a
brush opacity is more efficient than changing UIElement.Opacity of a whole element), and Transform and
RelativeTransform to rotate, scale, skew or translate the painted content. Predefined colours live in Brushes
and Colors; system colours live in SystemColors.
A Freezable brush can be declared once in a resource dictionary and shared by many elements. The Bookshelf spine
and a tiled "paper" background show gradients and a tiled DrawingBrush:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Book spine: a vertical gradient. -->
<LinearGradientBrush x:Key="SpineBrush" StartPoint="0,0" EndPoint="1,0">
<GradientStop Color="#2C3E5C" Offset="0.0" />
<GradientStop Color="#4A6FA5" Offset="0.5" />
<GradientStop Color="#2C3E5C" Offset="1.0" />
</LinearGradientBrush>
<!-- Subtle checker tile for the shelf background. -->
<DrawingBrush x:Key="PaperBrush" Viewport="0,0,16,16" ViewportUnits="Absolute"
TileMode="Tile" Stretch="None">
<DrawingBrush.Drawing>
<DrawingGroup>
<GeometryDrawing Brush="#FAF6EE">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,16,16" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#F1EBDD">
<GeometryDrawing.Geometry>
<GeometryGroup>
<RectangleGeometry Rect="0,0,8,8" />
<RectangleGeometry Rect="8,8,8,8" />
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</ResourceDictionary>
Source: Brushes overview
Brushes can also be created and frozen in code, which is what you want for brushes used by a DrawingVisual on a
background thread:
using System.Windows.Media;
namespace Bookshelf.Desktop.Graphics;
internal static class BookshelfBrushes
{
public static readonly Brush Accent = Freeze(new SolidColorBrush(Color.FromRgb(0x4A, 0x6F, 0xA5)));
public static readonly Brush AccentFill = Freeze(new SolidColorBrush(Color.FromArgb(0x33, 0x4A, 0x6F, 0xA5)));
public static readonly Pen AccentPen = FreezePen(new Pen(Accent, 1.5));
private static T Freeze<T>(T brush) where T : Freezable
{
brush.Freeze();
return brush;
}
private static Pen FreezePen(Pen pen)
{
pen.Freeze();
return pen;
}
}
Source: Brushes overview
Transforms
A Transform maps points from one coordinate space to another through a 3 by 3 affine matrix (WPF uses row vectors,
so the matrix is row-major and its last column is always 0, 0, 1).
| Class | Properties |
|---|---|
|
|
|
|
|
|
|
|
|
A raw |
|
Several transforms applied in order |
By default a transform is centred on the origin (0,0) of the element, its upper-left corner. Either set CenterX and
CenterY in the element coordinate space, or set the element property RenderTransformOrigin to a point relative to
the element size, for example 0.5,0.5 for its centre.
A FrameworkElement has two transform properties, and the difference is important.
| Property | Behaviour |
|---|---|
|
Applied after the layout pass; does not change the space the element takes or what its neighbours do; cheaper, and the right choice for animation |
|
Applied before layout, so the parent sees the transformed size and re-arranges around it; a |
The same transform classes are Freezable and Animatable, so a RotateTransform can be animated through a name
or by a path in a storyboard, as shown below. In Bookshelf Desktop, a "loading" spinner is a Path arc rotated by an
animated RenderTransform, and a vertical column header uses a LayoutTransform so the header row grows to fit.
<StackPanel Orientation="Horizontal" Margin="12">
<!-- A rotated element still occupies its original slot: RenderTransform. -->
<Border Background="#E8EEF6" Padding="8,4" Margin="0,0,16,0"
RenderTransformOrigin="0.5,0.5">
<Border.RenderTransform>
<RotateTransform Angle="-6" />
</Border.RenderTransform>
<TextBlock Text="New arrival" FontWeight="SemiBold" />
</Border>
<!-- A rotated element makes the parent adjust to the new size: LayoutTransform. -->
<Border Background="#E8EEF6" Padding="8,4">
<Border.LayoutTransform>
<RotateTransform Angle="-90" />
</Border.LayoutTransform>
<TextBlock Text="Shelf A" FontWeight="SemiBold" />
</Border>
</StackPanel>
Source: Transforms overview
Effects
An Effect (namespace System.Windows.Media.Effects) processes the rendered pixels of an element and is set through
UIElement.Effect. The built-in effects are BlurEffect (Radius, KernelType, RenderingBias) and
DropShadowEffect (BlurRadius, Color, Direction, Opacity, ShadowDepth, RenderingBias). Both expose a
RenderingBias that tells WPF whether to favour speed or quality. For anything else, derive from ShaderEffect
and supply a compiled pixel shader.
| Item | Notes |
|---|---|
|
Built in, hardware accelerated when the GPU supports pixel shader 2.0; their properties can be animated |
|
Abstract; you load a |
|
Obsolete since .NET Framework 4: using the class throws; use |
Two caveats from the documentation: pixel shader 2.0 shaders run when rendering in software, but pixel shader 3.0 shaders do not run in software, and a pixel shader 3.0 shader on a machine whose hardware lacks support is ignored. With several video cards, behaviour follows the least capable card.
The Bookshelf card below uses a drop shadow that gets stronger when the card is selected. A shadow is an
Effect on the card Border; avoid effects on large containers, because the effect is applied to the whole visual
subtree of the element it is set on.
<Border x:Name="Card" Width="140" Height="190" CornerRadius="8" Background="White" Margin="12">
<Border.Effect>
<DropShadowEffect BlurRadius="12" ShadowDepth="3" Direction="270"
Color="Black" Opacity="0.25" RenderingBias="Performance" />
</Border.Effect>
<TextBlock Text="The Left Hand of Darkness" TextWrapping="Wrap" Margin="10"
FontWeight="SemiBold" />
</Border>
Source: DropShadowEffect class
A custom ShaderEffect for a "dim read books" greyscale effect follows the pattern from the official sample: a
static PixelShader, a Brush input property registered as a sampler and one dependency property per constant. The
shader source is a separate .fx file that you compile with the DirectX Effect-Compiler tool to a .ps file, and add
to the project as a Resource.
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;
namespace Bookshelf.Desktop.Effects;
/// <summary>Fades a visual toward grey; the amount is a shader constant.</summary>
public sealed class DimEffect : ShaderEffect
{
private static readonly PixelShader Shader = new()
{
UriSource = new Uri("pack://application:,,,/Bookshelf.Desktop;component/Effects/Dim.ps"),
};
public static readonly DependencyProperty InputProperty =
RegisterPixelShaderSamplerProperty(nameof(Input), typeof(DimEffect), 0);
public static readonly DependencyProperty AmountProperty =
DependencyProperty.Register(
nameof(Amount),
typeof(double),
typeof(DimEffect),
new UIPropertyMetadata(0.0, PixelShaderConstantCallback(0)));
public DimEffect()
{
PixelShader = Shader;
UpdateShaderValue(InputProperty);
UpdateShaderValue(AmountProperty);
}
public Brush Input
{
get => (Brush)GetValue(InputProperty);
set => SetValue(InputProperty, value);
}
public double Amount
{
get => (double)GetValue(AmountProperty);
set => SetValue(AmountProperty, value);
}
}
Source: ShaderEffect class
Effects are rendered per frame. For a complex but static part of the tree, WPF 4 added BitmapCache (set through
UIElement.CacheMode) and BitmapCacheBrush, which render that part once to a bitmap and reuse it. The bitmap stays
responsive to input. This is the first thing to try when a heavily nested card list scrolls slowly.
Source: Graphics and multimedia
DrawingVisual
A DrawingVisual is the lightest drawable object in WPF. It has no layout and no events, which is why it is fast.
To show one you need a host: a FrameworkElement that keeps the visuals in a VisualCollection and overrides two
members so the framework can find its visual children.
-
VisualChildrenCountreturns how many child visuals there are. -
GetVisualChild(int)returns the child at an index.
You then draw by calling DrawingVisual.RenderOpen(), which returns a DrawingContext, drawing with its
DrawRectangle, DrawEllipse, DrawLine, DrawGeometry, DrawText and similar methods, and closing it (a using
statement does that). The host element must have Visibility set to Visible, or none of its children are visible,
and hit testing goes through VisualTreeHelper.HitTest.
The Bookshelf details pane has a reading-progress sparkline: one polyline of the pages read per day. It draws as a
single StreamGeometry in one DrawingVisual, which is far lighter than a hundred Line shapes.
using System.Globalization;
using System.Windows;
using System.Windows.Media;
namespace Bookshelf.Desktop.Graphics;
/// <summary>Draws pages read per day as a line, with the last point highlighted.</summary>
public sealed class ReadingSparkline : FrameworkElement
{
private readonly VisualCollection _children;
private readonly DrawingVisual _visual = new();
public ReadingSparkline()
{
_children = new VisualCollection(this) { _visual };
}
public static readonly DependencyProperty ValuesProperty =
DependencyProperty.Register(
nameof(Values),
typeof(IReadOnlyList<double>),
typeof(ReadingSparkline),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender, OnValuesChanged));
public IReadOnlyList<double>? Values
{
get => (IReadOnlyList<double>?)GetValue(ValuesProperty);
set => SetValue(ValuesProperty, value);
}
protected override int VisualChildrenCount => _children.Count;
protected override Visual GetVisualChild(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _children.Count);
return _children[index];
}
protected override Size MeasureOverride(Size availableSize) =>
new(
double.IsInfinity(availableSize.Width) ? 120 : availableSize.Width,
double.IsInfinity(availableSize.Height) ? 32 : availableSize.Height);
protected override Size ArrangeOverride(Size finalSize)
{
Redraw(finalSize);
return finalSize;
}
private static void OnValuesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
((ReadingSparkline)d).Redraw(((ReadingSparkline)d).RenderSize);
private void Redraw(Size size)
{
using DrawingContext dc = _visual.RenderOpen();
var values = Values;
if (values is null || values.Count < 2 || size.Width <= 0 || size.Height <= 0)
{
return;
}
double max = Math.Max(values.Max(), 1);
double stepX = size.Width / (values.Count - 1);
Point PointAt(int i) => new(i * stepX, size.Height - (values[i] / max) * (size.Height - 4) - 2);
// StreamGeometry is the light-weight way to describe a line that never changes after it is built.
var geometry = new StreamGeometry();
using (StreamGeometryContext ctx = geometry.Open())
{
ctx.BeginFigure(PointAt(0), isFilled: false, isClosed: false);
for (int i = 1; i < values.Count; i++)
{
ctx.LineTo(PointAt(i), isStroked: true, isSmoothJoin: true);
}
}
geometry.Freeze();
dc.DrawGeometry(null, BookshelfBrushes.AccentPen, geometry);
dc.DrawEllipse(BookshelfBrushes.Accent, null, PointAt(values.Count - 1), 3, 3);
var label = new FormattedText(
values[^1].ToString("0", CultureInfo.CurrentCulture),
CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface("Segoe UI"),
10,
BookshelfBrushes.Accent,
VisualTreeHelper.GetDpi(this).PixelsPerDip);
dc.DrawText(label, new Point(size.Width - label.Width, 0));
}
}
Source: Using DrawingVisual objects
Using the control is one line, bound to a list the view model exposes:
<local:ReadingSparkline Width="160" Height="36"
Values="{Binding SelectedBook.PagesPerDay}" />
Source: Using DrawingVisual objects
The StreamGeometry is opened once, filled by a StreamGeometryContext and then frozen; that is what the
geometry overview recommends for complex geometries that need no binding, animation or modification. The
PixelsPerDip argument to FormattedText is required in current .NET versions so text is sharp on high-DPI
displays; the Documents, Printing & Text page of this section covers text drawing in more detail.
Animation
WPF animates dependency properties by applying a timeline to them. Three requirements for a property to be
animatable: it is a dependency property, it belongs to a class that derives from DependencyObject and implements
IAnimatable, and a compatible animation type exists. Most UI properties of elements, brushes, transforms and
geometries qualify; Opacity, Width, Height, a SolidColorBrush.Color or a RotateTransform.Angle are typical
targets. You can use animations in styles, templates and for non-visual objects.
Animation types
Animation classes follow a strict naming convention, one set per property type.
| Pattern | Meaning |
|---|---|
|
A "From/To/By" animation: interpolate from a start to an end value, or add an offset |
|
Any number of key frames with their own interpolation (discrete, linear, spline, or easing); some types, such as |
|
Produces values from a |
|
The abstract base you derive from for a custom animation |
Animations are timelines, so they share the timing properties of Timeline:
| Property | Default | Meaning |
|---|---|---|
|
1 second for an animation |
How long one iteration takes |
|
|
Play backwards after reaching the end |
|
1.0 (play once) |
Iteration count, a duration, or |
|
|
Hold the final value after the end, or |
|
none specified |
Delay before the timeline starts |
|
1 |
Relative speed of time for the timeline |
FillBehavior is the source of the most common animation bug. With the default HoldEnd, an animation that ended
keeps overriding the property, so a later attempt to set that property in code appears to do nothing (the value is
set, but the animation still wins). Use FillBehavior="Stop", remove the storyboard, or call BeginAnimation with a
null animation to regain control.
Storyboards
A Storyboard is a container timeline: it adds targeting information to the animations it holds, through two
attached properties.
-
Storyboard.TargetNamenames the object to animate. -
Storyboard.TargetPropertynames the property, as a property path such asOpacityor(UIElement.RenderTransform).(RotateTransform.Angle).
Storyboarding is a framework-level feature, so the target must belong to the namescope of a FrameworkElement or
FrameworkContentElement. A FrameworkElement is named with Name; a Freezable such as a brush or transform is
named with x:Name, and in code it is registered with RegisterName. When a name is not usable (a Freezable in a
style, for example) you target indirectly, through the property path of the owning element. In a Style you may
not set TargetName (the storyboard targets the styled element); in a ControlTemplate it may only refer to the
template’s own children.
A storyboard is started by a BeginStoryboard trigger action, normally inside an EventTrigger, or in code by
Storyboard.Begin. The Bookshelf book card fades and slides in when it is loaded:
<Border x:Name="BookCard" Width="140" Height="190" CornerRadius="8" Background="White"
RenderTransformOrigin="0.5,0.5" Opacity="0">
<Border.RenderTransform>
<TransformGroup>
<ScaleTransform x:Name="CardScale" ScaleX="0.9" ScaleY="0.9" />
<TranslateTransform x:Name="CardShift" Y="16" />
</TransformGroup>
</Border.RenderTransform>
<Border.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="BookCard"
Storyboard.TargetProperty="Opacity"
From="0" To="1" Duration="0:0:0.30" />
<DoubleAnimation Storyboard.TargetName="CardScale"
Storyboard.TargetProperty="ScaleX"
To="1" Duration="0:0:0.30" />
<DoubleAnimation Storyboard.TargetName="CardScale"
Storyboard.TargetProperty="ScaleY"
To="1" Duration="0:0:0.30" />
<DoubleAnimation Storyboard.TargetName="CardShift"
Storyboard.TargetProperty="Y"
To="0" Duration="0:0:0.30">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<TextBlock Text="{Binding Title}" Margin="10" TextWrapping="Wrap" FontWeight="SemiBold" />
</Border>
Source: Storyboards overview
Source: Animation overview
The animations here have no From on the transform properties. A From/To/By animation without a From starts at the
current base value of the property, which keeps the storyboard reusable.
Controlling a storyboard
A storyboard started with BeginStoryboard can be controlled by trigger actions when the BeginStoryboard is given
a Name: PauseStoryboard, ResumeStoryboard, SetStoryboardSpeedRatio, SkipStoryboardToFill, StopStoryboard
and RemoveStoryboard. In code, call Storyboard.Begin(FrameworkElement, bool isControllable) with true, and then
use Pause, Resume, Seek, SkipToFill, Stop and Remove. The interactive actions take effect on the next tick
of the timing engine, shortly before the next render, not immediately.
The Bookshelf "refresh" spinner is a repeating rotation that the view starts and stops with the view model IsBusy
state. The storyboard is a resource, the code-behind only listens to the view model, so the view model stays free of
UI types:
<Window x:Class="Bookshelf.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Bookshelf Desktop" Height="640" Width="960">
<Window.Resources>
<Storyboard x:Key="SpinStoryboard" RepeatBehavior="Forever">
<DoubleAnimation Storyboard.TargetName="SpinnerRotation"
Storyboard.TargetProperty="Angle"
From="0" To="360" Duration="0:0:1" />
</Storyboard>
</Window.Resources>
<Grid>
<Path x:Name="Spinner" Data="M 12,2 A 10,10 0 1 1 2,12" Stroke="#4A6FA5" StrokeThickness="3"
StrokeStartLineCap="Round" StrokeEndLineCap="Round"
Width="24" Height="24" Visibility="Collapsed"
HorizontalAlignment="Right" VerticalAlignment="Top" Margin="12"
RenderTransformOrigin="0.5,0.5">
<Path.RenderTransform>
<RotateTransform x:Name="SpinnerRotation" />
</Path.RenderTransform>
</Path>
</Grid>
</Window>
using System.ComponentModel;
using System.Windows;
using System.Windows.Media.Animation;
namespace Bookshelf.Desktop;
public partial class MainWindow : Window
{
private Storyboard? _spin;
public MainWindow(BooksViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
viewModel.PropertyChanged += OnViewModelPropertyChanged;
}
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(BooksViewModel.IsBusy) && sender is BooksViewModel { IsBusy: var busy })
{
SetSpinner(busy);
}
}
private void SetSpinner(bool busy)
{
_spin ??= (Storyboard)FindResource("SpinStoryboard");
if (busy)
{
Spinner.Visibility = Visibility.Visible;
// true makes the storyboard controllable, so it can be stopped and removed later.
_spin.Begin(this, isControllable: true);
}
else
{
_spin.Remove(this);
Spinner.Visibility = Visibility.Collapsed;
}
}
}
Source: Storyboards overview
A few rules apply when you put storyboards in a Style or ControlTemplate: the timing system freezes a storyboard
there to make it thread-safe, so it cannot use dynamic resource references or data-binding expressions in its
properties, and you cannot attach event handlers to storyboard or animation events in XAML. Animations started by a
property Trigger hand off to other trigger animations, but compose with EventTrigger and method-started
animations. For data-bound or animated animation properties, the clock is not regenerated automatically; reapply the
storyboard to pick up the change.
Easing functions
An easing function reshapes the progress of an animation so the motion is not linear. Set it through the
EasingFunction property of an animation, or on an EasingDoubleKeyFrame, and choose its direction with
EasingMode: EaseIn (follows the formula), EaseOut (the reverse of it) or EaseInOut (in for the first half,
out for the second).
| Class | Effect |
|---|---|
|
Pulls back slightly before moving on |
|
A bounce ( |
|
A spring oscillating until it rests ( |
|
Circular, sine and exponential curves |
|
Power curves with exponents 2, 3, 4 and 5 |
|
A power curve with a |
You can write your own by deriving from EasingFunctionBase and overriding EaseInCore (and CreateInstanceCore,
because easing functions are Freezable); EasingMode then works for free. A bounce when a book is dropped on a
shelf looks like this:
<DoubleAnimation Storyboard.TargetName="BookSpine" Storyboard.TargetProperty="Height"
From="30" To="170" Duration="0:0:0.9">
<DoubleAnimation.EasingFunction>
<BounceEase Bounces="2" Bounciness="3" EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
Source: Easing functions
Animating in code
You do not need a storyboard to animate one property. Every Animatable object has BeginAnimation, and an
AnimationClock can be applied directly. The Bookshelf toast that confirms "Book saved" can fade out from code:
using System.Windows;
using System.Windows.Media.Animation;
namespace Bookshelf.Desktop.Graphics;
internal static class Fade
{
public static void Out(UIElement element, TimeSpan duration)
{
var animation = new DoubleAnimation
{
To = 0,
Duration = duration,
FillBehavior = FillBehavior.Stop,
};
// Collapse the element once the animation completes, so the final value is not held forever.
animation.Completed += (_, _) => element.Visibility = Visibility.Collapsed;
// HandoffBehavior.SnapshotAndReplace is the default: it starts from the current animated value.
element.BeginAnimation(UIElement.OpacityProperty, animation);
}
}
Source: Animation overview
FillBehavior.Stop returns the property to its base value after the animation ends, which is why the code collapses
the element in Completed. Passing a null animation to BeginAnimation removes the animation, which is how to
regain control of an animated property.
Multimedia
WPF has two playback classes for audio and video. Both can use the Windows Media Player 10 control for playback, and both support the media types that Windows Media Player supports on the machine.
| Aspect | MediaElement |
MediaPlayer |
|---|---|---|
Kind |
A |
Not an element; cannot be used in XAML |
Display |
Placed in the visual tree |
Rendered through a |
State model |
Has loaded and unloaded states ( |
Stateless |
Use |
UI media: a player in a window |
Performance-sensitive or audio-only scenarios, drawing-based UI |
Media can be driven in two modes, decided by the Clock property:
-
Independent mode (
Clockisnull, the default): the media drives itself; you setSource, callPlay,Pause,StopandClose, and changePositionandSpeedRatio. -
Clock mode (
Clockis notnull): aMediaTimelinedrives playback, either created in code or inside aStoryboard. The control methods of the element are unavailable and throwInvalidOperationException; use theClockControllerinstead.
Playback behaviour of MediaElement is set by LoadedBehavior (default Play) and UnloadedBehavior (default
Close), both of type MediaState with values Play, Pause, Stop, Close and Manual. The interactive
methods work only when LoadedBehavior is Manual. With the defaults, a plain <MediaElement Source="..."/>
starts when it loads and, once it ends, closes the media and releases the resources, including video memory.
UnloadedBehavior takes priority over a MediaClock, which takes priority over LoadedBehavior.
Sizing needs care: a MediaElement has ActualWidth and ActualHeight of zero until content loads, and always for
audio-only content. Set Width or Height, but not both, to keep the aspect ratio. Setting both stretches the media
to that fixed size. Setting LoadedBehavior to Play or Pause makes WPF pre-roll the media so that the size is
known; after MediaOpened is raised, the sizes are available.
The scenario in Bookshelf Desktop is a sample-audio preview for audiobooks: a small player on the details pane with
play, pause, a position slider and error handling. The media file is a content file, not a resource: set the
build action to Content with CopyToOutputDirectory set to PreserveNewest or Always.
<StackPanel Margin="12" Width="320">
<MediaElement x:Name="Preview"
Source="{Binding SelectedBook.SampleUri}"
LoadedBehavior="Manual" UnloadedBehavior="Stop"
MediaOpened="OnPreviewOpened"
MediaEnded="OnPreviewEnded"
MediaFailed="OnPreviewFailed" />
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<Button Content="Play" Click="OnPlay" Margin="0,0,6,0" />
<Button Content="Pause" Click="OnPause" Margin="0,0,6,0" />
<Button Content="Stop" Click="OnStop" />
</StackPanel>
<Slider x:Name="PositionSlider" Margin="0,8,0,0" Minimum="0"
Thumb.DragStarted="OnSeekStarted" Thumb.DragCompleted="OnSeekCompleted" />
<TextBlock x:Name="Status" Foreground="#9A1F1F" TextWrapping="Wrap" />
</StackPanel>
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;
namespace Bookshelf.Desktop.Views;
public partial class SamplePlayerView : UserControl
{
private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(250) };
private bool _seeking;
public SamplePlayerView()
{
InitializeComponent();
_timer.Tick += (_, _) =>
{
if (!_seeking && Preview.NaturalDuration.HasTimeSpan)
{
PositionSlider.Value = Preview.Position.TotalSeconds;
}
};
}
private void OnPreviewOpened(object sender, RoutedEventArgs e)
{
// The natural size and duration are only known after MediaOpened.
PositionSlider.Maximum = Preview.NaturalDuration.HasTimeSpan
? Preview.NaturalDuration.TimeSpan.TotalSeconds
: 0;
Status.Text = string.Empty;
}
private void OnPreviewEnded(object sender, RoutedEventArgs e)
{
_timer.Stop();
Preview.Stop();
PositionSlider.Value = 0;
}
private void OnPreviewFailed(object? sender, ExceptionRoutedEventArgs e)
{
_timer.Stop();
Status.Text = $"The sample cannot be played: {e.ErrorException.Message}";
}
private void OnPlay(object sender, RoutedEventArgs e)
{
Preview.Play();
_timer.Start();
}
private void OnPause(object sender, RoutedEventArgs e)
{
Preview.Pause();
_timer.Stop();
}
private void OnStop(object sender, RoutedEventArgs e)
{
Preview.Stop();
_timer.Stop();
PositionSlider.Value = 0;
}
private void OnSeekStarted(object sender, DragStartedEventArgs e) => _seeking = true;
private void OnSeekCompleted(object sender, DragCompletedEventArgs e)
{
Preview.Position = TimeSpan.FromSeconds(PositionSlider.Value);
_seeking = false;
}
}
Source: Multimedia overview
MediaElement raises MediaOpened, MediaEnded and MediaFailed (and buffering events for streams), and exposes
Position, NaturalDuration, Volume, Balance, IsMuted, SpeedRatio and ScrubbingEnabled. Always handle
MediaFailed: a missing codec or file does not throw at the call site, it is only reported through that event.
Source: MediaElement class
For a video drawn into a Drawing, a MediaPlayer is attached to a VideoDrawing:
using System.Windows;
using System.Windows.Media;
namespace Bookshelf.Desktop.Graphics;
internal static class TrailerDrawing
{
public static VideoDrawing Create(Uri uri, Rect area)
{
var player = new MediaPlayer();
player.Open(uri);
var drawing = new VideoDrawing
{
Rect = area,
Player = player,
};
player.Play();
return drawing;
}
}
Source: Multimedia overview
3-D in Brief
WPF has a modest retained 3-D system. Microsoft Learn is explicit that it is not designed to be a full-featured
game development platform. 3-D content lives inside a Viewport3D, which is a normal 2-D element that acts as a
window into the scene, and you cannot interpenetrate 2-D and 3-D objects within it.
A scene needs these parts:
| Part | Role |
|---|---|
Coordinate space |
The origin is the centre of the viewport; +x right, +y up (unlike 2-D), +z toward the viewer |
Camera |
|
Model |
|
Material |
|
Light |
|
Container |
|
Transform and animation |
|
A Bookshelf cover preview that shows the cover as a textured plane, slightly rotated, is a short scene:
<Viewport3D Width="220" Height="260" ClipToBounds="True">
<Viewport3D.Camera>
<PerspectiveCamera Position="0,0,3.2" LookDirection="0,0,-1" UpDirection="0,1,0"
FieldOfView="45" NearPlaneDistance="0.5" FarPlaneDistance="20" />
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<AmbientLight Color="#555555" />
<DirectionalLight Color="White" Direction="-1,-1,-2" />
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-0.8,-1.1,0 0.8,-1.1,0 -0.8,1.1,0 0.8,1.1,0"
Normals="0,0,1 0,0,1 0,0,1 0,0,1"
TextureCoordinates="0,1 1,1 0,0 1,0"
TriangleIndices="0 1 2 1 3 2" />
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource="pack://application:,,,/Assets/DefaultCover.png" />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.BackMaterial>
<DiffuseMaterial Brush="#3F5F8A" />
</GeometryModel3D.BackMaterial>
<GeometryModel3D.Transform>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Axis="0,1,0" Angle="-18" />
</RotateTransform3D.Rotation>
</RotateTransform3D>
</GeometryModel3D.Transform>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
Source: 3-D graphics overview
For anything beyond simple scenes, such as loading model files or game-like rendering, WPF has no built-in answer: the usual choices are a third-party 3-D library or hosting a Direct3D surface through interop (see the Interop with Win32, Windows Forms & Web Content page of this section once published).
Rendering Tiers and the GPU
A rendering tier classifies the graphics hardware of the machine. WPF defines three:
| Tier | Definition | Requirements |
|---|---|---|
0 |
No hardware acceleration; everything is software rendered |
DirectX version lower than 9.0 |
1 |
Some features accelerated |
DirectX 9.0 or later, video RAM of at least 60 MB, pixel shader 2.0 or later |
2 |
Most features accelerated |
DirectX 9.0 or later, video RAM of at least 120 MB, pixel shader 2.0 and vertex shader 2.0 or later, at least 4 multitexture units |
Tier 1 was redefined in .NET Framework 4 to include only DirectX 9.0 and later hardware; DirectX 7 and 8 hardware is
tier 0. The tier is read at run time from RenderCapability.Tier for the current thread’s dispatcher, and the
RenderCapability.TierChanged event fires when it changes, for example when a window moves to another monitor or a
remote session starts. RenderCapability also reports pixel shader support through IsPixelShaderVersionSupported
and the largest bitmap the hardware can create through MaxHardwareTextureSize.
using System.Windows.Media;
namespace Bookshelf.Desktop.Graphics;
public static class RenderingInfo
{
/// <summary>The rendering tier, 0 to 2, of the dispatcher on the calling thread.</summary>
public static int CurrentTier => RenderCapability.Tier >> 16;
/// <summary>True when expensive visuals (shadows, blur, 3-D) should be turned off.</summary>
public static bool PreferSimpleVisuals => CurrentTier < 2;
public static void Watch(Action<int> onChanged) =>
RenderCapability.TierChanged += (_, _) => onChanged(CurrentTier);
}
Source: RenderCapability class
The tier tells you which features are hardware accelerated. For tier 1 and 2, most 2-D rendering, most 3-D rasterisation, radial gradients, 3-D lighting and sub-pixel text are. Content that is not hardware accelerated, whatever the tier:
-
printed content, which always uses the WPF software pipeline;
-
anything rendered with
RenderTargetBitmap.Render; -
tiled content with a
TileBrushwhoseTileModeisTile; -
surfaces larger than the maximum texture size of the hardware (commonly 2048 by 2048 or 4096 by 4096 pixels);
-
operations whose video RAM need exceeds what the card has;
-
layered windows on operating systems without the Windows Display Driver Model (a non-issue on current Windows); a window with
AllowsTransparency="True",WindowStyle="None"and a transparentBackgroundis a layered window.
WPF also exposes four registry values for rendering: disabling hardware acceleration, the maximum multisample value for 3-D anti-aliasing, a required driver date check and use of the reference rasterizer. These are for diagnosing driver problems; do not ship an application that changes them.
RenderOptions
RenderOptions is a static class of attached properties that trade quality for speed on a single element or
brush, plus one process-wide setting.
| Member | Use |
|---|---|
|
Resampling algorithm for scaled bitmaps; |
|
Edge rendering of non-text primitives; |
|
With |
|
Whether text can use ClearType; needed when the text sits on a transparent surface |
|
The render mode preference for the whole process, for example forcing software rendering when a driver misbehaves |
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Bookshelf.Desktop.Graphics;
internal static class RenderTuning
{
/// <summary>Faster resampling for an image that is being zoomed or animated.</summary>
public static void PrepareForZoom(UIElement cover) =>
RenderOptions.SetBitmapScalingMode(cover, BitmapScalingMode.LowQuality);
/// <summary>Keep a static tiled background cached.</summary>
public static void CacheTile(DrawingBrush brush)
{
RenderOptions.SetCachingHint(brush, CachingHint.Cache);
// Re-render the tile when it grows to twice or shrinks to half the cached size.
RenderOptions.SetCacheInvalidationThresholdMinimum(brush, 0.5);
RenderOptions.SetCacheInvalidationThresholdMaximum(brush, 2.0);
}
/// <summary>Crisp, aliased one-pixel dividers between shelves.</summary>
public static void Crisp(UIElement line) =>
RenderOptions.SetEdgeMode(line, EdgeMode.Aliased);
/// <summary>Decode a cover straight to thumbnail size instead of full size.</summary>
public static BitmapImage LoadThumbnail(Uri uri)
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = uri;
bitmap.DecodePixelWidth = 140;
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.EndInit();
bitmap.Freeze();
return bitmap;
}
}
The thumbnail loader applies the imaging advice from the same page: by default WPF decodes an image to its full size
and then scales it down, so decode to the size you need and combine many small images into one where you can. The
page shows BitmapScalingMode and CachingHint with the cache thresholds; EdgeMode and the BitmapImage
members DecodePixelWidth and CacheOption come from the API reference, not from that page.
Source: RenderOptions class
Performance Guidance
The WPF documentation and the rendering model point to the same short list:
| Do | Why |
|---|---|
Prefer |
Shapes are full elements with layout and events; drawings are lighter |
Freeze brushes, pens, geometries and transforms that never change |
A frozen |
Use |
No binding, animation or change tracking overhead |
Animate |
Layout properties trigger a layout pass on every frame |
Avoid |
An effect is applied to the whole visual subtree each frame |
Use |
The tree is rendered once and reused |
Use |
Faster resampling avoids dropped frames |
Decode images to display size |
Less memory and faster loading |
Avoid |
Called out by the rendering-tier documentation |
Check the tier and offer a simple mode |
Software rendering is much slower for effects and 3-D |
Measure before you tune. The WPF Performance Suite (Perforator and Visual Profiler) is the documented tool for rendering analysis, but it ships with the old Windows SDK for .NET Framework 4; on a current setup, use the Live Visual Tree and Live Property Explorer of Visual Studio described in the Development Environment & Tooling page of this section, and the performance profiler of Visual Studio 2026.
WPF, WinUI 3 and WinForms
| Concern | WPF | WinUI 3 | Windows Forms |
|---|---|---|---|
Rendering model |
Retained-mode visual tree on Direct3D 9, vector based |
Retained-mode visual tree on the Windows composition engine |
Immediate-mode GDI+ ( |
Shapes and geometry |
|
|
|
Brushes |
Many, including |
|
|
Animation |
|
|
|
Effects |
|
Composition effects and shadows through the composition layer |
None built in |
Media |
|
|
Windows Media Player ActiveX control, or a third-party player |
3-D |
|
None in XAML; use Direct3D interop |
None |
The WinUI 3 and WinForms columns summarise the commonly documented differences and are not drawn from the WPF
pages cited here; check the WinUI and WinForms documentation (see the Windows App Platforms page of this
section) before relying on a specific member. The practical consequences when porting: WPF storyboards and easing
functions port to WinUI with small changes; DrawingVisual, VisualBrush, ShaderEffect and 3-D do not.
Pitfalls
-
A property stuck after its animation ends. The default
FillBehaviorisHoldEnd; the animated value overrides later writes to the property. UseFillBehavior="Stop", callRemove, orBeginAnimationwithnull. -
Animating a property that is not a dependency property. The animation system needs a dependency property on a
DependencyObjectthat implementsIAnimatable. A plain CLR property, such as a view model property, cannot be the target of a storyboard. -
TargetNamenot found. The target must be in the namescope: name elements withName, freezables withx:Name, and register names withRegisterNamein code. In aControlTemplate,TargetNamecan refer only to template children. -
Binding in storyboards inside a
StyleorControlTemplate. Dynamic resource and binding expressions prevent freezing and are not allowed; animate a bound element property instead, or build the storyboard in code. -
Using
LayoutTransformfor animation. It makes every frame a layout pass; useRenderTransformplusRenderTransformOrigin. -
TranslateTransformas aLayoutTransform. It has no effect, because layout moves the element back. -
Effects on big containers. A
DropShadowEffecton a rootGridre-renders the whole window content. Put the effect on the smallest element, or useBitmapCache. -
A bitmap effect.
BitmapEffectclasses are obsolete since .NET Framework 4 and throw; useEffect. -
Pixel shader 3.0 on a software or older renderer. PS 3.0 effects are skipped without an error on hardware that lacks support, and do not run in software rendering; keep a fallback visual.
-
Media as a project resource. A
MediaElementcannot play a media file embedded as aResource; set the build action toContentwith a copy-to-output setting. -
Ignoring
MediaFailed. A missing codec or file shows nothing and does not throw; handle the event and show a message. -
Calling
Playin clock mode. It throwsInvalidOperationException; drive theMediaClockthrough itsClockController. -
Media that never releases. Under default behaviour a finished
MediaElementcloses itself, but one set toManualkeeps video memory until you callClose. CallClosewhen you are done with it. -
MediaElementwith bothWidthandHeight. The video is stretched to a fixed size and the aspect ratio is lost; set one of them. -
Visual host that does not report its children. A
DrawingVisualhost must override bothVisualChildrenCountandGetVisualChild, and itsVisibilitymust beVisible, or nothing is drawn or hit tested. -
Thread affinity. A frozen
Freezablecan be shared across threads; an unfrozen one belongs to the thread that created it. Build brushes and geometries on a worker thread only if you freeze them before handing them over. -
Assuming the tier on a remote desktop. Hardware acceleration may be off in a remote session; listen to
TierChangedand degrade visuals rather than caching the first value. -
Animations without an off switch. Heavy motion can be a problem for some users. The system exposes a preference for client-area animation through
SystemParameters; honour it by shortening durations to zero when it is off (see the Accessibility & UI Automation page of this section once published).