using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; using XFEToolBox.WpfCore.Behaviors; namespace XFEToolBox.WpfCore.Controls; /// /// 单行文本控件:默认以省略号展示溢出内容,悬停时滚动显示完整文本。 /// public partial class ScrollTextBlock : UserControl { public double RollingTimeMillisecond { get => (double)GetValue(RollingTimeMillisecondProperty); set => SetValue(RollingTimeMillisecondProperty, value); } public static readonly DependencyProperty RollingTimeMillisecondProperty = DependencyProperty.Register(nameof(RollingTimeMillisecond), typeof(double), typeof(ScrollTextBlock), new PropertyMetadata(5000d)); public bool AutoAlignment { get => (bool)GetValue(AutoAlignmentProperty); set => SetValue(AutoAlignmentProperty, value); } public static readonly DependencyProperty AutoAlignmentProperty = DependencyProperty.Register(nameof(AutoAlignment), typeof(bool), typeof(ScrollTextBlock), new PropertyMetadata(true, OnLayoutPropertyChanged)); public bool RollingBack { get => (bool)GetValue(RollingBackProperty); set => SetValue(RollingBackProperty, value); } public static readonly DependencyProperty RollingBackProperty = DependencyProperty.Register(nameof(RollingBack), typeof(bool), typeof(ScrollTextBlock), new PropertyMetadata(false, OnLayoutPropertyChanged)); public bool NeedRolling { get => (bool)GetValue(NeedRollingProperty); set => SetValue(NeedRollingProperty, value); } public static readonly DependencyProperty NeedRollingProperty = DependencyProperty.Register(nameof(NeedRolling), typeof(bool), typeof(ScrollTextBlock), new PropertyMetadata(false)); public bool IsRolling { get => (bool)GetValue(IsRollingProperty); set => SetValue(IsRollingProperty, value); } public static readonly DependencyProperty IsRollingProperty = DependencyProperty.Register(nameof(IsRolling), typeof(bool), typeof(ScrollTextBlock), new PropertyMetadata(false, OnIsRollingChanged)); public bool AutoRolling { get => (bool)GetValue(AutoRollingProperty); set => SetValue(AutoRollingProperty, value); } public static readonly DependencyProperty AutoRollingProperty = DependencyProperty.Register(nameof(AutoRolling), typeof(bool), typeof(ScrollTextBlock), new PropertyMetadata(false, OnLayoutPropertyChanged)); public string InnerText { get => (string)GetValue(InnerTextProperty); set => SetValue(InnerTextProperty, value); } public static readonly DependencyProperty InnerTextProperty = DependencyProperty.Register(nameof(InnerText), typeof(string), typeof(ScrollTextBlock), new PropertyMetadata("请输入文本", OnLayoutPropertyChanged)); public Brush InnerForeground { get => (Brush)GetValue(InnerForegroundProperty); set => SetValue(InnerForegroundProperty, value); } public static readonly DependencyProperty InnerForegroundProperty = DependencyProperty.Register(nameof(InnerForeground), typeof(Brush), typeof(ScrollTextBlock), new PropertyMetadata(new SolidColorBrush(Colors.Black))); public Brush InnerBackground { get => (Brush)GetValue(InnerBackgroundProperty); set => SetValue(InnerBackgroundProperty, value); } public static readonly DependencyProperty InnerBackgroundProperty = DependencyProperty.Register(nameof(InnerBackground), typeof(Brush), typeof(ScrollTextBlock), new PropertyMetadata(new SolidColorBrush(Colors.Transparent))); public double InnerFontSize { get => (double)GetValue(InnerFontSizeProperty); set => SetValue(InnerFontSizeProperty, value); } public static readonly DependencyProperty InnerFontSizeProperty = DependencyProperty.Register(nameof(InnerFontSize), typeof(double), typeof(ScrollTextBlock), new PropertyMetadata(13d, OnLayoutPropertyChanged)); public FontFamily InnerFontFamily { get => (FontFamily)GetValue(InnerFontFamilyProperty); set => SetValue(InnerFontFamilyProperty, value); } public static readonly DependencyProperty InnerFontFamilyProperty = DependencyProperty.Register(nameof(InnerFontFamily), typeof(FontFamily), typeof(ScrollTextBlock), new PropertyMetadata(SystemFonts.MessageFontFamily, OnLayoutPropertyChanged)); public FontWeight InnerFontWeight { get => (FontWeight)GetValue(InnerFontWeightProperty); set => SetValue(InnerFontWeightProperty, value); } public static readonly DependencyProperty InnerFontWeightProperty = DependencyProperty.Register(nameof(InnerFontWeight), typeof(FontWeight), typeof(ScrollTextBlock), new PropertyMetadata(FontWeights.Normal, OnLayoutPropertyChanged)); // 保留这些属性以兼容既有 XAML 调用。 public Thickness InnerTextMargin { get => (Thickness)GetValue(InnerTextMarginProperty); set => SetValue(InnerTextMarginProperty, value); } public static readonly DependencyProperty InnerTextMarginProperty = DependencyProperty.Register(nameof(InnerTextMargin), typeof(Thickness), typeof(ScrollTextBlock), new PropertyMetadata(new Thickness())); public double InnerTextOpacity { get => (double)GetValue(InnerTextOpacityProperty); set => SetValue(InnerTextOpacityProperty, value); } public static readonly DependencyProperty InnerTextOpacityProperty = DependencyProperty.Register(nameof(InnerTextOpacity), typeof(double), typeof(ScrollTextBlock), new PropertyMetadata(1d)); public VerticalAlignment InnerTextVerticalAlignment { get => (VerticalAlignment)GetValue(InnerTextVerticalAlignmentProperty); set => SetValue(InnerTextVerticalAlignmentProperty, value); } public static readonly DependencyProperty InnerTextVerticalAlignmentProperty = DependencyProperty.Register(nameof(InnerTextVerticalAlignment), typeof(VerticalAlignment), typeof(ScrollTextBlock), new PropertyMetadata(VerticalAlignment.Center)); public HorizontalAlignment InnerTextHorizontalAlignment { get => (HorizontalAlignment)GetValue(InnerTextHorizontalAlignmentProperty); set => SetValue(InnerTextHorizontalAlignmentProperty, value); } public static readonly DependencyProperty InnerTextHorizontalAlignmentProperty = DependencyProperty.Register(nameof(InnerTextHorizontalAlignment), typeof(HorizontalAlignment), typeof(ScrollTextBlock), new PropertyMetadata(HorizontalAlignment.Center)); public TextAlignment InnerTextAlignment { get => (TextAlignment)GetValue(InnerTextAlignmentProperty); set => SetValue(InnerTextAlignmentProperty, value); } public static readonly DependencyProperty InnerTextAlignmentProperty = DependencyProperty.Register(nameof(InnerTextAlignment), typeof(TextAlignment), typeof(ScrollTextBlock), new PropertyMetadata(TextAlignment.Center, OnLayoutPropertyChanged)); public TextDecorationCollection InnerTextDecorations { get => (TextDecorationCollection)GetValue(InnerTextDecorationsProperty); set => SetValue(InnerTextDecorationsProperty, value); } public static readonly DependencyProperty InnerTextDecorationsProperty = DependencyProperty.Register(nameof(InnerTextDecorations), typeof(TextDecorationCollection), typeof(ScrollTextBlock), new PropertyMetadata(null)); private bool _isLoaded; private double _fullTextWidth; public ScrollTextBlock() => InitializeComponent(); private void ScrollTextBlock_Loaded(object sender, RoutedEventArgs e) { _isLoaded = true; UpdateOverflowState(); } private void ScrollTextBlock_Unloaded(object sender, RoutedEventArgs e) { _isLoaded = false; StopRollingAnimation(); } private void ScrollTextBlock_SizeChanged(object sender, SizeChangedEventArgs e) => QueueOverflowUpdate(); private void ScrollTextBlock_MouseEnter(object sender, MouseEventArgs e) { UpdateOverflowState(); if (!BindingOperations.IsDataBound(this, IsRollingProperty)) SetCurrentValue(IsRollingProperty, NeedRolling); } private void ScrollTextBlock_MouseLeave(object sender, MouseEventArgs e) { if (!BindingOperations.IsDataBound(this, IsRollingProperty)) SetCurrentValue(IsRollingProperty, false); } public void StartRolling() { UpdateOverflowState(); if (!NeedRolling) return; if (IsRolling) StartRollingAnimation(); else SetCurrentValue(IsRollingProperty, true); } public void EndRolling() { if (IsRolling) SetCurrentValue(IsRollingProperty, false); else StopRollingAnimation(); } private static void OnIsRollingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var control = (ScrollTextBlock)d; if ((bool)e.NewValue && control.NeedRolling) control.StartRollingAnimation(); else control.StopRollingAnimation(); } private static void OnLayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((ScrollTextBlock)d).QueueOverflowUpdate(); private void QueueOverflowUpdate() { if (!_isLoaded) return; _ = Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(UpdateOverflowState)); } private void UpdateOverflowState() { if (!_isLoaded || ActualWidth <= 0) return; var text = InnerText ?? string.Empty; var typeface = new Typeface(InnerFontFamily ?? SystemFonts.MessageFontFamily, FontStyles.Normal, InnerFontWeight, FontStretches.Normal); var formattedText = new FormattedText(text, CultureInfo.CurrentUICulture, FlowDirection, typeface, InnerFontSize, InnerForeground ?? Brushes.Black, VisualTreeHelper.GetDpi(this).PixelsPerDip); _fullTextWidth = Math.Ceiling(formattedText.WidthIncludingTrailingWhitespace); NeedRolling = !string.IsNullOrEmpty(text) && _fullTextWidth > ActualWidth + 0.5; ellipsisTextBlock.TextAlignment = NeedRolling && AutoAlignment ? TextAlignment.Left : InnerTextAlignment; if (!NeedRolling) { SetCurrentValue(IsRollingProperty, false); StopRollingAnimation(); } else if (AutoRolling) { SetCurrentValue(IsRollingProperty, true); } else if (IsRolling) { StartRollingAnimation(); } } private void StartRollingAnimation() { if (!_isLoaded || !NeedRolling || ActualWidth <= 0) return; StopRollingAnimation(showEllipsis: false); ellipsisTextBlock.Visibility = Visibility.Hidden; scrollViewer.Visibility = Visibility.Visible; var distance = Math.Max(0, _fullTextWidth - ActualWidth); var autoReverse = RollingBack; if (!RollingBack) { var gap = new Border { Width = Math.Max(24, ActualWidth * 0.25) }; var copy = CreateTextCopy(); stackPanel.Children.Add(gap); stackPanel.Children.Add(copy); distance = _fullTextWidth + gap.Width; } if (distance <= 0) return; var animation = new DoubleAnimation { From = 0, To = distance, Duration = TimeSpan.FromMilliseconds(Math.Max(800, RollingTimeMillisecond)), RepeatBehavior = RepeatBehavior.Forever, AutoReverse = autoReverse }; scrollViewer.BeginAnimation(ScrollViewerBehavior.HorizontalOffsetProperty, animation, HandoffBehavior.SnapshotAndReplace); } private void StopRollingAnimation(bool showEllipsis = true) { scrollViewer.BeginAnimation(ScrollViewerBehavior.HorizontalOffsetProperty, null); scrollViewer.ScrollToHorizontalOffset(0); while (stackPanel.Children.Count > 1) stackPanel.Children.RemoveAt(stackPanel.Children.Count - 1); scrollViewer.Visibility = Visibility.Hidden; ellipsisTextBlock.Visibility = showEllipsis ? Visibility.Visible : Visibility.Hidden; } private TextBlock CreateTextCopy() => new() { Text = InnerText, Foreground = InnerForeground, Background = InnerBackground, FontSize = InnerFontSize, FontFamily = InnerFontFamily, FontWeight = InnerFontWeight, TextAlignment = TextAlignment.Left, TextDecorations = InnerTextDecorations, VerticalAlignment = InnerTextVerticalAlignment, TextWrapping = TextWrapping.NoWrap }; }