使用像素着色器进行 WPF 父窗口着色






4.97/5 (13投票s)
本文讨论了在使用对话框显示时,使用像素着色器为父窗口着色。
引言
在本文中,我将讨论如何使用像素着色器,当父窗口打开一个模态对话框时,使其实现黑白渐变效果。这有点像 Windows XP 在你点击“关机”时所做的。我认为这个功能非常酷,因为它迫使用户将注意力集中在新弹出的对话框上,而且由于可以在 WPF 中利用像素着色器,因此无需存储父窗口的静态图像并对其进行修改(这使得父窗口在被灰化时动画可以继续进行)。使用像素着色器的另一个好处是,可以轻松更改所应用效果的类型,在本文中,我还将展示如何应用模糊效果和更改不透明度。
注意:示例项目依赖于安装的 DirectX SDK;如果您未安装该 SDK 并且不想下载它,您应该仍然可以通过删除项目配置中所有生成后步骤来编译该项目。这些步骤将在文章后面更详细地讨论。
背景
每当我点击 XP 机器上的“关机”时,都会出现一个对话框,询问我是否要待机、关机或重启。由于这是一个必须在所有当前正在发生的事情之前完成的系统对话框,Windows XP 的开发人员认为,如果屏幕的其余部分缓慢地淡入黑白,那将很不错。我认为这看起来很酷,而且用户友好。关于它有两件事有点困扰我;第一,我找不到一种简单的方法在我的 WinForm 应用程序中做到这一点,第二,Windows XP 似乎存储了对话框出现时屏幕外观的图像,然后将其颜色耗尽。令人恼火的是,任何动画或视频都会停止,或者看起来像是停止了(你会发现你正在观看的 YouTube 剪辑的声音还在继续)。
由于 WPF 允许我们使用像素着色器对 `UIElement` 应用效果,因此现在可以轻松克服这两个限制。在本文中,我将演示如何创建一个附加属性,该属性允许您使用 XAML 启用此功能,这很酷,因为它意味着您可以使用普通绑定来配置属性。我还会展示如何创建一些基本的像素着色器,这些着色器可以以类似的方式控制(通过给定因子调整效果的使用量)。我为本文实现的各种效果是
- 黑白
- Opacity
- 棕褐色(嗯,有点棕褐色;我很难把它弄好,而且当我做的时候它看起来太红了)
- 失去焦点
像素着色器
本文中我不会深入探讨像素着色器的细节,其他人已经创建了详细的文档,比我解释得更好,但我会介绍它们的基础知识。
像素着色器基本上是运行在你的显卡上而不是你的主 CPU 上的小程序。它们会对你输入的任何纹理或图像执行一次。是的,这是一次每像素的执行。对于分辨率高于 1024x768 的全屏 UI 来说,这是大量的执行。这就是为什么现代显卡今天的性能如此强大的原因。本质上,你设置一些全局变量,通常是你想要着色的纹理,以及你想要将第一个纹理混合的另一个纹理。然后,你的着色器程序主方法会用 (x, y) 坐标调用,并期望你返回你希望着色器在该位置放置的颜色。你可以返回全局纹理的实际颜色,或者你可以操作它,改变颜色甚至移动它。这一切都非常非常酷。
编译像素着色器
Visual Studio 不知道如何编译像素着色器文件,您必须安装 DirectX SDK 才能做到。每当我喜欢在 WPF 项目中使用像素着色器时,我都会设置生成,以便 Visual Studio 在生成后步骤中调用外部编译器。这可以在项目属性页中配置
fxc.exe 是 FX Compiler 可执行文件,您在下载 DirectX SDK 时会获得它。在此示例中,我使用了 **2009 年 3 月** 版本;如果您想使用其他版本,则需要相应地调整路径。从命令行运行该命令以查看可用的命令行开关。
要下载 SDK,请在此处访问 这里。如果您不想下载 SDK,仍然可以运行此示例项目,因为我还包含了编译好的着色器文件。
黑白着色器
为了进一步解释实际着色器程序的工作原理,这是我的黑白着色器实现
// 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;
}
如果您认为类型名称看起来很奇怪,那是因为它不是 C#,着色器是用自己的语言编程的。
创建 UI 效果
为了能够从 C# 操作效果(即,向其传递参数),我们需要将其封装在一个 `ShaderEffect` 类中。我实现了一个名为 `GradientUIElementEffect` 的类,它继承自 `ShaderEffect` 并将两个输入(`Input` 和 `Factor`)公开为依赖项属性。
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); }
}
}
由于此类仅公开输入纹理和因子,因此它决定了我们可以使用此着色器做什么,并允许我们使用标准的 WPF 动画类对其进行动画;在这种情况下,使用 `SingleAnimation` 来随时间动画化 `Factor` 属性。
附加行为
为了能够使用着色器通过 `GradientUIElementEffect` 类自动着色某个对象,必须将其设置为可通过绑定使用,这会引起一些问题,因为我想设置三个不同的内容
- 效果(或效果文件名),`EffectFilename`。
- 渐变渐出时间(“淡出”所需的时间),`GradientOutTime`。
- 渐变渐入时间(“渐入”所需的时间),`GradientInTime`。
单独设置这些附加属性不成问题,但当所有这些属性都必须设置为使行为生效时,就需要一种存储中间状态的机制。我选择了一种方法,即引入另一个附加属性,一个不是打算由开发人员显式通过 XAML 使用的,而是由其他三个附加属性隐式使用的。此属性的类型为 `ShaderParameters`,如下定义
public class ShaderParameters
{
public string EffectFileName { get; set; }
public double GradientOutTime { get; set; }
public double GradientInTime { get; set; }
}
通过将此类公开为附加属性,可以将在其他三个属性设置在其上的对象的状态存储在着色器上。因此,所需的附加属性是
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));
...
}
并且这些属性可以这样从 XAML 访问
<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>
`GradientOutTime` 和 `GradientInTime` 的实现很简单(它们只是将新的 `double` 值存储在 `ShaderParameters` 对象中,然后该对象被存储回设置了初始属性的对象上,使用 `ShadeParametersProperty`)。
`EffectFilenameProperty` 负责在 `UIElement` 上设置实际效果;它通过创建 `ShaderParameters` 并从其中获取渐变渐入/渐出时间来完成。如果这些属性尚未设置,则使用默认值。如果渐变渐入/渐出属性已设置,则使用 `ShaderParametersProperty` 查找 `ShaderParameters`,将效果文件名添加到其中,然后使用当前 `ShaderParameters` 集创建效果。这样,三个附加属性可以按任何顺序设置,效果将始终使用所有已设置的属性进行设置。
处理此问题的 `EffectFileNameChanged` 方法实现如下
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);
}
}
从这个实现可以看出,它只对 `Window` 对象有效。这满足了我的需求,但可以轻松修改以适应更通用的 `UIElement`,只要存在一个合适的事件可以应用效果。设置效果非常简单
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;
}
}
效果被创建、配置并附加到 `UIElement`,当 `Window` 上的激活/停用事件触发时,控制渐变的动画会像这样创建
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);
}
}
}
着色器示例
关注点
此示例应用程序有点局限性,因为它期望效果文件位于应用程序的启动路径;不过,这很容易修复,可以为更动态地查找和访问效果文件提供支持。由于它们是通过 `Uri` 引用的,它们甚至可以是从 Web 服务器获取的。
实际上,实现高级着色器不是我的专长(看看我的棕褐色和模糊实现就知道了),但这没关系,因为效果文件独立于编译后的应用程序,所以它们是一个可以轻松委托给团队中图形技能娴熟的开发人员的实体。
一如既往,欢迎对代码或文章提出任何意见。
历史
- 2010-03-28:第一个版本。
- 2010-03-29:修正了拼写错误。