WPF isn’t perfect. yet.

I was trying the other day to make a button with an image look like it’s disabled, but I couldn’t make the image gray.

I searched the internet for a solution but couldn’t find anything that satisfied me.
Don’t get me wrong, some of the solutions did get the job done, but I have this problem that I must have everything in the most elegant way. Cest la vie.

I’ve decided to find a solution myself, and I didn’t want to limit myself only to the gray color, and to the "IsEnabled false" state because I was involved in a project that also required displaying button images in a bluish hue until the mouse if over them, and only than present them in full color.

One of the approaches that I considered was using the FormatConvertedBitmap class inside the XAML, as the Image.Source value, but WPF’s designer doesn’t seem to work with it (no image is shown), and this class lets me convert only to a gray scale image and not to colors other than gray.

Another approach I saw was of Microsoft in Visual Studio 2010. As you know, the UI of Visual Studio 2010 is written in WPF so I wanted to see how they fixed the problem. I saw (using Reflector, of course) that they created a GrayscaleImageConverter. On one hand the usage of this converter looks cumbersome because there is not a direct way to activate it when the image is disabled, and will probably force me to use a style; but on the other hand it does allow me to set a color other than gray, although it was written in a very limiting way (I guess VS doesn’t need more then 128×128 images with different types of image formats).

I’ve decided to take a different approach: First, I created my own MonotoneImage class which derives from the original Image class, and I rewrote the OnRender method to change the hue of the image.

I’ve created 2 dependency properties: BiasColor (of type Color) and IsFullColor (of type Boolean).

I was thinking a lot about the IsFullColor property. On my first attempt I was using the IsEnabled property to decide if to draw the image in gray, but than I thought it might limit me when I’ll use it with the "mouse over" scenario, so I’ve decided to create the IsFullColor property and bind the IsEnabled property to it. You might wonder why I didn’t create a property like IsMonotone (which has a more elegant name). The reason is I wanted the colorful state to be the positive state. That way it’s easier to use binding the IsEnabled or IsMouseOver properties without using value converters to convert ‘true’ to ‘false’ and vice versa.

The BiasColor property controls the hue of the monotone image. The default is White, which makes the image grayscale, but you can choose any other color to make the image appear in that color.
I also wanted to be able to make the color transformation smooth using animation, so I needed to find a way to control the impact of the bias color, which means finding a way to control for each pixel how much I want to see its original color, and how much I want to see the bias color.
I ‘ve decided to use the bias color’s unused alpha channel for that purpose. On one hand I has an unused property that I could harness for my needs, and on the other hand it made sense to specify the alpha channel to control that behavior; So, the closer the bias color’s alpha channel is to 0, the less we’ll see the bias channel, and the closer it gets to 255 (FF), the more we’ll see the bias color and not the original color of each pixel.

Notice that you we can’t animate the actual alpha channel, because the alpha property (Color.A) is not a dependency property, hence not animatable. What we can do is use the ColorAnimation and use the same RGB values, but different A value.

I based my conversion methods on Microsoft’s GrayscaleImageConverter, but I’ve made some changes to them in order to support a larger variety of images, and to support the bias color’s alpha channel impact.

Last, I’ve extended Image’s default style to bind the IsFullColor property to the IsEnabled property.

Now I can use the image and make it gray when it’s disabled, or bind the IsFullColor to the IsMouseOver property to make the image in some color when the mouse is not over the image, and displaying it in full color when the mouse is over the image.

I hope you’ll find this class useful.

Haim.

 

I’m adding some images to demonstrate:

1. The original image:
 image

2. The image with BiasColor #FF6D58FF (255 alpha, 109 red, 88 green, 255 blue):
image

3. The image with BiasColor #B36D58FF (179 alpha, 109 red, 88 green, 255 blue):
image

4. The image with BiasColor #5D6D58FF (93 alpha, 109 red, 88 green, 255 blue):
image

5. The image with White BiasColor:
image

 

Here is the code of the class:

/// <summary>
/// Represents a control that displays an image with a monotone color.
/// </summary>
public class MonotoneImage : Image
{
    #region Ctors

    /// <summary>
    /// Initializes the MonotoneImage class.
    /// </summary>
    static MonotoneImage()
    {
        ///Crete a new style, based on Image's default style:
        Style defaultStyle = new Style(typeof(MonotoneImage),
            StyleProperty.DefaultMetadata.DefaultValue as Style);

        ///Bind the "IsFullColor" property to the "IsEnabled" property:
        defaultStyle.Setters.Add(new Setter(MonotoneImage.IsFullColorProperty,
            new Binding("IsEnabled") { RelativeSource = RelativeSource.Self }));

        defaultStyle.Seal();

        StyleProperty.OverrideMetadata(typeof(MonotoneImage),
            new FrameworkPropertyMetadata(defaultStyle));
    }

    #endregion

    #region Overridden Members

    protected override void OnRender(System.Windows.Media.DrawingContext dc)
    {
        if (IsFullColor)
        {
            base.OnRender(dc);
        }
        else
        {
            ///Get the source image that needs to be converted to a monotone image:
            BitmapSource source = this.Source as BitmapSource;

            if (source == null)
            {
                base.OnRender(dc);
                return;
            }

            ///The converted monotone image:
            BitmapSource target;

            Color biasColor = this.BiasColor;

            if (source.Format == PixelFormats.Bgra32)
            {
                target = ConvertToMonotoneImage(source, biasColor);
            }
            else if (biasColor == Colors.White)
            {
                ///If the bias color is white,
                ///convert the image to a grayscale image:
                FormatConvertedBitmap bitmap = new FormatConvertedBitmap();
                bitmap.BeginInit();
                bitmap.DestinationFormat = PixelFormats.Gray32Float;
                bitmap.Source = source;
                bitmap.EndInit();
                target = bitmap;
            }
            else
            {
                ///If the bias color is not white,
                ///convert the image to a Bgr32 pixel format
                ///and create a monotone image:
                FormatConvertedBitmap bitmap = new FormatConvertedBitmap();
                bitmap.BeginInit();
                bitmap.DestinationFormat = PixelFormats.Bgr32;
                bitmap.Source = source;
                bitmap.EndInit();
                target = ConvertToMonotoneImage(bitmap, biasColor);
            }

            if (target.CanFreeze)
            {
                target.Freeze();
            }

            dc.DrawImage(target, new Rect(new Point(), base.RenderSize));
        }
    }

    #endregion


    #region Dependency Properties

    /// <summary>
    /// Gets or sets the color of which the palette will be created.
    /// </summary>
    public Color BiasColor
    {
        get { return (Color)GetValue(BiasColorProperty); }
        set { SetValue(BiasColorProperty, value); }
    }

    public static readonly DependencyProperty BiasColorProperty =
        DependencyProperty.Register("BiasColor", typeof(Color), typeof(MonotoneImage),
        new FrameworkPropertyMetadata(Colors.White,
            FrameworkPropertyMetadataOptions.AffectsRender));



    /// <summary>
    /// Gets or sets whether to display the image in full color,
    /// or on a single color.
    /// </summary>
    public bool IsFullColor
    {
        get { return (bool)GetValue(IsFullColorProperty); }
        set { SetValue(IsFullColorProperty, value); }
    }

    public static readonly DependencyProperty IsFullColorProperty =
        DependencyProperty.Register("IsFullColor", typeof(bool), typeof(MonotoneImage),
        new FrameworkPropertyMetadata(true,
            FrameworkPropertyMetadataOptions.AffectsRender));

    #endregion


    #region Non-Public Methods

    /// <summary>
    /// Converts the specified image to a single-color image,
    /// with a color palette of the specified bias color.
    /// </summary>
    /// <param name="inputImage">The image to convert.</param>
    /// <param name="biasColor">The color to create the palette for.</param>
    /// <returns></returns>
    private static BitmapSource ConvertToMonotoneImage(BitmapSource inputImage, Color biasColor)
    {
        if (inputImage == null)
        {
            throw new ArgumentNullException("inputImage");
        }

        if (inputImage.Format != PixelFormats.Bgra32
            && inputImage.Format != PixelFormats.Bgr32)
        {
            throw new ArgumentException("The image format is not the of expected type.", "inputImage");
        }

        int stride = inputImage.PixelWidth * 4;

        ///Create a pixel-array for the converted image,
        ///and copy the input image pixels into it:
        byte[] pixels = new byte[inputImage.PixelWidth * inputImage.PixelHeight * 4];
        inputImage.CopyPixels(pixels, stride, 0);

        float alphaRatio = (float)biasColor.A / 256f;

        if (inputImage.Format == PixelFormats.Bgra32 || inputImage.Format == PixelFormats.Bgr32)
        {
            for (int i = 0; (i + 4) < pixels.Length; i += 4)
            {
                ///Canculate the color intensity of the pixel:
                float pixelStrength = ((pixels[i] * 0.0004296875f) + (pixels[i + 1] * 0.002304687f))
                    + (pixels[i + 2] * 0.001171875f);

                ///Set the color of the pixel:
                pixels[i] += (byte)((pixelStrength * biasColor.B - pixels[i]) * alphaRatio);
                pixels[i + 1] += (byte)((pixelStrength * biasColor.G - pixels[i + 1]) * alphaRatio);
                pixels[i + 2] += (byte)((pixelStrength * biasColor.R - pixels[i + 2]) * alphaRatio);
            }
        }

        return BitmapSource.Create(inputImage.PixelWidth,
            inputImage.PixelHeight,
            inputImage.DpiX, inputImage.DpiY,
            inputImage.Format, inputImage.Palette,
            pixels, stride);
    }

    #endregion
}
Advertisements