diff --git a/Ink Canvas/Helpers/AutoFontSizeHelper.cs b/Ink Canvas/Helpers/AutoFontSizeHelper.cs new file mode 100644 index 00000000..1ae9464f --- /dev/null +++ b/Ink Canvas/Helpers/AutoFontSizeHelper.cs @@ -0,0 +1,252 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Threading; +using System.Windows.Markup; +using System.Windows.Media.TextFormatting; +using System.Windows.Media.Imaging; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Shapes; +using System.Windows.Data; +using System.Windows.Ink; +using System.Windows.Interop; +using System.Windows.Media.Animation; +using System.Windows.Navigation; + +namespace Ink_Canvas.Helpers +{ + /// + /// 让 TextBlock 在可用宽度不足时自动缩小字号(只缩小不放大),用于避免英文等长文本被截断。 + /// + public static class AutoFontSizeHelper + { + public static readonly DependencyProperty IsEnabledProperty = + DependencyProperty.RegisterAttached( + "IsEnabled", + typeof(bool), + typeof(AutoFontSizeHelper), + new PropertyMetadata(false, OnIsEnabledChanged)); + + public static void SetIsEnabled(DependencyObject element, bool value) => element.SetValue(IsEnabledProperty, value); + public static bool GetIsEnabled(DependencyObject element) => (bool)element.GetValue(IsEnabledProperty); + + public static readonly DependencyProperty MinFontSizeProperty = + DependencyProperty.RegisterAttached( + "MinFontSize", + typeof(double), + typeof(AutoFontSizeHelper), + new PropertyMetadata(6d, OnSizingPropertyChanged)); + + public static void SetMinFontSize(DependencyObject element, double value) => element.SetValue(MinFontSizeProperty, value); + public static double GetMinFontSize(DependencyObject element) => (double)element.GetValue(MinFontSizeProperty); + + public static readonly DependencyProperty MaxFontSizeProperty = + DependencyProperty.RegisterAttached( + "MaxFontSize", + typeof(double), + typeof(AutoFontSizeHelper), + new PropertyMetadata(double.NaN, OnSizingPropertyChanged)); + + public static void SetMaxFontSize(DependencyObject element, double value) => element.SetValue(MaxFontSizeProperty, value); + public static double GetMaxFontSize(DependencyObject element) => (double)element.GetValue(MaxFontSizeProperty); + + public static readonly DependencyProperty StepProperty = + DependencyProperty.RegisterAttached( + "Step", + typeof(double), + typeof(AutoFontSizeHelper), + new PropertyMetadata(0.5d, OnSizingPropertyChanged)); + + public static void SetStep(DependencyObject element, double value) => element.SetValue(StepProperty, value); + public static double GetStep(DependencyObject element) => (double)element.GetValue(StepProperty); + + private static readonly DependencyProperty IsAdjustingProperty = + DependencyProperty.RegisterAttached( + "IsAdjusting", + typeof(bool), + typeof(AutoFontSizeHelper), + new PropertyMetadata(false)); + + private static void SetIsAdjusting(DependencyObject element, bool value) => element.SetValue(IsAdjustingProperty, value); + private static bool GetIsAdjusting(DependencyObject element) => (bool)element.GetValue(IsAdjustingProperty); + + private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var tb = d as TextBlock; + if (tb == null) return; + + if ((bool)e.NewValue) + { + tb.SizeChanged += TextBlock_OnSizeChanged; + tb.Loaded += TextBlock_OnLoaded; + tb.Unloaded += TextBlock_OnUnloaded; + + try + { + var dpd = DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, typeof(TextBlock)); + dpd?.AddValueChanged(tb, TextBlock_OnTextChanged); + } + catch + { + // 忽略:极端情况下 descriptor 可能不可用 + } + + // 让第一次布局完成后再做一次调整(避免 ActualWidth=0) + tb.Dispatcher.BeginInvoke(new Action(() => TryAdjust(tb)), DispatcherPriority.Loaded); + } + else + { + tb.SizeChanged -= TextBlock_OnSizeChanged; + tb.Loaded -= TextBlock_OnLoaded; + tb.Unloaded -= TextBlock_OnUnloaded; + + try + { + var dpd = DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, typeof(TextBlock)); + dpd?.RemoveValueChanged(tb, TextBlock_OnTextChanged); + } + catch + { + } + } + } + + private static void OnSizingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TextBlock tb && GetIsEnabled(tb)) + { + tb.Dispatcher.BeginInvoke(new Action(() => TryAdjust(tb)), DispatcherPriority.Loaded); + } + } + + private static void TextBlock_OnLoaded(object sender, RoutedEventArgs e) + { + if (sender is TextBlock tb) tb.Dispatcher.BeginInvoke(new Action(() => TryAdjust(tb)), DispatcherPriority.Loaded); + } + + private static void TextBlock_OnUnloaded(object sender, RoutedEventArgs e) + { + // 这里不做额外处理;事件解绑由 IsEnabled 关闭或对象销毁处理 + } + + private static void TextBlock_OnSizeChanged(object sender, SizeChangedEventArgs e) + { + if (sender is TextBlock tb) TryAdjust(tb); + } + + private static void TextBlock_OnTextChanged(object sender, EventArgs e) + { + if (sender is TextBlock tb) tb.Dispatcher.BeginInvoke(new Action(() => TryAdjust(tb)), DispatcherPriority.Loaded); + } + + private static void TryAdjust(TextBlock tb) + { + if (tb == null) return; + if (!GetIsEnabled(tb)) return; + if (GetIsAdjusting(tb)) return; + + // 没有可用宽度时跳过 + var availableWidth = tb.ActualWidth; + if (double.IsNaN(availableWidth) || availableWidth <= 1) return; + + // 文本为空时不需要调整 + var text = tb.Text; + if (string.IsNullOrEmpty(text)) return; + + var min = GetMinFontSize(tb); + if (double.IsNaN(min) || min <= 0) min = 6d; + + var step = GetStep(tb); + if (double.IsNaN(step) || step <= 0.01) step = 0.5d; + + var max = GetMaxFontSize(tb); + if (double.IsNaN(max) || max <= 0) + { + max = tb.FontSize; + } + + if (double.IsNaN(max) || max <= 0) return; + + // 只做“缩小不放大” + var startFont = Math.Min(tb.FontSize, max); + if (startFont < min) startFont = min; + + SetIsAdjusting(tb, true); + try + { + // 如果当前已合适,直接回到 max(但不超过原本 fontSize),避免之前缩小后再变短不恢复 + // 注意:恢复也只在不超过 max 的范围内 + var desiredAtMax = MeasureTextWidth(tb, text, max); + if (desiredAtMax > 0 && desiredAtMax <= availableWidth + 0.5) + { + if (tb.FontSize != max) tb.FontSize = max; + return; + } + + double font = startFont; + double desired = MeasureTextWidth(tb, text, font); + if (desired <= 0) return; + + // 逐步减小直到适配或触底 + while (font > min && desired > availableWidth + 0.5) + { + font = Math.Max(min, font - step); + desired = MeasureTextWidth(tb, text, font); + if (desired <= 0) break; + } + + if (!double.IsNaN(font) && font > 0 && Math.Abs(tb.FontSize - font) > 0.01) + { + tb.FontSize = font; + } + } + finally + { + SetIsAdjusting(tb, false); + } + } + + private static double MeasureTextWidth(TextBlock tb, string text, double fontSize) + { + try + { + var dpi = VisualTreeHelper.GetDpi(tb); + var culture = CultureInfo.CurrentUICulture; + + // 使用 TextBlock 自身的语言/流向 + if (tb.Language != null) + { + try + { + culture = tb.Language.GetEquivalentCulture(); + } + catch + { + } + } + + var typeface = new Typeface(tb.FontFamily, tb.FontStyle, tb.FontWeight, tb.FontStretch); + var formatted = new FormattedText( + text, + culture, + tb.FlowDirection, + typeface, + fontSize, + tb.Foreground, + dpi.PixelsPerDip); + + // 这里用包含尾随空白的宽度更接近实际布局 + return formatted.WidthIncludingTrailingWhitespace; + } + catch + { + return -1; + } + } + } +} + diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 11d3c1ae..516b44f9 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -10,6 +10,7 @@ xmlns:Windows="clr-namespace:Ink_Canvas.Windows" xmlns:props="clr-namespace:Ink_Canvas.Properties" xmlns:i18n="clr-namespace:Ink_Canvas.MarkupExtensions" + xmlns:helpers="clr-namespace:Ink_Canvas.Helpers" mc:Ignorable="d" AllowsTransparency="True" WindowStyle="None" @@ -47,6 +48,34 @@ + + + + + + +