I'm using Visual Studio, C#, XAML, WPF.
In my program I have XAML buttons with white png icons.
I want to have it so you can switch to a theme with black icons by choosing the theme from a ComboBox.
Instead of creating a new set of black png images, is there a way with XAML and C# I can invert the color of the white icons?
<Button x:Name="btnInfo" HorizontalAlignment="Left" Margin="10,233,0,0" VerticalAlignment="Top" Width="22" Height="22" Cursor="Hand" Click="buttonInfo_Click" Style="{DynamicResource ButtonSmall}">
<Image Source="Resources/Images/info.png" Width="5" Height="10" Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="1,0,0,0"/>
</Button>
Double-click the image with your left mouse button to display the image editing window. Click the Recolor button and find the Color Modes setting. Select the Negative option, which adjusts the image to invert the colors.
If you want to do it quickly, by default, the hotkey shortcut for inverting colors is Ctrl + I on Windows, or Command + I on Apple computers. You can also select a specific portion of the image, to only invert the selected section.
Thanks for this question. It gave me a chance to learn something new. :)
Your goal is, once you know what you're doing, very easy to achieve. WPF supports the use of GPU shaders to modify images. They are fast at run-time (since they execute in your video card) and easy to apply. And in the case of the stated goal to invert the colors, very easy to implement as well.
To start with, you'll need the shader code. Shaders are written in a language called High Level Shader Language, or HLSL. Here is an HLSL "program" that will invert the input color:
sampler2D input : register(s0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
float4 color = tex2D(input, uv);
float alpha = color.a;
color = 1 - color;
color.a = alpha;
color.rgb *= alpha;
return color;
}
But, Visual Studio doesn't handle this kind of code directly. You'll need to make sure you have the DirectX SDK installed, which will give you the fxc.exe compiler, used to compile shader code.
I compiled the above with this command line:
fxc /T ps_3_0 /E main /Fo<my shader file>.ps <my shader file>.hlsl
Where, of course, you replace <my shader file>
with your actual file name.
(Note: I did this manually, but you can of course create a custom build action in your project to do the same.)
You can then include the .ps
file in your project, setting the "Build Action" to "Resource".
That done, you now need to create the ShaderEffect
class that will use it. That looks like this:
class InvertEffect : ShaderEffect
{
private static readonly PixelShader _shader =
new PixelShader { UriSource = new Uri("pack://application:,,,/<my shader file>.ps") };
public InvertEffect()
{
PixelShader = _shader;
UpdateShaderValue(InputProperty);
}
public Brush Input
{
get { return (Brush)GetValue(InputProperty); }
set { SetValue(InputProperty, value); }
}
public static readonly DependencyProperty InputProperty =
ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(InvertEffect), 0);
}
Key points to the above code:
static readonly
field. Since the .ps
file is included as a resource, I can refer to it using the pack:
scheme, as "pack://application:,,,/<my shader file>.ps"
. Again, you will need to replace <my shader file>
with the actual file name, of course.PixelShader
property to the shader object. You must also call UpdateShaderValue()
to initialize the shader, for each property used as input to the shader (in this case, there's only the one).Input
property is special: it requires the use of RegisterPixelShaderSamplerProperty()
to register the dependency property.DependencyProperty.Register()
. But they would require a special PropertyChangedCallback
value, obtained by calling ShaderEffect.PixelShaderConstantCallback()
with the register index declared in the shader code for that parameter.That's all there is to it!
You can use the above in XAML simply by setting a UIElement.Effect
property to an instance of the InvertEffect
class. For example:
<Window x:Class="TestSO45093399PixelShader.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:l="clr-namespace:TestSO45093399PixelShader"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Rectangle Width="100" Height="100">
<Rectangle.Fill>
<LinearGradientBrush>
<GradientStop Color="Black" Offset="0"/>
<GradientStop Color="White" Offset="1"/>
</LinearGradientBrush>
</Rectangle.Fill>
<Rectangle.Effect>
<l:InvertEffect/>
</Rectangle.Effect>
</Rectangle>
</Grid>
</Window>
When you run that, you'll notice that even though the gradient is defined as black in the upper-left transitioning to white in the lower-right, it's displayed the opposite way, with white in the upper-left and black in the lower-right.
Finally, on the off-chance you want to just get this working immediately and don't have access to the fxc.exe compiler, here's a version of the above that has the compiled shader code embedded as Base64. It's tiny, so this is a practical alternative to compiling and including the shader as a resource.
class InvertEffect : ShaderEffect
{
private const string _kshaderAsBase64 =
@"AAP///7/HwBDVEFCHAAAAE8AAAAAA///AQAAABwAAAAAAQAASAAAADAAAAADAAAAAQACADgAAAAA
AAAAaW5wdXQAq6sEAAwAAQABAAEAAAAAAAAAcHNfM18wAE1pY3Jvc29mdCAoUikgSExTTCBTaGFk
ZXIgQ29tcGlsZXIgMTAuMQCrUQAABQAAD6AAAIA/AAAAAAAAAAAAAAAAHwAAAgUAAIAAAAOQHwAA
AgAAAJAACA+gQgAAAwAAD4AAAOSQAAjkoAIAAAMAAAeAAADkgQAAAKAFAAADAAgHgAAA/4AAAOSA
AQAAAgAICIAAAP+A//8AAA==";
private static readonly PixelShader _shader;
static InvertEffect()
{
_shader = new PixelShader();
_shader.SetStreamSource(new MemoryStream(Convert.FromBase64String(_kshaderAsBase64)));
}
public InvertEffect()
{
PixelShader = _shader;
UpdateShaderValue(InputProperty);
}
public Brush Input
{
get { return (Brush)GetValue(InputProperty); }
set { SetValue(InputProperty, value); }
}
public static readonly DependencyProperty InputProperty =
ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(InvertEffect), 0);
}
Finally, I'll note that the link offered in Bradley's comment does have a whole bunch of these kinds of shader-implemented effects. The author of those implemented the HLSL and the ShaderEffect
objects only slightly differently from the way I show here, so if you want to see other examples of effects and different ways to implement them, browsing that code would be a great place to look.
Enjoy!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With