Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF Parent Window Shading Using Pixel Shaders

0.00/5 (No votes)
29 Mar 2010 1  
This article discusses using Pixel Shaders to shade the main window when a dialog is showing.

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.

article.png

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 UIElements 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:

// These two globals must be present on any shader that
// is to be used by the attached properties in this project

// This is the source data, the texture or image to shade
sampler2D input : register(s0);

// This is the factor, or amount of shading to apply
float factor : register(c0);

// This is the "main" method taking X,Y coordinates
// and returning a color
float4 main(float2 uv : TEXCOORD) : COLOR
{
    // Grab the color at XY from the imput
    float4 clr = tex2D(input, uv.xy);

    // Calculate the average of the RGB elements
    // This yields a black and white image if
    // this value is applied to RG and B of the color
    float avg = (clr.r + clr.g + clr.b) / 3.0;

    // Set the output color by using the factor global
    // in a way such that if factor is 0, all of the original
    // color is used, otherwise the closer to 1.0 the factor
    // gets the more black and white the color gets
    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)));

    // This property is not intended to be set using XAML.
    // It stores the composite state of the other three.
    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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here