开发环境
VS2022
.NET 8.0
MVVM Toolkit 8.2.2
需求
开发中需要实现按照成绩动态指名,以展示当前的竞赛成绩的一个实时情况及变化。
即如下效果:
需求分析
按照接收到的信息,就是要将获取到的集合排序,并且要将排序前后的变化,要能在UI上动态的表示出来,以直观的显示排名的变化效果。
UI上的排名上升与下降的实现,本质就是当前显示控件位置的变化,最方便的方式肯定是在Canvas上设置它的Top位置了,然后再有一个从原位置到新位置的过度动画,那么就好了。
按上述思路,首先想到的就是自定义控件,完全自定义控件有点麻烦,最后决定使用常用的集合控件 ItemsControl(其子控件也行,但仅用ListView尝试过)来进行实现。
代码实现
VM及Model:
internal partial class MainWindowViewModel : ObservableRecipient
{
[ObservableProperty]
ObservableCollection<Person> persons =
[
new Person() { Id = 1, Name = "张三", Age = 18, Gender = "男", Address = "北京", Grade = "一年级" ,Y=50,OldY=50,Score=40},
new Person() { Id = 2, Name = "李四", Age = 19, Gender = "女", Address = "上海", Grade ="二年级",Y=100,OldY=100,Score=60},
new Person() { Id = 3, Name = "王五", Age = 20, Gender = "男", Address = "广州", Grade = "三年级" ,Y=150,OldY=150,Score=90},
];
Timer timer;
public MainWindowViewModel()
{
timer = new Timer(OnTimer, null, 0,1000);
}
private void OnTimer(object? state)
{
Dispatcher.CurrentDispatcher.Invoke(() =>
{
Random random = new();
var index = random.Next(0, 3);
Persons[index].Score = random.Next(0, 100);
var sorts = Persons.OrderBy(p => p.Score);
int i = 0;
foreach (var item in sorts)
{
item.Id = ++i;
item.Y = i * 50;
}
});
}
}
public partial class Person : ObservableObject
{
[ObservableProperty]
private int id;
[ObservableProperty]
private string name;
[ObservableProperty]
private int age;
[ObservableProperty]
private string gender;
[ObservableProperty]
private string address;
[ObservableProperty]
private string grade;
private int y;
public int Y
{
get => y;
set
{
if (y != value)
{
OldY = y; //记录旧值
SetProperty(ref y, value);
}
}
}
[ObservableProperty]
private int oldY;
[ObservableProperty]
private int score;
}
Xaml中绑定如下,注意下述代码中的ZContentPresenter为自定义控件:
<ItemsControl
x:Name="myItemsControl"
Margin="10"
ItemsSource="{Binding Persons}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<control:ZContentPresenter
x:Name="presenter"
Content="{Binding}"
Top="{Binding Y}">
<ContentPresenter.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Id}" />
<TextBox Text="{Binding Name}" />
<TextBox Text="{Binding Age}" />
<TextBox Text="{Binding Y}" />
</StackPanel>
</DataTemplate>
</ContentPresenter.ContentTemplate>
</control:ZContentPresenter>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
对于 UI中的ZContentPresenter为自定义控件,其代码如下:
public class ZContentPresenter : ContentPresenter
{
public ZContentPresenter()
{
}
public int Top
{
get { return (int)GetValue(TopProperty); }
set { SetValue(TopProperty, value); }
}
public static readonly DependencyProperty TopProperty =
DependencyProperty.Register("Top", typeof(int), typeof(ZContentPresenter), new PropertyMetadata(0, TopChanged));
private static void TopChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ZContentPresenter control)
{
var oldValue = (int)e.OldValue;
var newValue = (int)e.NewValue;
var parent=(ContentPresenter)control.VisualParent;
StartAnimation((double)(oldValue), (double)(newValue), parent);
}
}
private static void StartAnimation(double from, double to, FrameworkElement element)
{
Storyboard storyboard = new();
DoubleAnimation animation = new()
{
From = from,
To = to,
Duration = TimeSpan.FromSeconds(0.5),
AutoReverse = false,
RepeatBehavior = new RepeatBehavior(1)
};
Storyboard.SetTarget(animation, element);
Storyboard.SetTargetProperty(animation, new PropertyPath(Canvas.TopProperty));
//Storyboard.SetTargetProperty(animation, new PropertyPath("(Canvas.Top)"));
storyboard.Children.Add(animation);
storyboard.Begin();
/* storyboard.Completed += (sender, e) =>
{
storyboard.Stop();
};*/
}
}
那为什么不直接在ItemContainerStyle中直接使用样式与Trigger中设置动画来实现呢?
这涉及到Trigger不能侦听Y值的实时变化,另外还有一个问题就是在Animation中不能绑定From与To值,若From或To采用绑定,会导致出现报错:无法冻结此 Storyboard 时间线树供跨线程使用。
注意事项
1. 自定义控件中的
Storyboard.SetTargetProperty(animation, new PropertyPath("(Canvas.Top)"));
这种写法与
Storyboard.SetTargetProperty(animation, new PropertyPath(Canvas.TopProperty));
是等价的,并且不能将括号去掉。
2. 另外就是一定要绑定Top属性为你指定的离Canvas顶部的距离,本例中以Y值进行绑定
3. 虽然已经将ItemsControl中的DataTemplate的ContentPresneter改用了ZContentPresneter(即使将ContentPresenter.ContentTemplate也改为了ZContentPresneter.ContentTemplate,也没有效果),但若要改写ItemsControl的ItemContainerStyle,它的TargetType仍还是只能为ContentPresenter,它的默认容器就是ContentPresenter,暂未发现如何将默认的容器改为ZContentPresneter。也就是说目前还只能如下设置:
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="0" />
<Setter Property="Canvas.Top" Value="{Binding OldY}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Y, UpdateSourceTrigger=PropertyChanged}" Value="50">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
AutoReverse="false"
RepeatBehavior="1"
Storyboard.TargetProperty="(Canvas.Top)"
From="5"
To="20"
Duration="0:0:1" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</ItemsControl.ItemContainerStyle>