WPF——动态排名图表实现

开发环境

VS2022

.NET 8.0

MVVM Toolkit 8.2.2

需求

开发中需要实现按照成绩动态指名,以展示当前的竞赛成绩的一个实时情况及变化。

即如下效果:

需求分析

按照接收到的信息,就是要将获取到的集合排序,并且要将排序前后的变化,要能在UI上动态的表示出来,以直观的显示排名的变化效果。

UI上的排名上升与下降的实现,本质就是当前显示控件位置的变化,最方便的方式肯定是在Canvas上设置它的Top位置了,然后再有一个从原位置到新位置过度动画,那么就好了。

按上述思路,首先想到的就是自定义控件,完全自定义控件有点麻烦,最后决定使用常用的集合控件 ItemsControl(其子控件也行,但仅用ListView尝试过)来进行实现。

代码实现

VM及Model:

cs 复制代码
    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为自定义控件:

XML 复制代码
 <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为自定义控件,其代码如下:

cs 复制代码
    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));

是等价的,并且不能将括号去掉。

  1. 另外就是一定要绑定Top属性为你指定的离Canvas顶部的距离,本例中以Y值进行绑定

  2. 虽然已经将ItemsControl中的DataTemplate的ContentPresneter改用了ZContentPresneter(即使将ContentPresenter.ContentTemplate 也改为了ZContentPresneter.ContentTemplate,也没有效果),但若要改写ItemsControl的ItemContainerStyle,它的TargetType仍还是只能为ContentPresenter,它的默认容器就是ContentPresenter,暂未发现如何将默认的容器改为ZContentPresneter。也就是说目前还只能如下设置:

XML 复制代码
            <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>
相关推荐
月落.13 小时前
WPF的<ContentControl>控件
wpf
就是有点傻13 小时前
WPF中的依赖属性
开发语言·wpf
wangnaisheng13 小时前
【WPF】把一个Window放在左上角/右上角顶格显示
wpf
WineMonk13 小时前
.NET WPF CommunityToolkit.Mvvm框架
.net·wpf·mvvm
月落.13 小时前
WPF中的INotifyPropertyChanged接口
wpf
界面开发小八哥13 小时前
界面控件DevExpress WPF中文教程:Data Grid——卡片视图设置
.net·wpf·界面控件·devexpress·ui开发
平凡シンプル13 小时前
WPF 打包
wpf
VickyJames13 小时前
基于XAML框架和跨平台项目架构设计的深入技术分析
wpf·开源分享·unoplatform·winui3·项目架构
冷眼Σ(-᷅_-᷄๑)17 小时前
WPF缩放动画和平移动画叠加后会发生什么?
wpf·动画
△曉風殘月〆19 小时前
WPF MVVM入门系列教程(二、依赖属性)
c#·wpf·mvvm