首页 > 解决方案 > 如何在 WPF 中实现图像轮播,其中所选项目始终是第一个?

问题描述

我正在创建一个 WPF 应用程序来充当视频游戏库的前端,并且我正在尝试模仿 Netflix UI。此应用程序的功能之一是循环浏览游戏图像以选择您要玩的游戏。

所需的行为与箭头浏览 a 中的项目时的行为不同ListBox:当您箭头浏览 a 中的项目时ListBox,您的选择会上下移动。我希望实现的行为是,当您浏览项目时,所选项目始终位于第一个位置,并且项目在选择器中循环。这方面的术语将是一个轮播,其中所选项目的索引为 0。

我的实现很差,为了提供一些上下文,这是我的界面当前外观的图片: 我当前的实现

为了实现这一点,我相信我应该做的是扩展StackPanel类或者实现我自己的Panel. 但是自定义面板的细节有点复杂,很难获得。我想展示我为实现这一点所做的工作,但我对这些实现非常不满意,我想就我应该朝着正确实现的方向获得一些建议。

以下是我尝试过的一些细节。

上面的截图是我创建的一个GameList类的结果,它实现INotifyPropertyChanged并包含了 15 种不同游戏的属性。

    private GameMatch game0;
    public GameMatch Game0
    {
        get { return game0; }
        set
        {
            if (game0 != value)
            {
                game0 = value;
                PropertyChanged(this, new PropertyChangedEventArgs("Game0"));
            }
        }
    }

    private GameMatch game1;
    public GameMatch Game1
    {
        get { return game1; }
        set
        {
            if (game1 != value)
            {
                game1 = value;
                PropertyChanged(this, new PropertyChangedEventArgs("Game1"));
            }
        }
    }

    // identical code for games 2-10

    private GameMatch game11;
    public GameMatch Game11
    {
        get { return game11; }
        set
        {
            if (game11 != value)
            {
                game11 = value;
                PropertyChanged(this, new PropertyChangedEventArgs("Game11"));
            }
        }
    }

    private GameMatch game12;
    public GameMatch Game12
    {
        get { return game12; }
        set
        {
            if (game12 != value)
            {
                game12 = value;
                PropertyChanged(this, new PropertyChangedEventArgs("Game12"));
            }
        }
    }

我已经在我的 XAML 中布置了图像并添加了足够多的内容,以便它们从屏幕边缘跑出:

    <StackPanel Name="GameImages" Orientation="Horizontal">
    <Border BorderThickness="2" BorderBrush="AntiqueWhite">
        <Image Name="Image_Game1" Source="{Binding CurrentGameList.Game1.FrontImage}"/>
    </Border>

    <Image Source="{Binding CurrentGameList.Game2.FrontImage}"/>

    <!-- identical images for games 3-10 -->

    <Image Source="{Binding CurrentGameList.Game11.FrontImage}" />

    <Image Source="{Binding CurrentGameList.Game12.FrontImage}" />
</StackPanel>

我实现了一个ListCycle类,它可以采用任意列表和要循环的项目计数。如果有帮助,这里是ListCycle. 它通过跟踪列表中应显示在屏幕上给定位置的项目的索引来处理列表的循环。

public class ListCycle<T>
{
    // list of games or whatever you want
    public List<T> GenericList { get; set; }

    // indexes currently available to display
    // will cycle around the size of the generic list
    public int[] indices;

    public ListCycle(List<T> genericList, int activeCycleCount)
    {
        GenericList = genericList;
        indices = new int[activeCycleCount];
        InitializeIndices();
    }

    private void InitializeIndices()
    {
        if (GenericList != null)
        {
            int lastIndex = -1;
            for (int i = 0; i < indices.Length; i++)
            {
                indices[i] = GetNextIndex(lastIndex);
                lastIndex = indices[i];
            }
        }
    }

    private int GetNextIndex(int currentIndex)
    {
        currentIndex += 1;
        if (currentIndex == GenericList.Count)
        {
            currentIndex = 0;
        }
        return currentIndex;
    }

    private int GetPreviousIndex(int currentIndex)
    {
        currentIndex -= 1;
        if (currentIndex == -1)
        {
            currentIndex = GenericList.Count - 1;
        }
        return currentIndex;
    }

    public int GetIndexValue(int index)
    {
        return indices[index];
    }

    public T GetItem(int index)
    {
        return GenericList[indices[index]];
    }

    public void CycleForward()
    {
        for (int i = 0; i < indices.Length; i++)
        {
            if (i + 1 < indices.Length - 1)
            {
                indices[i] = indices[i + 1];
            }
            else
            {
                indices[i] = GetNextIndex(indices[i]);
            }
        }
    }

    public void CycleBackward()
    {
        for (int i = indices.Length - 1; i >= 0; i--)
        {
            if(i - 1 >= 0)
            {
                indices[i] = indices[i - 1];
            }
            else
            {
                indices[i] = GetPreviousIndex(indices[i]);
            }
        }
    }
}

因此,当您按右时,我会向前循环并重置游戏图像。当您按左时,我会向后循环并重置游戏图像。RefreshGames 方法负责更新我的游戏列表中的所有这些游戏属性。

private void RefreshGames()
{
        Game0 = gameCycle.GetItem(0);
        Game1 = gameCycle.GetItem(1);
        Game2 = gameCycle.GetItem(2);
        Game3 = gameCycle.GetItem(3);
        Game4 = gameCycle.GetItem(4);
        Game5 = gameCycle.GetItem(5);
        Game6 = gameCycle.GetItem(6);
        Game7 = gameCycle.GetItem(7);
        Game8 = gameCycle.GetItem(8);
        Game9 = gameCycle.GetItem(9);
        Game10 = gameCycle.GetItem(10);
        Game11 = gameCycle.GetItem(11);
        Game12 = gameCycle.GetItem(12);
 }

这种方法有效,但效果不佳。它不是动态的,它不能很好地扩展,也不能很好地执行。一次一张地浏览图像表现得很好,但试图快速浏览它们,有点慢并且感觉很笨重。这不是一个很好的用户体验。

我尝试了第二种方法,使用绑定到我的游戏列表的列表框。为了将游戏循环到左侧,我将从游戏列表中删除第一项并将其插入到列表的末尾。要向右移动,我将删除列表末尾的项目并将其插入索引 0。这也有效,但它的性能也不是很好。

所以我正在寻找一种更好的方法来实现这一点,以提供更好的性能(更平滑的滚动)和更动态的建议(即这种方法在超宽显示器上可能效果不佳 - 12 场比赛可能不够,具体取决于宽度的图像)。我不是在寻找任何人来为我解决它,而是指出我正确的方向,因为我对 WPF 非常陌生。

我的感觉是我应该扩展堆栈面板类并改变循环浏览项目的方式,或者创建我自己的面板。任何人都可以确认这是否是最好的方法,如果是的话,请给我一些好的资源来帮助我了解如何创建一个自定义面板来改变导航的完成方式?我一直在阅读有关创建自定义面板的文章,以尝试了解该过程。

作为 WPF 的新手,我想确保我不会陷入困境或试图重新发明已经存在的轮子。所以问题是自定义面板是否是解决这个问题的正确方法?

标签: c#wpflistbox

解决方案


我相信我应该做的是扩展StackPanel课程

WPF鼓励组合现有的Controls过度继承;StackPanel在您的情况下,如果您可以通过第二种方法实现相同的效果,那么继承外观对于您的目的来说太复杂了:

我会从我的游戏列表中删除第一项并将其插入到列表的末尾

这确实看起来更像是惯用的 WPF,尤其是当您尝试遵循 MVVM 设计模式时。

或者也许创建我自己的面板

这不是一个简单的步骤,特别是如果您是 WPF 新手,但这对您来说非常有趣。这可能是一种方法,特别是如果您在内部依赖StackPanel(组合)而不是从它继承

示例实现ItemsControl

我将使用一个ItemsControl可以为您显示数据集合(在您的情况下,您有一些GameMatch)。

首先定义接口后面的数据,即GameMatch. 让我们给每个人GameMatch一个名字和一个变量IsSelected,它告诉我们是否选择了游戏(即在第一个位置)。我没有展示INotifyPropertyChanged实现,但它应该适用于两个属性。

public class GameMatch : INotifyPropertyChanged {

    public string Name { get => ...; set => ...; }

    public bool IsSelected { get => ...; set => ...;  }
}

您的轮播界面对 的集合感兴趣,GameMatch因此让我们创建一个对象来对其进行建模。我们的图形界面将绑定到Items属性以显示游戏集合。它还将绑定到已实现的两个命令,例如将列表向左或向右移动。您可以使用RelayCommand来创建命令。简而言之,aCommand只是一个可以执行的操作,您可以轻松地从界面中引用它。

public class GameCollection {

    // Moves selection to next game
    public ICommand SelectNextCommand { get; }

    // Moves selection to previous game
    public ICommand SelectPreviousCommand { get; }
    
    public ObservableCollection<GameMatch> Items { get; } = new ObservableCollection<GameMatch> {
        new GameMatch() { Name = "Game1" },
        new GameMatch() { Name = "Game2" },
        new GameMatch() { Name = "Game3" },
        new GameMatch() { Name = "Game4" },
        new GameMatch() { Name = "Game5" },
    };

    public GameCollection() {
        SelectNextCommand = new RelayCommand(() => ShiftLeft());
        SelectPreviousCommand = new RelayCommand(() => ShiftRight());
        SelectFirstItem();
    }

    private void SelectFirstItem() {
        foreach (var item in Items) {
            item.IsSelected = item == Items[0];
        }
    }

    public void ShiftLeft() {
        // Moves the first game to the end
        var first = Items[0];
        Items.RemoveAt(0);
        Items.Add(first);
        SelectFirstItem();
    }

    private void ShiftRight() {
        // Moves the last game to the beginning
        var last = Items[Items.Count - 1];
        Items.RemoveAt(Items.Count - 1);
        Items.Insert(0, last);
        SelectFirstItem();
    }
}

这里的关键是ObservableCollection类,它会在视图发生变化时告诉视图(例如,每次我们在其中移动项目时),因此视图将更新以反映这一点。

然后,视图(您的 XAML)应指定如何显示游戏集合。我们将使用ItemsControl水平布局项目:

<StackPanel>
    <ItemsControl ItemsSource="{Binding Items}">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel Orientation="Horizontal"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Border Margin="10" Background="Beige" BorderBrush="Black" Width="150" Height="50">
                    <Border.Style>
                        <Style TargetType="Border">
                            <Setter Property="BorderThickness" Value="1" />
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding IsSelected}" Value="true">
                                    <Setter Property="BorderThickness" Value="5" />
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </Border.Style>
                    <TextBlock Text="{Binding Name}"/>
                </Border>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
        <Button Content="Previous" Command="{Binding SelectPreviousCommand}"/>
        <Button Content="Next" Command="{Binding SelectNextCommand}"/>
    </StackPanel>
</StackPanel>

请注意,ItemsControl ItemsSource="{Binding Items}"它告诉ItemsControl显示属性中的所有对象Items。该ItemsControl.ItemsPanel部分告诉将它们布置在水平方向StackPanel上。该ItemsControl.ItemTemplate部分解释了每个游戏应该如何显示,并且DataTriggerinside 告诉 WPF 增加所选项目的边框粗细。最后,StackPanel底部的 显示两个Button调用SelectPreviousCommandSelectLeftCommand在我们的GameCollection.

最后,您应该将DataContext整个事情设置为一个新的GameCollection

public partial class MainWindow : Window {
    public MainWindow() {
        InitializeComponent();
        DataContext = new GameCollection();
    }
}

从那里您可以根据需要自定义 UI。

最后结果

动画和平滑滚动

这是一个完全不同的话题,但您可以在单击其中一个按钮时触发所有项目的翻译动画。


推荐阅读