WPF DataGrid列リサイズで水平スクロールが暴走する問題と対策まとめ【Thumb自前実装で安定化】

WPF の DataGrid を使っていると、列が多くなったタイミングで「列幅をちょっと動かしただけなのに、急にものすごい横スクロールが発生する」「元の幅に戻せなくなる」といった違和感のある挙動に出会うことがあります。本記事では、この現象の原因を整理しつつ、Thumb をフックした自前実装による安定した列リサイズ方法、Auto 列幅の再計測テクニック、MinWidth を使ったガード方法まで、実運用レベルで役立つ回避策をまとめます。

目次

WPF DataGrid の列リサイズで起こる「水平スクロール暴走」現象

まずは問題の現象を整理します。対象は WPF の標準 DataGrid コントロールで、列が多く水平スクロールバーが表示されている状態です。

発生する症状

  • 複数列を表示し、水平スクロールバーが出る程度まで列を広げる。
  • 水平スクロールバーを右端まで動かした状態で、中央付近の列ヘッダーのグリッパー(列ヘッダー右側のドラッグ用 Thumb)を操作する。
  • 一度最小幅(ほぼゼロ)まで左方向へ縮め、そのままグリッパーを右方向へドラッグすると…
  • 列幅がドラッグ量に対して極端に増加し、スクロール位置も大きく移動してしまう。
  • 結果として、「少し広げたいだけなのに急に何千ピクセルも広がる」「ユーザー操作で元の幅に戻しにくい」といった不自然な挙動になる。

特に、先頭列や末尾列では発生しにくく、「中央付近の列」「右端までスクロールしている」という条件が揃ったときに再現しやすいのが特徴です。

再現条件の整理

再現条件を手順ベースで整理すると次の通りです。

  1. DataGrid に複数列を定義し、列幅を調整して「水平スクロールバー」が表示される状態にする。
  2. 水平スクロールバーを右端までドラッグし、右端の列が見えている状態にする。
  3. 先頭列や末尾列ではない「中央付近の列」のヘッダー右側グリッパーをつかむ。
  4. 左方向にドラッグして列幅を最小(ほぼゼロ)まで縮める。
  5. つづけて同じグリッパーを右方向にドラッグすると、ドラッグ量に対して異常に大きく列幅が広がる。

この条件を表にまとめると次のようになります。

条件内容現象発生のしやすさ
列数水平スクロールバーが必要な程度に多い必須
スクロール位置水平スクロールバーが右端付近高
操作する列先頭/末尾以外の中央付近の列高
ドラッグ方向最小幅まで縮めた直後に逆方向へ拡大高
MinWidth 設定有り/無しに関わらず発生影響小(ただし折りたたみ防止には有効)

この挙動は、WPF DataGrid の既定実装における座標計算・スクロールオフセット補正の問題と考えられ、アプリ側で「設定を変えるだけ」で完全に止めるのは難しい挙動です。そのため、グリッパーのドラッグ処理をアプリ側で肩代わりするのが現実的な解決策となります。

解決方針の全体像

本記事で取り上げる対策は大きく 3 つです。

対策内容主な効果
① グリッパーの自前実装Thumb.DragDelta をフックし、ドラッグ量をそのまま列幅に反映する暴走的な幅拡大/スクロールの発生を抑止
② Auto 列幅の再計測0 → Auto に設定し直してレイアウトを安定させる初期レイアウトの不安定さ/ヘッダーとセル幅のズレを軽減
③ MinWidth によるガード最小幅を設定して折り畳みや極端な細さを防ぐ誤操作時の視認性の確保、安全網

特に① グリッパーの自前実装が最も効果が高く、これだけで問題が再現しなくなるケースがほとんどです。② と ③ は補助的なチューニングとして組み合わせるイメージです。

なぜ「暴走」するのか?挙動の背景

内部実装の詳細までは公開されていませんが、挙動から推測できるポイントを整理しておきます。

  • DataGrid は ScrollViewer の中でヘッダーとセルをレイアウトし、水平スクロール位置(HorizontalOffset)を考慮して列ヘッダーの座標を計算しています。
  • 既定の列リサイズでは、「ドラッグ開始時の列幅」「ドラッグ中のマウス座標」「スクロールオフセット」などを組み合わせて新しい列幅を計算していると考えられます。
  • 右端までスクロールした状態で中央列を極端に狭くした直後に拡大すると、内部で持っている「基準位置」と実際の座標がズレ、結果的にドラッグ量が過大に解釈されてしまう可能性があります。

つまり、既定の計算式がスクロールオフセットに敏感すぎることが原因のひとつと考えられます。であれば、もっと単純な計算(「現在幅 + ドラッグ量」)に置き換えてしまうことで現象を回避できる、というのが本記事のアプローチです。

対策①:グリッパーの DragDelta をフックして列幅を自前更新

最も確実な対策は、DataGridColumnHeader のテンプレート内にある Thumb(グリッパー)にイベントハンドラーを仕込み、ドラッグ量をそのまま列幅に加算する仕組みを自前実装することです。

実現イメージ

  • 各列ヘッダーがロードされたタイミングで、右・左グリッパー(PART_RightHeaderGripper / PART_LeftHeaderGripper)を取得する。
  • Thumb.DragDelta イベントで args.HorizontalChange を受け取り、現在の ActualWidth に加算した値を新しい幅として設定する。
  • 幅は DataGridLengthUnitType.Pixel で固定値として持つことで、Auto 幅の再評価に振り回されないようにする。
  • MinWidth を下回らないように Math.Max でガードする。

XAML 側の設定例

まずは DataGrid に対して DataGridColumnHeader のスタイルを追加し、Loaded イベントをフックします。

<DataGrid x:Name="TheDataGrid"
          AutoGenerateColumns="False">
    <DataGrid.Resources>
        <Style TargetType="{x:Type DataGridColumnHeader}">
            <EventSetter Event="Loaded" Handler="ColumnHeader_Loaded" />
        </Style>
    </DataGrid.Resources>

    <DataGrid.Columns>
        <DataGridTextColumn Header="Field 1"
                            Binding="{Binding Field_1}"
                            MinWidth="50" />
        <DataGridTextColumn Header="Field 2"
                            Binding="{Binding Field_2}"
                            MinWidth="50" />
        <DataGridTextColumn Header="Field 3"
                            Binding="{Binding Field_3}"
                            MinWidth="50" />
        <DataGridTextColumn Header="Field 4"
                            Binding="{Binding Field_4}"
                            MinWidth="50" />
    </DataGrid.Columns>
</DataGrid>

ここでは例として 4 列のテキスト列を定義し、すべての列ヘッダーに同じスタイル(Loaded イベントハンドラー)を適用しています。

C# コードビハインド例

続いて、Loaded 時にグリッパーを探し出して DragDelta をフックするコードです。

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

private void ColumnHeader_Loaded(object sender, RoutedEventArgs e)
{
    var header = (DataGridColumnHeader)sender;

    // 右側 / 左側のグリッパーをそれぞれフック
    AttachGripper(header, "PART_RightHeaderGripper", isLeft: false);
    AttachGripper(header, "PART_LeftHeaderGripper",  isLeft: true);
}

private void AttachGripper(DataGridColumnHeader header, string partName, bool isLeft)
{
    if (header.Template.FindName(partName, header) is Thumb grip)
    {
        grip.DragDelta += (s, args) =>
        {
            // 左グリッパーをドラッグした場合は方向が逆になる
            double delta = isLeft ? -args.HorizontalChange : args.HorizontalChange;

            var col = header.Column;
            if (col == null) return;

            double newWidth = col.ActualWidth + delta;

            // MinWidth を下回らないようにガード
            newWidth = Math.Max(col.MinWidth, newWidth);

            // Auto ではなく Pixel 固定で幅を持たせる
            col.Width = new DataGridLength(newWidth, DataGridLengthUnitType.Pixel);

            args.Handled = true;
        };
    }
}

この実装で得られる効果

  • スクロール位置や内部座標計算に依存せず、ドラッグ量と列幅の変化が 1:1 で対応するようになります。
  • 右端までスクロールした状態で中央列をリサイズしても、幅が「暴走」せず、ユーザーのドラッグに忠実な動きになります。
  • リサイズ後の列幅は Auto ではなく ピクセル固定になるため、データ内容やレイアウトの変化に左右されません。

実装時の注意点

  • ColumnHeader のテンプレートをカスタマイズしている場合、グリッパーの部品名(PART_RightHeaderGripper など)が存在しないケースがあります。その場合はテンプレート側で Thumb を定義し、適切な名前を付けてください。
  • FrozenColumnCount を使っている場合も基本的な考え方は同じですが、固定列とスクロール列で別のテンプレートを使っていることがあります。どちらのスタイルにも EventSetter を適用するか、状況に応じて使い分けましょう。
  • 「左側にだけグリッパーを出す」「右側だけを有効にする」など、UI ポリシーに応じて AttachGripper を制限することもできます。

MVVM で実装したい場合のアプローチ

上記のサンプルは最も分かりやすいコードビハインド形式ですが、プロジェクトの規約で「View からコードビハインドを極力なくしたい」という場合も多いと思います。その場合は、次のようなアプローチが考えられます。

  • Attached Behavior(添付ビヘイビア)として列リサイズロジックを切り出す。
  • Microsoft.Xaml.Behaviors.Wpf や Blend SDK を用いて、Interaction.Behaviors から Thumb の drag を処理する。
  • DataGrid に対して「列リサイズを安定化するビヘイビア」を 1 つアタッチし、そのビヘイビア内で Loaded イベントやテンプレートの部品名を解決する。

概念的なコードイメージは次のようになります(簡略化例)。

public class StableColumnResizeBehavior : Behavior<DataGrid>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.Loaded += OnGridLoaded;
    }

    private void OnGridLoaded(object sender, RoutedEventArgs e)
    {
        var headers = FindVisualChildren<DataGridColumnHeader>(AssociatedObject);
        foreach (var header in headers)
        {
            AttachGripper(header, "PART_RightHeaderGripper", isLeft: false);
            AttachGripper(header, "PART_LeftHeaderGripper",  isLeft: true);
        }
    }

    // AttachGripper は先ほどの実装とほぼ同じ
}

このようにしておくと、XAML 側では DataGrid に対してビヘイビアを 1 行追加するだけで、すべての列リサイズを安定化できるようになります。

対策②:Auto 列幅の「再計測」で初期レイアウトの不安定さを抑える

列幅を DataGridLength.Auto にしている場合、データ読み込み直後に「ヘッダーとセルの幅が微妙に合わない」「少しドラッグしただけで幅が大きく変わる」といった初期レイアウトの不安定さが出ることがあります。

これを抑える簡単なテクニックが、一度 0 幅にしてから Auto に戻す「再計測」です。

Auto 列再計測のサンプルコード

using System.Windows.Threading;

public void ResetAutoColumnWidths(DataGrid grid)
{
    // レイアウトが一度確定した後(DispatcherPriority.Render)に再計測
    grid.Dispatcher.Invoke(() =>
    {
        foreach (var c in grid.Columns)
        {
            // いったん 0 にしてから…
            c.Width = new DataGridLength(0);
        }

        foreach (var c in grid.Columns)
        {
            // Auto に戻すことでヘッダーとセルの計測をそろえる
            c.Width = DataGridLength.Auto;
        }
    }, DispatcherPriority.Render);
}

いつ呼び出すのがよいか

  • ItemsSource にデータをセットした直後。
  • 列定義(Columns)の変更を行った後。
  • ウィンドウサイズの変更などでレイアウトが大きく変わった後。

呼び出しタイミングの例としては、ViewModel 側でデータのロードが完了したタイミングでイベントを発行し、View で受け取って ResetAutoColumnWidths を呼ぶ、といったパターンが挙げられます。

再計測の効果

項目再計測なし再計測あり
ヘッダーとセルの幅のズレ表示直後に数ピクセルのズレが出る場合ありほぼ一致し、見た目が安定
ドラッグ時の感度ごく小さなドラッグで大きく変化することがあるドラッグ量に比例した自然な変化になりやすい
「暴走」現象への影響直接の解決にはならない発生頻度が下がるケースもあるが、根本対処にはならない

あくまで「初期レイアウトの安定化」を目的としたテクニックですが、列リサイズ全体の体感を大きく改善できるので、特に Auto 幅を多用している DataGrid では併用をおすすめします。

対策③:MinWidth で誤操作と視認性低下を防ぐ

MinWidth は今回の「幅の暴走」そのものを止めてくれるわけではありませんが、列を完全に折りたたんでしまうことを防ぎ、誤操作時の視認性を確保するために重要です。

MinWidth の設定例

<DataGridTextColumn Header="ユーザー名"
                    Binding="{Binding UserName}"
                    MinWidth="80" />

<DataGridTextColumn Header="メールアドレス"
                    Binding="{Binding Email}"
                    MinWidth="120" />

MinWidth 設定の目安

  • 番号・ID 列など、内容が短い列:40 ~ 60 ピクセル程度。
  • テキスト列(日本語名など):80 ~ 120 ピクセル程度。
  • 日付列:90 ~ 120 ピクセル程度(書式による)。

また、必要に応じて MaxWidth も設定しておくと、ユーザーが誤って列を極端に広げてしまうのを防げます。ただし、MaxWidth が小さすぎると「フルパス表示ができない」「重要な情報が途中で切れる」など UX を損なう場合もあるため、列ごとに慎重に決める必要があります。

実装をまとめたサンプル構成

ここまでの対策を組み合わせた、実践的な構成例を紹介します。

想定シナリオ

  • WPF DataGrid(AutoGenerateColumns = False)を使用。
  • 列リサイズ時の「暴走」対策として、Thumb.DragDelta の自前実装を適用。
  • データ読み込み完了時に Auto 列幅の再計測を実施。
  • 各列には MinWidth を設定し、誤操作に備える。

XAML 全体イメージ

<Grid>
    <DataGrid x:Name="TheDataGrid"
              AutoGenerateColumns="False"
              ItemsSource="{Binding Items}">

        <DataGrid.Resources>
            <Style TargetType="{x:Type DataGridColumnHeader}">
                <EventSetter Event="Loaded" Handler="ColumnHeader_Loaded" />
            </Style>
        </DataGrid.Resources>

        <DataGrid.Columns>
            <DataGridTextColumn Header="ID"
                                Binding="{Binding Id}"
                                MinWidth="50"
                                Width="Auto" />

            <DataGridTextColumn Header="名前"
                                Binding="{Binding Name}"
                                MinWidth="100"
                                Width="Auto" />

            <DataGridTextColumn Header="メールアドレス"
                                Binding="{Binding Email}"
                                MinWidth="120"
                                Width="Auto" />

            <DataGridTextColumn Header="登録日"
                                Binding="{Binding CreatedAt}"
                                MinWidth="100"
                                Width="Auto" />
        </DataGrid.Columns>
    </DataGrid>
</Grid>

コードビハインドでの組み合わせ例

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MainViewModel();

        // データロード完了後に Auto 再計測を行う例
        this.Loaded += (s, e) =>
        {
            // ItemsSource がバインドされた後に呼ばれるようにタイミングを調整
            ResetAutoColumnWidths(TheDataGrid);
        };
    }

    private void ColumnHeader_Loaded(object sender, RoutedEventArgs e)
    {
        var header = (DataGridColumnHeader)sender;
        AttachGripper(header, "PART_RightHeaderGripper", isLeft: false);
        AttachGripper(header, "PART_LeftHeaderGripper",  isLeft: true);
    }

    private void AttachGripper(DataGridColumnHeader header, string partName, bool isLeft)
    {
        if (header.Template.FindName(partName, header) is Thumb grip)
        {
            grip.DragDelta += (s, args) =>
            {
                double delta = isLeft ? -args.HorizontalChange : args.HorizontalChange;

                var col = header.Column;
                if (col == null) return;

                double newWidth = col.ActualWidth + delta;
                newWidth = Math.Max(col.MinWidth, newWidth);

                col.Width = new DataGridLength(newWidth, DataGridLengthUnitType.Pixel);
                args.Handled = true;
            };
        }
    }

    public void ResetAutoColumnWidths(DataGrid grid)
    {
        grid.Dispatcher.Invoke(() =>
        {
            foreach (var c in grid.Columns)
            {
                c.Width = new DataGridLength(0);
            }
            foreach (var c in grid.Columns)
            {
                c.Width = DataGridLength.Auto;
            }
        }, DispatcherPriority.Render);
    }
}

このように ① 〜 ③ を組み合わせることで、列リサイズ時にスクロールが暴走しないだけでなく、初期表示時の見た目も安定した DataGrid を構築できます。

運用上の注意点と設計のコツ

最後に、実際の業務アプリケーションでこの対策を適用する際のポイントをいくつか挙げます。

「列幅のポリシー」を決めておく

  • ユーザーがリサイズした列幅をアプリ終了後も保持するか(設定ファイルなどに保存するか)。
  • 幅はすべて固定値で運用するのか、一部 Auto 幅の列を残すのか。
  • ウィンドウリサイズ時に列幅を再計算するのか、そのままにしてスクロール量で調整するのか。

これらをあらかじめ決めておくと、実装がブレずに済みます。今回のようにピクセル幅固定にする場合は、「リサイズ後は固定幅になる」ことを前提に UI を設計するとユーザーの混乱を抑えられます。

あえて「リサイズ禁止」にする列を決める

何でもかんでもユーザーにリサイズさせるのではなく、

  • 行番号、チェックボックス列、アイコン列などはリサイズ不可にする。
  • 操作ボタン列(編集・削除など)は幅を固定にしておき、「触れない列」として扱う。

といったポリシーを決めておくと、ユーザー操作がシンプルになり、不具合の温床も減らすことができます。リサイズを禁止するには、該当列の CanUserResize を false にするなどの方法があります。

パフォーマンスと見た目のバランス

Auto 幅は便利ですが、行数が多い場合には計測コストがかかります。すべてを Auto にするのではなく、

  • 文字列が比較的短く、幅が読みやすい列は固定幅(Pixel)運用にする。
  • 内容の長さにばらつきがある列(説明文など)は Auto にして、必要に応じてユーザーがリサイズできるようにする。

といった棲み分けをすると、「見た目」と「軽快さ」のバランスが取りやすくなります。

修正後の動作確認チェックリスト

最後に、実装が正しく機能しているかを確認するためのチェックリストを掲載します。実装後の受け入れテストに活用してください。

  • 列を広げて水平スクロールバーを表示できること。
  • 水平スクロールバーを最右端まで動かし、中央付近の列ヘッダーの右グリッパーをドラッグする。
  • 中央列を最小幅まで縮め、そのまま右方向へドラッグしても、列幅がドラッグ量に比例して滑らかに変化し、「暴走」しないこと。
  • Auto 幅を使用している列で、ウィンドウ表示直後にヘッダーとセルの幅がずれていないこと(ResetAutoColumnWidths を適用している場合)。
  • MinWidth を下回る幅にならないこと。極端に細い列や折り畳み状態にならないこと。
  • FrozenColumnCount を設定している場合、固定列とスクロール列の両方で同様に自然なリサイズができること。
  • 大量データ(数千行)を表示しても、列リサイズ操作時に UI が固まらないこと。

まとめ:グリッパーを「自前化」して安定した DataGrid に

WPF DataGrid の列リサイズで、水平スクロールが極端に動いてしまう現象は、フレームワーク側の座標計算とスクロールオフセットの取り扱いが絡んだ「素直には制御しづらい」挙動です。設定値をあれこれ変えるだけでは根本的な解決にならないことが多く、Thumb.DragDelta をフックして列幅を自前で更新するのが、現時点で最も安定した対処法と言えます。

加えて、Auto 列幅の再計測で初期レイアウトを安定させ、MinWidth / MaxWidth でユーザー操作範囲を適切にガードすることで、見た目・操作性ともにストレスの少ない DataGrid を実現できます。複数の DataGrid を抱える業務アプリでは、一度これらのパターンをコンポーネント化・ビヘイビア化しておき、全画面で統一的に適用していくことをおすすめします。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次