DataGridで特定の列のセルを全選択する

C#
スポンサーリンク

セル全選択じゃなくて列のセル全選択がしたい

DataGridでは「Ctrl+A」を押すことですべての行、列のセルを選択状態にできます。
これは既定の動作です。

さんさめ
さんさめ

違う。そうじゃない。
私は列の全選択がしたいんだ

一応、任意のセルをクリックしてから以下のいずれかの操作を行うと、
それっぽい選択ができます。

  • クリックを離さず目的のセルまでドラッグする
  • Shiftキーを押しながら目的のセルをクリックする
  • Ctrl+Shift+↓キー(もしくは↑キー)で上端か下端まで範囲選択

…どれもまぁまぁ手間です。ドラッグは数百行あったらもうやってらんないですし、
最後の操作は知らなかったら絶対やらないですね。

Shiftキーを押しながら目的のセルをクリックするのが、
操作としてはある程度楽なのですが、
頻繁に列選択したい場合は結構な手間となります。

さんさめ
さんさめ

よし、列選択する処理を作ろう

Viewにしか依存しない処理なので、
とりあえずビハインドにゴリゴリ書いてみます。

列セル選択するコード例その1(お行儀が良いパターン)

以下は、ボタンを押したときに、
選択中のセルがあったら、その列のセルを全選択するコードです。

private void Button_Click(object sender, RoutedEventArgs e)
{
    // 選択中のセルを取得し列を特定
    var columns = MyDataGrid.SelectedCells
        .Where(x => x.IsValid)
        .Select(x => x.Column)
        .Distinct()
        .ToArray();

    // 全選択したいので行に関してはItemsから引っ張る
    var items = MyDataGrid.Items
        .OfType<object>()
        .ToArray();

    // 列と行からDataGridCellInfoを作成
    var cellInfoArray = items
        .SelectMany(x => columns.Select(y => new DataGridCellInfo(x, y)))
        .ToArray();

    // 順次SelectedCellsに追加する
    MyDataGrid.SelectedCells.Clear();
    foreach (var cellInfo in cellInfoArray)
    {
        MyDataGrid.SelectedCells.Add(cellInfo);
    }

    // 選択されたことが分かるように
    // 最後に選択されたセルにスクロールさせる
    var lastCellInfo = cellInfoArray.LastOrDefault(x => x.IsValid);
    if (!lastCellInfo.Equals(default(DataGridCellInfo)))
    {
        MyDataGrid.ScrollIntoView(lastCellInfo);
        MyDataGrid.CurrentCell = lastCellInfo;
    }
}

早速使ってみます。

すると…

無事、列のセル全選択されました。

「なんかスクロールは効いてないけど?」
という場合は、フォーカスがButtonに取られちゃってるので
配置しているButtonのFocusableプロパティをFalseにしてみてください。

また、DataGridのSelectionUnitプロパティが、
「Cell」か、「CellOrRowHeader」の時じゃないと、
SelectedCellsをいじるあたりで例外になるので注意しましょう。
(既定値は「FullRow(セルの選択はできず常に行選択になる)」です)

ともあれ、これで表題である列のセル全選択処理ができました。
めでたしめでたし…

さんさめ
さんさめ

なんかもたつくぞ…?

リフレクションでinternalなAddRegionメソッドを使って高速化

実はこの列選択処理、
行数や列数が増えると明らかにカクつきます。
( 1000行程度では体感しにくいので、場合によってはこれで十分ですが)

どこが重いのか眺めてみたところ、
どうやらforeachで1つずつSelectedCellsに追加しているところが重そうです。

さんさめ
さんさめ

いかにも重そうではあるが…
Ctrl+Aのときはこんなカクつきなんてなかったぞ?

そうです、Ctrl+Aによる全選択ではこんなに待たされません。
なにやら非公開機能がありそうです。

というわけで、さっそくReferenceSourceを見に行きました。
(ReferenceSourceは.Net Frameworkですが業務で作っているアプリケーションは
まだ.Net Coreでは動かしてないのです…)

まずは、DataGrid.SelectedCellsがIList<DataGridCellInfo>なので、
具象クラスを調べます。

どうやら「System.Windows.Controls.SelectedCellsCollection」らしいです。

が、このクラスはほとんど実装がないので、
次は基底クラスの
System.Windows.Controls.VirtualizedCellInfoCollection」を見てみます。

Addメソッドがありました。色々Validationをした後、
最終的にAddRegionメソッドによって実際に選択セルを追加しているみたいです。

AddメソッドによるAddRegion呼出しでは
列の個数1, 行の個数1で呼んでいるだけな(Regionである意味がない)ので、
これをforeachで回すのは何やら無駄が多そうなことが分かります。

さんさめ
さんさめ

これ使わせてもらったら速くなりそうだな…

というわけでさっそくやってみました。

列セル選択するコード例その2(リフレクションを使うパターン)

コードは以下のようになります。

private void Button_Click(object sender, RoutedEventArgs e)
{
    // 選択中のセルを取得し列を特定
    var columns = MyDataGrid.SelectedCells
        .Where(x => x.IsValid)
        .Select(x => x.Column)
        .Distinct()
        .ToArray();

    // リフレクションでAddRegionメソッドをとってきて呼び出す。
    var type = MyDataGrid.SelectedCells.GetType();
    var addRegionMethodInfo = type.GetMethod("AddRegion",
        System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);

    // 列を飛び飛びで選択することがありえるので、
    // あえて別々に呼び出す
    foreach (var column in columns)
    {
        addRegionMethodInfo?.Invoke(MyDataGrid.SelectedCells,
            new object[]
            {
                0,                      /* rowIndex */
                column.DisplayIndex,    /* columnIndex */
                MyDataGrid.Items.Count, /* rowCount */
                1,                      /* columnCount */
            });
    }

    // 選択されたことが分かるように
    // 最後に選択されたセルにスクロールさせる
    var lastCellInfo = MyDataGrid.SelectedCells.LastOrDefault(x => x.IsValid);
    if (!lastCellInfo.Equals(default(DataGridCellInfo)))
    {
        MyDataGrid.ScrollIntoView(lastCellInfo);
        MyDataGrid.CurrentCell = lastCellInfo;
    }
}

挙動を保ったまま爆速になりました。

リフレクションで取得したMethodInfoはキャッシュして、
使いまわした方がより良いですね。

また、一括でSelectedCellsに追加が行われるため、
SelectedCellsChangedイベントを購読しているときも、
無駄にたくさん来ることがなくなります。

念のため補足しておくと、この処理は.Net Core 3.1でも動きます。

ただ、当たり前ですが、
将来実装が変わってメソッドがなくなったら動かなくなるので、
そのあたりは自己責任でお願いします。

さんさめ
さんさめ

DataGridとか闇が深過ぎるから
今更実装変えないっしょ…(フラグ)

ここまでお読みいただきありがとうございました。

コメント

タイトルとURLをコピーしました