如何在 WPF 中实现图像轮播,其中所选项目始终是第一个?
How can I implement a carousel of images in WPF where the selected item is always the first one?
我正在创建一个 WPF 应用程序作为视频游戏库的前端,我正在尝试模仿 Netflix UI。此应用程序的其中一项功能是循环浏览游戏图像以 select 您想要玩的游戏。
所需的行为与在 ListBox
中用箭头浏览项目时的行为不同:当您在 ListBox
中浏览项目时,您的 selection 会上下移动.我要实现的行为是,当您用箭头浏览项目时,selected 项目始终位于第一个位置,并且项目在 selector 之间循环。这个术语是轮播,其中 selected 项目位于索引 0。
我的实现很糟糕,为了提供一些背景信息,下面是我的界面当前外观的图片:
My current implementation
为了实现这一点,我认为我应该做的是扩展 StackPanel
class 或者实现我自己的 Panel
。但是自定义面板的详细信息有点复杂且难以获得。我想展示我为实现这一点所做的工作,但我对这些实现非常不满意,我想就我应该朝哪个方向正确实现获得一些建议。
以下是我尝试过的一些细节。
上面的屏幕截图是我创建的 GameList
class 的结果,它实现了 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
class,它可以接受任意列表和要循环的项目数。如果有帮助,这里是 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 的新手。
我的感觉是我应该扩展堆栈面板 class 并更改您循环浏览项目的方式或者创建我自己的面板。任何人都可以确认这是否是最好的方法,如果是的话,请给我一些好的资源来帮助我了解如何创建一个自定义面板来改变导航的完成方式?我一直在阅读有关创建自定义面板的文章,以尝试了解该过程。
作为 WPF 的新手,我想确保我不会陷入困境或试图重新发明已经存在的轮子。所以问题是自定义面板是否是解决这个问题的正确方法?
我会尽力为您指明正确的方向。如果您还没有检查过,我会尝试让您的应用程序遵循 MVVM 模式。在您的情况下,ViewModel 将有一个 ObservableCollection
的“游戏”。然后,您可以将 ItemsControl
的来源绑定到该集合。
就让轮播工作而言,我认为您想要走的路是创建自定义 ItemsControl
或 ListBox
。您可以覆盖样式并创建一些自定义行为以使轮播按照您希望的方式工作。
如果您有更具体的问题,我可能会提供更多帮助。
I believe what I should do is extend the StackPanel
class
WPF encourages 现有组合 Controls
过度继承;在您的情况下,继承 StackPanel
对于您的目的来说看起来太复杂了,而您可以使用第二种方法实现相同的目的:
I would remove the first item from my list of games and insert it at the end of the list
这确实看起来更像是惯用的 WPF,尤其是当您尝试遵循 MVVM 设计模式时。
or maybe creating my own panel
这不是一个容易的步骤,尤其是如果您是 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
属性 以显示游戏集合。
它还将绑定到两个 commands that are implemented such as to shift the list to the left or to the right. You can use the RelayCommand
以创建命令。简而言之,Command
只是一个被执行的动作,您可以轻松地从界面中引用它。
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
class 它会在视图发生变化时通知视图(例如,每次我们在其中移动项目时),因此视图会更新以反映这一点。
然后,视图(您的 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
部分解释了每个游戏应该如何显示,而 DataTrigger
部分告诉 WPF 增加所选项目的边框粗细。最后,底部的StackPanel
显示了两个Button
,在我们的GameCollection
.
中调用了SelectPreviousCommand
和SelectLeftCommand
最后,你应该将整个东西的 DataContext
设置为一个新的 GameCollection
:
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
DataContext = new GameCollection();
}
}
从那里您可以根据需要自定义 UI。
动画和平滑滚动
这是一个完全不同的话题,但是您可以在单击其中一个按钮时触发所有项目的翻译动画。
我正在创建一个 WPF 应用程序作为视频游戏库的前端,我正在尝试模仿 Netflix UI。此应用程序的其中一项功能是循环浏览游戏图像以 select 您想要玩的游戏。
所需的行为与在 ListBox
中用箭头浏览项目时的行为不同:当您在 ListBox
中浏览项目时,您的 selection 会上下移动.我要实现的行为是,当您用箭头浏览项目时,selected 项目始终位于第一个位置,并且项目在 selector 之间循环。这个术语是轮播,其中 selected 项目位于索引 0。
我的实现很糟糕,为了提供一些背景信息,下面是我的界面当前外观的图片: My current implementation
为了实现这一点,我认为我应该做的是扩展 StackPanel
class 或者实现我自己的 Panel
。但是自定义面板的详细信息有点复杂且难以获得。我想展示我为实现这一点所做的工作,但我对这些实现非常不满意,我想就我应该朝哪个方向正确实现获得一些建议。
以下是我尝试过的一些细节。
上面的屏幕截图是我创建的 GameList
class 的结果,它实现了 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
class,它可以接受任意列表和要循环的项目数。如果有帮助,这里是 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 的新手。
我的感觉是我应该扩展堆栈面板 class 并更改您循环浏览项目的方式或者创建我自己的面板。任何人都可以确认这是否是最好的方法,如果是的话,请给我一些好的资源来帮助我了解如何创建一个自定义面板来改变导航的完成方式?我一直在阅读有关创建自定义面板的文章,以尝试了解该过程。
作为 WPF 的新手,我想确保我不会陷入困境或试图重新发明已经存在的轮子。所以问题是自定义面板是否是解决这个问题的正确方法?
我会尽力为您指明正确的方向。如果您还没有检查过,我会尝试让您的应用程序遵循 MVVM 模式。在您的情况下,ViewModel 将有一个 ObservableCollection
的“游戏”。然后,您可以将 ItemsControl
的来源绑定到该集合。
就让轮播工作而言,我认为您想要走的路是创建自定义 ItemsControl
或 ListBox
。您可以覆盖样式并创建一些自定义行为以使轮播按照您希望的方式工作。
如果您有更具体的问题,我可能会提供更多帮助。
I believe what I should do is extend the
StackPanel
class
WPF encourages 现有组合 Controls
过度继承;在您的情况下,继承 StackPanel
对于您的目的来说看起来太复杂了,而您可以使用第二种方法实现相同的目的:
I would remove the first item from my list of games and insert it at the end of the list
这确实看起来更像是惯用的 WPF,尤其是当您尝试遵循 MVVM 设计模式时。
or maybe creating my own panel
这不是一个容易的步骤,尤其是如果您是 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
属性 以显示游戏集合。
它还将绑定到两个 commands that are implemented such as to shift the list to the left or to the right. You can use the RelayCommand
以创建命令。简而言之,Command
只是一个被执行的动作,您可以轻松地从界面中引用它。
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
class 它会在视图发生变化时通知视图(例如,每次我们在其中移动项目时),因此视图会更新以反映这一点。
然后,视图(您的 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
部分解释了每个游戏应该如何显示,而 DataTrigger
部分告诉 WPF 增加所选项目的边框粗细。最后,底部的StackPanel
显示了两个Button
,在我们的GameCollection
.
SelectPreviousCommand
和SelectLeftCommand
最后,你应该将整个东西的 DataContext
设置为一个新的 GameCollection
:
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
DataContext = new GameCollection();
}
}
从那里您可以根据需要自定义 UI。
动画和平滑滚动
这是一个完全不同的话题,但是您可以在单击其中一个按钮时触发所有项目的翻译动画。