1
votes

I have a chromeless button in a WPF application that uses the XAML style below: However, there are 2 things that I'm trying to do with it.

  1. I want the displayed image to be one of two images, depending on a binding property from the datacontext of the template the button is in. The property is a boolean. Show one image if False, the other if True.
  2. I would like the image shown to change when the mouse hovers over the button and to be the image that would be shown when above bound property is True.

I've tried several things, but nothing really seems to work. I tried a converter with the bound value like this:

        <x:ArrayExtension x:Key="ThumbsDown" Type="BitmapImage">
            <BitmapImage UriSource="/Elpis;component/Images/thumbDown.png" />
            <BitmapImage UriSource="/Elpis;component/Images/thumbBan.png" />
        </x:ArrayExtension>

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            var data = (object[])parameter;

            if (data.Length != 2 || value == null || value.GetType() != typeof(bool))
                return data[0];

            if(!((bool)value))
                return data[0];
            else
                return data[1];
        }

<Button Name="btnThumbDown" Grid.Column="1" Style="{StaticResource NoChromeButton}" 
                                            VerticalAlignment="Center" 
                                            HorizontalAlignment="Center" 
                                            Background="Transparent"
                                            Click="btnThumbDown_Click">
                                <Image  Width="32" Height="32" Margin="2" 
                                        Source="{Binding Banned, 
                                                Converter={StaticResource binaryChooser}, 
                                                ConverterParameter={StaticResource ThumbsDown}}"/>
                            </Button>

But this causes 2 problems. Nothing I do for the hover image works anymore and the designer throws an exception.

The opacity trigger from below can go, as I really just want the hover now.

Any thoughts on how to make this properly work?

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style x:Key="NoChromeButton" TargetType="{x:Type ButtonBase}">
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
        <Setter Property="HorizontalContentAlignment" Value="Center"/>
        <Setter Property="VerticalContentAlignment" Value="Center"/>
        <Setter Property="Padding" Value="1"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ButtonBase}">
                    <Grid x:Name="Chrome" Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
                        <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                                          Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" 
                                          SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Opacity" Value="0.75"/>
                        </Trigger>
                        <Trigger Property="IsMouseOver" Value="False">
                            <Setter Property="Opacity" Value="1.0"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>
2

2 Answers

3
votes

You're going to have to do several things:

  1. Set up ImageBrush resources for each of the images you want to show.
  2. Create a ControlTemplate for the Button that has a built-in Style with a mouse-over Trigger.
  3. Set the image on the Button by binding the Background property to one of the ImageBrush resources from point 1.
  4. Use the mouse-over Trigger to change the Background property to the other ImageBrush resource.

See the code below for a simple example (you'll have to use your own images in place of "up" and "down").

    <Window x:Class="ButtonHover.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:my="clr-namespace:ButtonHover"
            Title="MainWindow" Height="227" Width="280">
        <Window.Resources>
            <ImageBrush x:Key="imageABrush"
                        ImageSource="/ButtonHover;component/Resources/up.png"/>
            <ImageBrush x:Key="imageBBrush"
                        ImageSource="/ButtonHover;component/Resources/down.png"/>
            <ControlTemplate x:Key="buttonTemplate" TargetType="Button">
                <Border BorderThickness="3" BorderBrush="Black">
                    <Border.Style>
                        <Style>
                            <Style.Setters>
                                <Setter Property="Border.Background"
                                        Value="{StaticResource imageABrush}"/>
                            </Style.Setters>
                            <Style.Triggers>
                                <Trigger Property="Button.IsMouseOver" Value="True">
                                    <Setter Property="Border.Background"
                                            Value="{StaticResource imageBBrush}"/>
                                </Trigger>
                            </Style.Triggers>
                        </Style>
                    </Border.Style>
                </Border>
            </ControlTemplate>
        </Window.Resources>
        <Grid>
            <Button Height="75"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Width="75"
                    Template="{StaticResource buttonTemplate}" />
        </Grid>
    </Window>

The key here is implementing the trigger on the Style of the Border rather than on the Button directly. I couldn't get it to work when trying to set a property of the Border from a Style applied to the Button. You can also replace the Border with a Rectangle or any other ContentControl with a BackGround property.

From here, customize as you see fit for your application.

1
votes

I would solve this by adding an Image control to the ControlTemplate of the button, and then create a MultiDataTrigger with one condition for IsMouseOver and one condition for the boolean property that you want to bind to. The trigger then sets the source of the image when active.

Below is a style that accomplishes this. I've assumed that the button has a DataContext which contains the boolean property, and that the boolean property is called MyBoolean.

<Style x:Key="NoChromeButton" TargetType="{x:Type ButtonBase}">
    <Setter Property="Background" Value="Transparent"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
    <Setter Property="HorizontalContentAlignment" Value="Center"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    <Setter Property="Padding" Value="1"/>
    <Setter Property="Template">
        <Setter.Value>
            <!-- I assume that the button has a DataContext with a boolean property called MyBoolean -->
            <ControlTemplate TargetType="{x:Type ButtonBase}">
                <Grid x:Name="Chrome" Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
                    <!-- Not sure about what the button should look like, so I made it an image to the left
                            and the button's content to the right -->
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="auto" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <Image x:Name="ButtonImage" Grid.Column="0" Source="/Elpis;component/Images/thumbBan.png" />
                    <ContentPresenter Grid.Column="1"
                        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                                    Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" 
                                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                                    VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                </Grid>
            <ControlTemplate.Triggers>
                <!-- As I understand it, ThumbBan should be shown when IsMouseOver == True OR the bound boolean is true,
                        so if you invert that you could say that the ThumbDown image should be shown
                        when IsMouseOver == false AND the bound boolean is false, which is what this trigger does -->
                    <MultiDataTrigger>
                        <MultiDataTrigger.Conditions>
                            <Condition Binding="{Binding IsMouseOver, ElementName=Chrome}" Value="False" />
                            <Condition Binding="{Binding MyBoolean}" Value="False" />
                        </MultiDataTrigger.Conditions>
                        <MultiDataTrigger.Setters>
                            <Setter TargetName="ButtonImage" Property="Source" Value="/Elpis;component/Images/thumbDown.png" />
                        </MultiDataTrigger.Setters>
                    </MultiDataTrigger>
                <Trigger Property="IsMouseOver" Value="True">
                    <Setter Property="Opacity" Value="0.75"/>
                </Trigger>
                <!-- The trigger that sets opacity to 1 for IsMouseOver false is not needed, since 1 is the 
                        default and will be the opacity as long as the trigger above is not active -->
            </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

You might have to switch which image should be displayed when the trigger is active, if I misunderstod the requirements.

The trick here is to use a MultiDataTrigger so that the two conditions you mention can be combined. Usually you would probably use a Trigger instead of a DataTrigger when binding to IsMouseOver of a control. But since you need a DataTrigger for the boolean property the IsMouseOver binding can be written as a DataTrigger by using the ElementName property of the binding. By doing that you make is possible to use a MultiDataTrigger to combine the two.

UPDATE:

To add support for customizing the images used, and also which property to bind to, for each button instance I would subclass the Button class and add a couple of DependencyProperties.

public class ImageButton : Button
{
    public static readonly DependencyProperty ActiveImageUriProperty =
        DependencyProperty.RegisterAttached("ActiveImageUri", typeof(Uri), typeof(ImageButton),
            new PropertyMetadata(null));

    public static readonly DependencyProperty InactiveImageUriProperty =
        DependencyProperty.RegisterAttached("InactiveImageUri", typeof(Uri), typeof(ImageButton),
            new PropertyMetadata(null));

    public static readonly DependencyProperty IsActiveProperty =
        DependencyProperty.RegisterAttached("IsActive", typeof(bool), typeof(ImageButton),
            new PropertyMetadata(false));

    public Uri ActiveImageUri
    {
        get { return (Uri)GetValue(ActiveImageUriProperty); }
        set { SetValue(ActiveImageUriProperty, value); }
    }

    public Uri InactiveImageUri
    {
        get { return (Uri)GetValue(InactiveImageUriProperty); }
        set { SetValue(InactiveImageUriProperty, value); }
    }

    public bool IsActive
    {
        get { return (bool)GetValue(IsActiveProperty); }
        set { SetValue(IsActiveProperty, value); }
    }
}

This class could then be used in the following way:

<SomeNamespace:ImageButton Height="23" Width="100" Content="Button 1"
    ActiveImageUri="/Elpis;component/Images/thumbBan.png"
    InactiveImageUri="/Elpis;component/Images/thumbDown.png"
    IsActive="{Binding MyBoolean}" />

<SomeNamespace:ImageButton Height="23" Width="100" Content="Button 2"
    ActiveImageUri="/Elpis;component/Images/someOtherImage.png"
    InactiveImageUri="/Elpis;component/Images/yetAnotherImage.png"
    IsActive="{Binding SomeOtherBooleanProperty}" />

The control template could then be modified to look like this:

<Style TargetType="SomeNamespace:ImageButton">
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="BorderThickness" Value="1" />
    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
    <Setter Property="HorizontalContentAlignment" Value="Center" />
    <Setter Property="VerticalContentAlignment" Value="Center" />
    <Setter Property="Padding" Value="1" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type SomeNamespace:ImageButton}">
                <Grid x:Name="Chrome" Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="auto" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <Image x:Name="ButtonImage" Grid.Column="0"
                        Source="{Binding ActiveImageUri, RelativeSource={RelativeSource TemplatedParent}}" />
                    <ContentPresenter Grid.Column="1"
                        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                        Margin="{TemplateBinding Padding}" RecognizesAccessKey="True"
                        SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                        VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
                </Grid>
                <ControlTemplate.Triggers>
                    <!-- The "active" image should be shown when IsMouseOver == True OR the bound boolean is true,
                        so if you invert that you could say that the "inactive" image should be shown
                        when IsMouseOver == false AND the bound boolean is false, which is what this trigger does -->
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsMouseOver" Value="False" />
                            <Condition Property="IsActive" Value="False" />
                        </MultiTrigger.Conditions>
                        <MultiTrigger.Setters>
                            <Setter TargetName="ButtonImage" Property="Source"
                                Value="{Binding InactiveImageUri, RelativeSource={RelativeSource TemplatedParent}}" />
                        </MultiTrigger.Setters>
                    </MultiTrigger>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="Opacity" Value="0.75" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The major changes here is that the source for the image is set to the values of the dependency properties instead of hard coded URIs, and that the MultiDataTrigger has been changed into a MultiTrigger which binds to the dependency properties. Previously the path to the boolean property was also hard coded, but now that is configurable by changing the binding for the IsActive property when creating the button, as shown in the example above.