長文を設定したTextBlockに省略表示させる

2025年1月2日木曜日

C# WPF カスタムコントロール

記事のカテゴリー: C#、.NET 9、WPF

長文を設定したTextBlockを配置すると(とくに行数が多いと)UIが縦に伸びて全体を見渡すことが難しくなります。このようなときに初期状態ではTextBlockのテキストを短縮表示して、末尾に追加したリンクをクリックすることで短縮表示と全体表示を切り替えられるカスタマイズを考えます。

添付ビヘイビアで実装します。

C#:

public static class MaxLinesBehavior
{
    private const string DefaultShowOverflowText = "全体を表示する";
    private const string DefaultHideOverflowText = "省略する";

    // Text プロパティの代わりとして使う Text2 添付プロパティ
    public static readonly DependencyProperty Text2Property = DependencyProperty.RegisterAttached(
        "Text2", typeof(string), typeof(MaxLinesBehavior), new FrameworkPropertyMetadata(null, OnPropertyChanged));

    public static string? GetText2(DependencyObject obj)
    {
        return (string?)obj.GetValue(Text2Property);
    }

    public static void SetText2(DependencyObject obj, string? value)
    {
        obj.SetValue(Text2Property, value);
    }

    // 最大行数の添付プロパティ
    public static readonly DependencyProperty MaxLinesProperty = DependencyProperty.RegisterAttached(
        "MaxLines", typeof(int), typeof(MaxLinesBehavior), new FrameworkPropertyMetadata(0, OnPropertyChanged));

    public static int GetMaxLines(DependencyObject obj)
    {
        return (int)obj.GetValue(MaxLinesProperty);
    }

    public static void SetMaxLines(DependencyObject obj, int value)
    {
        obj.SetValue(MaxLinesProperty, value);
    }

    // 最大行数を超過したテキストを現在非表示にしているかどうかを取得、または設定する添付プロパティ
    public static readonly DependencyProperty HideOverflowProperty = DependencyProperty.RegisterAttached(
        "HideOverflow", typeof(bool), typeof(MaxLinesBehavior), new FrameworkPropertyMetadata(true, OnPropertyChanged));

    public static bool GetHideOverflow(DependencyObject obj)
    {
        return (bool)obj.GetValue(HideOverflowProperty);
    }

    public static void SetHideOverflow(DependencyObject obj, bool value)
    {
        obj.SetValue(HideOverflowProperty, value);
    }

    // 超過したテキストを表示するリンクで使うテキストの添付プロパティ
    public static readonly DependencyProperty ShowOverflowTextProperty = DependencyProperty.RegisterAttached(
        "ShowOverflowText", typeof(string), typeof(MaxLinesBehavior), new FrameworkPropertyMetadata(DefaultShowOverflowText, OnPropertyChanged));

    public static string GetShowOverflowText(DependencyObject obj)
    {
        return (string)obj.GetValue(ShowOverflowTextProperty);
    }

    public static void SetShowOverflowText(DependencyObject obj, string value)
    {
        obj.SetValue(ShowOverflowTextProperty, value);
    }

    // 超過したテキストを隠すリンクで使うテキストの添付プロパティ
    public static readonly DependencyProperty HideOverflowTextProperty = DependencyProperty.RegisterAttached(
        "HideOverflowText", typeof(string), typeof(MaxLinesBehavior), new FrameworkPropertyMetadata(DefaultHideOverflowText, OnPropertyChanged));

    public static string GetHideOverflowText(DependencyObject obj)
    {
        return (string)obj.GetValue(HideOverflowTextProperty);
    }

    public static void SetHideOverflowText(DependencyObject obj, string value)
    {
        obj.SetValue(HideOverflowTextProperty, value);
    }

    private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is TextBlock textBlock)
        {
            UpdateInlines(textBlock);
        }
    }

    private static void UpdateInlines(TextBlock textBlock)
    {
        string? text2 = GetText2(textBlock);
        int maxLines = GetMaxLines(textBlock);
        bool hideOverflow = GetHideOverflow(textBlock);
        textBlock.Inlines.Clear();

        // (1) Text2 が空の場合
        if (string.IsNullOrEmpty(text2))
        {
            return;
        }

        string[] lines = text2.Split('\n');

        // (2) Text2 の行数が MaxLines 以内の場合、テキスト全体を表示する
        if ((maxLines <= 0) || (lines.Length <= maxLines))
        {
            textBlock.Inlines.Add(new Run(text2));
        }

        // (3) Text2 の行数が MaxLines より大きい場合
        // (3-1) かつ超過したテキストを隠す場合、一部のテキストと表示リンクを表示する
        else if (hideOverflow)
        {
            textBlock.Inlines.Add(new Run(string.Join('\n', lines.Take(maxLines)) + '\n'));
            Hyperlink link = new(new Run(GetShowOverflowText(textBlock)));
            WeakEventManager<Hyperlink, RoutedEventArgs>.AddHandler(link, nameof(link.Click), ShowOverflowLink_Click);
            textBlock.Inlines.Add(link);
        }

        // (3-2) かつ超過したテキストを表示する場合、テキスト全体と非表示リンクを表示する
        else
        {
            textBlock.Inlines.Add(new Run(text2 + '\n'));
            Hyperlink link = new(new Run(GetHideOverflowText(textBlock)));
            WeakEventManager<Hyperlink, RoutedEventArgs>.AddHandler(link, nameof(link.Click), HideOverflowLink_Click);
            textBlock.Inlines.Add(link);
        }
    }

    private static void ShowOverflowLink_Click(object? sender, RoutedEventArgs e)
    {
        if ((sender is not Hyperlink link) || (link.Parent is not TextBlock textBlock))
        {
            return;
        }

        SetHideOverflow(textBlock, false);
    }

    private static void HideOverflowLink_Click(object? sender, RoutedEventArgs e)
    {
        if ((sender is not Hyperlink link) || (link.Parent is not TextBlock textBlock))
        {
            return;
        }

        SetHideOverflow(textBlock, true);
    }
}

XAML:

<TextBlock
    local:MaxLinesBehavior.Text2="これは長文です。&#10;2&#10;3&#10;4&#10;5"
    local:MaxLinesBehavior.MaxLines="3" />