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 @@
+
+
+
+
+
+
+