WPF で ScrollViewer がスクロールしない原因と解決方法

縦方向の StackPanel に置いた ScrollViewer がスクロールしないのは、StackPanel が高さを制約せずに測定するためである。原因と、Grid・DockPanel・MaxHeight による解決方法と選択基準を解説する。

概要

WPF の ScrollViewer は、内部の要素がビューポートより大きいときにスクロールバーを表示し、スクロールを可能にするコントロールである。
しかし StackPanel の中に ScrollViewer を置くと、内容がどれだけ増えてもスクロールバーが現れず、要素が下方向に伸び続ける問題が起きる。
本記事では、この現象がレイアウトの測定処理に起因することを説明し、コンテナの選び方による解決方法と選択基準を整理する。


前提・対象環境

本記事の表は、上記の環境で実際に計測したものである。
計測の条件は、それぞれの表の説明に書いた。
この環境で確認しているのは次の点である。


問題

縦方向の StackPanel の中に ScrollViewer を配置し、その中へ多数の要素を並べても、スクロールバーが表示されずに画面外まで要素が伸びてしまうことがある。

<StackPanel>
    <TextBlock Text="ヘッダー" />
    <ScrollViewer VerticalScrollBarVisibility="Auto">
        <StackPanel>
            <!-- 大量の項目 -->
        </StackPanel>
    </ScrollViewer>
</StackPanel>

VerticalScrollBarVisibility="Auto" を指定しているにもかかわらず、ScrollViewer は内容全体の高さまで広がり、スクロールバーは現れない。

同じ高さの領域に同じ 12 項目を表示した 2 つの画面。StackPanel の中に置いた側はスクロールバーが無く 7 項目目で切れている。Grid の Star 行に置いた側はスクロールバーが表示されている。
同じ高さの領域に、同じヘッダーと 12 項目を表示したもの。左は ScrollViewer を StackPanel の中に置いた場合で、スクロールバーが出ず領域の下端で内容が切れている。右は Grid の * 行に置いた場合で、スクロールバーが現れて残りの項目まで到達できる。

原因・背景

原因は、StackPanel が子要素を測定する際に渡す利用可能サイズにある。
StackPanel は積み重ねる方向(縦方向の場合は高さ)について、子要素へ無限の利用可能サイズを渡して測定する。
公式ドキュメントでも、StackPanel はスタックする方向で子要素を制約しないことが示されている。

ScrollViewer は、与えられた高さより内容が大きいときにだけスクロールバーを表示する。
ところが StackPanel から無限の高さを渡されると、ScrollViewer は内容全体が収まる高さを要求し、オーバーフローが発生しない。
このため、スクロールバーは表示されず、ScrollViewer 自体が内容と同じ高さまで伸びてしまう。

問題の本質は ScrollViewer 側ではなく、それを囲む StackPanel が高さを制約していない点にある。


この違いは、親のレイアウトだけを変えて ScrollViewer の高さを読めば確かめられる。

親のレイアウト Extent Viewport Scrollable スクロールバー
StackPanel の中 800 800 0 Collapsed
Grid の中 800 200 600 Visible
DockPanel の中(LastChildFill) 800 200 600 Visible
StackPanel + Height を明示 800 200 600 Visible
StackPanel + MaxHeight 800 200 600 Visible

.NET 10 / Windows 11 で、高さ 20px の行を 40 個(合計 800px)持つ ScrollViewer を、高さ 200px に制約した親の中へ置いて測った結果。親のレイアウト以外の条件は同一である。

StackPanel の中では ViewportHeight が ExtentHeight と同じ 800 になっている。 200px に制約されたはずの親の中にいながら、内容全体が収まる高さを確保している。
その結果 ScrollableHeight は 0 となり、スクロールバーは Collapsed のまま現れない。

Grid と DockPanel では ViewportHeight が 200 に収まり、差の 600 が ScrollableHeight になる。
最終行のとおり、StackPanel のままでも ScrollViewer に高さを明示すれば同じ結果になる。制約が伝わりさえすればよい。


解決方法

ScrollViewer を、高さが制約されるコンテナに配置する。
具体的には、Grid の *(Star)指定の行に置くか、DockPanel で残り領域に収めるか、明示的な Height・MaxHeight を与える。
これにより ScrollViewer へ有限の高さが渡され、内容がその高さを超えたときにスクロールバーが表示される。


実装例

Grid の Star 行に配置する

Grid の行を「固定サイズの行」と「残り領域を埋める * 行」に分け、スクロールさせたい ScrollViewer を * 行へ置く。

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <TextBlock Grid.Row="0" Text="ヘッダー" />

    <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
        <StackPanel>
            <!-- 大量の項目 -->
        </StackPanel>
    </ScrollViewer>
</Grid>

* 行は親の残り高さを受け取るため、ScrollViewer には有限の高さが渡される。
内容がその高さを超えると、スクロールバーが自動的に表示される。

DockPanel で残り領域に収める

DockPanel は最後の子要素を残り領域へ広げる LastChildFill(既定で有効)を持つ。
ヘッダーを上端にドッキングし、ScrollViewer を最後の子として残り領域に収める。

<DockPanel>
    <TextBlock DockPanel.Dock="Top" Text="ヘッダー" />

    <ScrollViewer VerticalScrollBarVisibility="Auto">
        <StackPanel>
            <!-- 大量の項目 -->
        </StackPanel>
    </ScrollViewer>
</DockPanel>

DockPanel.Dock を指定しない最後の子は残り領域に収まるため、ScrollViewer の高さが制約される。


注意点

構成 実体化した ListBoxItem CanContentScroll(値の出どころ)
ListBox Height=180 10 True (DefaultStyle)
ScrollViewer.CanContentScroll=False 2,000 False (Local)
VirtualizingPanel.IsVirtualizing=False 2,000 True (DefaultStyle)
CollectionView.GroupDescriptions だけ(GroupStyle なし) 10 True (DefaultStyle)
CollectionView.GroupDescriptions + GroupStyle 2,000 False (TemplateTrigger)
外側の ScrollViewer の中(Height=180) 2,000 True (DefaultStyle)
StackPanel の中(Height=180) 2,000 True (DefaultStyle)

.NET 10 / Windows 11 で、2,000 件を持つ ListBox を条件ごとに表示し、実体化された ListBoxItem を数えた結果。外側の ScrollViewer と StackPanel には高さ 180 を与え、ListBox 自体には高さを与えていない。


代替案・比較

コンテナ スクロールの挙動 適するケース
StackPanel(縦) 高さを制約しないため、明示的な Height・MaxHeight を与えない限り ScrollViewer はスクロールしない 内容が短く、スクロールが不要な単純な積み重ね
Grid の * 行 残り高さが渡り、内容超過時にスクロールする ヘッダー・フッターと可変領域を明確に分けたい画面
DockPanel(LastChildFill) 残り領域に収まり、内容超過時にスクロールする 端固定要素と本体領域を組み合わせる画面
明示的な Height・MaxHeight 指定した高さを超えるとスクロールする 高さの上限を固定したい部分的なリスト

まとめ

StackPanel はスタック方向で子要素に無限の高さを渡すため、内部の ScrollViewer はオーバーフローを検知できずスクロールしない。
問題の本質は ScrollViewer ではなく、それを囲むコンテナが高さを制約していない点にある。

選択の基準は次のとおりである。

いずれの場合も、ScrollViewer へ有限の高さが渡る構成にすることが解決の要点である。