セル全選択じゃなくて列のセル全選択がしたい
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とか闇が深過ぎるから
今更実装変えないっしょ…(フラグ)
ここまでお読みいただきありがとうございました。
コメント