Introduction
In this article, I will discuss how to use Pixel Shaders to achieve a gradient change to black and white for the parent window when a modal dialog is opened by it. This is kind of what Windows XP does when you click "Turn off Computer". I think this feature is quite cool as it forces the user to focus on the dialog that just popped up, and because it's possible to leverage Pixel Shaders in WPF, this can be done without storing a static image of the parent window and modifying that (which in turn allows animations to continue in the parent window as it's being grayed out). Another cool thing about using Pixel Shaders is that it makes it easy to change the type of effect applied, and in this article, I'll also show how to apply a blur effect and change the opacity.
Note: The sample project relies on a DirectX SDK being installed; if you do not have this installed and you don't want to download it, you should still be able to compile the project by removing all the post-build steps from the project configuration. These steps are discussed in more detail later in the article.
Background
When ever I click "Turn Off Computer" on my XP box, a dialog appears asking me if I want to Stand By, Turn Off, or Restart. As this is a system dialog which has to be completed before everything else that is currently happening, the developers of Windows XP thought it would be neat if the rest of your screen slowly faded into black and white. I think this looks cool and is user friendly. Two things about it have bugged me a bit though; first, I couldn't find an easy way to do the same thing in my WinForm apps, and secondly, Windows XP seems to store an image of what the screen looked like when the dialog appeared and then drain the colour out of that. The annoying thing about this is that any animations or videos stop, or appear to stop (you'll find the sound of the YouTube clip you were just watching keeps going).
As WPF allows us to apply effects to our UIElement
s using Pixel Shaders, it has now become easy to overcome both these limitations. In this article, I'll demonstrate how to create an attached property that allows you to enable this feature using XAML, which is cool because it means you can use normal bindings to configure the properties. I'll also show how to create some basic Pixel Shaders that can be controlled in a similar way (adjusting how much of the effect to use by a given factor). The different effects I've implemented for this article are:
- Black and White
- Opacity
- Sepia (well, kind of sepia; I'm struggling to get it right, and it looks way too red when I do it)
- Blur
Pixel Shaders
I'll not go into the finer details of Pixel Shaders in this article, other people have already created detailed documents explaining them better than I can, but I will go through the very basics of them.
Pixel Shaders are basically small programs that run on your graphics card rather than your main CPU. They are executed once per pixel for whatever texture or image you feed as input to them. Yes, that is Once Per Pixel. That is a lot of executions for a full screen UI running in a resolution above 1024x768. Which is why modern graphics cards have to have the awesome performance they do today. In essence, you set up some global variables, these are normally the texture you want to shade and maybe some other texture you want to blend the first one with. Then, your shader program main method gets called with (x, y) coordinates and expect you to return what color you want the shader to put in that location. You could return the actual color of the global texture, or you can manipulate it, changing the color or even moving it around. It's all really, really cool.
Compiling Pixel Shaders
Visual Studio doesn't know how to compile the Pixel Shader files, you must have the DirectX SDK to do that. Whenever I use Pixel Shaders in my WPF projects, I like to set up the build so that Visual Studio calls the external compiler as part of the post-build step. This can be configured in the project properties pages:
fxc.exe is the FX Compiler executable, you get it when you download the DirectX SDK. In this example, I've used the March 2009 version; if you want to use a different version, you need to adjust the paths accordingly. Run the command from the command line to see what command line switches are available.
To download the SDK, go here. If you do not want to download the SDK, you should still be able to run this sample project as I have included the compiled shader files as well.
Black and White Shader
To explain further how the actual shader program works, here's my implementation of the Black and White shader:
sampler2D input : register(s0);
float factor : register(c0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
float4 clr = tex2D(input, uv.xy);
float avg = (clr.r + clr.g + clr.b) / 3.0;
clr.r = (avg * factor) + (clr.r * (1.0 - factor));
clr.g = (avg * factor) + (clr.g * (1.0 - factor));
clr.b = (avg * factor) + (clr.b * (1.0 - factor));
return clr;
}
If you think the type names look weird, it's because it's not C#, shaders are programmed in their own language.
Creating the UI Effect
In order to be able to manipulate the effect (i.e., pass arguments to it) from C#, we need to wrap it up in a ShaderEffect
class. I've implemented a class called GradientUIElementEffect
that extends from the ShaderEffect
and exposes the two inputs (Input
and Factor
) as dependency properties.
public class GradientUIElementEffect : ShaderEffect
{
public static readonly DependencyProperty InputProperty =
ShaderEffect.RegisterPixelShaderSamplerProperty("Input",
typeof(GradientUIElementEffect), 0);
public static readonly DependencyProperty GradientProperty =
DependencyProperty.Register("Gradient", typeof(float),
typeof(GradientUIElementEffect),
new UIPropertyMetadata(0.0f, PixelShaderConstantCallback(0)));
public GradientUIElementEffect(PixelShader shader)
{
PixelShader = shader;
UpdateShaderValue(InputProperty);
UpdateShaderValue(GradientProperty);
}
public Brush Input
{
get { return (Brush)GetValue(InputProperty); }
set { SetValue(InputProperty, value); }
}
public float Gradient
{
get { return (float)GetValue(GradientProperty); }
set { SetValue(GradientProperty, value); }
}
}
As this class exposes only the input texture and a factor, it dictates what we can do with this shader, and it allows us to animate it using standard WPF animation classes on it; in this case, the SingleAnimation
to animate the Factor
property over time.
Attaching the behavior
In order to be able to use the shaders to automatically shade something using the GradientUIElementEffect
class, it has to be set up so that it is available through binding, and this causes some issues since I want to set three different things:
- Effect (or effect file name),
EffectFilename
.
- Gradient out time (the time it takes to "fade out"),
GradientOutTime
.
- Gradient in time (the time it takes to "fade in"),
GradientInTime
.
Setting these individually using attached properties isn't a problem, but when they all have to be set for the behaviour to work, some mechanism of storing the intermediate state is required. I've gone for an approach where I introduce yet another attached property, one that isn't intended to be used by the developer explicitly through XAML, but implicitly by the other three attached properties. This property will be of type ShaderParameters
, as defined below:
public class ShaderParameters
{
public string EffectFileName { get; set; }
public double GradientOutTime { get; set; }
public double GradientInTime { get; set; }
}
By exposing such a class using an attached property, it is possible to store the state of the shader on the object that has the other three properties set. Therefore, the attached properties required are:
public static class PixelShadeOnActivatedStateBehaviour
{
public class ShaderParameters
{
public string EffectFileName { get; set; }
public double GradientOutTime { get; set; }
public double GradientInTime { get; set; }
}
public static DependencyProperty EffectFileNameProperty =
DependencyProperty.RegisterAttached("EffectFileName",
typeof(string),
typeof(PixelShadeOnActivatedStateBehaviour),
new FrameworkPropertyMetadata(null,
new PropertyChangedCallback(
PixelShadeOnActivatedStateBehaviour.EffectFileNameChanged)));
public static DependencyProperty GradientOutTimeProperty =
DependencyProperty.RegisterAttached("GradientOutTime",
typeof(double),
typeof(PixelShadeOnActivatedStateBehaviour),
new FrameworkPropertyMetadata(0.0,
new PropertyChangedCallback(
PixelShadeOnActivatedStateBehaviour.GradientOutTimeChanged)));
public static DependencyProperty GradientInTimeProperty =
DependencyProperty.RegisterAttached("GradientInTime",
typeof(double),
typeof(PixelShadeOnActivatedStateBehaviour),
new FrameworkPropertyMetadata(0.0,
new PropertyChangedCallback(
PixelShadeOnActivatedStateBehaviour.GradientInTimeChanged)));
public static DependencyProperty ShadeParametersProperty =
DependencyProperty.RegisterAttached("ShadeParameters",
typeof(ShaderParameters),
typeof(PixelShadeOnActivatedStateBehaviour),
new PropertyMetadata(null));
...
}
And these properties can then be accessed from XAML like this:
<Window x:Class="Bornander.UI.Dialogs.Test.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:test="clr-namespace:Bornander.UI.Dialogs.Test"
xmlns:dialogs="clr-namespace:Bornander.UI.Dialogs;assembly=Bornander.UI.Dialogs"
Title="Test Window"
WindowStyle="ThreeDBorderWindow"
Height="480"
Width="640"
dialogs:PixelShadeOnActivatedStateBehaviour.EffectFileName=
"{Binding Path=EffectFilename}"
dialogs:PixelShadeOnActivatedStateBehaviour.GradientOutTime=
"{Binding Path=GradientOutTime}"
dialogs:PixelShadeOnActivatedStateBehaviour.GradientInTime=
"{Binding Path=GradientInTime}">
<Grid>
The implementation of GradientOutTime
and GradientInTime
is trivial (they simply store the new double
values in the ShaderParameters
object that is then stored back on to the object on which the initial property was set using ShadeParametersProperty
).
EffectFilenameProperty
is responsible for setting up the actual effect on the UIElement
; it does that by creating ShaderParameters
and grabbing the gradient in/out times from that. If those properties have not yet been set, the default values are used instead. If the gradient in/out properties have already been set, then ShaderParameters
is looked up using ShaderParametersProperty
, the effect file name is added to them, and then the effect is created using the current set of ShaderParameters
. This way, the three attached properties can be set in any order and the effect will always be setup using all the set properties.
The EffectFileNameChanged
method which handles this is implemented like this:
private static void EffectFileNameChanged(DependencyObject target,
DependencyPropertyChangedEventArgs e)
{
Window window = target as Window;
if (window != null)
{
if ((e.NewValue != null) && (e.OldValue == null))
{
window.Deactivated += HandleWindowDeactivated;
window.Activated += HandleWindowActivated;
}
else
{
if ((e.NewValue == null) && (e.OldValue != null))
{
window.Deactivated -= HandleWindowDeactivated;
window.Activated -= HandleWindowActivated;
}
}
ShaderParameters parameters = GetOrCreateShadeParameters(target);
parameters.EffectFileName = (string)e.NewValue;
SetupEffect(window, parameters.EffectFileName);
target.SetValue(PixelShadeOnActivatedStateBehaviour.ShadeParametersProperty,
parameters);
}
}
As can be seen from this implementation, it only works on the Window
object. That suits my needs, but can easily be modified to cope with a more generic UIElement
as long as a suitable event exists on which the effect can be applied to. To set up the effect is pretty straightforward:
private static void SetupEffect(UIElement element, string effectFilename)
{
if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(element))
{
Uri uri = new Uri(string.Format(@"{0}\{1}",
AppDomain.CurrentDomain.BaseDirectory, effectFilename),
UriKind.Absolute);
GradientUIElementEffect effect =
new GradientUIElementEffect(new PixelShader { UriSource = uri });
effect.Gradient = 0.0f;
element.Effect = effect;
}
}
The effect is created and configured and attached to the UIElement
, and when the activated/deactivated events on the Window
fires, the animation that controls the gradient is created like this:
private static void CreateAnimation(object sender, bool isOutAnimation)
{
Window window = sender as Window;
if (window != null)
{
ShaderParameters parameters = GetOrCreateShadeParameters(window);
GradientUIElementEffect effect = window.Effect as GradientUIElementEffect;
if (effect != null)
{
float targetValue = isOutAnimation ? 1.0f : 0.0f;
Duration duration = new Duration(TimeSpan.FromSeconds(isOutAnimation ?
parameters.GradientOutTime : parameters.GradientInTime));
SingleAnimation animation = new SingleAnimation(targetValue, duration);
effect.BeginAnimation(GradientUIElementEffect.GradientProperty, animation);
}
}
}
Shader Examples
Black and White
Opacity
Sepia
Blur
Points of Interest
This example app is a bit limited in that it expects the effect files to be located at the start up path of the application; that can easily be fixed though, to cater for a more dynamic way of finding and accessing effect files. As they're referenced using a Uri
, they could even have been fetched from a web server for example.
Actually, implementing advanced shaders isn't my specialty (which is obvious from looking at my Sepia and Blur implementations), but that's fine because the effect files are external to the compiled application, so they're an entity that is easily delegated to the graphically skilled developers on the team.
As always, any comments on the code or the article are most welcome.
History
- 2010-03-28: First version.
- 2010-03-29: Fixed typos.