WPF の Label でアンダーバーが消える理由と回避方法

WPF の Label にアンダーバー(_)を含む文字列を表示すると画面上で消える。原因である ContentPresenter.RecognizesAccessKey の働きと、影響を受けるコントロールの範囲、4 つの回避方法を実測付きで解説する。

概要

WPF の Label コントロールに _ (アンダーバー)を含む文字列を設定すると、その文字が画面に表示されず消えてしまうことがある。
これは WPF の仕様によるものであり、原因と代表的な回避方法を整理する。

この事象は Label 固有の不具合として扱われることが多いが、実際には ButtonCheckBox など複数のコントロールで同じ結果になる。
本記事では、影響を受けるコントロールの範囲を実測で確定させたうえで、4 つの回避方法とその選択基準を示す。


前提・対象環境

本記事に載せた図と実測値は、上記の環境で実際にアプリケーションを起動して取得したものである。
他のバージョンやテーマでは確認していない。 既定テンプレートの RecognizesAccessKey に依存する挙動であるため、テーマや ControlTemplate を差し替えた環境では結果が変わりうる。


問題

LabelContent にアンダーバーを含む文字列(例: my_variable)を設定したとき、画面には myvariable のようにアンダーバーが欠落した状態で表示される。
また、_F のように書くと F に下線が付いた状態で表示されることがある。
バインドしている文字列データにアンダーバーが含まれている場合でも同様に消えてしまうため、動的なデータ表示でも問題が発生する。

ファイルパス、識別子、データベースの列名、スネークケースのキーなど、アンダーバーを含む文字列を画面に出す場面は多い。
それらをそのまま Label に流し込むと、開発中は気付かず、実データを表示した段階で初めて表示崩れが露見する。


原因・背景

Label は内部で AccessText というコントロールを使ってテキストを描画している。
AccessText はアンダーバーをアクセスキー(Alt キーと組み合わせてフォーカス移動を行うショートカット機能)の目印として解釈する。

具体的な挙動は以下のとおりである。

入力文字列 画面上の表示 解釈
_File File F がアクセスキーとして登録される
my_var myvar v がアクセスキーとして登録される
name_ name_ 直後に文字が無いため、アンダーバーがそのまま残る

上表の 3 例を実際に描画した結果が次の画像である。

WPF の Label に _File、my_var、name_ を設定して実行した画面。_File は File、my_var は myvar と表示され、アンダーバーが消えている。name_ だけはアンダーバーが残っている。
.NET 10 / Windows 11 で Label に各文字列を設定した実行結果。左が XAML の記述、右が実際の描画である。_Filemy_var ではアンダーバーが失われる一方、name_ は直後に対象の文字が無いためアクセスキーとして解釈されず、そのまま表示される。

このため、データにアンダーバーが含まれているだけで、意図しない表示崩れが起きる。
なお、アンダーバーが消えるのは直後に文字が続く場合に限られるため、文字列中のどの位置にアンダーバーがあるかで結果が変わる。

アンダーバーが複数ある場合、アクセスキーとして登録されるのは最初の 1 つだけである。
a_b_c を与えると b がアクセスキーになり、2 つ目のアンダーバーはそのまま表示される。

AccessText を挟むかどうかを決めているもの

AccessText が使われるかどうかは、Label という型そのものではなく、既定の ControlTemplate に置かれた ContentPresenterRecognizesAccessKey プロパティで決まる。
ContentPresenter は、RecognizesAccessKeyTrue で、かつ文字列にアンダーバーが含まれるときにだけ AccessText を生成する。
アンダーバーを含まない文字列では、RecognizesAccessKeyTrue であっても通常の TextBlock が使われる。

visual ツリーを実際にたどると、この差がそのまま現れる。

Content 構築される visual ツリー
Status Running LabelBorderContentPresenterTextBlock
Status _Running LabelBorderContentPresenterAccessTextTextBlock

アンダーバーを含む場合だけ AccessText が 1 段挟まる。
つまり「Label がアンダーバーを食べる」のではなく、「RecognizesAccessKey="True"ContentPresenter が食べる」というのが正確な理解である。

影響を受けるコントロールの範囲

RecognizesAccessKeyTrue にしている既定テンプレートは Label だけではない。
各コントロールに同じ文字列 my_var を与え、visual ツリー上に AccessText が生成されるかを調べた結果が次の表である。

コントロール アンダーバーの扱い 対象プロパティ
Label 消える Content
Button 消える Content
CheckBox 消える Content
RadioButton 消える Content
ToggleButton 消える Content
GroupBox 消える Header のみ
Expander 消える Header のみ
TabItem 消える Header のみ
MenuItem 消える Header のみ(トップレベル・サブメニューとも)
TreeViewItem 消えない
ListBoxItem 消えない
ComboBoxItem 消えない
StatusBarItem 消えない
TextBlock 消えない

上表は目視ではなく、各コントロールに同じ文字列を与えて表示し、visual ツリーに AccessText が現れるかを走査して求めている。

15 種類のコントロールに my_var を与え、AccessText が生成されるかを調べた表。Label、Button、CheckBox、RadioButton、ToggleButton、GroupBox、Expander、TabItem、MenuItem のトップレベルとサブメニューでは disappears。TreeViewItem、ListBoxItem、ComboBoxItem、StatusBarItem、TextBlock では kept。
.NET 10 / Windows 11 での実測結果。disappears は visual ツリーに AccessText が生成されたこと、kept は生成されなかったことを示す。ComboBoxItem とサブメニューの MenuItem は、ポップアップを開いてコンテナを実体化させてから調べている。

Header を持つこれらのコントロールでは、RecognizesAccessKey="True" なのは Header を描画する ContentPresenter だけである。
本体の Content を描画する側の構成はコントロールによって異なる。

対象コントロール自身のテンプレートに属する ContentPresenter を数えた表。GroupBox は 2 個で片方が True。Expander は 1 個で ExpandSite が False。TabItem は 1 個で contentPresenter が True。MenuItem はトップレベル・サブメニューとも 2 個で、Icon が False、ヘッダー側が True。
.NET 10 / Windows 11 で、各コントロールを実際に表示して ContentPresenter を数えた結果。TemplatedParent が対象コントロール自身であるものだけを数えている。name はテンプレート内での名前で、(unnamed) は名前が付いていないことを示す。

読み取れることは 3 点ある。

ExpanderHeaderContentPresenter は、Expander 自身のテンプレートには無い。
自身のテンプレートにあるのは ExpandSiteContent 用、RecognizesAccessKeyFalse)だけである。
Header を描く側は、ヘッダーの ToggleButton のテンプレート内にある。

TabItem は逆で、自身のテンプレートにあるのは Header 用の 1 個だけである。
選択中の Content は、親の TabControl にある PART_SelectedContentHost が描画する。

MenuItem は階層でテンプレートが変わるが、個数と RecognizesAccessKey は変わらない。
トップレベルでもサブメニューでも Icon 用(False)とヘッダー用(True)の 2 個であり、
ヘッダー用の名前だけが (unnamed)menuHeaderContainer で異なる。

つまり ExpanderTabItem は、HeaderContent を描く ContentPresenter が別のコントロールのテンプレートに分かれている。
ContentPresenter がどのテンプレートに属するかは、TemplatedParent を見ると判別できる。

visual ツリーを単に走査すると、子コントロールのテンプレート部品まで数に入る。
上図は TemplatedParent が対象コントロール自身であるものだけを数えており、この判別条件はシーンのコードに書いてある。
目で追って数えると Expander のヘッダー用 ContentPresenterExpander 自身のものと取り違える。

いずれの場合も、RecognizesAccessKey="True" なのは Header を描く側だけである。
このため、同じコントロールでもヘッダーに置いた文字列だけアンダーバーが消える。

主要なコントロールについて、同じ文字列を与えて描画した結果を次に示す。

my_var という同じ文字列を Label、Button、CheckBox、GroupBox のヘッダー、ListBoxItem、TextBlock に設定した実行画面。前の 4 つは myvar と表示され、ListBoxItem と TextBlock だけが my_var と表示されている。
.NET 10 / Windows 11 で、同じ文字列 my_var を各コントロールに与えた実行結果。LabelButtonCheckBoxGroupBox のヘッダーではアンダーバーが失われ、ListBoxItemTextBlock ではそのまま表示される。差は既定テンプレートの ContentPresenter.RecognizesAccessKey によって生じている。

ListBoxItemComboBoxItem で問題が起きないのは、一覧に並ぶ項目にアクセスキーを割り当てる意味がないためである。
逆に言えば、ItemTemplate の中に LabelButton を置いた場合は、そこでアンダーバーが消える。


4 つの回避方法

回避方法は 4 つある。用途に合わせて使い分ける。

方法 1 だけが「アクセスキー機能を残したまま表示を直す」手段であり、方法 2〜4 はいずれもアクセスキー機能を捨てる代わりに文字列をそのまま扱えるようにする手段である。

方法 1:アンダーバーを 2 つ重ねてエスケープする

__ と 2 つ続けて書くことで、画面に 1 つのアンダーバーが表示される。
XAML 側で静的に文字列を設定しているケースに向いている。

<Label Content="my__variable" />

動的データに対して置換で対応する場合は、次のように書く。

// 表示用にアンダーバーをエスケープする。元の値は書き換えない。
public string DisplayName => Name.Replace("_", "__");

この置換は必ず 1 回だけ適用する
すでにエスケープ済みの文字列へ再度適用すると a_ba____b となり、画面には a__b と表示されて誤りになる。
プロパティのゲッターで都度算出する形にしておくと、二重適用を避けやすい。


方法 2:TextBlock コントロールに変更する

アクセスキー機能や Target プロパティによるフォーカス制御が不要であれば、LabelTextBlock に変更するのが最も単純な対応である。
TextBlockAccessText を使わないため、アンダーバーをそのまま表示できる。

<TextBlock Text="my_variable" />

バインドの場合も同様にそのまま動作する。

<TextBlock Text="{Binding VariableName}" />

方法 3:ContentTemplate で TextBlock を使う

Label を維持しつつ、動的バインドのデータにアンダーバーが含まれる場合でも正しく表示するには、ContentTemplateTextBlock を指定する。
これにより、LabelContentAccessText ではなく TextBlock で描画させることができる。

<Label Content="{Binding VariableName}">
    <Label.ContentTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding}" />
        </DataTemplate>
    </Label.ContentTemplate>
</Label>

アプリ全体で統一したい場合は、このテンプレートをスタイルとして定義する方法もある。

<Style x:Key="PlainLabel" TargetType="Label">
    <Setter Property="ContentTemplate">
        <Setter.Value>
            <DataTemplate>
                <TextBlock Text="{Binding}" />
            </DataTemplate>
        </Setter.Value>
    </Setter>
</Style>

方法 4:ControlTemplate で RecognizesAccessKey を False にする

原因そのものを断つ方法である。
ControlTemplate を差し替え、ContentPresenterRecognizesAccessKeyFalse に指定する。

<Label Content="{Binding VariableName}">
    <Label.Template>
        <ControlTemplate TargetType="Label">
            <ContentPresenter RecognizesAccessKey="False" />
        </ControlTemplate>
    </Label.Template>
</Label>

ContentPresenter が既定の文字列表示で AccessText を選ばなくなるため、アンダーバーはそのまま表示される。

この設定が効くのは、ContentPresenter が文字列から表示要素を選ぶ既定の経路だけである。
ContentTemplate を明示した場合はそちらのテンプレートが優先され、ContentAccessText を直接置いた場合はその要素がそのまま描画される。
いずれの場合も RecognizesAccessKey の値は結果に影響しない。

既定の外観を保ちたい場合は、ControlTemplate を全面的に差し替えるのではなく、方法 3 を選ぶほうが影響範囲が小さい。

4 つの方法をそれぞれ実行すると、いずれも同じ表示結果になる。

4 つの回避方法を適用した WPF アプリの画面。エスケープした Label、TextBlock、ContentTemplate を差し替えた Label、RecognizesAccessKey を False にした Label のいずれもが my_variable と表示している。
4 つの回避方法を同一アプリ上で実行した結果。__ でのエスケープ、TextBlock への変更、ContentTemplate の差し替え、RecognizesAccessKey="False" のいずれでも my_variable が欠落せずに表示される。

選択の分岐点

4 つのどれを選ぶかは、次の順に決まる。

1. アクセスキー機能(Target によるフォーカス移動)を使うか。
使うなら方法 1 しかない。方法 2・3・4 はいずれもアクセスキーの解釈を止めるため、Target が働かなくなる。

2. 表示する文字列が静的か、バインドされたデータか。
静的なら方法 1 で足りる。バインドされたデータに対して __ を作るには ViewModel 側で置換が要り、View の表示ルールを ViewModel へ持ち込むことになる。動的なデータには方法 3 が向く。

3. その Label を大量に並べるか。
後述のとおり、アンダーバーを含む文字列を与えた Label はレイアウト時間が約 3 倍になる。方法 3 はこの増加も避けられるため、一覧やグリッドでは方法 3 が有利である。

4. 既に独自の ControlTemplate を持っているか。
持っているなら、そのテンプレート内の ContentPresenterRecognizesAccessKey="False" を足すだけで済む(方法 4)。持っていない場合、この方法のために既定テンプレートを再実装するのは割に合わない。


方法別の比較

方法 メリット デメリット 適するケース
方法 1: __ でエスケープ XAML 1 箇所の修正で済む。アクセスキーを維持できる 動的データには ViewModel 側の処理が必要。二重適用の危険がある 静的な文字列で、アクセスキーも使いたい場合
方法 2: TextBlock に変更 最もシンプルで軽量 LabelTarget 機能と既定余白を失う 表示専用でアクセスキーが不要な場合
方法 3: ContentTemplate を変更 動的バインドに対応でき、既定の外観を保てる XAML 量が増える。Content の型を選ばない Label を維持しつつ動的表示が必要
方法 4: RecognizesAccessKey="False" 原因を直接無効化できる。ContentTemplate が不要 ControlTemplate の再実装が必要。既定の文字列表示にしか効かない 独自テンプレートを既に持っている場合

描画コストへの副作用

AccessText が挟まると visual が 1 段増えるため、描画コストにも影響する。
非仮想化の StackPanelLabel を 1,000 個並べて比較すると、アンダーバーを含む文字列を与えた場合のレイアウト時間は、含まない場合の約 3 倍になる。
一方、方法 3 の ContentTemplate を適用した Label は、アンダーバーを含まない Label とほぼ同じコストに収まる。

大量のテキストを表示する画面でアンダーバー入りのデータを扱う場合、方法 3 は表示の正しさと描画コストの両方に効く。
コントロール選択と描画コストの関係は「WPF で Label を大量配置すると遅い原因と TextBlock への置き換え指針」で詳しく扱う。


注意点


まとめ

WPF の Label でアンダーバーが消えるのは、既定テンプレートの ContentPresenterRecognizesAccessKey="True" であり、文字列を AccessText として描画するためである。
Label 固有の問題ではなく、ButtonCheckBoxRadioButtonGroupBoxExpanderTabItemMenuItemHeader でも同じ結果になる。

分岐点はアクセスキーを使うかどうかにある。使うなら方法 1、使わないなら Label の外観をどこまで保ちたいかで方法 2〜4 から選ぶ。
動的なデータを扱う画面では、表示の正しさと描画コストの両方に効く方法 3 が既定の選択肢になる。