VisualStateGroup 在另一个 VisualStateGroup 中触发动画
我不理解关于 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 VisualStateGroup
s 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
请注意
GotoState
方法如何简单地获取州名称,而不对其所属组进行任何限定。虽然组分隔了控件可以在任意时间保持的一组状态(每个组一个状态),但状态名称在管理器中的所有状态中应该是唯一的。另请注意,您从中派生的
ToggleButton
具有与该类关联的以下属性:-这表明您继承的逻辑将在某个时刻使用状态调用
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:-This indicates that the logic you have inherit will at some point call
GotoState
with the state "Normal". Typically controls only have oneUpdateStates
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 callGotoState
multiple times with one state name from each group. Hence at the point when the control loses focus theToggleButton
callsGotoState
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
fromToggleButton
don't do anything. However you do have "Normal". The VSM has no way of knowing that theToggleButton
use of "Normal" is intended to choose from its default template's "CommonStates" group. Instead it just finds aVisualState
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)