我目前正在寻找一种方法来制作带有多个用户控件的 wpf 窗口,这些控件依次滑入和滑出可见区域,类似于“Stellaris”启动器(这是我能想到的最好的例子找到我想要的):
我以前使用过this Question成功创建一个带有 2 个滑入和滑出的视觉元素的窗口,但我无法找出超过 2 个元素的最佳实践。
我的计划是使用 4 个 Storyboard,从当前位置滑动到堆栈面板中每个控件的位置,如下所示:
<Grid Grid.Column="1">
<Grid.Resources>
<Storyboard x:Key="SlideFirst">
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
From="{Binding CurrentPosition}" To="0" Duration="0:0:0:3" />
</Storyboard>
<Storyboard x:Key="SlideSecond">
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
From="{Binding CurrentPosition}" To="650" Duration="0:0:0:3" />
</Storyboard>
<Storyboard x:Key="SlideThird">
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
From="{Binding CurrentPosition}" To="1300" Duration="0:0:0:3" />
</Storyboard>
<Storyboard x:Key="SlideForth">
<DoubleAnimation Storyboard.TargetProperty="(UIElement.RenderTransform).(TranslateTransform.Y)"
From="{Binding CurrentPosition}" To="1950" Duration="0:0:0:3" />
</Storyboard>
</Grid.Resources>
<StackPanel>
<StackPanel.Style>
<Style TargetType="StackPanel">
<Style.Triggers>
<DataTrigger Binding="{Binding CurrentControl}" Value="0">
<DataTrigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource SlideFirst}" />
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
但这会导致异常:
InvalidOperationException: Cannot freeze this Storyboard timeline tree for use across threads.
理论上,我可以为每种可能的状态(1->2、1->3、1->4、2->1、2->3 ...)制作一个 Storyboard,但这已经是4 个控件的 12 个 Storyboard。一定有更简单的方法。
如何使用 Storyboard根据当前位置在多个元素之间滑动?
最佳答案
您应该创建一个?用户控制or custom
控制that hosts a
列表框 to display the sections and the buttons to navigate between them. You then animate the
ScrollViewer` 导航到所选部分。
这使得实现动态化,意味着您在添加新部分时不必添加新的动画等。
- 创建抽象基类或接口(interface),例如
SectionItem
。它是所有部分项(数据模型)的模板,包含通用属性和逻辑。 - 每个部分(例如新闻、DLC、模组)都实现此基类/接口(interface),并添加到公共(public)集合中,例如
Sections
在 View 模型中。 - 创建
UserControl
或定制Control
SectionsView
。SectionsView
承载导航按钮并将显示各个部分或SectionItem
项目。按下按钮时,将执行该部分的动画导航。 - 这个
SectionView
暴露ItemsSource
绑定(bind)到 View 模型的Sections
的属性收藏。 - 创建
DataTemplate
对于每个SectionItem
。该模板定义了实际部分的外观。这些模板将添加到ResourceDictionary
SectionView
的. - 为
ScrollViewer
制作动画ListBox
的SectionsView
必须实现DependencyProperty
例如NavigationOffset
。这是必要的,因为ScrollViewer
仅提供修改其偏移量的方法。
创建部分项目
每个项目都必须扩展基类 SectionItem
:
SectionItem.cs
public abstract class SectionItem : INotifyPropertyChanged
{
public SectionItem(Section id)
{
this.id = id;
}
private Section id;
public Section Id
{
get => this.id;
set
{
this.id = value;
OnPropertyChanged();
}
}
private string title;
public string Title
{
get => this.title;
set
{
this.title = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
实现实际剖面模型
class DlcSection : SectionItem
{
public DlcSection(Section id) : base(id)
{
}
}
class SettingsSection : SectionItem
{
public SettingsSection(Section id) : base(id)
{
}
}
class NewsSection : SectionItem
{
public NewsSection(Section id) : base(id)
{
}
}
enum
用作 SectionItem
的节 ID和CommandParameter
Section.cs
public enum Section
{
None = 0,
Dlc,
Settings,
News
}
实现SectionsView
SectionsView
延伸UserControl
(或 Control
)并封装 SectionItem
的显示项目及其导航。为了触发导航,它公开了一个路由命令 NavigateToSectionRoutedCommand
:
SectionsView.xaml.cs
public partial class SectionsView : UserControl
{
#region Routed commands
public static readonly RoutedUICommand NavigateToSectionRoutedCommand = new RoutedUICommand(
"Navigates to section by section ID which is an enum value of the enumeration 'Section'.",
nameof(SectionsView.NavigateToSectionRoutedCommand),
typeof(SectionsView));
#endregion Routed commands
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
"ItemsSource",
typeof(IEnumerable),
typeof(SectionsView),
new PropertyMetadata(default(IEnumerable)));
public IEnumerable ItemsSource
{
get => (IEnumerable) GetValue(SectionsView.ItemsSourceProperty);
set => SetValue(SectionsView.ItemsSourceProperty, value);
}
public static readonly DependencyProperty NavigationOffsetProperty = DependencyProperty.Register(
"NavigationOffset",
typeof(double),
typeof(SectionsView),
new PropertyMetadata(default(double), SectionNavigator.OnNavigationOffsetChanged));
public double NavigationOffset
{
get => (double) GetValue(SectionsView.NavigationOffsetProperty);
set => SetValue(SectionsView.NavigationOffsetProperty, value);
}
private ScrollViewer Navigator { get; set; }
public SectionsView()
{
InitializeComponent();
this.Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (TryFindVisualChildElement(this.SectionItemsView, out ScrollViewer scrollViewer))
{
this.Navigator = scrollViewer;
}
}
private static void OnNavigationOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as SectionsView).Navigator.ScrollToVerticalOffset((double) e.NewValue);
}
private void NavigateToSection_OnExecuted(object sender, ExecutedRoutedEventArgs e)
{
SectionItem targetSection = this.SectionItemsView.Items
.Cast<SectionItem>()
.FirstOrDefault(section => section.Id == (Section) e.Parameter);
if (targetSection == null)
{
return;
}
double verticalOffset = 0;
if (this.Navigator.CanContentScroll)
{
verticalOffset = this.SectionItemsView.Items.IndexOf(targetSection);
}
else
{
var sectionContainer =
this.SectionItemsView.ItemContainerGenerator.ContainerFromItem(targetSection) as UIElement;
Point absoluteContainerPosition = sectionContainer.TransformToAncestor(this.Navigator).Transform(new Point());
verticalOffset = this.Navigator.VerticalOffset + absoluteContainerPosition.Y;
}
var navigationAnimation = this.Resources["NavigationAnimation"] as DoubleAnimation;
navigationAnimation.From = this.Navigator.VerticalOffset;
navigationAnimation.To = verticalOffset;
BeginAnimation(SectionNavigator.NavigationOffsetProperty, navigationAnimation);
}
private void NavigateToSection_OnCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = e.Parameter is Section;
}
private bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild resultElement)
where TChild : DependencyObject
{
resultElement = null;
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is Popup popup)
{
childElement = popup.Child;
}
if (childElement is TChild)
{
resultElement = childElement as TChild;
return true;
}
if (TryFindVisualChildElement(childElement, out resultElement))
{
return true;
}
}
return false;
}
}
SectionsView.xaml
<UserControl x:Class="SectionsView">
<UserControl.Resources>
<!-- Animation can be changed, but name must remain the same -->
<DoubleAnimation x:Key="NavigationAnimation" Storyboard.TargetName="Root" Storyboard.TargetProperty="NavigationOffset"
Duration="0:0:0.3">
<DoubleAnimation.EasingFunction>
<PowerEase EasingMode="EaseIn" Power="5" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!-- DataTemplates for different section items -->
<DataTemplate DataType="{x:Type local:DlcSection}">
<Grid Height="200" Background="Green">
<TextBlock Text="{Binding Title}" FontSize="18" />
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type local:SettingsSection}">
<Grid Height="200" Background="OrangeRed">
<TextBlock Text="{Binding Title}" FontSize="18" />
</Grid>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:NewsSection}">
<Grid Height="200" Background="Yellow">
<TextBlock Text="{Binding Title}" FontSize="18" />
</Grid>
</DataTemplate>
</UserControl.Resources>
<UserControl.CommandBindings>
<CommandBinding Command="{x:Static local:SectionNavigator.NavigateToSectionRoutedCommand}"
Executed="NavigateToSection_OnExecuted" CanExecute="NavigateToSection_OnCanExecute" />
</UserControl.CommandBindings>
<Grid>
<StackPanel>
<Button Content="Navigate" Command="{x:Static local:SectionNavigator.NavigateToSectionRoutedCommand}"
CommandParameter="{x:Static local:Section.News}" />
<Button Content="Navigate" Command="{x:Static local:SectionNavigator.NavigateToSectionRoutedCommand}"
CommandParameter="{x:Static local:Section.Dlc}" />
<Button Content="Navigate" Command="{x:Static local:SectionNavigator.NavigateToSectionRoutedCommand}"
CommandParameter="{x:Static local:Section.Settings}" />
<!-- ScrollViewer.CanContentScroll is set to False to enable smooth scrolling for large (high) items -->
<ListBox x:Name="SectionItemsView"
Height="250"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:SectionNavigator}, Path=Sections}"
ScrollViewer.CanContentScroll="False" />
</StackPanel>
</Grid>
</UserControl>
用法
ViewModel.cs
class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<SectionItem> Sections { get; set; }
public ViewModel()
{
this.Sections = new ObservableCollection<SectionItem>
{
new NewsSection(Section.News) {Title = "News"},
new DlcSection(Section.Dlc) {Title = "DLC"},
new SettingsSection(Section.Settings) {Title = "Settings"}
};
}
}
MainWindow.xaml
<Window>
<Window.Resources>
<ViewModel />
</Window.Resources>
<SectionsView ItemsSource="{Binding Sections}" />
</Window>
关于c# - WPF动画: How to slide between multiple elements within a stackpanel?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62991128/