長文を扱うTextBlockに省略表示させる

2025年1月2日木曜日

C#

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

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

C#:

public static class TextBlockExtensions
{
    private const string DefaultShowAllText = "全体を表示する";
    private const string DefaultAbbreviateText = "省略する";

    // TextBlock に表示するテキストの添付プロパティ
    // Text プロパティの代わりに AbbreviateableText を使用する
    public static readonly DependencyProperty AbbreviateableTextProperty
        = DependencyProperty.RegisterAttached(
            "AbbreviateableText",
            typeof(string),
            typeof(TextBlockExtensions),
            new PropertyMetadata(null, OnPropertyChanged));

    public static string? GetAbbreviateableText(DependencyObject obj)
    {
        return (string?)obj.GetValue(AbbreviateableTextProperty);
    }

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

    // 最大行数の添付プロパティ
    public static readonly DependencyProperty MaxLinesProperty
        = DependencyProperty.RegisterAttached(
            "MaxLines",
            typeof(int),
            typeof(AbbreviateableTextBehavior),
            new PropertyMetadata(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 IsAbbreviatedProperty
        = DependencyProperty.RegisterAttached(
            "IsAbbreviated",
            typeof(bool),
            typeof(AbbreviateableTextBehavior),
            new PropertyMetadata(false, OnPropertyChanged));

    public static bool GetIsAbbreviated(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsAbbreviatedProperty);
    }

    public static void SetIsAbbreviated(DependencyObject obj, bool value)
    {
        obj.SetValue(IsAbbreviatedProperty, value);
    }

    // 全体を表示するリンクの文字列の添付プロパティ
    public static readonly DependencyProperty ShowAllTextProperty
        = DependencyProperty.RegisterAttached(
            "ShowAllText",
            typeof(string),
            typeof(AbbreviateableTextBehavior),
            new PropertyMetadata(DefaultShowAllText, OnPropertyChanged));

    public static string GetShowAllText(DependencyObject obj)
    {
        return (string)obj.GetValue(ShowAllTextProperty);
    }

    public static void SetShowAllText(DependencyObject obj, string value)
    {
        obj.SetValue(ShowAllTextProperty, value);
    }

    // 省略するリンクの文字列の添付プロパティ
    public static readonly DependencyProperty AbbreviateTextProperty
        = DependencyProperty.RegisterAttached(
            "AbbreviateText",
            typeof(string),
            typeof(AbbreviateableTextBehavior),
            new PropertyMetadata(DefaultAbbreviateText, OnPropertyChanged));

    public static string GetAbbreviateText(DependencyObject obj)
    {
        return (string)obj.GetValue(AbbreviateTextProperty);
    }

    public static void SetAbbreviateText(DependencyObject obj, string value)
    {
        obj.SetValue(AbbreviateTextProperty, value);
    }

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

    // Inlines プロパティを更新して AbbreviateableText 添付プロパティのテキストを表示する
    private static void UpdateInlines(TextBlock textBlock)
    {
        string? abbreviateableText = GetAbbreviateableText(textBlock);
        int maxLines = GetMaxLines(textBlock);
        bool isAbbreviated = GetIsAbbreviated(textBlock);
        textBlock.Inlines.Clear();

        // (1) テキストが空の場合 -> 何も表示しない
        if (string.IsNullOrEmpty(abbreviateableText))
        {
            return;
        }

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

        // (2) テキストの行数が最大行数以内の場合 -> テキスト全体を表示
        if ((maxLines <= 0) || (lines.Length <= maxLines))
        {
            textBlock.Inlines.Add(new Run(abbreviateableText));
        }

        // (3) テキストの行数が最大行数より大きい場合
        // (3-1) 省略する場合 -> 一部のテキストと全体を表示するリンクを表示
        else if (isAbbreviated)
        {
            textBlock.Inlines.Add(new Run(string.Join('\n', lines.Take(maxLines)) + '\n'));
            Hyperlink link = new Hyperlink(new Run(GetShowAllText(textBlock)));
            textBlock.Inlines.Add(link);
            link.Click += (sender, e) =>
            {
                SetIsAbbreviated(textBlock, false);
            };
        }

        // (3-2) 全体を表示する場合 -> テキスト全体と省略するリンクを表示
        else
        {
            textBlock.Inlines.Add(new Run(abbreviateableText + '\n'));
            Hyperlink link = new Hyperlink(new Run(GetAbbreviateText(textBlock)));
            textBlock.Inlines.Add(link);
            link.Click += (sender, e) =>
            {
                SetIsAbbreviated(textBlock, true);
            };
        }
    }
}

XAML:

<TextBlock
    local:TextBlockExtensions.AbbreviateableText="これは長文です。"
    local:TextBlockExtensions.IsAbbreviated="True"
    local:TextBlockExtensions.MaxLines="3" />