VisualStateGroup 在另一个 VisualStateGroup 中触发动画

发布于 2024-09-08 19:21:24 字数 4626 浏览 2 评论 0原文

我不理解关于 VisualStateGroup 的一些基本知识。我读过的所有内容都让我相信它们是正交的。也就是说,一个组中的状态更改不会影响其他组。事实上,如果情况并非如此,它们就毫无意义。

然而,为了理解我遇到的一些奇怪行为,我整理了一个简单的示例,该示例表明一个组中的状态更改可以触发另一个组中的动画。我试图理解这是怎么回事。

我想要的只是一个基于 ToggleButton 的控件,在切换时具有一种外观 (IsChecked == true),无论它是否具有焦点或鼠标是否位于其上方,例如例子。

首先,我有一个非常简单的控件,可以在自定义组中的自定义状态之间进行转换:

using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;

[TemplateVisualState(Name = normalState, GroupName = activationGroupName)]
[TemplateVisualState(Name = hoverState, GroupName = activationGroupName)]
[TemplateVisualState(Name = activatedState, GroupName = activationGroupName)]
public class CustomControl : ToggleButton
{
    private const string activationGroupName = "Activation";
    private const string normalState = "Normal";
    private const string hoverState = "Hover";
    private const string activatedState = "Activated";

    public CustomControl()
    {
        this.DefaultStyleKey = typeof(CustomControl);

        this.Checked += delegate
        {
            this.UpdateStates();
        };
        this.Unchecked += delegate
        {
            this.UpdateStates();
        };
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        this.UpdateStates();
    }

    protected override void OnMouseEnter(MouseEventArgs e)
    {
        base.OnMouseEnter(e);
        this.UpdateStates();
    }

    protected override void OnMouseLeave(MouseEventArgs e)
    {
        base.OnMouseLeave(e);
        this.UpdateStates();
    }

    private void UpdateStates()
    {
        var state = normalState;

        if (this.IsChecked.HasValue && this.IsChecked.Value)
        {
            state = activatedState;
        }
        else if (this.IsMouseOver)
        {
            state = hoverState;
        }

        VisualStateManager.GoToState(this, state, true);
    }
}

其次,我有一个该控件的模板,可以根据当前状态更改背景颜色:

<Style TargetType="local:CustomControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:CustomControl">
                <Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="Activation">
                            <VisualStateGroup.Transitions>
                                <VisualTransition To="Normal" GeneratedDuration="00:00:0.2"/>
                            </VisualStateGroup.Transitions>

                            <VisualState x:Name="Normal"/>

                            <VisualState x:Name="Hover">
                                <Storyboard>
                                    <ColorAnimation Duration="00:00:0.2" To="Red" Storyboard.TargetName="grid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"/>
                                </Storyboard>
                            </VisualState>

                            <VisualState x:Name="Activated">
                                <Storyboard>
                                    <ColorAnimation Duration="00:00:0.2" To="Blue" Storyboard.TargetName="grid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"/>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>

                    <Grid x:Name="grid" Background="White">
                        <TextBlock>ToggleButton that should be white normally, red on hover, and blue when checked</TextBlock>
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

在大多数情况下,它可以工作,但是当控件丢失时焦点它会转换回正常状态。

我可以通过显式处理控件中的焦点更改并执行状态更新来解决此问题:

public CustomControl()
{
    this.DefaultStyleKey = typeof(CustomControl);

    this.Checked += delegate
    {
        this.UpdateStates();
    };
    this.Unchecked += delegate
    {
        this.UpdateStates();
    };

    // explicitly handle focus changes
    this.GotFocus += delegate
    {
        this.UpdateStates();
    };
    this.LostFocus += delegate
    {
        this.UpdateStates();
    };
}

但是,对于我来说为什么必须这样做毫无意义。

为什么一个 VisualStateGroup 中的状态更改会导致另一个 VisualStateGroup 定义的动画执行?对我来说,实现既定目标的最简单、最正确的方法是什么?

谢谢

There's something fundamental about VisualStateGroups that I'm not understanding. Everything I've read has led me to believe that they are orthogonal. That is, a state change in one group won't affect other groups. Indeed, they would be rather pointless if this were not the case.

However, in an attempt to understand some odd behavior I was encountering, I put together a simple example that shows that a state change in one group can trigger an animation in another. I'm trying to understand how this can be.

All I want is a ToggleButton-based control to have one appearance when toggled (IsChecked == true) regardless of whether it has focus or whether the mouse is over it, for example.

Firstly, I have a very simple control that transitions between custom states in a custom group:

using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;

[TemplateVisualState(Name = normalState, GroupName = activationGroupName)]
[TemplateVisualState(Name = hoverState, GroupName = activationGroupName)]
[TemplateVisualState(Name = activatedState, GroupName = activationGroupName)]
public class CustomControl : ToggleButton
{
    private const string activationGroupName = "Activation";
    private const string normalState = "Normal";
    private const string hoverState = "Hover";
    private const string activatedState = "Activated";

    public CustomControl()
    {
        this.DefaultStyleKey = typeof(CustomControl);

        this.Checked += delegate
        {
            this.UpdateStates();
        };
        this.Unchecked += delegate
        {
            this.UpdateStates();
        };
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        this.UpdateStates();
    }

    protected override void OnMouseEnter(MouseEventArgs e)
    {
        base.OnMouseEnter(e);
        this.UpdateStates();
    }

    protected override void OnMouseLeave(MouseEventArgs e)
    {
        base.OnMouseLeave(e);
        this.UpdateStates();
    }

    private void UpdateStates()
    {
        var state = normalState;

        if (this.IsChecked.HasValue && this.IsChecked.Value)
        {
            state = activatedState;
        }
        else if (this.IsMouseOver)
        {
            state = hoverState;
        }

        VisualStateManager.GoToState(this, state, true);
    }
}

Secondly, I have a template for this control that changes the background color based on the current state:

<Style TargetType="local:CustomControl">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:CustomControl">
                <Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="Activation">
                            <VisualStateGroup.Transitions>
                                <VisualTransition To="Normal" GeneratedDuration="00:00:0.2"/>
                            </VisualStateGroup.Transitions>

                            <VisualState x:Name="Normal"/>

                            <VisualState x:Name="Hover">
                                <Storyboard>
                                    <ColorAnimation Duration="00:00:0.2" To="Red" Storyboard.TargetName="grid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"/>
                                </Storyboard>
                            </VisualState>

                            <VisualState x:Name="Activated">
                                <Storyboard>
                                    <ColorAnimation Duration="00:00:0.2" To="Blue" Storyboard.TargetName="grid" Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"/>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>

                    <Grid x:Name="grid" Background="White">
                        <TextBlock>ToggleButton that should be white normally, red on hover, and blue when checked</TextBlock>
                    </Grid>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

For the most part it works, but when the control loses focus it transitions back to the normal state.

I can get around this by explicitly handling focus changes in my control and enacting a state update:

public CustomControl()
{
    this.DefaultStyleKey = typeof(CustomControl);

    this.Checked += delegate
    {
        this.UpdateStates();
    };
    this.Unchecked += delegate
    {
        this.UpdateStates();
    };

    // explicitly handle focus changes
    this.GotFocus += delegate
    {
        this.UpdateStates();
    };
    this.LostFocus += delegate
    {
        this.UpdateStates();
    };
}

However, it makes no sense to me why I have to do this.

Why is a state change in one VisualStateGroup causing an animation defined by another to execute? And what is the simplest, most correct way for me to achieve my stated goal?

Thanks

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

無處可尋 2024-09-15 19:21:24

请注意 GotoState 方法如何简单地获取州名称,而不对其所属组进行任何限定。虽然组分隔了控件可以在任意时间保持的一组状态(每个组一个状态),但状态名称在管理器中的所有状态中应该是唯一的。

另请注意,您从中派生的 ToggleButton 具有与该类关联的以下属性:-

[TemplateVisualStateAttribute(Name = "Normal", GroupName = "CommonStates")]

这表明您继承的逻辑将在某个时刻使用状态调用 GotoState “普通的”。通常,控件只有一个 UpdateStates 方法(正如您所拥有的),并且在任何状态可能发生更改的任何时候都会调用该方法。该函数将依次使用每组中的一个状态名称多次调用 GotoState。因此,当控件失去焦点时,ToggleButton 调用 GotoState 来设置“Unfocused”状态,但同时也会使用“Normal”和“Checked”来调用它”或“未选中”。

现在您已经有了自己的模板,其中不存在“Unfocused”、“Checked”状态,因此从 ToggleButton 调用 GotoState 不会执行任何操作。然而你确实有“正常”。 VSM 无法知道 ToggleButton 使用“Normal”是为了从其默认模板的“CommonStates”组中进行选择。相反,它只是找到一个名为“Normal”的 VisualState,在本例中,它位于您的“Activiation”组中。因此,您现有的“激活”状态将替换为“正常”。

将您的“正常”更改为“废话”,事情就会按您的预期进行。

(这有助于记住,尽管组名称存在于 TemplateVisualStateAttribute 中,但它们实际上并没有多大作用,我能想到的它们唯一的用途是在 Blend UI 中)

Note how the GotoState method simply takes the state name without any qualification as to which group it belongs to. Whilst the groups separate the set of states that a control can hold at any one time (one state per group) the state names are expected to be unique across all states in the manager.

Note also that the ToggleButton from which you derive has the following attribute associated with the class:-

[TemplateVisualStateAttribute(Name = "Normal", GroupName = "CommonStates")]

This indicates that the logic you have inherit will at some point call GotoState with the state "Normal". Typically controls only have one UpdateStates method (as you have) and at any point where any of the states may have changed it will be called. This function in turn will call GotoState multiple times with one state name from each group. Hence at the point when the control loses focus the ToggleButton calls GotoState to set the "Unfocused" state but along with that will also call it with "Normal" and either "Checked" or "Unchecked".

Now you've got your own template where the states "Unfocused", "Checked" are not present so those calls to GotoState from ToggleButton don't do anything. However you do have "Normal". The VSM has no way of knowing that the ToggleButton use of "Normal" is intended to choose from its default template's "CommonStates" group. Instead it just finds a VisualState with the name "Normal" which in this case is found in your "Activiation" group. Hence your existing state of "Activated" is replaced with your "Normal".

Change your "Normal" to "Blah" and things work as you expect.

(It helps to remember that the group names don't really do a lot despite their presence in the TemplateVisualStateAttribute, their only use I can think of is in the Blend UI)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文