WPF の ListBox に長文テキストを表示すると TextWrapping="Wrap" を付けているのに 1 行で切れてしまい、行の高さが伸びない――。特に 2 列 Grid に並べて GridSplitter で幅を変える画面で起きやすい現象です。本稿では原因を測定(Measure)/配置(Arrange)の仕組みから説明し、最短で直る設定と、レイアウトの作法・落とし穴・実務でのチェックリストまでを網羅的に解説します。
問題の概要と再現コード
以下のように、ListBox のアイテム テンプレートで TextBlock に TextWrapping="Wrap" を指定しても、テキストが折り返されず 1 行で途切れることがあります。Grid に 2 列配置し、間に GridSplitter を置くと発生率が上がります。
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <!-- ← ここが Auto だと起きやすい -->
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0" ItemsSource="{Binding LongTexts}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" TextWrapping="Wrap"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch"/>
<ListBox Grid.Column="2" ItemsSource="{Binding OtherTexts}"/>
</Grid>
ズバリの結論(最短で直す)
最も手軽で効果が高いのは、ListBox に ScrollViewer.HorizontalScrollBarVisibility="Disabled" を付けることです。これにより ListBox 内部の ScrollViewer が横スクロールを想定しなくなり、アイテムの測定幅が列幅に固定されます。結果としてテキストが正しく折り返され、行の高さも内容に応じて自動で拡張されます。
<ListBox
ItemsSource="{Binding LongTexts}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
HorizontalContentAlignment="Stretch"> <!-- 併用推奨 -->
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" TextWrapping="Wrap"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
なぜ折り返されないのか ― WPF の Measure/Arrange を正しく理解する
原因はレイアウト段階の「幅の認識」にあります。WPF の Measure は親要素が子要素に「使ってよい幅・高さ」を渡し、子はその制約内で望ましいサイズ(DesiredSize)を返します。ListBox のテンプレートには ScrollViewer が内包されていますが、横方向スクロールが許可されている場合(Auto/Visible)、ScrollViewer は子に対して横幅を 無限大(∞) として測定する振る舞いをしがちです。このとき、
TextBlock TextWrapping="Wrap"は「与えられた幅で折り返す」という仕様なので、幅が無限だと折り返す必要が無いと判断します。- 結果、アイテムは 1 行表示のままになり、行の高さも伸びません。
さらに、Grid の列幅が Auto だと「内容に合わせて広がる」ため、広がる → 無限幅とみなす → 折り返さないという悪循環が生まれます。GridSplitter でリサイズしても、ListBox が「実幅」を安定的に認識できないと同じ問題が続きます。
解決策・対処方法(比較表)
| 施策 | 効果・ポイント | 補足 |
|---|---|---|
① ListBox に ScrollViewer.HorizontalScrollBarVisibility="Disabled" を付与 | 横スクロールを前提にしなくなるため、アイテムの測定幅が列幅に固定。 テキストが正しく折り返され、行の高さも内容に応じて拡張。 | 最も簡単で確実。既存テンプレートに手を入れず適用可。 |
② ListBox に HorizontalContentAlignment="Stretch" を設定 | 各 ListBoxItem が列幅いっぱいに広がり、内部要素が「無限幅」と誤認しにくくなる。 | ①の併用を推奨。横スクロールを無効化できない場合の代替にも。 |
③ Grid の列幅を *(スター)指定にする | GridSplitter でリサイズしても各列が実幅を持ち続け、ListBox に正しい幅が伝わる。 | 左右 2 列なら * / *、中央に Splitter を置く構成が安定。 |
④ 表示専用なら TextBlock を使う | TextBox より軽量で描画が安定。編集が不要ならこちらが推奨。 | 編集が必要な場合のみ TextBox TextWrapping="Wrap" を選択。 |
⑤ 最終手段:MaxWidth を強制 | アイテム内テキストの最大幅を ListBox の幅に束縛して強制的に折り返す。 | 局所対応。根本原因(横スクロール前提・Auto 幅)も合わせて見直し推奨。 |
推奨構成(サンプル)
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/> <!-- GridSplitter -->
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
```
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" TextWrapping="Wrap"/>
</DataTemplate>
</ListBox.ItemTemplate>
<!-- あるいは全アイテムに Stretch を強制 -->
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ListBox.ItemContainerStyle>
```
「幅が無限だと折り返さない」― よくある落とし穴
- ScrollViewer の横スクロールが許可されている
内部 ScrollViewer が横方向にスクロールできると、子の測定に無限幅を渡すことがあり、折り返しが無効化されます。まずはHorizontalScrollBarVisibility="Disabled"。 - Grid の列幅が
Auto
内容に応じて広がるため、ListBox が「自分の幅」を測定できなくなります。レイアウトが確定する*指定に見直しましょう。 - ItemsPanel を StackPanel(水平)にしている
独自のItemsPanelTemplateで横向き StackPanel を使うと、横幅があふれてもスクロール前提になりがちです。既定のVirtualizingStackPanel(垂直)を基本とし、必要時のみ変更します。 - ListBoxItem が Stretch していない
既定ではコンテンツ幅で収まり、列幅いっぱいに広がりません。HorizontalContentAlignment="Stretch"を忘れずに。 - 長大な「空白なし単語」
非常に長い ID などは単語境界で改行されません。必要に応じてゼロ幅スペース​を差し込むか、TextTrimmingの併用を検討します。
TextBlock と TextBox の選び分け
| 要素 | 折り返しの指定 | 性能・用途 | 注意点 |
|---|---|---|---|
TextBlock | TextWrapping="Wrap" | 軽量・描画が速い。表示専用なら最有力。 | 選択や編集が不要な箇所に最適。 |
TextBox | TextWrapping="Wrap"(必要に応じ AcceptsReturn="True") | 編集可能。行高さは内容に追従。 | 大量アイテムでは描画コストが上がるため、仮想化の維持に留意。 |
「どうしても横スクロールを残したい」ケースの対処
横スクロールを完全には消せない要件でも、以下の幅固定テクニックで折り返しを成立させられます。
<TextBlock
Text="{Binding}"
TextWrapping="Wrap"
MaxWidth="{Binding RelativeSource={RelativeSource AncestorType=ListBox}, Path=ActualWidth}"/>
これはテキストの最大幅を ListBox の実幅に束縛することで、ScrollViewer が無限幅を渡していても実質的に折り返しを発生させる手法です。根本原因は残るため、レイアウト全体の見直しと併用してください。
GridSplitter と列幅のベストプラクティス
- 左右 2 列なら
<ColumnDefinition Width="*"/> <ColumnDefinition Width="5"/> <ColumnDefinition Width="*"/>を基本形に。 - 固定幅が必要な列は
Autoではなく100のような「実数値」を使うと安定。Autoは「内容依存」のため、測定順序に引きずられやすい。 - Splitter は列間(専用 5px 列)に置き、隣接列に MinWidth を設定して折り返し時の極端な再配置を回避。
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="200"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="2*" MinWidth="300"/>
</Grid.ColumnDefinitions>
仮想化(Virtualization)との関係
既定の ListBox は VirtualizingStackPanel を使ってアイテムの仮想化を行います。HorizontalScrollBarVisibility="Disabled" や HorizontalContentAlignment="Stretch" は、仮想化を無効化しません。ただし、テキスト折り返しで行高が可変になると再測定が増えるため、数万件規模では描画コストが上がる可能性があります。以下の設定で滑らかさと可読性のバランスを取りましょう。
<ListBox
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
VirtualizingPanel.ScrollUnit="Pixel"
ScrollViewer.CanContentScroll="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
HorizontalContentAlignment="Stretch"/>
- Recycling はアイテムコンテナの再利用で GC 負荷を低減。
- ScrollUnit=”Pixel” は可変高さアイテムと相性が良い(細かいスクロール単位)。
デバッグのコツ(原因切り分け)
- 幅の可視化:テンプレート内のルートに背景色を付け、
Stretchできているか目で確認。 - 実幅の確認:一時的に
TextBlock.Textに{Binding RelativeSource={RelativeSource AncestorType=ListBox}, Path=ActualWidth, StringFormat=Width:\{0\}}を足し、Measure が列幅を見ているか確認。 - 列幅変更の追従:
GridSplitterで幅変更後も折り返しが追従しない場合は、列がAutoになっていないか再点検。
落ちやすい罠のパターン集
- DataTemplate 内でさらに ScrollViewer をネスト:余計な ScrollViewer は無限幅の温床。基本は 1 つに。
- ItemsPanel を WrapPanel に変更:カード風には便利だが、横方向が自由になりやすく折り返し要件と衝突しがち。
- アイテムに固定幅を持たせる:アイテム側で
Widthを固定すると列リサイズに追従しない。MaxWidth束縛が安全。
運用チェックリスト(そのまま使える)
| チェック | OK 条件 | メモ |
|---|---|---|
| 横スクロール無効 | ScrollViewer.HorizontalScrollBarVisibility="Disabled" | 最優先。迷ったらまずこれ。 |
| Stretch 指定 | HorizontalContentAlignment="Stretch" | ItemContainerStyle で一括適用が便利。 |
| 列幅の安定 | Grid 列は * 指定 | Split 可能な 2 列は */* が基本。 |
| 可変高さ対応 | VirtualizationMode="Recycling" + ScrollUnit="Pixel" | 大量データでの滑らかさを確保。 |
| 長大単語対策 | ゼロ幅スペースや TextTrimming | 要件に応じて表示優先/可読性優先を選択。 |
実例:スタイルで共通化する
画面全体の ListBox に同じ規約を適用したいなら、リソース化して再利用すると安全です。
<Window.Resources>
<Style x:Key="WrappingListBoxStyle" TargetType="ListBox">
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True"/>
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling"/>
<Setter Property="VirtualizingPanel.ScrollUnit" Value="Pixel"/>
</Style>
TextBox を使う場合の注意点(編集シナリオ)
TextBox TextWrapping="Wrap"に加えて、手入力で改行を許すならAcceptsReturn="True"。- 既定の境界描画で視認性が強くなるため、読み取り専用表示では
IsReadOnly="True"とBorderThickness="0"で見た目を落ち着かせる。 - 大量表示時は描画コストが大きいので、仮想化設定とスクロール単位の見直しを忘れずに。
<DataTemplate>
<TextBox Text="{Binding Detail}"
TextWrapping="Wrap"
AcceptsReturn="True"
IsReadOnly="{Binding IsReadOnly}"
BorderThickness="0"/>
</DataTemplate>
原因別・最短回答(クイックリファレンス)
| 症状 | 原因 | 即効策 |
|---|---|---|
| 折り返されない / 行が伸びない | 横スクロール前提で無限幅測定 | ScrollViewer.HorizontalScrollBarVisibility="Disabled" |
| Splitter で幅を変えても折り返し不安定 | 列幅が Auto | 列を * 指定に |
| 一部アイテムだけ 1 行になる | ListBoxItem が Stretch していない | HorizontalContentAlignment="Stretch" |
| 長い英数字がはみ出す | 単語境界なし | ​ を挿入 / トリミング併用 |
すべてを統合した完成サンプル
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MinWidth="240"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*" MinWidth="240"/>
</Grid.ColumnDefinitions>
```
<ListBox.ItemTemplate>
<DataTemplate>
<Border Padding="6" Margin="2" Background="#08000000">
<TextBlock Text="{Binding}" TextWrapping="Wrap"/>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
```
まとめ ― 原因と対処の指針
- 横スクロールを無効化して「幅は列幅まで」という制約をはっきりさせる。
- Item を Stretch させて列幅いっぱいを使う。
- Grid の列は
*指定で安定した実幅を ListBox に伝える。 - 表示専用は TextBlock、編集は TextBox と役割を分ける。
- 長大単語や超大量データなど例外条件では、MaxWidth 束縛や仮想化設定で微調整。
以上を守れば、「TextWrapping を付けているのに折り返されない」「行の高さが伸びない」といった不思議な挙動は再現しなくなります。根本は「レイアウトエンジンに正しい幅の制約を渡すこと」。そのカギが、ScrollViewer の横スクロール無効化とStretch、そして列の * 指定です。

コメント