1

A user requested a grayscale version of my WPF application. The application supports different color themes and has text and images. Obviously, one way to this is to create a grayscale theme. But in my mind, it might be better to add a toggle that grayscales the current theme on demand. This got me to wondering if there is a way to grayscale an entire WPF application. I looked at pixel shaders briefly but it doesn't appear I can apply one globally to an app. Looking for suggestions on how to do this.

Mike Ward
  • 3,211
  • 1
  • 29
  • 42
  • 3
    I think you listed your options. A grey theme or a pixel shader applied to your mainwindow. – Andy Jul 22 '20 at 15:08
  • 1
    See https://stackoverflow.com/questions/45093399/how-to-invert-color-of-xaml-png-images-using-c/45096471#45096471. The example shader in my answer is for inverting, but there's all the information there you need to make a B&W shader instead. Set the windows' `Effect` property to your shader, and you're done. – Peter Duniho Jul 22 '20 at 16:27
  • 1
    Depending on the "colorfulness" of your UI, a pixel shader might not be a good choice. You may lose contrast between similarly bright colors. With a dedicated theme, you would obviously avoid that problem. – Clemens Jul 22 '20 at 17:39

1 Answers1

3

While a dedicated theme would be the best choice, it's a tremendous amount of work.

Since it's a single user ask - you may want to try a pixel shader and gather some feedback from the user and see if it works. If not, you can look into doing a dedicated theme.

Here is a pixel shader that works really well with different colors, contrasts, and brightness of colors. It uses the HSP color model, which you can learn about here:

http://alienryderflex.com/hsp.html

sampler2D implicitInput : register(s0);
float factor : register(c0);

/// <summary>The brightness offset.</summary>
/// <minValue>-1</minValue>
/// <maxValue>1</maxValue>
/// <defaultValue>0</defaultValue>
float brightness : register(c1);

float4 main(float2 uv : TEXCOORD) : COLOR
{
    float4 pixelColor = tex2D(implicitInput, uv);
  pixelColor.rgb /= pixelColor.a;
    
  // Apply brightness.
  pixelColor.rgb += brightness;
    
  // Return final pixel color.
  pixelColor.rgb *= pixelColor.a;

    float4 color = pixelColor;

    float pr = .299;
    float pg = .587;
    float pb = .114;
    
    float gray = sqrt(color.r * color.r * pr + color.g * color.g * pg + color.b * color.b * pb);

  float4 result;    
  result.r = (color.r - gray) * factor + gray;
  result.g = (color.g - gray) * factor + gray;
  result.b = (color.b - gray) * factor + gray;
  result.a = color.a;
    
  return result;
}

You will need to compile the shader to a .ps file.

Here is the wrapper class:

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;

namespace Shaders
{
    /// <summary>
    /// Represents a grayscale pixel shader effect using the HSP method.
    /// </summary>
    public class GrayscaleHspEffect : ShaderEffect
    {
        /// <summary>
        /// Identifies the Input property.
        /// </summary>
        public static readonly DependencyProperty InputProperty = RegisterPixelShaderSamplerProperty("Input", typeof(GrayscaleHspEffect), 0);

        /// <summary>
        /// Identifies the Factor property.
        /// </summary>
        public static readonly DependencyProperty FactorProperty = DependencyProperty.Register("Factor", typeof(double), typeof(GrayscaleHspEffect), new UIPropertyMetadata(0D, PixelShaderConstantCallback(0)));

        /// <summary>
        /// Identifies the Brightness property.
        /// </summary>
        public static readonly DependencyProperty BrightnessProperty = DependencyProperty.Register("Brightness", typeof(double), typeof(GrayscaleHspEffect), new UIPropertyMetadata(0D, PixelShaderConstantCallback(1)));

        /// <summary>
        /// Creates a new instance of the <see cref="GrayscaleHspEffect"/> class.
        /// </summary>
        public GrayscaleHspEffect()
        {
            var pixelShader = new PixelShader();
            pixelShader.UriSource = new Uri("/Shaders;component/Effects/GrayscaleHspEffect.ps", UriKind.Relative);

            PixelShader = pixelShader;

            UpdateShaderValue(InputProperty);
            UpdateShaderValue(FactorProperty);
            UpdateShaderValue(BrightnessProperty);
        }

        /// <summary>
        /// Gets or sets the <see cref="Brush"/> used as input for the shader.
        /// </summary>
        public Brush Input
        {
            get => ((Brush)(GetValue(InputProperty)));
            set => SetValue(InputProperty, value);
        }

        /// <summary>
        /// Gets or sets the factor used in the shader.
        /// </summary>
        public double Factor
        {
            get => ((double)(GetValue(FactorProperty)));
            set => SetValue(FactorProperty, value);
        }

        /// <summary>
        /// Gets or sets the brightness of the effect.
        /// </summary>
        public double Brightness
        {
            get => ((double)(GetValue(BrightnessProperty)));
            set => SetValue(BrightnessProperty, value);
        }
    }
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
Keithernet
  • 2,349
  • 1
  • 10
  • 16
  • I did something similar to what you describe here but without the brightness setting. The shader solution produces a very acceptable grayscale in the case of my application. – Mike Ward Jul 22 '20 at 19:33