Кнопка без хрома WPF с привязанным состоянием изображения и наведенным изображением

#c# #wpf #xaml

#c# #wpf #xaml

Вопрос:

У меня есть кнопка без хрома в приложении WPF, которая использует приведенный ниже стиль XAML: однако есть 2 вещи, которые я пытаюсь с ней сделать.

  1. Я хочу, чтобы отображаемое изображение было одним из двух изображений, в зависимости от свойства привязки из datacontext шаблона, в котором находится кнопка. Свойство является логическим. Показывает одно изображение, если False, другое, если True.
  2. Я бы хотел, чтобы отображаемое изображение менялось при наведении курсора мыши на кнопку и было изображением, которое будет отображаться, когда указанное выше свойство привязки имеет значение True.

Я пробовал несколько вещей, но, похоже, ничего не работает. Я попробовал конвертер с привязанным значением, подобным этому:

         <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>
  

Но это вызывает 2 проблемы. Ничего, что я делаю для наведенного изображения, больше не работает, и дизайнер выдает исключение.

Может сработать триггер непрозрачности снизу, так как я действительно просто хочу, чтобы он зависал сейчас.

Есть мысли о том, как заставить это правильно работать?

 <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>
  

Ответ №1:

Вам нужно будет сделать несколько вещей:

  1. Настройте ImageBrush ресурсы для каждого из изображений, которые вы хотите отобразить.
  2. Создайте ControlTemplate для кнопки, которая имеет встроенный Style указатель мыши Trigger .
  3. Установите изображение на Button , привязав Background свойство к одному из ImageBrush ресурсов из пункта 1.
  4. Используйте наведение курсора Trigger , чтобы изменить Background свойство на другой ImageBrush ресурс.

Смотрите код ниже для простого примера (вам придется использовать свои собственные изображения вместо «вверх» и «вниз»).

     <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>
  

Ключевым моментом здесь является реализация триггера на Style из Border , а не на Button напрямую. Я не смог заставить ее работать при попытке установить свойство Border из Style , применяемое к Button . Вы также можете заменить Border на Rectangle или любое другое ContentControl свойство BackGround .

Отсюда настройте так, как считаете нужным для своего приложения.

Комментарии:

1. Как я могу заставить стиль работать для нескольких кнопок, которые имеют разные изображения?

2. Я бы предложил либо создать UserControl , либо подкласс Button , который может ссылаться как на «обычное» изображение, так и на изображение «Наведения курсора мыши». Однако, похоже, что Эрик опередил меня.

Ответ №2:

Я бы решил это, добавив Image элемент управления к ControlTemplate кнопке, а затем создал MultiDataTrigger с одним условием для IsMouseOver и одним условием для логического свойства, к которому вы хотите привязаться. Затем триггер устанавливает источник изображения, когда он активен.

Ниже приведен стиль, который выполняет это. Я предположил, что кнопка имеет DataContext, который содержит свойство boolean, и что свойство boolean называется 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>
  

Возможно, вам придется переключить, какое изображение должно отображаться при активном триггере, если я неправильно понимаю требования.

Хитрость здесь в том, чтобы использовать MultiDataTrigger , чтобы можно было объединить два упомянутых вами условия. Обычно вы, вероятно, использовали бы Trigger вместо DataTrigger при привязке к IsMouseOver элементу управления. Но поскольку вам нужен DataTrigger для логического свойства, IsMouseOver привязка может быть записана как DataTrigger , используя ElementName свойство привязки. Делая это, вы можете использовать MultiDataTrigger для объединения двух.

Обновить:

Чтобы добавить поддержку настройки используемых изображений, а также того, к какому свойству привязываться, для каждого экземпляра кнопки я бы подкласс Button класса и добавил пару 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); }
    }
}
  

Затем этот класс можно было бы использовать следующим образом:

 <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}" />
  

Шаблон управления затем может быть изменен, чтобы выглядеть следующим образом:

 <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>
  

Основные изменения здесь заключаются в том, что для источника изображения установлены значения свойств зависимости вместо жестко закодированных URI, и что MultiDataTrigger был изменен на MultiTrigger , который привязывается к свойствам зависимости. Ранее путь к логическому свойству также был жестко запрограммирован, но теперь это настраивается путем изменения привязки к IsActive свойству при создании кнопки, как показано в примере выше.

Комментарии:

1. Хорошо … вот одна проблема с этим… Этот стиль используется на МНОГИХ разных кнопках, все с разными изображениями. Поэтому мне нужно сделать это таким образом, чтобы изображения были установлены вне стиля, который просто хранится в файле style.xaml и повторно отображается на каждой кнопке.

2. Я обновил ответ, чтобы разрешить настройку того, какие изображения использовать и к какому логическому свойству привязываться.

3. ПРИЯТНО … попробую сегодня вечером. Подумал, что для этого потребуется некоторый код, но я полностью согласен с тем, что это просто новый тип управления.

4. Рад, что смог помочь! Просто проверяя, вы знаете, что на самом деле приняли другой ответ, верно? Просто хотел убедиться, что это было сделано намеренно.

5. О боже! Извините … нет, я полностью хотел принять ваш. Исправлено 🙂