【问题标题】:WPF Animation: How to slide between multiple elements within a stackpanel?WPF 动画:如何在堆栈面板中的多个元素之间滑动?
【发布时间】:2020-07-20 08:07:36
【问题描述】:

我目前正在寻找一种方法来制作一个带有多个 UserControls 的 wpf 窗口,它一个接一个地滑入和滑出可见区域,类似于“Stellaris”启动器 (这是我能做到的最好的例子找到我想要的)

我之前使用this Question 成功创建了一个包含 2 个视觉元素滑入和滑出的窗口,但我无法找出超过 2 个元素的最佳实践。

我的计划是使用 4 个故事板从当前位置滑动到堆栈面板中每个控件的位置,如下所示:

            <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:无法冻结此 Storyboard 时间线树以供跨线程使用。

理论上,我可以为每个可能的状态(1->2、1->3、1->4、2->1、2->3 ...)制作一个故事板,但这已经是4 个控件的 12 个故事板。一定有更简单的方法。

如何使用 Storyboards 根据当前位置在多个元素之间滑动?

【问题讨论】:

标签: c# wpf storyboard


【解决方案1】:

你应该创建一个 ? UserControlor customControlthat hosts aListBoxto display the sections and the buttons to navigate between them. You then animate theScrollViewer`导航到选定的部分。

这使得实现动态化,意味着您在添加新部分时不必添加新动画等。

  1. 创建抽象基类或接口,例如SectionItem。它是所有部分项(数据模型)的模板,包含通用属性和逻辑。
  2. 每个部分(例如,新闻、DLC、Mods)都实现了这个基类/接口,并被添加到一个公共集合中,例如Sections 在视图模型中。
  3. 创建UserControl 或自定义Control SectionsViewSectionsView 承载导航按钮并将显示各个部分或SectionItem 项目。按下按钮时,将执行到该部分的动画导航。
  4. 这个SectionView 公开了一个绑定到视图模型的Sections 集合的ItemsSource 属性。
  5. 为每个SectionItem 创建一个DataTemplate。该模板定义了实际部分的外观。这些模板被添加到SectionViewResourceDictionary
  6. 要为ListBoxScrollViewer 设置动画,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 用作SectionItemCommandParameter 的部分ID

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>

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-06-18
    • 1970-01-01
    • 2011-01-23
    • 1970-01-01
    • 1970-01-01
    • 2011-07-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多