Управление новым визуальным состоянием кнопок

Я хочу добавить новое состояние «Активировано» к кнопке WPF, и я хочу избежать повторного создания элемента управления с нуля.

Это новое состояние связано со свойством зависимости IsActivated и должно изменить цвет фона кнопки. Вот таблица истинности взаимодействия между свойствами зависимостей IsEnabled и IsActivated:

Таблица истинности отношений между IsActivated и IsEnabled

Я написал класс, расширяющийся от Button, создал свойства зависимостей и в обратном вызове IsActivated вычислил визуальное состояние кнопки.

Проблема в том, что тип ButtonBase уже управляет визуальным состоянием через ChangeVisualState, которую нельзя переопределить.

После управления обратными вызовами обоих свойств зависимостей взаимодействие между IsActivated и IsEnabled работает так, как предполагалось, но нажатие кнопки или наведение мыши на кнопку переопределяет визуальное состояние Activated.

Возможно ли это сделать с помощью визуальных состояний или мне следует использовать простые триггеры?

Код элемента управления до сих пор:

using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace OrganizationName.BasicControls.Primitive
{
    public class MultiStateButton : Button
    {
        static MultiStateButton()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiStateButton), new FrameworkPropertyMetadata(typeof(MultiStateButton)));
            IsEnabledProperty.OverrideMetadata(typeof(MultiStateButton), new FrameworkPropertyMetadata(propertyChangedCallback: IsEnabledCallback));
        }

        internal void ChangeVisualState(bool useTransitions)
        {
            if (!IsEnabled)
            {
                VisualStateManager.GoToState(this, "Disabled", useTransitions);
            }
            else if (IsActivated)
            {
                VisualStateManager.GoToState(this, "Activated", useTransitions);
            }
            else if (IsPressed)
            {
                VisualStateManager.GoToState(this, "Pressed", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Normal", useTransitions);
            }
        }

        #region IsEnabled override
        private static void IsEnabledCallback(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            MultiStateButton multiStateButton = o as MultiStateButton;
            if (multiStateButton == null) return;

            multiStateButton.ChangeVisualState(true);
        }
        #endregion IsEnabled override

        #region DP IsActivated 
        public bool IsActivated
        {
            get { return (bool)GetValue(IsActivatedProperty); }
            set { SetValue(IsActivatedProperty, value); }
        }

        private static void IsActivatedCallback(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            MultiStateButton multiStateButton = o as MultiStateButton;
            if (multiStateButton == null) return;

            multiStateButton.ChangeVisualState(true);
        }

        private readonly static FrameworkPropertyMetadata IsActivatedMetadata = new FrameworkPropertyMetadata
        {
            PropertyChangedCallback = IsActivatedCallback,
            DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
        };

        public static readonly DependencyProperty IsActivatedProperty =
            DependencyProperty.Register("IsActivated", typeof(bool), typeof(MultiStateButton), IsActivatedMetadata);
        #endregion DP IsActivated
    }
}


Стиль кнопки по умолчанию:

<sys:Double x:Key="ButtonCornerRadiusValue">5</sys:Double>

<CornerRadius x:Key="ButtonCornerRadius"
              TopLeft="{StaticResource ButtonCornerRadiusValue}"
              BottomLeft="{StaticResource ButtonCornerRadiusValue}"
              TopRight="{StaticResource ButtonCornerRadiusValue}"
              BottomRight="{StaticResource ButtonCornerRadiusValue}"/>
<CornerRadius x:Key="ButtonCornerRadiusLeft"
              TopLeft="{StaticResource ButtonCornerRadiusValue}"
              BottomLeft="{StaticResource ButtonCornerRadiusValue}"/>
<CornerRadius x:Key="ButtonCornerRadiusRight"
              TopRight="{StaticResource ButtonCornerRadiusValue}"
              BottomRight="{StaticResource ButtonCornerRadiusValue}"/>
<CornerRadius x:Key="ButtonCornerRadiusTop"
              TopRight="{StaticResource ButtonCornerRadiusValue}"
              TopLeft="{StaticResource ButtonCornerRadiusValue}"/>
<CornerRadius x:Key="ButtonCornerRadiusBottom"
              BottomLeft="{StaticResource ButtonCornerRadiusValue}"
              BottomRight="{StaticResource ButtonCornerRadiusValue}"/>

<Style x:Key="{x:Type primitives:MultiStateButton}"
       TargetType="{x:Type primitives:MultiStateButton}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type primitives:MultiStateButton}">
                <Border TextBlock.Foreground="{TemplateBinding Foreground}"
                        x:Name="Border"
                        CornerRadius="{StaticResource ButtonCornerRadius}"
                        Background="White"
                        BorderThickness="1">
                    <Border.Effect>
                        <DropShadowEffect Color="#CDD5E3"
                                          ShadowDepth="0"
                                          BlurRadius="13"
                                          Direction="0"/>
                    </Border.Effect>
                    <Border.BorderBrush>
                        <RadialGradientBrush Center="0.5,0.5"
                                             RadiusY="1"
                                             RadiusX="5"
                                             GradientOrigin="0.5,0.5">
                            <RadialGradientBrush.GradientStops>
                                <GradientStopCollection>
                                    <GradientStop Color="Transparent"
                                                  Offset="0" />
                                    <GradientStop Color="Transparent"
                                                  Offset="0.5" />
                                    <GradientStop Color="{StaticResource BorderPushedColor}"
                                                  Offset="0.8" />
                                    <GradientStop Color="{StaticResource BorderPushedColor}"
                                                  Offset="1" />
                                </GradientStopCollection>
                            </RadialGradientBrush.GradientStops>
                        </RadialGradientBrush>
                    </Border.BorderBrush>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal"/>
                            <VisualState x:Name="MouseOver"/>
                            <VisualState x:Name="Pressed">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetProperty="(Border.Width)"
                                                     Storyboard.TargetName="LeftBorder"
                                                     Duration="00:00:00"
                                                     To="10"/>
                                    <DoubleAnimation Storyboard.TargetProperty="(Border.Width)"
                                                     Storyboard.TargetName="RightBorder"
                                                     Duration="00:00:00"
                                                     To="10"/>
                                    <DoubleAnimation Storyboard.TargetProperty="(Border.Height)"
                                                     Storyboard.TargetName="TopBorder"
                                                     Duration="00:00:00"
                                                     To="10"/>
                                    <DoubleAnimation Storyboard.TargetProperty="(Border.Height)"
                                                     Storyboard.TargetName="BottomBorder"
                                                     Duration="00:00:00"
                                                     To="10"/>
                                    <DoubleAnimation Storyboard.TargetProperty="(Border.Effect).(DropShadowEffect.BlurRadius)"
                                                     Storyboard.TargetName="Border"
                                                     Duration="00:00:00"
                                                     To="10"/>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Disabled">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.Background)"
                                                                  Storyboard.TargetName="Border">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource SkyblueLight}"/>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Activated">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.Background)"
                                                                   Storyboard.TargetName="Border">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource ReflexBlue}"/>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Grid Background="Transparent">
                        <ContentPresenter Margin="2"
                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                          RecognizesAccessKey="True"/>
                        <Border BorderThickness="0"
                                Width="0"
                                HorizontalAlignment="Left"
                                CornerRadius="{StaticResource ButtonCornerRadiusLeft}"
                                x:Name="LeftBorder">
                            <Border.Background>
                                <LinearGradientBrush StartPoint="1,0.5" EndPoint="0,0.5">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Offset="0" Color="Transparent"/>
                                        <GradientStop Offset="1" Color="#D9DDE4"/>
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Border.Background>
                        </Border>
                        <Border BorderThickness="0"
                                Width="0"
                                HorizontalAlignment="Right"
                                CornerRadius="{StaticResource ButtonCornerRadiusRight}"
                                x:Name="RightBorder">
                            <Border.Background>
                                <LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Offset="0" Color="Transparent"/>
                                        <GradientStop Offset="1" Color="#D9DDE4"/>
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Border.Background>
                        </Border>

                        <Border BorderThickness="0"
                                Height="0"
                                VerticalAlignment="Top"
                                CornerRadius="{StaticResource ButtonCornerRadiusTop}"
                                x:Name="TopBorder">
                            <Border.Background>
                                <LinearGradientBrush StartPoint="0.5,1" EndPoint="0.5,0">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Offset="0" Color="Transparent"/>
                                        <GradientStop Offset="1" Color="#D9DDE4"/>
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Border.Background>
                        </Border>

                        <Border BorderThickness="0"
                                Height="0"
                                VerticalAlignment="Bottom"
                                CornerRadius="{StaticResource ButtonCornerRadiusBottom}"
                                x:Name="BottomBorder">
                            <Border.Background>
                                <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Offset="0" Color="Transparent"/>
                                        <GradientStop Offset="1" Color="#D9DDE4"/>
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Border.Background>
                        </Border>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

person nkoniishvt    schedule 21.11.2019    source источник
comment
Я немного запутался? Есть ли причина, по которой вы не можете просто использовать ToggleButton? Они существуют для хранения состояния в свойстве IsChecked.   -  person Tronald    schedule 21.11.2019
comment
@Tronald Это действительно то, что я упустил из виду для IsActivated DP. Следует отметить, что я урезал класс, в настоящее время он содержит другие DP, используемые в ControlTemplate.   -  person nkoniishvt    schedule 22.11.2019


Ответы (1)


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

Когда дело доходит до реализации нового визуального состояния для существующего элемента управления, вам вообще не нужно использовать VisualStateManager. Особенно, если вы не используете анимацию.

Вместо этого я рекомендую использовать Triggers.

Если вы хотите продолжать использовать свой элемент управления MultiStateButton, вы можете просто сделать что-то вроде этого:

<ControlTemplate ...>
    ...
    <ControlTemplate.Triggers>
        <MultiTrigger>
            <MultiTrigger.Conditions>
                <Condition Property="IsEnabled" Value="True" />
                <Condition Property="IsActivated" Value="True" />
            </MultiTrigger.Conditions>
            <Setter TargetName="Border" Property="Background" Value="{StaticResource ReflexBlue}" />
        </MultiTrigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

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

public static class MultiStateButtonProperties
{
    public static readonly DependencyProperty IsActivatedProperty = DependencyProperty.RegisterAttached("IsActivated", typeof(bool), typeof(MultiStateButtonProperties), new FrameworkPropertyMetadata(false));

    public static bool GetIsActivated(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsActivatedProperty);
    }

    public static void SetIsActivated(DependencyObject obj, bool value)
    {
        obj.SetValue(IsActivatedProperty, value);
    }
}

Затем в шаблоне управления вашего стиля вы можете использовать MultiTrigger, как показано выше, и сделать что-то вроде этого:

<ControlTemplate ...>
    ...
    <ControlTemplate.Triggers>
        <MultiTrigger>
            <MultiTrigger.Conditions>
                <Condition Property="IsEnabled" Value="True" />
                <Condition Property="ap:MultiStateButtonProperties.IsActivated" Value="True" />
            </MultiTrigger.Conditions>
            <Setter TargetName="Border" Property="Background" Value="{StaticResource ReflexBlue}" />
        </MultiTrigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

Надеюсь, это поможет.

person Keithernet    schedule 21.11.2019
comment
Кажется, что триггеры — это то, что нужно, но я хотел быть уверен, что ничего не упускаю из виду. Вместо этого я не думал об использовании присоединенных свойств. Спасибо за ваше решение. Есть ли недостаток в использовании триггеров вместо визуальных состояний? Я только начинаю их использовать, поэтому я хотел решить ситуацию с помощью визуальных состояний. - person nkoniishvt; 22.11.2019
comment
На самом деле нет большого недостатка в использовании триггеров. На самом деле, если вы извлечете шаблоны элементов управления с помощью Blend для Visual Studio, вы увидите, что Microsoft использует триггеры почти для всех элементов управления. Я создал и поддерживаю большую библиотеку тем WPF и теперь почти исключительно использую триггеры. Недостатком VisualStateManager является то, что раскадровки замораживают ресурсы. Триггеры этого не делают и являются более динамичными. - person Keithernet; 22.11.2019