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

StackPanel の中に置いた ScrollViewer がスクロールしないのは、StackPanel が高さを制約しないためである。原因と、Grid や DockPanel を使った解決方法・選択基準を解説する。

概要

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 項目を表示したもの。左は ScrollViewerStackPanel の中に置いた場合で、スクロールバーが出ず領域の下端で内容が切れている。右は Grid* 行に置いた場合で、スクロールバーが現れて残りの項目まで到達できる。

原因・背景

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

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

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


解決方法

ScrollViewer を、高さが制約されるコンテナに配置する。
具体的には、Grid*(Star)指定の行に置くか、DockPanel で残り領域に収めるか、明示的な HeightMaxHeight を与える。
これにより 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 の高さが制約される。


注意点


代替案・比較

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

まとめ

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

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

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