在WPF中创建一个无限居中的旋转木马控件

3
我正在尝试在WPF中创建一个无限居中的走马灯,就像这个概念图。我想出的当前解决方案是使用一个ListBox,将所有图像加载到ObservableCollection中,然后修改它以创建移动的假象。

enter image description here

我对这个解决方案有两个问题。首先,我似乎无法将其置于中心位置。 ListBox被对齐到左侧,没有办法让它溢出到两侧。无论我的窗口大小如何,它都应该始终在中间显示一个控制台,每侧一个,还应显示半个控制台以指示仍有更多可供选择。
第二个问题并不那么重要,但我正在寻找一种适当的方法,可以在以后的选择中允许更流畅的过渡。
这是我的现有代码:
XAML:
<Window x:Class="SystemMenu.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">

<DockPanel>
    <Button Content="left" Height="20" Click="Left_Click" DockPanel.Dock="Top" />
    <Button Content="right" Height="20" Click="Right_Click" DockPanel.Dock="Top" />

    <ListBox x:Name="LoopPanel" ItemsSource="{Binding Path=SampleData}" SelectedIndex="3" ScrollViewer.HorizontalScrollBarVisibility="Hidden" ScrollViewer.VerticalScrollBarVisibility="Hidden" ScrollViewer.CanContentScroll="False">
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <WrapPanel IsItemsHost="True" />
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
        <ListBox.ItemTemplate>
            <DataTemplate>
                    <Image Source="{Binding}"/>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</DockPanel>

后台代码:

    public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = this;
    }

    ObservableCollection<string> sampleData = new ObservableCollection<string>();
    public ObservableCollection<string> SampleData
    {
        get
        {
            if (sampleData.Count <= 0)
            {
                sampleData.Add(@"Nintendo 64.png");
                sampleData.Add(@"Nintendo Famicom.png");
                sampleData.Add(@"Super Nintendo Entertainment System.png");
                sampleData.Add(@"Nintendo Entertainment System.png");
                sampleData.Add(@"Sony PlayStation.png");
            }
            return sampleData;
        }
    }

    private void Right_Click(object sender, RoutedEventArgs e)
    {
        var firstItem = SampleData.First();
        SampleData.Remove(firstItem);
        SampleData.Insert(SampleData.Count, firstItem);
    }

    private void Left_Click(object sender, RoutedEventArgs e)
    {
        var lastItem = SampleData.Last();
        SampleData.Remove(lastItem);
        SampleData.Insert(0, lastItem);
    }
}

编辑: 我找到了一个解决居中列表框问题的扩展程序。调用 LoopPanel.ScrollToCenterOfView(sampleData[2]); 似乎可以将图片居中... 现在有任何想法如何动画转换吗? :)

 public static class ItemsControlExtensions
    {
        public static void ScrollToCenterOfView(this ItemsControl itemsControl, object item)
        {
            // Scroll immediately if possible
            if (!itemsControl.TryScrollToCenterOfView(item))
            {
                // Otherwise wait until everything is loaded, then scroll
                if (itemsControl is ListBox) ((ListBox)itemsControl).ScrollIntoView(item);
                itemsControl.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
                {
                    itemsControl.TryScrollToCenterOfView(item);
                }));
            }
        }

        private static bool TryScrollToCenterOfView(this ItemsControl itemsControl, object item)
        {
            // Find the container
            var container = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as UIElement;
            if (container == null) return false;

            // Find the ScrollContentPresenter
            ScrollContentPresenter presenter = null;
            for (Visual vis = container; vis != null && vis != itemsControl; vis = VisualTreeHelper.GetParent(vis) as Visual)
                if ((presenter = vis as ScrollContentPresenter) != null)
                    break;
            if (presenter == null) return false;

            // Find the IScrollInfo
            var scrollInfo =
                !presenter.CanContentScroll ? presenter :
                presenter.Content as IScrollInfo ??
                FirstVisualChild(presenter.Content as ItemsPresenter) as IScrollInfo ??
                presenter;

            // Compute the center point of the container relative to the scrollInfo
            Size size = container.RenderSize;
            Point center = container.TransformToAncestor((Visual)scrollInfo).Transform(new Point(size.Width / 2, size.Height / 2));
            center.Y += scrollInfo.VerticalOffset;
            center.X += scrollInfo.HorizontalOffset;

            // Adjust for logical scrolling
            if (scrollInfo is StackPanel || scrollInfo is VirtualizingStackPanel)
            {
                double logicalCenter = itemsControl.ItemContainerGenerator.IndexFromContainer(container) + 0.5;
                Orientation orientation = scrollInfo is StackPanel ? ((StackPanel)scrollInfo).Orientation : ((VirtualizingStackPanel)scrollInfo).Orientation;
                if (orientation == Orientation.Horizontal)
                    center.X = logicalCenter;
                else
                    center.Y = logicalCenter;
            }

            // Scroll the center of the container to the center of the viewport
            if (scrollInfo.CanVerticallyScroll) scrollInfo.SetVerticalOffset(CenteringOffset(center.Y, scrollInfo.ViewportHeight, scrollInfo.ExtentHeight));
            if (scrollInfo.CanHorizontallyScroll) scrollInfo.SetHorizontalOffset(CenteringOffset(center.X, scrollInfo.ViewportWidth, scrollInfo.ExtentWidth));
            return true;
        }

        private static double CenteringOffset(double center, double viewport, double extent)
        {
            return Math.Min(extent - viewport, Math.Max(0, center - viewport / 2));
        }
        private static DependencyObject FirstVisualChild(Visual visual)
        {
            if (visual == null) return null;
            if (VisualTreeHelper.GetChildrenCount(visual) == 0) return null;
            return VisualTreeHelper.GetChild(visual, 0);
        }
    }

我会查看Windows Phone中类似无限滚动控件的PivotControl源代码,以了解他们是如何实现的。 - Liero
1个回答

1

我认为我不会像你这样做。也就是说,在ListBox中添加和删除项并不能给你足够的控制位置,也不能平滑地旋转动画,而对于这种UI,我认为这是一种期望 :)

我可能会选择一个Canvas,然后将ClipToBounds设置为true。然后只需要计算位置,因为你不需要圆形轮播,所以位置很简单,也没有缩放。

假设你的所有图像都是100 x 100。所以item0将在@ -50,0,item1 @ 50,0(技术上可能是75,0或其他位置,因为你需要一些间距),等等。由于你正在计算位置并且它们是绝对的,对于Canvas,ClipToBounds=true将剪切两端,并且您将能够旋转动画。


我对这个答案感到犹豫。虽然图形效果更好,但是如果没有某种预制的可滚动/可选择/可加载的集合容器(如ItemsSource、SelectedItem、SelectedIndex等),你将失去很多功能。您是否有关于如何使用您的方法实现这些功能的建议? - goobering
1
但是正是这个功能在妨碍你。像ListBox或其他预构建的滚动条一样,它们期望项目相对于彼此保持在同一位置。它还将是一个艰巨的任务,要使换行工作,因为滚动条希望您从0到X滚动,从X到0滚动。不是以循环方式。无论如何,您都必须重新实现滚动才能获得酷炫的动画效果。如果您想在两侧添加滚动箭头,可以在RepeatButton上制作一些箭头。不认为您需要SelectedIndex。SelectedItem只是您单击的图像。 - SledgeHammer
第二部分...将每个图像放在透明网格的顶部,这样您就不必单击实际图像本身,而是单击周围的正方形。即,如果您有一个圆圈,除非您有透明网格背景,否则您必须单击实际圆圈,您将无法单击角落。您可以相当容易地封装所有内容在自定义控件中。我认为,绕过所有内置功能并将其强行塞入ListBox中实现动画滚动、换行等功能会更加困难。 - SledgeHammer
第三部分:哈哈..最终,我认为ListBox不太可能显示左侧的部分项,因为它期望项0从像素0开始。您可以尝试将ListBox放入ClipToBounds=True的网格中,并将ListBox的边距设置为-50,看看是否符合您的要求。那会将左侧的项目剪切一半,但您永远不会在左侧看到完整的项目,滚动条也会被剪切。就个人而言,在这种UI中,我会做一个覆盖滚动箭头。标准滚动条看起来很奇怪。 - SledgeHammer
“...你没有做一个圆形的旋转木马...”也许我理解有误或者没有详细说明我的问题...但是我想要一个永无止境的旋转木马(循环项目)。我不确定我是否喜欢这个解决方案。我感觉我失去了对项目的控制,并且在图像大小和位置方面“硬编码”的比我想要的多...不冒犯,但我更同意@goobering的看法,但没有任何好的替代方案。感谢所有的建议! - NeoID
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接