Introduction
Currently, I'm working on an image editor, which will use Pixel Shader effects. To save a lot of time manually hard coding each effect's control interface, I built a simple Reflection based controller system for the effects. This article shows the technique behind it. This is my first article that I publish here on CodeProject.
The Code
The Reflection control is achieved using a few custom attributes. The Reflection code looks for these attributes, and depending on the contained information of the attributes, it builds the user interface.
The UML diagram of the attributes looks like this:
The ShaderVersion
attribute specifies the shader's version. This is necessary because in WPF 4.0, Pixel Shader level 2.0 and 3.0 are both supported, so you need to check if the rendering hardware supports that level or not.
The IValueController
interface specifies a general interface for parameter controlling. Both DoubleCovnverter
and Point2DValueConverter
implement this interface. They are used to specify the effect's parameter minimum, maximum, and default value. Also, they contain a description that will be shown on the user interface. The main Reflection code is placed in a separate user control, which is named EffectOptionDialog
. The XAML is pretty simple for it:
<UserControl x:Class="RefactorShaderControl.EffectOptoonDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignHeight="300"
d:DesignWidth="300">
<ScrollViewer>
<StackPanel x:Name="ControlRoot"/>
</ScrollViewer>
</UserControl>
It defines a StackPanel
, encapsulated in a ScrollViewer
. The controls for the shader will be added to this StackPanel
. The code for the control looks like this:
public partial class EffectOptoonDialog : UserControl
{
public EffectOptoonDialog()
{
InitializeComponent();
}
public static readonly DependencyProperty ControledEffectProperty =
DependencyProperty.Register("ControledEffect",
typeof(ShaderEffect), typeof(EffectOptoonDialog));
public ShaderEffect ControledEffect
{
get { return (ShaderEffect)GetValue(ControledEffectProperty); }
set
{
SetValue(ControledEffectProperty, value);
Clear();
if (value != null) Build();
}
}
private void Clear()
{
ControlRoot.Children.Clear();
GC.Collect();
}
private void Build()
{
Type effType = ControledEffect.GetType();
bool hwsupport = true;
Attribute[] memberatribs;
Attribute[] effatribs = Attribute.GetCustomAttributes(effType);
foreach (Attribute atrib in effatribs)
{
if (atrib is ShaderVersion)
{
ShaderVersion v = (ShaderVersion)atrib;
if (!RenderCapability.IsPixelShaderVersionSupported(v.Major, v.Minor))
{
MessageBox.Show("Pixel shader level " + v.ToString() +
" required by this effect is not supported by your hardware",
"Warning", MessageBoxButton.OK, MessageBoxImage.Error);
hwsupport = false;
}
}
}
if (!hwsupport) return;
MemberInfo[] members = effType.GetMembers();
Label descrpt;
foreach (MemberInfo member in members)
{
if (member.MemberType == MemberTypes.Property)
{
memberatribs = Attribute.GetCustomAttributes(member);
foreach (Attribute atr in memberatribs)
{
PropertyInfo pi = effType.GetProperty(member.Name);
if (atr is DoubleValueContoller)
{
DoubleValueContoller ctrl = (DoubleValueContoller)atr;
descrpt = new Label();
descrpt.Content = ctrl.Description;
ControlRoot.Children.Add(descrpt);
Slider slider = new Slider();
slider.Minimum = ctrl.Minimum;
slider.Maximum = ctrl.Maximum;
slider.Value = ctrl.Curent;
slider.Margin = new Thickness(10, 0, 0, 0);
Binding doublebind = new Binding();
doublebind.Source = ControledEffect;
doublebind.Mode = BindingMode.TwoWay;
doublebind.Path = new PropertyPath(pi);
slider.SetBinding(Slider.ValueProperty, doublebind);
ControlRoot.Children.Add(slider);
}
else if (atr is Point2DValueController)
{
Point2DValueController pctrl = (Point2DValueController)atr;
descrpt = new Label();
descrpt.Content = pctrl.Description;
ControlRoot.Children.Add(descrpt);
Point2DContol pointc = new Point2DContol();
pointc.Maximum = pctrl.Maximum;
pointc.Minimum = pctrl.Minimum;
pointc.Value = pctrl.Curent;
pointc.Margin = new Thickness(10, 0, 0, 0);
Binding pointbind = new Binding();
pointbind.Source = ControledEffect;
pointbind.Mode = BindingMode.TwoWay;
pointbind.Path = new PropertyPath(pi);
pointc.SetBinding(Point2DContol.ValueProperty, pointbind);
ControlRoot.Children.Add(pointc);
}
}
}
}
}
}
What It Does
The first thing that this Reflection code does is that it lists the specified Shader
class' attributes. If it finds a ShaderVersion
class among them, then it checks that the level is supported by the Graphics hardware. If it isn't, then it stops the user interface creation, because there's no point going further.
For Pixel Shader level 2.0, there's a software render fallback; however, this can be awfully slow when you run your code on a slow CPU and use large images. For level 3.0, there's no software fallback provided.
After this, the code looks for properties in the Shader
class that have a DoubleValueController
or a Point2DValueController
attribute attached to them. If it finds one, then it creates a control for the property and binds the Shader
's property to the Control
's value. I use two way binding, because this way, if you change one of the controlled values from the code, it will be reflected on the GUI.
An important thing to keep in mind is that you can only use bindings on Dependency Properties. So if you create custom controls, always implement their properties as Dependency Properties.
Using the Code
The control can be used like this:
BloomEffect eff = new BloomEffect();
EffectTarget.Effect = eff;
EffectOptions.ControledEffect = eff;
Building Your Own Value Controllers
For other pixel shader property types (float3
and float4
, which are mapped as Point3D
and Color
classes by .NET), you need to build your own controller, and then you can control every kind of shader based on this code.
A good starting example may be Point2DControl
.
XAML:
<UserControl x:Name="Point2D"
x:Class="RefactorShaderControl.Controls.Point2DContol"
xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
xmlns:mc=http://schemas.openxmlformats.org/markup-compatibility/2006
xmlns:d=http://schemas.microsoft.com/expression/blend/2008
mc:Ignorable="d" Height="50" d:DesignWidth="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Label Content="X:" Grid.Column="0" Grid.Row="0"/>
<Label Content="Y:" Grid.Column="0" Grid.Row="1"/>
<Slider x:Name="XValue" ValueChanged="ValueChanged"
Grid.Row="0" Grid.Column="1"/>
<Slider x:Name="YValue" ValueChanged="ValueChanged"
Grid.Row="1" Grid.Column="1"/>
</Grid>
</UserControl>
C# code:
public partial class Point2DContol : UserControl
{
bool exec;
public Point2DContol()
{
InitializeComponent();
Maximum = new Size(10, 10);
Minimum = new Size(0, 0);
Value = new Size(0, 0);
exec = true;
}
public static readonly DependencyProperty MinimumProperty =
DependencyProperty.Register("Minimum", typeof(Size), typeof(Point2DContol));
public static readonly DependencyProperty MaximumProperty =
DependencyProperty.Register("Maximum", typeof(Size), typeof(Point2DContol));
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(Size), typeof(Point2DContol));
public Size Minimum
{
get { return (Size)GetValue(MinimumProperty); }
set
{
SetValue(MinimumProperty, value);
XValue.Minimum = value.Width;
YValue.Minimum = value.Height;
}
}
public Size Maximum
{
get { return (Size)GetValue(MaximumProperty); }
set
{
SetValue(MaximumProperty, value);
XValue.Maximum = value.Width;
YValue.Maximum = value.Height;
}
}
public Size Value
{
get { return (Size)GetValue(ValueProperty); }
set
{
SetValue(ValueProperty, value);
if (exec)
{
XValue.Value = value.Width;
YValue.Value = value.Height;
}
else exec = true;
}
}
private void ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
exec = false;
Value = new Size(XValue.Value, YValue.Value);
}
}
This is a really simple control; it encapsulates two slider controls, which control the value of the X and Y axis (width and height). From the sample, you can see that each slider's value is set from the code instead of binding. This is done this way because I couldn't get it to work the other way. The exec
variable used in the ValueChanged
event and Value
setting code is used to prevent stack overflow from circular reference. I know it's not the prettiest way to do this, but I'm not an expert on WPF yet.
The Applciation
The finished application has three effects, and it looks like this:
It has three effects: two color effects and a distort. The channel mixer was created by me, while the others are simply taken from the Shazzam tools library. The source code also contains the source code for these effects. I used only Pixel Shader level 2.0 effects to maximize compatibility. The image used in the program is taken near the place where I live.
Points of Interest