Earlier today I was ranting about lack of built-in support for grayed-out image buttons in WPF. I’ve come up with two workarounds; one correct, and one workable.
The first one I figured out by looking at some C# code here. The idea is to use a FormatConvertedBitmap to convert the original image to grayscale. This works, but then kills the alpha channel so your transparent PNGs aren’t transparent anymore. Thus, I also use the OpaqueMask property of Image set to an ImageBrush based on the original image. It’s nasty, complicated, and (probably, in a real app) slow. Here’s the markup:
<TextBlock>
This is a grayed Image loaded from a BitmapImage, with an opacity mask :
<Button IsEnabled="false">
<Image Height="16">
<Image.Source>
<FormatConvertedBitmap DestinationFormat="Gray32Float">
<FormatConvertedBitmap.Source>
<BitmapImage UriSource="Images\OutdentHS.png" />
</FormatConvertedBitmap.Source>
</FormatConvertedBitmap>
</Image.Source>
<Image.OpacityMask>
<ImageBrush>
<ImageBrush.ImageSource>
<BitmapImage UriSource="Images\OutdentHS.png" />
</ImageBrush.ImageSource>
</ImageBrush>
</Image.OpacityMask>
</Image>
</Button>
</TextBlock>
As you can see, the original image is read into FormatConvertedBitmap, which is used to convert to Gray32Float (Gray8 is probably better), and used as the Source of an Image. The Image’s OpaqueMask is set to the original image via an ImageBrush. As you test this out, don’t use Expression Blend; the designer view renders the grayscale Image elements as blank, even though in IE or a desktop XAML app it looks fine.
I also have NFI how this solution could be generalized into a style so you don’t have to repeat this for every button. Obviously the original image and grayscale version could be expressed as resources (I did it my way for clarity), but it’s a long way from there to a trigger based on IsEnabled that replaces an image with a grayscale version of itself.
That leads me to the other solution, which blows but is workable. I found that here. The idea is that you don’t grayscale the image at all; you lower its opacity so it looks more faded, and in the case of a gray control background, grayed out. This happens to be easily expressed as a style, like so:
<Window.Resources>
<!-- El Cheapo hack to make images within disabled toolbar buttons appear 'grayed out'. This doesn't
gray them at all, but lowers their opacity so the (usually gray) background of the button shows through.
If WPF had a built-in facility for grayscaling images in disabled buttons, this kind of icky kludgery wouldn't
be necessary -->
<Style TargetType="{x:Type Image}" x:Key="toolbarImageStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type Button}, AncestorLevel=1}, Path=IsEnabled}" Value="False">
<Setter Property="Opacity" Value="0.50"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
Any Images contained within a button and with Style set to toolbarImageStyle will have their opacity adjusted to 50% when the button is disabled. The original post called for an opacity of 25%, but that was too faded in my opinion. Adjust the value to taste, but don’t expect to exactly duplicate the true grayscale effect; colors are still visible in the buttons, they just look a bit faded.
So, there you have it. I’m going with the latter solution since it’s easily workable and almost right, but I’m still fuming over WPF’s inability to handle a feature that MFC has had for the duration of my programming career. Pretty soon I’ll be like those old mainframe curmudgeons who bitch about C++ and insist C and 64K of RAM is all anyone ever needed. Then I’ll know it’s time to quit the software industry and go into yoga instruction full time.
Alright -- how about HOT state?
First, Adam, thanks for a good laugh. I’ve been beating my head against this particular problem for hours now, thinking I’d lost my Google mojo because I couldn’t turn up a ‘real’ solution. I mean, there’s got to be Property trigger for this, or something equally straightforward, right? But noooo…
Like you, I’d found the opacity trick on the MSDN forums, and it looks like that’s what I’ll be using. Of course, that doesn’t address the issue of changing the button image to a hot state when hovering over the button. Any suggestions for that?
Thanks again.
If I understand your
If I understand your question, mouseovers are a bit easier. The
IsMouseOverproperty can be used with a trigger to munge control properties when the mouse is over. The “Button Overview” article in the WPF SDK offers this little snippet:Hope that helps. Glad you liked the post.