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

2025年4月19日土曜日

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

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

長文を設定したTextBlockに初期では省略表示させ、必要なら全文表示に切り替えることのできるカスタムコントロールを考えます。ちなみに、ChatGPTに協力してもらいました。以前作ったもの(長文を設定したTextBlockに省略表示させる)と別バージョンです。

以前作ったものとの違い

以前にも同じ趣旨のカスタムコントロールを作っています(長文を設定したTextBlockに省略表示させるを参照してください)。以前作ったものはTextBlockの文章を右端で折り返さない前提でした。今回は逆に文章を折り返す前提のものになります。

実装

文章に加えて現在の状態(省略表示しているか、全文表示しているか)を示す部品を配置したかったので、Controlを継承したクラスを作成して内部にTextBlockを持つ形になりました。外観(XAMLで書いたスタイル)とコードビハインド(C#のコード)の二部構成になります。

このような構成やコントロールの名前はChatGPTの提案が色濃い部分です。

外観

Fluentデザインになじむような外観になっています(なっているつもり)。PART_TextBlockが内部のTextBlockChevronIconが現在の表示の状態を表す矢印アイコンです。矢印アイコンは実際にはアイコンフォントを使ったTextBlockになっています。

このスタイルはアプリケーションのリソース(AppクラスのResourcesプロパティなど)に書いてください。

XAML:

<Style x:Key="ChevronTextBlockStyle" TargetType="TextBlock">
    <Setter Property="FontFamily" Value="Segoe Fluent Icons, Segoe MDL2 Assets" />
    <Setter Property="FontSize" Value="11" />
</Style>
<Style TargetType="{x:Type local:ExpandableTextBlock}">
    <Setter Property="ToolTipService.InitialShowDelay" Value="100" />
    <Setter Property="ToolTipService.Placement" Value="Relative" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:ExpandableTextBlock}">
                <Border
                    x:Name="Border"
                    Background="{TemplateBinding Background}"
                    BorderBrush="{TemplateBinding BorderBrush}"
                    BorderThickness="{TemplateBinding BorderThickness}"
                    CornerRadius="{TemplateBinding Border.CornerRadius}"
                    ToolTip="{TemplateBinding ToolTip}"
                    ToolTipService.InitialShowDelay="{TemplateBinding ToolTipService.InitialShowDelay}"
                    ToolTipService.Placement="{TemplateBinding ToolTipService.Placement}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*" />
                            <ColumnDefinition Width="Auto" />
                        </Grid.ColumnDefinitions>
                        <!--  内部の TextBlock  -->
                        <TextBlock
                            x:Name="PART_TextBlock"
                            Grid.Column="0"
                            Padding="{TemplateBinding Padding}"
                            VerticalAlignment="Top"
                            FontFamily="{TemplateBinding FontFamily}"
                            FontSize="{TemplateBinding FontSize}"
                            FontStretch="{TemplateBinding FontStretch}"
                            FontStyle="{TemplateBinding FontStyle}"
                            FontWeight="{TemplateBinding FontWeight}"
                            Foreground="{TemplateBinding Foreground}"
                            Text="{TemplateBinding Text}"
                            TextTrimming="CharacterEllipsis"
                            TextWrapping="Wrap"
                            ToolTipService.InitialShowDelay="100"
                            ToolTipService.Placement="Relative" />
                        <!--  右上の矢印アイコン  -->
                        <TextBlock
                            x:Name="ChevronIcon"
                            Grid.Column="1"
                            Margin="4,4,4,0"
                            VerticalAlignment="Top"
                            Style="{StaticResource ChevronTextBlockStyle}"
                            Text="&#xE70D;"
                            Visibility="{TemplateBinding ChevronIconVisibility}" />
                    </Grid>
                </Border>
                <ControlTemplate.Triggers>
                    <!--  表示が切り替わったら矢印アイコンの向きを逆にする  -->
                    <Trigger Property="IsExpanded" Value="True">
                        <Setter TargetName="ChevronIcon" Property="Text" Value="&#xE70E;" />
                    </Trigger>
                    <!--  省略表示中はツールチップに全文を設定する  -->
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="AutoTooltip" Value="True" />
                            <Condition Property="ChevronIconVisibility" Value="Visible" />
                            <Condition Property="IsExpanded" Value="False" />
                        </MultiTrigger.Conditions>
                        <Setter TargetName="Border" Property="ToolTip" Value="{Binding Text, RelativeSource={RelativeSource TemplatedParent}}" />
                    </MultiTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

コードビハインド

コードビハインドでは文章の高さや行の高さを計算して、内部のTextBlockや矢印アイコンの表示を制御します。TextBlockに短縮表示させる方法については、MaxHeightプロパティに最大行数分の高さを指定して超過分を表示させないようにしています。

C#:

public class ExpandableTextBlock : Control
{
    static ExpandableTextBlock()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ExpandableTextBlock), new FrameworkPropertyMetadata(typeof(ExpandableTextBlock)));
    }

    public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
        "Text", typeof(string), typeof(ExpandableTextBlock), new FrameworkPropertyMetadata(string.Empty, OnPropertyChanged));

    // 最大行数のプロパティ
    public static readonly DependencyProperty MaxLinesProperty = DependencyProperty.Register(
        "MaxLines", typeof(int), typeof(ExpandableTextBlock), new FrameworkPropertyMetadata(3, OnPropertyChanged));
   
    // 全体表示しているかどうかを表すプロパティ
    public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(
        "IsExpanded", typeof(bool), typeof(ExpandableTextBlock), new FrameworkPropertyMetadata(false, OnPropertyChanged));

    private static readonly DependencyPropertyKey ChevronIconVisibilityPropertyKey = DependencyProperty.RegisterReadOnly(
        "ChevronIconVisibility", typeof(Visibility), typeof(ExpandableTextBlock), new FrameworkPropertyMetadata(Visibility.Collapsed));

    // そもそも省略表示が必要かどうかを表すプロパティ
    public static readonly DependencyProperty ChevronIconVisibilityProperty = ChevronIconVisibilityPropertyKey.DependencyProperty;

    // 省略表示中にツールチップに全文を設定するかどうかを表すプロパティ
    public static readonly DependencyProperty AutoTooltipProperty = DependencyProperty.Register(
        "AutoTooltip", typeof(bool), typeof(ExpandableTextBlock), new FrameworkPropertyMetadata(false));

    private TextBlock? textBlock;

    public string Text
    {
        get { return (string)GetValue(TextProperty); }
        set { SetValue(TextProperty, value); }
    }

    public int MaxLines
    {
        get { return (int)GetValue(MaxLinesProperty); }
        set { SetValue(MaxLinesProperty, value); }
    }

    public bool IsExpanded
    {
        get { return (bool)GetValue(IsExpandedProperty); }
        set { SetValue(IsExpandedProperty, value); }
    }

    public Visibility ChevronIconVisibility
    {
        get { return (Visibility)GetValue(ChevronIconVisibilityProperty); }
        private set { SetValue(ChevronIconVisibilityPropertyKey, value); }
    }

    public bool AutoTooltip
    {
        get { return (bool)GetValue(AutoTooltipProperty); }
        set { SetValue(AutoTooltipProperty, value); }
    }

    private static void OnPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        if (obj is not ExpandableTextBlock control)
        {
            return;
        }

        control.UpdateDisplay();
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        textBlock = GetTemplateChild("PART_TextBlock") as TextBlock;

        if (textBlock != null)
        {
            textBlock.Loaded += TextBlock_Loaded;
            textBlock.SizeChanged += TextBlock_SizeChanged;
        }
    }

    // コントロールがクリックされると省略表示と全文表示を切り替える
    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        if (ChevronIconVisibility == Visibility.Visible)
        {
            IsExpanded = !IsExpanded;
        }

        base.OnMouseLeftButtonDown(e);
    }

    private void TextBlock_Loaded(object sender, RoutedEventArgs e)
    {
        UpdateDisplay();
    }

    private void TextBlock_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        if (e.WidthChanged)
        {
            UpdateDisplay();
        }
    }

    // 文章の「高さ」を計算して、ChevronIconVisibility と内部 TextBlock の MaxHeight に反映する 
    private void UpdateDisplay()
    {
        double GetHeight(TextBlock textBlock)
        {
            return new FormattedText(
                Text ?? string.Empty,
                CultureInfo.CurrentCulture,
                FlowDirection.LeftToRight,
                new Typeface(textBlock.FontFamily, textBlock.FontStyle, textBlock.FontWeight, textBlock.FontStretch),
                textBlock.FontSize,
                textBlock.Foreground,
                VisualTreeHelper.GetDpi(textBlock).PixelsPerDip
            )
            {
                MaxTextWidth = textBlock.ActualWidth,
            }.Height;
        }

        if ((textBlock == null) || (textBlock.ActualWidth == 0))
        {
            if (textBlock != null)
            {
                textBlock.MaxHeight = double.PositiveInfinity;
            }

            return;
        }

        double lineHeight = !double.IsNaN(textBlock.LineHeight) ? textBlock.LineHeight : textBlock.FontSize * textBlock.FontFamily.LineSpacing;
        ChevronIconVisibility = (GetHeight(textBlock) > lineHeight * MaxLines) ? Visibility.Visible : Visibility.Collapsed;
        textBlock.MaxHeight = ((ChevronIconVisibility != Visibility.Visible) || IsExpanded) ? double.PositiveInfinity : MaxLines * lineHeight;
    }
}

使用例

XAML:

<local:ExpandableTextBlock
    AutoTooltip="True"
    MaxLines="3"
    Text="長い文章" />