WPF弹幕控件源码讲解与踩坑回顾

WPF弹幕控件源码讲解与踩坑回顾

基于c#写的WPF弹幕控件,近乎完美,该控件简单,适用性强,只有一个添加弹幕的方法。该控件考虑了,效率性能,重复利用等问题。目前只有从左往右一个方向,不过控件设计之初考虑到诸如此类的问题,因此有需要的再进行二次开发即可,代码的可阅读性还是蛮高的。下载地址https://download.csdn.net/download/shizu11zz/10983463,内含源码。
接下来将要根据源码来解说开发过程需要注意到的问题以及已经踩过的坑,望后来人鉴之。

首先,看BulletControl.xaml页面
WPF弹幕控件源码讲解与踩坑回顾

1.用Canvas做弹幕的背景屏幕,Canvas外面用ScrollViewer再封装一层,这样的目的是当BulletControl宽度小于chuan窗口宽度时,文字飘出Canva的部分不予显示,这样就可以实现B站那种弹幕的进出方式。
2.该处定义了一个TextBlock,并在该TextBlock的loaded方法中进行初始化操作,目的是为了获取当前弹幕的文字高度,因为文字的高度是会根据文字大小变化的。而且如果在BulletControl的初始化方法内部去读取该值时则会失败,因为TextBlock还未加载出来,高度是0,顺便一提,获取控件的实时高度是ActualHeight()这个方法

接下来,看代码部分
3.给控件添加动画进行水平移动的方法我就不细说了,具体可将ReadyShoot()和Shoot() 这两个方法连起来看一下,贴个简单一点的代码段吧

 DoubleAnimation animation = new DoubleAnimation(0, new TimeSpan(0, 0, 0, 0, 5000));
            TextBlock Barrage = new TextBlock();
            Barrage.Text = "jahahahha";
            screen.Children.Add(Barrage);
            Barrage.SetValue(Canvas.LeftProperty, screenWidth);
            bullet.Barrage.SetValue(Canvas.TopProperty, bullet.Y_Position);
            animation.To = -Barrage.ActualWidth;
            Barrage.BeginAnimation(Canvas.LeftProperty, animation);

4.出于复用考虑我在TextBlock到达屏幕尾时,将它又移动到了屏幕起始位置,这样子的话下一次我只需给他的Text从新赋值就可以再利用了,特别注意!!!之前在做移动动画的时候将Canvas.LeftProperty绑定给了Animation,这里必须调用TextBlock.BeginAnimation(Canvas.LeftProperty, null);这个方法将属性赋空,否则是无法移动TextBlock到屏幕初始地方的!

5.关于 DoubleAnimation的Completed事件,直接通过该事件的返回值我们是无法找到它所属的TextBlock的,所以只能将Animation和TextBlock1对1的绑定在Bullet类里,进而通过事件将TextBlock传出来

6.突然觉得脑袋里想的很多,但打字表达出来就有点词不达意,所以还是看源码吧各位,下面是源码:

BulletControl.xaml:

<UserControl x:Name="main"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="FontSize" Value="{Binding FontSize,ElementName=main}"/>
            <Setter Property="Foreground" Value="{Binding Foreground,ElementName=main}"/>
            <Setter Property="FontFamily" Value="{Binding FontFamily,ElementName=main}"/>
        </Style>
    </UserControl.Resources>
    <!--用ScrollViewer封装,这样文字在超出canvas后不会显示-->
    <ScrollViewer VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Hidden">
        <Canvas x:Name="screen" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"  >
            <!--用于在初始化时,获取到弹幕的高度-->
            <TextBlock x:Name="InitBlock" Visibility="Visible" Text="" Padding="0"  Canvas.Top="82" Canvas.Left="769" Loaded="InitBlock_Loaded"></TextBlock>
        </Canvas>
    </ScrollViewer>
</UserControl>

BulletControl类:

 /// <summary>
    /// BulletControl.xaml 的交互逻辑
    /// </summary>
    public partial class BulletControl : UserControl
    {
        public List<Bullet> bullets;
        public Dictionary<double, bool> YPositions;


        Queue<string> messlist = new Queue<string>();
        System.Windows.Forms.Timer timer;
        double screenWidth;

        public BulletControl()
        {
            InitializeComponent();
            bullets = new List<Bullet>();
            YPositions = new Dictionary<double, bool>();
            timer = new System.Windows.Forms.Timer();
            timer.Interval = Interval;
        }

        #region 私有事件
        /// <summary>
        ///此处预先加载主要是为了通过TestBullet获取字体高度
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void InitBlock_Loaded(object sender, RoutedEventArgs e)
        {
            TextBlock testBlock = sender as TextBlock;
            screenWidth = screen.ActualWidth;
            CutYPositions(screen.ActualHeight, testBlock.ActualHeight);
            timer.Tick += ReciveMess;
            timer.Start();
        }
        /// <summary>
        /// 将screenY轴根据字体高度进行区域分割
        /// </summary>
        /// <param name="screenHeight"></param>
        /// <param name="fontHeight"></param>
        private void CutYPositions(double screenHeight, double fontHeight)
        {
            for (double i = 0; i < screenHeight;)
            {
                YPositions[i] = false;
                i += fontHeight;
            }
        }

        /// <summary>
        /// 接收消息
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ReciveMess(object sender, EventArgs e)
        {
            //timer.Stop();
            for (int i = 0; i < MaxNum; i++)
            {
                if (messlist.Count != 0)
                {
                    ReadyShoot(messlist.Dequeue());
                }
                else
                {
                    break;
                }
            }
        }

        /// <summary>
        /// 发送弹幕准备工作
        /// </summary>
        /// <param name="mess"></param>
        private void ReadyShoot(string mess)
        {
            //GetBullet
            Bullet bullet = bullets.FirstOrDefault(x => x.IsUesd == false);
            if (bullet == null)
            {
                bullet = new Bullet(Speed);
                bullets.Add(bullet);
                //RecycleBind;
                bullet.OnRecycleYhandler += RecycleYPosition;
                bullet.OnRecycleBarrage += RecycleBarrage;
                bullet.OnBarrageClick += BarrageClick;

            }
            //GetPosition
            bullet.Y_Position = YPositions.FirstOrDefault(x => x.Value == false).Key;
            YPositions[bullet.Y_Position] = true;


            //MachineBullet
            bullet.IsUesd = true;
            bullet.Barrage.Text = mess;
            screen.Children.Add(bullet.Barrage);
            bullet.Barrage.SetValue(Canvas.LeftProperty, screenWidth);
            bullet.Barrage.SetValue(Canvas.TopProperty, bullet.Y_Position);
        }
        #endregion  

        #region  绑定事件
        /// <summary>
        /// 弹幕点击事件
        /// </summary>
        /// <param name="barrage"></param>
        private void BarrageClick(TextBlock barrage)
        {

        }
        /// <summary>
        /// 弹幕已完全在屏幕中加载出来,回收当前所在行
        /// </summary>
        /// <param name="barrage"></param>
        /// <param name="YPosition"></param>
        private bool RecycleYPosition(TextBlock barrage, double YPosition)
        {
            double v = (double)barrage.GetValue(Canvas.LeftProperty);
            if (v < screenWidth - barrage.ActualWidth)
            {
                YPositions[YPosition] = false;
                return true;
            }
            return false;
        }

        /// <summary>
        ///将屏幕上的弹幕进行回收
        /// </summary>
        /// <param name="barrage"></param>
        /// <returns></returns>
        private bool RecycleBarrage(TextBlock barrage)
        {
            screen.Children.Remove(barrage);
            return true;
        }
        #endregion

        #region 属性

        #region [属性]弹幕方向
        /// <summary>
        ///弹幕方向
        /// </summary>
        public static readonly DependencyProperty DirectProperty = DependencyProperty.Register("Direct", typeof(Direct), typeof(BulletControl), new PropertyMetadata(Direct.RightTpLeft));
        public Direct Direct
        {
            get { return (Direct)GetValue(DirectProperty); }
            set { SetValue(DirectProperty, value); }
        }
        #endregion

        #region [属性]发射时间间隔
        /// <summary>
        /// 发射时间间隔
        /// </summary>
        public static readonly DependencyProperty IntervalProperty = DependencyProperty.Register("Interval", typeof(int), typeof(BulletControl), new PropertyMetadata(1000,
            (d, e) =>
            {
                BulletControl control = d as BulletControl;
                control.timer.Stop();
                control.timer.Interval = control.Interval;
                control.timer.Start();
            },
            //Internal传入单位应为毫秒级,如果传入的值小于100,则当做秒去处理成毫秒
            (d, e) =>
            {
                if ((int)e < 100)
                    return (int)e * 1000;
                else
                    return e;
            }));
        /// <summary>
        /// 发射时间间隔,传入单位应为毫秒级,如果传入的值小于100,则当做秒去处理成毫秒
        /// </summary>
        public int Interval
        {
            get { return (int)GetValue(IntervalProperty); }
            set { SetValue(IntervalProperty, value); }
        }
        #endregion

        #region [属性]每次发射最大数目
        /// <summary>
        /// 每次发射最大数目 
        /// </summary>
        public static readonly DependencyProperty MaxNumProperty = DependencyProperty.Register("MaxNum", typeof(int), typeof(BulletControl), new PropertyMetadata(3));
        public int MaxNum
        {
            get { return (int)GetValue(MaxNumProperty); }
            set { SetValue(MaxNumProperty, value); }
        }
        #endregion

        #region [属性]弹幕速度
        /// <summary>
        /// 弹幕速度
        /// </summary>
        public static readonly DependencyProperty SpeedProperty = DependencyProperty.Register("Speed", typeof(int), typeof(BulletControl), new PropertyMetadata(5000, null,
            (d, e) =>
            {
                if ((int)e < 100)
                    return (int)e * 1000;
                else
                    return e;
            }));
        /// <summary>
        /// 弹幕速度,传入单位应为毫秒级,如果传入的值小于100,则当做秒去处理成毫秒
        /// </summary>
        public int Speed
        {
            get { return (int)GetValue(SpeedProperty); }
            set { SetValue(SpeedProperty, value); }
        }
        #endregion

        #endregion

        #region 该组件唯一的公共事件,添加弹幕消息的方法
        /// <summary>
        /// 该组件唯一的公共事件,添加弹幕消息的方法
        /// </summary>
        /// <param name="mess"></param>
        public void AddBullet(string mess)
        {
            messlist.Enqueue(mess);
        }
        #endregion
    }

Bullet类:

  public class Bullet
    {
        #region 委托事件
        public delegate bool RecycleYHandler(TextBlock barrage, double Y_Position);
        public event RecycleYHandler OnRecycleYhandler;
        public delegate bool RecycleBarrageHandler(TextBlock barrage);
        public event RecycleBarrageHandler OnRecycleBarrage;
        public delegate void BarrageClickHandler(TextBlock barrage);
        public event BarrageClickHandler OnBarrageClick;
        #endregion

        public TextBlock Barrage;
        public bool IsUesd { get; set; } = false;
        public double Y_Position { get; set; }
        DoubleAnimation animation;
        public Bullet(int speed)
        {
            Barrage = new TextBlock();
            Barrage.Loaded += Shoot;
            Barrage.PreviewMouseLeftButtonDown += BarrageClicked;
            animation = new DoubleAnimation(0, new TimeSpan(0, 0, 0, 0, speed));
            animation.Completed += RecycleBarrage;
            animation.CurrentTimeInvalidated += RecycleY_Position;
        }

        /// <summary>
        /// 弹幕点击事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void BarrageClicked(object sender, MouseButtonEventArgs e)
        {
            OnBarrageClick(Barrage);
        }

        /// <summary>
        /// 当弹幕加载完成后自行击发
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void Shoot(object sender, EventArgs args)
        {
            animation.To = -Barrage.ActualWidth;
            Barrage.BeginAnimation(Canvas.LeftProperty, animation);
        }

        /// <summary>
        /// 弹幕加载完成后Y坐标回收
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void RecycleY_Position(object sender, EventArgs e)
        {
            if (OnRecycleYhandler(Barrage, Y_Position))
                animation.CurrentTimeInvalidated -= RecycleY_Position;
        }

        /// <summary>
        /// 弹幕回收
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void RecycleBarrage(object sender, EventArgs e)
        {
            Barrage.BeginAnimation(Canvas.LeftProperty, null);
            IsUesd = false;
            if (OnRecycleBarrage(Barrage))
                animation.CurrentTimeInvalidated += RecycleY_Position;
        }
    }

看完给个赞吧,积分都不要了,源码都放出来了