实现提供了完整的弹幕功能,包括多行显示、不同颜色、头像支持、防重叠、椭圆形边框、透明度渐变、点击事件、字体样式设置、暂停/继续功能、过滤功能等。还包含了批处理队列处理逻辑、速率限制、错误处理和性能监控等功能。
This commit is contained in:
9
YwxAppWpfDanMu/Controls/DanMuControl.xaml
Normal file
9
YwxAppWpfDanMu/Controls/DanMuControl.xaml
Normal file
@@ -0,0 +1,9 @@
|
||||
<UserControl x:Class="YwxAppWpfDanMu.Controls.DanMuControl"
|
||||
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">
|
||||
<Canvas x:Name="DanMuCanvas" Background="Transparent" ClipToBounds="True"/>
|
||||
</UserControl>
|
||||
215
YwxAppWpfDanMu/Controls/DanMuControl.xaml.cs
Normal file
215
YwxAppWpfDanMu/Controls/DanMuControl.xaml.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using YwxAppWpfDanMu.Models;
|
||||
using YwxAppWpfDanMu.Services;
|
||||
using YwxAppWpfDanMu.Utils;
|
||||
|
||||
namespace YwxAppWpfDanMu.Controls
|
||||
{
|
||||
public partial class DanMuControl : UserControl
|
||||
{
|
||||
private readonly DanMuPool _danMuPool;
|
||||
private readonly DanMuDispatcher _dispatcher;
|
||||
private readonly DanMuQueueProcessor _queueProcessor;
|
||||
private readonly RateLimiter _rateLimiter;
|
||||
private readonly DanMuPerformanceMonitor _performanceMonitor;
|
||||
|
||||
internal List<DanMuTrack> _tracks = new List<DanMuTrack>();
|
||||
private readonly Dictionary<DanMuItem, DanMuTrack> _activeItems = new Dictionary<DanMuItem, DanMuTrack>();
|
||||
|
||||
public DanMuControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_danMuPool = new DanMuPool(this);
|
||||
_dispatcher = new DanMuDispatcher(Dispatcher);
|
||||
_queueProcessor = new DanMuQueueProcessor(_dispatcher);
|
||||
_rateLimiter = new RateLimiter(50); // 限制每秒50条
|
||||
_performanceMonitor = new DanMuPerformanceMonitor();
|
||||
|
||||
Loaded += OnLoaded;
|
||||
Unloaded += OnUnloaded;
|
||||
SizeChanged += OnSizeChanged;
|
||||
}
|
||||
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty LineCountProperty = DependencyProperty.Register(
|
||||
"LineCount", typeof(int), typeof(DanMuControl), new PropertyMetadata(5, OnLineCountChanged));
|
||||
|
||||
public int LineCount
|
||||
{
|
||||
get => (int)GetValue(LineCountProperty);
|
||||
set => SetValue(LineCountProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsPausedProperty = DependencyProperty.Register(
|
||||
"IsPaused", typeof(bool), typeof(DanMuControl), new PropertyMetadata(false));
|
||||
|
||||
public bool IsPaused
|
||||
{
|
||||
get => (bool)GetValue(IsPausedProperty);
|
||||
set => SetValue(IsPausedProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler<DanMuEventArgs> DanMuClick;
|
||||
public event EventHandler<DanMuEventArgs> DanMuAdded;
|
||||
public event EventHandler<DanMuEventArgs> DanMuRemoved;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public void AddDanMu(DanMuMessage message)
|
||||
{
|
||||
if (!_rateLimiter.TryAcquire())
|
||||
{
|
||||
// 超出速率限制,可以记录日志或采取其他措施
|
||||
return;
|
||||
}
|
||||
|
||||
_queueProcessor.Enqueue(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (DanMuFilter.ShouldFilter(message))
|
||||
return;
|
||||
|
||||
var args = new DanMuEventArgs(message);
|
||||
DanMuAdded?.Invoke(this, args);
|
||||
|
||||
if (!args.Handled)
|
||||
{
|
||||
_danMuPool.AddDanMu(message);
|
||||
}
|
||||
|
||||
_performanceMonitor.RecordAddedDanMu();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录错误日志
|
||||
_performanceMonitor.RecordError(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void AddDanMuBatch(IEnumerable<DanMuMessage> messages)
|
||||
{
|
||||
_queueProcessor.EnqueueBatch(() =>
|
||||
{
|
||||
foreach (var message in messages)
|
||||
{
|
||||
if (!_rateLimiter.TryAcquire())
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
if (DanMuFilter.ShouldFilter(message))
|
||||
continue;
|
||||
|
||||
var args = new DanMuEventArgs(message);
|
||||
DanMuAdded?.Invoke(this, args);
|
||||
|
||||
if (!args.Handled)
|
||||
{
|
||||
_danMuPool.AddDanMu(message);
|
||||
}
|
||||
|
||||
_performanceMonitor.RecordAddedDanMu();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_performanceMonitor.RecordError(ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void ClearAll()
|
||||
{
|
||||
_queueProcessor.Enqueue(() =>
|
||||
{
|
||||
_danMuPool.ClearAll();
|
||||
_performanceMonitor.RecordClear();
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
InitializeTracks();
|
||||
_danMuPool.Start();
|
||||
_performanceMonitor.Start();
|
||||
}
|
||||
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_danMuPool.Stop();
|
||||
_performanceMonitor.Stop();
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
InitializeTracks();
|
||||
}
|
||||
|
||||
private void InitializeTracks()
|
||||
{
|
||||
_tracks.Clear();
|
||||
|
||||
double trackHeight = ActualHeight / LineCount;
|
||||
for (int i = 0; i < LineCount; i++)
|
||||
{
|
||||
_tracks.Add(new DanMuTrack
|
||||
{
|
||||
Top = i * trackHeight,
|
||||
Height = trackHeight,
|
||||
Width = ActualWidth
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnLineCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DanMuControl control)
|
||||
{
|
||||
control.InitializeTracks();
|
||||
}
|
||||
}
|
||||
|
||||
internal void OnDanMuItemClick(DanMuItem item)
|
||||
{
|
||||
var args = new DanMuEventArgs(item.Message);
|
||||
DanMuClick?.Invoke(this, args);
|
||||
}
|
||||
|
||||
internal void OnDanMuItemRemoved(DanMuItem item)
|
||||
{
|
||||
var args = new DanMuEventArgs(item.Message);
|
||||
DanMuRemoved?.Invoke(this, args);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal class DanMuTrack
|
||||
{
|
||||
public double Top { get; set; }
|
||||
public double Height { get; set; }
|
||||
public double Width { get; set; }
|
||||
public double AvailablePosition { get; set; }
|
||||
public List<DanMuItem> ActiveItems { get; } = new List<DanMuItem>();
|
||||
}
|
||||
}
|
||||
}
|
||||
50
YwxAppWpfDanMu/Controls/DanMuFilter.cs
Normal file
50
YwxAppWpfDanMu/Controls/DanMuFilter.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using YwxAppWpfDanMu.Models;
|
||||
|
||||
namespace YwxAppWpfDanMu.Controls
|
||||
{
|
||||
public static class DanMuFilter
|
||||
{
|
||||
private static readonly List<string> _blockedKeywords = new List<string>();
|
||||
private static readonly List<Regex> _blockedPatterns = new List<Regex>();
|
||||
|
||||
public static void AddBlockedKeyword(string keyword)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(keyword) && !_blockedKeywords.Contains(keyword))
|
||||
{
|
||||
_blockedKeywords.Add(keyword);
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddBlockedPattern(string pattern)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(pattern))
|
||||
{
|
||||
_blockedPatterns.Add(new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ShouldFilter(DanMuMessage message)
|
||||
{
|
||||
if (message == null || string.IsNullOrWhiteSpace(message.Content))
|
||||
return true;
|
||||
|
||||
// 检查关键词
|
||||
foreach (var keyword in _blockedKeywords)
|
||||
{
|
||||
if (message.Content.Contains(keyword))
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查正则表达式
|
||||
foreach (var pattern in _blockedPatterns)
|
||||
{
|
||||
if (pattern.IsMatch(message.Content))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
YwxAppWpfDanMu/Controls/DanMuItem.xaml
Normal file
29
YwxAppWpfDanMu/Controls/DanMuItem.xaml
Normal file
@@ -0,0 +1,29 @@
|
||||
<UserControl x:Class="YwxAppWpfDanMu.Controls.DanMuItem"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="60" d:DesignWidth="200">
|
||||
<Grid>
|
||||
<Border x:Name="Border" CornerRadius="15" Background="#88000000" Padding="3">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Ellipse Width="30" Height="30" Grid.Column="0" Margin="0,0,3,0">
|
||||
<Ellipse.Fill>
|
||||
<ImageBrush x:Name="AvatarBrush" Stretch="UniformToFill"/>
|
||||
</Ellipse.Fill>
|
||||
</Ellipse>
|
||||
|
||||
<TextBlock x:Name="ContentText" Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="NoWrap"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
57
YwxAppWpfDanMu/Controls/DanMuItem.xaml.cs
Normal file
57
YwxAppWpfDanMu/Controls/DanMuItem.xaml.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using YwxAppWpfDanMu.Models;
|
||||
|
||||
namespace YwxAppWpfDanMu.Controls
|
||||
{
|
||||
public partial class DanMuItem : UserControl
|
||||
{
|
||||
public DanMuMessage Message { get; private set; }
|
||||
|
||||
public DanMuItem(DanMuMessage message)
|
||||
{
|
||||
InitializeComponent();
|
||||
Message = message;
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void UpdateUI()
|
||||
{
|
||||
ContentText.Text = Message.Content;
|
||||
ContentText.Foreground = new SolidColorBrush(Message.Color);
|
||||
ContentText.FontSize = Message.FontSize;
|
||||
ContentText.FontFamily = Message.FontFamily;
|
||||
ContentText.FontWeight = Message.FontWeight;
|
||||
ContentText.FontStyle = Message.FontStyle;
|
||||
Opacity = Message.Opacity;
|
||||
|
||||
if (!string.IsNullOrEmpty(Message.AvatarUrl))
|
||||
{
|
||||
// 实际项目中应该使用异步加载或缓存机制
|
||||
var image = new ImageSourceConverter().ConvertFromString(Message.AvatarUrl) as ImageSource;
|
||||
AvatarBrush.ImageSource = image;
|
||||
}
|
||||
else
|
||||
{
|
||||
AvatarBrush.ImageSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
|
||||
{
|
||||
base.OnMouseLeftButtonUp(e);
|
||||
RaiseEvent(new RoutedEventArgs(ClickEvent));
|
||||
}
|
||||
|
||||
public static readonly RoutedEvent ClickEvent = EventManager.RegisterRoutedEvent(
|
||||
"Click", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(DanMuItem));
|
||||
|
||||
public event RoutedEventHandler Click
|
||||
{
|
||||
add { AddHandler(ClickEvent, value); }
|
||||
remove { RemoveHandler(ClickEvent, value); }
|
||||
}
|
||||
}
|
||||
}
|
||||
152
YwxAppWpfDanMu/Controls/DanMuPool.cs
Normal file
152
YwxAppWpfDanMu/Controls/DanMuPool.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Threading;
|
||||
using YwxAppWpfDanMu.Models;
|
||||
|
||||
namespace YwxAppWpfDanMu.Controls
|
||||
{
|
||||
internal class DanMuPool
|
||||
{
|
||||
private readonly DanMuControl _parent;
|
||||
private readonly Queue<DanMuMessage> _messageQueue = new Queue<DanMuMessage>();
|
||||
private readonly DispatcherTimer _timer;
|
||||
private readonly Random _random = new Random();
|
||||
|
||||
public DanMuPool(DanMuControl parent)
|
||||
{
|
||||
_parent = parent ?? throw new ArgumentNullException(nameof(parent));
|
||||
|
||||
_timer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(50)
|
||||
};
|
||||
_timer.Tick += OnTimerTick;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_timer.Stop();
|
||||
}
|
||||
|
||||
public void AddDanMu(DanMuMessage message)
|
||||
{
|
||||
lock (_messageQueue)
|
||||
{
|
||||
_messageQueue.Enqueue(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearAll()
|
||||
{
|
||||
lock (_messageQueue)
|
||||
{
|
||||
_messageQueue.Clear();
|
||||
}
|
||||
|
||||
_parent.DanMuCanvas.Children.Clear();
|
||||
foreach (var track in _parent._tracks)
|
||||
{
|
||||
track.ActiveItems.Clear();
|
||||
track.AvailablePosition = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTimerTick(object sender, EventArgs e)
|
||||
{
|
||||
if (_parent.IsPaused)
|
||||
return;
|
||||
|
||||
lock (_messageQueue)
|
||||
{
|
||||
while (_messageQueue.Count > 0)
|
||||
{
|
||||
var message = _messageQueue.Dequeue();
|
||||
DispatchDanMu(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DispatchDanMu(DanMuMessage message)
|
||||
{
|
||||
// 找到最适合的轨道
|
||||
var track = FindBestTrack(message);
|
||||
if (track == null)
|
||||
return;
|
||||
|
||||
var danMuItem = new DanMuItem(message);
|
||||
danMuItem.Click += (s, args) => _parent.OnDanMuItemClick(danMuItem);
|
||||
|
||||
// 测量弹幕宽度
|
||||
danMuItem.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
var itemWidth = danMuItem.DesiredSize.Width;
|
||||
|
||||
// 设置初始位置
|
||||
Canvas.SetLeft(danMuItem, _parent.ActualWidth);
|
||||
Canvas.SetTop(danMuItem, track.Top + (track.Height - danMuItem.DesiredSize.Height) / 2);
|
||||
|
||||
_parent.DanMuCanvas.Children.Add(danMuItem);
|
||||
track.ActiveItems.Add(danMuItem);
|
||||
|
||||
// 创建动画
|
||||
var animation = new DoubleAnimation
|
||||
{
|
||||
From = _parent.ActualWidth,
|
||||
To = -itemWidth,
|
||||
Duration = TimeSpan.FromSeconds(5 + _random.NextDouble() * 3),
|
||||
FillBehavior = FillBehavior.Stop
|
||||
};
|
||||
|
||||
animation.Completed += (s, args) =>
|
||||
{
|
||||
_parent.DanMuCanvas.Children.Remove(danMuItem);
|
||||
track.ActiveItems.Remove(danMuItem);
|
||||
_parent.OnDanMuItemRemoved(danMuItem);
|
||||
};
|
||||
|
||||
// 添加透明度渐变效果
|
||||
var opacityAnimation = new DoubleAnimation
|
||||
{
|
||||
From = 1.0,
|
||||
To = 0.2,
|
||||
Duration = animation.Duration,
|
||||
BeginTime = TimeSpan.FromSeconds(animation.Duration.TimeSpan.TotalSeconds * 0.7)
|
||||
};
|
||||
|
||||
danMuItem.BeginAnimation(Canvas.LeftProperty, animation);
|
||||
danMuItem.BeginAnimation(UIElement.OpacityProperty, opacityAnimation);
|
||||
|
||||
// 更新轨道可用位置
|
||||
track.AvailablePosition = _parent.ActualWidth + itemWidth + 10; // 10为间距
|
||||
}
|
||||
|
||||
private DanMuControl.DanMuTrack FindBestTrack(DanMuMessage message)
|
||||
{
|
||||
if (_parent._tracks.Count == 0)
|
||||
return null;
|
||||
|
||||
// 随机选择一个轨道,检查是否有足够空间
|
||||
int startIndex = _random.Next(_parent._tracks.Count);
|
||||
for (int i = 0; i < _parent._tracks.Count; i++)
|
||||
{
|
||||
int index = (startIndex + i) % _parent._tracks.Count;
|
||||
var track = _parent._tracks[index];
|
||||
|
||||
// 如果轨道上没有弹幕,或者最后一个弹幕已经移动足够远
|
||||
if (track.ActiveItems.Count == 0 ||
|
||||
track.AvailablePosition < _parent.ActualWidth * 0.7)
|
||||
{
|
||||
return track;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有轨道都满了,选择最不拥挤的轨道
|
||||
return _parent._tracks.OrderBy(t => t.ActiveItems.Count).First();
|
||||
}
|
||||
}
|
||||
}
|
||||
13
YwxAppWpfDanMu/Controls/DanMuSettings.cs
Normal file
13
YwxAppWpfDanMu/Controls/DanMuSettings.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace YwxAppWpfDanMu.Controls
|
||||
{
|
||||
public static class DanMuSettings
|
||||
{
|
||||
public static Color DefaultColor { get; set; } = Colors.White;
|
||||
public static double DefaultFontSize { get; set; } = 14;
|
||||
public static FontFamily DefaultFontFamily { get; set; } = new FontFamily("Microsoft YaHei");
|
||||
public static double DefaultSpeed { get; set; } = 1.0;
|
||||
public static double DefaultOpacity { get; set; } = 0.4;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user