c# - 如何在 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 的新手,我想确保我不会陷入困境或试图重新发明已经存在的轮子。所以问题是自定义面板是否是解决这个问题的正确方法?
解决方案
我相信我应该做的是扩展
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
部分解释了每个游戏应该如何显示,并且DataTrigger
inside 告诉 WPF 增加所选项目的边框粗细。最后,StackPanel
底部的 显示两个Button
调用SelectPreviousCommand
和SelectLeftCommand
在我们的GameCollection
.
最后,您应该将DataContext
整个事情设置为一个新的GameCollection
:
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
DataContext = new GameCollection();
}
}
从那里您可以根据需要自定义 UI。
动画和平滑滚动
这是一个完全不同的话题,但您可以在单击其中一个按钮时触发所有项目的翻译动画。
推荐阅读
- javascript - 在 javascript 中以 mm/dd/yyyy 格式将十进制年份转换为日期
- oracle - Windows 10 64 位上的 Oracle 12c 客户端
- microsoft-teams - 选项卡身份验证流程后 MSTeams 存储身份验证令牌
- python - 有条件的查找和替换
- c - llvm 未定义的实验内在函数
- javascript - 使用 Axios 过滤 response.data 的 API 调用
- javascript - Vue 反应性,防止在使用输入 v-model 更新道具时重新渲染组件
- javascript - 我正在尝试使用 jimp 在我的 dicrod bo 上放置一个经过处理的图像来欢迎人们
- git - 来自 git diff --word-diff 的意外输出。它是一个错误吗?
- python - 元组到 Pandas 数据框