示例:WPF中Slider控件封装的缓冲播放进度条控件

时间:2023-03-08 16:32:31

原文:示例:WPF中Slider控件封装的缓冲播放进度条控件

一、目的:模仿播放器播放进度条,支持缓冲任务功能

二、进度:

实现类似播放器中带缓存的播放样式(播放区域、缓冲区域、全部区域等样式)

实现设置播放中断时满足缓存够一定数量才继续播放的功能

实现设置缓存数量最大限制,即缓存够一定数量即停止缓存,减少开销

实现缓存中缓存进度的获取

二、示例(GIF)

示例:WPF中Slider控件封装的缓冲播放进度条控件

三、实现:

1、UI部分

添加用户控件:BufferPlayControl.Xaml

设置Slider样式


  1. <!--Slider模板-->
  2. <Style x:Key="Slider_RepeatButton" TargetType="RepeatButton">
  3. <Setter Property="Focusable" Value="false" />
  4. <Setter Property="Template">
  5. <Setter.Value>
  6. <ControlTemplate TargetType="RepeatButton">
  7. <Border Background="{TemplateBinding Foreground}" CornerRadius="5" />
  8. </ControlTemplate>
  9. </Setter.Value>
  10. </Setter>
  11. </Style>
  12. <Style x:Key="Slider_RepeatButton1" TargetType="RepeatButton">
  13. <Setter Property="Focusable" Value="false" />
  14. <Setter Property="Template">
  15. <Setter.Value>
  16. <ControlTemplate TargetType="RepeatButton">
  17. <Border Background="{TemplateBinding Background}" CornerRadius="5" />
  18. </ControlTemplate>
  19. </Setter.Value>
  20. </Setter>
  21. </Style>
  22. <Style x:Key="Slider_Thumb" TargetType="Thumb">
  23. <Setter Property="Focusable" Value="false" />
  24. <Setter Property="Template">
  25. <Setter.Value>
  26. <ControlTemplate TargetType="Thumb">
  27. <Grid>
  28. <Grid.ColumnDefinitions>
  29. <ColumnDefinition/>
  30. <ColumnDefinition/>
  31. </Grid.ColumnDefinitions>
  32. <Border Background="{DynamicResource S_AccentBrush}"/>
  33. <Border Grid.ColumnSpan="2"
  34. CornerRadius="4"
  35. Background="{TemplateBinding Foreground}"
  36. Width="8" Height="8" Margin="-8"/>
  37. </Grid>
  38. </ControlTemplate>
  39. </Setter.Value>
  40. </Setter>
  41. </Style>
  42. <Style x:Key="Slider_CustomStyle" TargetType="Slider">
  43. <Setter Property="Focusable" Value="false" />
  44. <Setter Property="Template">
  45. <Setter.Value>
  46. <ControlTemplate TargetType="Slider">
  47. <Grid>
  48. <!--<Grid.Effect>
  49. <DropShadowEffect BlurRadius="20" ShadowDepth="1" />
  50. </Grid.Effect>-->
  51. <Border Grid.Column="1" BorderBrush="Transparent" BorderThickness="1" CornerRadius="8,0,0,8">
  52. <Track Grid.Column="1" Name="PART_Track">
  53. <Track.DecreaseRepeatButton>
  54. <RepeatButton Style="{StaticResource Slider_RepeatButton}"
  55. Foreground="{TemplateBinding Foreground}"
  56. Background="{TemplateBinding Background}"
  57. Command="Slider.DecreaseLarge"/>
  58. </Track.DecreaseRepeatButton>
  59. <Track.IncreaseRepeatButton>
  60. <RepeatButton Style="{StaticResource Slider_RepeatButton1}"
  61. Foreground="{TemplateBinding Foreground}"
  62. Background="{TemplateBinding Background}"
  63. Command="Slider.IncreaseLarge"/>
  64. </Track.IncreaseRepeatButton>
  65. <Track.Thumb>
  66. <Thumb Style="{StaticResource Slider_Thumb}" VerticalAlignment="Center"
  67. Foreground="{TemplateBinding Foreground}"
  68. Background="{TemplateBinding Background}"/>
  69. </Track.Thumb>
  70. </Track>
  71. </Border>
  72. </Grid>
  73. </ControlTemplate>
  74. </Setter.Value>
  75. </Setter>
  76. </Style>
  77. <Style x:Key="Slider_CustomStyle1" TargetType="Slider">
  78. <Setter Property="Focusable" Value="false" />
  79. <Setter Property="Template">
  80. <Setter.Value>
  81. <ControlTemplate TargetType="Slider">
  82. <Grid>
  83. <!--<Grid.Effect>
  84. <DropShadowEffect BlurRadius="20" ShadowDepth="1" />
  85. </Grid.Effect>-->
  86. <Border Grid.Column="1" BorderBrush="Transparent" BorderThickness="1" CornerRadius="8,0,0,8">
  87. <Track Grid.Column="1" Name="PART_Track">
  88. <Track.DecreaseRepeatButton>
  89. <RepeatButton Style="{StaticResource Slider_RepeatButton}"
  90. Foreground="{TemplateBinding Foreground}"
  91. Background="{TemplateBinding Background}"
  92. Command="Slider.DecreaseLarge"/>
  93. </Track.DecreaseRepeatButton>
  94. <Track.IncreaseRepeatButton>
  95. <RepeatButton Style="{StaticResource Slider_RepeatButton1}"
  96. Foreground="{TemplateBinding Foreground}"
  97. Background="{TemplateBinding Background}"
  98. Command="Slider.IncreaseLarge"/>
  99. </Track.IncreaseRepeatButton>
  100. <!--<Track.Thumb>
  101. <Thumb Style="{StaticResource Slider_Thumb}"
  102. Foreground="{TemplateBinding Foreground}"
  103. Background="{TemplateBinding Background}"/>
  104. </Track.Thumb>-->
  105. </Track>
  106. </Border>
  107. </Grid>
  108. </ControlTemplate>
  109. </Setter.Value>
  110. </Setter>
  111. </Style>

用两个Slider叠加,一个用来播放进度,一个用来缓冲进度


  1. <Grid>
  2. <Slider Height="5" Value="{Binding ElementName=control,Path=BufferValue,Mode=TwoWay}"
  3. Maximum="{Binding ElementName=control,Path=MaxValue}"
  4. Minimum="{Binding ElementName=control,Path=MinValue}"
  5. SmallChange="{Binding ElementName=control,Path=SmallChange}"
  6. Background="{DynamicResource S_GrayNotice}"
  7. Foreground="Gray"
  8. Style="{StaticResource Slider_CustomStyle1}" VerticalAlignment="Center"
  9. IsHitTestVisible="False"/>
  10. <Slider Height="5" Value="{Binding ElementName=control,Path=Value,Mode=TwoWay}"
  11. Maximum="{Binding ElementName=control,Path=MaxValue}"
  12. Minimum="{Binding ElementName=control,Path=MinValue}"
  13. SmallChange="{Binding ElementName=control,Path=SmallChange}"
  14. Background="Transparent"
  15. Foreground="{DynamicResource S_AccentBrush}"
  16. Style="{StaticResource Slider_CustomStyle}" VerticalAlignment="Center"/>
  17. </Grid>

2、用户控件设置依赖属性


  1. /// <summary> 绑定最小值 </summary>
  2. public double MinValue
  3. {
  4. get { return (double)GetValue(MinValueProperty); }
  5. set { SetValue(MinValueProperty, value); }
  6. }
  7. // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
  8. public static readonly DependencyProperty MinValueProperty =
  9. DependencyProperty.Register("MinValue", typeof(double), typeof(BufferPlayControl), new PropertyMetadata(0.0, (d, e) =>
  10. {
  11. BufferPlayControl control = d as BufferPlayControl;
  12. if (control == null) return;
  13. //double config = e.NewValue as double;
  14. }));
  15. /// <summary> 绑定最大值 </summary>
  16. public double MaxValue
  17. {
  18. get { return (double)GetValue(MaxValueProperty); }
  19. set { SetValue(MaxValueProperty, value); }
  20. }
  21. // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
  22. public static readonly DependencyProperty MaxValueProperty =
  23. DependencyProperty.Register("MaxValue", typeof(double), typeof(BufferPlayControl), new PropertyMetadata(100.0, (d, e) =>
  24. {
  25. BufferPlayControl control = d as BufferPlayControl;
  26. if (control == null) return;
  27. //double config = e.NewValue as double;
  28. }));
  29. /// <summary> 绑定最小偏移量 </summary>
  30. public double SmallChange
  31. {
  32. get { return (double)GetValue(SmallChangeProperty); }
  33. set { SetValue(SmallChangeProperty, value); }
  34. }
  35. // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
  36. public static readonly DependencyProperty SmallChangeProperty =
  37. DependencyProperty.Register("SmallChange", typeof(double), typeof(BufferPlayControl), new PropertyMetadata(0.1, (d, e) =>
  38. {
  39. BufferPlayControl control = d as BufferPlayControl;
  40. if (control == null) return;
  41. //double config = e.NewValue as double;
  42. }));
  43. /// <summary> 设置当前播放值 </summary>
  44. public double Value
  45. {
  46. get { return (double)GetValue(ValueProperty); }
  47. set { SetValue(ValueProperty, value); }
  48. }
  49. // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
  50. public static readonly DependencyProperty ValueProperty =
  51. DependencyProperty.Register("Value", typeof(double), typeof(BufferPlayControl), new PropertyMetadata(30.0, (d, e) =>
  52. {
  53. BufferPlayControl control = d as BufferPlayControl;
  54. if (control == null) return;
  55. //double config = e.NewValue as double;
  56. }));
  57. /// <summary> 设置当前缓冲值 </summary>
  58. public double BufferValue
  59. {
  60. get { return (double)GetValue(BufferValueProperty); }
  61. set { SetValue(BufferValueProperty, value); }
  62. }
  63. // Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
  64. public static readonly DependencyProperty BufferValueProperty =
  65. DependencyProperty.Register("BufferValue", typeof(double), typeof(BufferPlayControl), new PropertyMetadata(50.0, (d, e) =>
  66. {
  67. BufferPlayControl control = d as BufferPlayControl;
  68. if (control == null) return;
  69. //double config = e.NewValue as double;
  70. }));

3、测试代码

测试代码UI部分:开始、暂停、继续和显示进度、缓冲进度


  1. <GroupBox Header="缓冲播放进度条">
  2. <StackPanel>
  3. <wpfcontrollib:BufferPlayControl x:Name="control_bufferPlay"/>
  4. <TextBlock x:Name="txt_persent"/>
  5. <TextBlock Text="{Binding ElementName=control_bufferPlay,Path=Value}"/>
  6. <Button Content="开始" Click="Button_Click"/>
  7. <Button x:Name="btn_play" Content="暂停" Click="Button_Click_1"/>
  8. </StackPanel>
  9. </GroupBox>

测试代码后台逻辑:

点击播放代码部分


  1. List<IBufferPlayEntity> bufferPlays = new List<IBufferPlayEntity>();
  2. // Message:构造1000个测试数据
  3. for (int i = 0; i < 1000; i++)
  4. {
  5. BufferPlayEntity entity = new BufferPlayEntity();
  6. bufferPlays.Add(entity);
  7. }
  8. // Message:初始化控件
  9. this.control_bufferPlay.MinValue = 0;
  10. this.control_bufferPlay.Value = 0;
  11. this.control_bufferPlay.BufferValue = 0;
  12. this.control_bufferPlay.MaxValue = bufferPlays.Count;
  13. // Message:开始缓冲引擎
  14. BufferPlayEngine bufferPlayEngine = new BufferPlayEngine(bufferPlays);
  15. bufferPlayEngine.RefreshCapacity(5);
  16. bufferPlayEngine.Start();
  17. Action<bool, int, int> action = (l, k, n) =>
  18. {
  19. Application.Current.Dispatcher.Invoke(() =>
  20. {
  21. if (l)
  22. {
  23. this.txt_persent.Text = "缓冲完成..";
  24. }
  25. else
  26. {
  27. string p = (Convert.ToDouble(k) * 100 / Convert.ToDouble(n)).ToString();
  28. this.txt_persent.Text = "缓冲中.." + p + "%";
  29. }
  30. });
  31. };
  32. // Message:刷新播放进度
  33. Task.Run(() =>
  34. {
  35. for (int i = 0; i < bufferPlays.Count; i++)
  36. {
  37. // Message:设置当前播放进度值
  38. Application.Current.Dispatcher.Invoke(() =>
  39. {
  40. this.control_bufferPlay.Value = i;
  41. });
  42. // Message:检查当前是否已经暂停
  43. while (true)
  44. {
  45. bool result = false;
  46. Application.Current.Dispatcher.Invoke(() =>
  47. {
  48. result = this.btn_play.Content.ToString() == "暂停";
  49. });
  50. if (result) break;
  51. Thread.Sleep(1000);
  52. }
  53. Thread.Sleep(100);
  54. // Message:阻塞等待当前进度是否可以播放
  55. bufferPlayEngine.GetWaitCurrent(l => l == bufferPlays[i], action);
  56. }
  57. });
  58. // Message:刷新下载进度
  59. Task.Run(() =>
  60. {
  61. while (true)
  62. {
  63. Thread.Sleep(100);
  64. Application.Current.Dispatcher.Invoke(() =>
  65. {
  66. this.control_bufferPlay.BufferValue = bufferPlayEngine.GetBufferSize((int)this.control_bufferPlay.Value);
  67. });
  68. }
  69. });

点击暂停或继续代码部分:


  1. Button button = sender as Button;
  2. button.Content = button.Content.ToString() == "暂停" ? "继续" : "暂停";

测试任务实体:继承任务抽象类基类或接口,实现一个随机等待1-2秒完成的方法


  1. public class BufferPlayEntity : BufferPlayEntityBase
  2. {
  3. public int IsLoaded { get; set; }
  4. Random random = new Random();
  5. public override void DoStart()
  6. {
  7. Thread.Sleep(random.Next(1, 2) * 1000);
  8. }
  9. }

4、核心缓冲引擎:

BufferPlayEngine:

设置可播放容量:在播放过程中,播放阻塞后需要缓冲的容量

设置播放缓冲总量:为了节省性能,当达到当前播放容量时,停止继续缓冲

设置并行任务数量:多线程执行任务的并行数量

原理:

Start()方法:后台创建多个缓冲线程去根据当前播放的任务去执行缓冲任务,获取第一个没有下载的任务,当任务超过最大缓冲容量时不执行下载

GetWaitCurrent()方法:如果当前任务已经完成则直接返回,如果当前任务未完成则需要等待可执行播放数量Capacity设置的数量都下载完成时才取消阻塞返回要执行的任务;

GetBufferSize()方法:获取当前已经缓冲好的数量,用于更新缓冲区域进度条


  1. /// <summary> 缓冲播放引擎 </summary>
  2. public class BufferPlayEngine
  3. {
  4. /// <summary> 可播放容器量 </summary>
  5. public int Capacity { get; set; } = 10;
  6. /// <summary> 总缓冲容器量 </summary>
  7. public int CapacityTotal { get; set; } = 10;
  8. /// <summary> 同时下载的任务数量 </summary>
  9. public int TaskCount { get; set; } = 5;
  10. // Message:所有的文件列表
  11. List<IBufferPlayEntity> _entitys = new List<IBufferPlayEntity>();
  12. // Message:当前播放的节点
  13. IBufferPlayEntity _current;
  14. public BufferPlayEngine(List<IBufferPlayEntity> entitys)
  15. {
  16. _entitys = entitys;
  17. _current = entitys.First();
  18. }
  19. /// <summary> 刷新缓冲数量 </summary>
  20. public void RefreshCapacity(int count)
  21. {
  22. // Do:可播放队列设置15s
  23. this.Capacity = count * 5;
  24. ////Do:后台缓存最多队列设置成5分钟
  25. this.CapacityTotal = count * 2 * 10;
  26. }
  27. CancellationTokenSource cts = new CancellationTokenSource();
  28. Semaphore _semaphore1 = new Semaphore(1, 1);
  29. /// <summary> 开始播放 </summary>
  30. public void Start()
  31. {
  32. if (cts != null)
  33. {
  34. cts.Cancel();
  35. _semaphore1.WaitOne();
  36. }
  37. cts = new CancellationTokenSource();
  38. // Message:启动当前位置的顺序下载任务
  39. Task.Run(() =>
  40. {
  41. // Message:并行运行
  42. ParallelLoopResult result = Parallel.For(0, this.TaskCount, k =>
  43. {
  44. while (true)
  45. {
  46. if (cts.IsCancellationRequested) break;
  47. int index = this._entitys.FindIndex(l => l == _current);
  48. var downs = _entitys.Skip(index).Take(this.CapacityTotal).Where(l => l.IsLoaded == 0);
  49. // Message:超出最大下载缓存数量则等待
  50. if (downs == null || downs.Count() == 0)
  51. {
  52. Thread.Sleep(1000);
  53. continue;
  54. }
  55. downs.FirstOrDefault()?.Start();
  56. }
  57. }
  58. );
  59. _semaphore1.Release();
  60. }, cts.Token);
  61. }
  62. /// <summary> 停止引擎 </summary>
  63. public void Stop()
  64. {
  65. if (cts != null)
  66. {
  67. cts.Cancel();
  68. }
  69. flag = false;
  70. }
  71. bool flag = true;
  72. Semaphore _semaphore = new Semaphore(1, 1);
  73. /// <summary> 获取下好的文件 返回null则需要等待 </summary>
  74. public IBufferPlayEntity GetWaitCurrent(Predicate<IBufferPlayEntity> match, Action<bool, int, int> action)
  75. {
  76. var result = this._entitys.Find(match);
  77. int now = this._entitys.FindIndex(match);
  78. _current = result;
  79. if (result.IsLoaded == 2)
  80. {
  81. return result;
  82. }
  83. else
  84. {
  85. // Message:停止上一个获取任务
  86. flag = false;
  87. _semaphore.WaitOne();
  88. flag = true;
  89. var waitCache = _entitys.Skip(now).Take(this.Capacity).ToList();
  90. while (!waitCache.TrueForAll(l => l.IsLoaded == 2))
  91. {
  92. if (!flag)
  93. {
  94. _semaphore.Release();
  95. return null;
  96. }
  97. Thread.Sleep(500);
  98. action(false, waitCache.FindAll(l => l.IsLoaded == 2).Count, waitCache.Count);
  99. }
  100. action(true, waitCache.FindAll(l => l.IsLoaded == 2).Count, waitCache.Count);
  101. _semaphore.Release();
  102. return result;
  103. }
  104. }
  105. /// <summary> 获取下好的文件 返回null则需要等待 </summary>
  106. public IBufferPlayEntity GetWaitCurrent(int index, Action<bool, int, int> action)
  107. {
  108. var result = this._entitys[index];
  109. return this.GetWaitCurrent(l => l == result, action);
  110. }
  111. /// <summary> 获取当前缓存完的位置 </summary>
  112. public int GetBufferSize(Predicate<IBufferPlayEntity> match)
  113. {
  114. int index = this._entitys.FindIndex(l => l == _current);
  115. return this.GetBufferSize(index);
  116. }
  117. /// <summary> 获取当前缓存完的位置 </summary>
  118. public int GetBufferSize(int index)
  119. {
  120. var isdown = _entitys.Skip(index).LastOrDefault(l => l.IsLoaded == 2);
  121. if (isdown == null) return 0;
  122. return this._entitys.FindIndex(l => l == isdown);
  123. }
  124. /// <summary> 清理缓存数据 </summary>
  125. public void Clear()
  126. {
  127. }
  128. // Message:是否是向前播放
  129. bool _isForward = true;
  130. /// <summary> 反向播放 </summary>
  131. public void RefreshPlayMode(bool forward)
  132. {
  133. if (_isForward = forward) return;
  134. _isForward = forward;
  135. _entitys.Reverse();
  136. }
  137. }

缓冲引擎任务执行接口和抽象基类

IBufferPlayEntity:


  1. /// <summary> 缓冲任务接口 </summary>
  2. public interface IBufferPlayEntity
  3. {
  4. /// <summary> 是否执行完成 </summary>
  5. int IsLoaded
  6. {
  7. get;
  8. set;
  9. }
  10. /// <summary> 开始任务 </summary>
  11. void Start();
  12. }

BufferPlayEntityBase:


  1. /// <summary> 缓冲任务抽象基类 </summary>
  2. public abstract class BufferPlayEntityBase : IBufferPlayEntity
  3. {
  4. /// <summary> 执行状态 1=正在执行 2=执行完成 0=未执行 -1=执行错误 </summary>
  5. public int IsLoaded { get; set; }
  6. public void Start()
  7. {
  8. this.IsLoaded = 1;
  9. try
  10. {
  11. this.DoStart();
  12. }
  13. catch (Exception ex)
  14. {
  15. Debug.WriteLine(ex);
  16. this.IsLoaded = -1;
  17. }
  18. this.IsLoaded = 2;
  19. }
  20. public abstract void DoStart();
  21. }

GitHub: