こんにちは、C#プログラマーのさんさめです。
本記事のサンプルは書きかけです。
参考にできるところはあるかもしれませんが、
基本的にこのままでは使いにくいです。
DataGridで、いわゆるページ内検索のような
挙動を実現したいと思いました。
前回の記事はこちらです。
【WPF】DataGridでページ内検索的なことがしたいその1
前回は、
セルの中身と検索文字列を比較し
次々にフォーカスを移動させる部分を実装しました。
ただし、以下のような問題がありました。
- 進む方向のみの対応
- ヒットしたセルの総数が分からず
何が起きているか理解しづらい - ヒット部分のハイライトが無いため分かりづらい
今回は、戻る方向の検索と、
全体でいくつのセルにヒットしたかの表示に対応しました。
サンプルコード
今回も、まずはサンプルコードを載せます。
相変わらず、まだまだ発展途上です。
<Window
x:Class="DataGridSearch.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Width="400"
Height="400">
<DockPanel Margin="4">
<Button
Margin="4"
Click="Button_Click"
Content="検索"
DockPanel.Dock="Top" />
<Grid>
<DataGrid
x:Name="MainDataGrid"
Margin="4"
CanUserAddRows="False"
CanUserDeleteRows="False"
RowHeaderWidth="0"
SelectionUnit="CellOrRowHeader"
VirtualizingPanel.IsVirtualizing="True" />
<DockPanel
x:Name="SearchBox"
Margin="0 4 0 0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Visibility="Collapsed">
<Button
Click="Button_Click_1"
Content="X"
DockPanel.Dock="Right" />
<Button
Click="OnClickSearchNextButton"
Content="↓"
DockPanel.Dock="Right" />
<Button
Click="OnClickSearchPrevButton"
Content="↑"
DockPanel.Dock="Right" />
<TextBox
x:Name="SearchResultTextBox"
HorizontalAlignment="Right"
BorderThickness="0 1 1 1"
DockPanel.Dock="Right"
Foreground="Gray"
GotFocus="SearchResultTextBox_GotFocus"
IsReadOnly="True" />
<TextBox
x:Name="SearchTextBox"
MinWidth="120"
BorderThickness="1 1 0 1" />
</DockPanel>
</Grid>
</DockPanel>
</Window>
次に、コードビハインドです。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var people = Enumerable.Range(0, 200)
.Select(x => new Person()
{
Name = System.IO.Path.GetRandomFileName(),
Age = x,
Address = Guid.NewGuid().ToString(),
})
.ToList();
MainDataGrid.ItemsSource = people;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
SearchBox.Visibility = Visibility.Visible;
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
SearchBox.Visibility = Visibility.Collapsed;
}
private int currentIndex = 0;
private string lastSearchWord;
private readonly List<(int rowIndex, int columnIndex)> hitList
= new List<(int rowIndex, int columnIndex)>();
private void OnClickSearchNextButton(object sender, RoutedEventArgs e)
{
var text = SearchTextBox.Text;
if (string.IsNullOrEmpty(text))
{
return;
}
if (text == lastSearchWord)
{
if (hitList.Count == 0) { return; }
currentIndex++;
if (currentIndex >= hitList.Count)
{
currentIndex = 0;
}
FocusAndScroll(currentIndex);
return;
}
CalcHitList(text);
currentIndex = 0;
FocusAndScroll(currentIndex);
}
private void OnClickSearchPrevButton(object sender, RoutedEventArgs e)
{
var text = SearchTextBox.Text;
if (string.IsNullOrEmpty(text))
{
return;
}
if (text == lastSearchWord)
{
if (hitList.Count == 0) { return; }
currentIndex--;
if (currentIndex < 0)
{
currentIndex = hitList.Count - 1;
}
FocusAndScroll(currentIndex);
return;
}
CalcHitList(text);
currentIndex = hitList.Count - 1;
FocusAndScroll(currentIndex);
}
private void CalcHitList(string text)
{
hitList.Clear();
currentIndex = 0;
// DataGridのセルの中身を片っ端から確認
for (int i = 0; i < MainDataGrid.Items.Count; i++)
{
var item = MainDataGrid.Items[i];
for (int j = 0; j < MainDataGrid.Columns.Count; j++)
{
var column = MainDataGrid.Columns[j];
var cellContent = column.OnCopyingCellClipboardContent(item)?.ToString();
if (cellContent?.Contains(text) == true)
{
hitList.Add((i, j));
}
}
}
lastSearchWord = text;
}
private void FocusAndScroll(int hitIndex)
{
if (hitList.Count == 0)
{
SearchResultTextBox.Text = "0 / 0";
return;
}
var (rowIndex, columnIndex) = hitList[hitIndex];
var item = MainDataGrid.Items[rowIndex];
var column = MainDataGrid.Columns[columnIndex];
var cellInfo = new DataGridCellInfo(item, column);
MainDataGrid.SelectedCells.Clear();
MainDataGrid.SelectedCells.Add(cellInfo);
MainDataGrid.CurrentCell = cellInfo;
MainDataGrid.ScrollIntoView(item, column);
SearchResultTextBox.Text = $"{hitIndex + 1} / {hitList.Count}";
}
private void SearchResultTextBox_GotFocus(object sender, RoutedEventArgs e)
{
SearchTextBox.Focus();
}
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
}
挙動の確認
動いているところを見てみましょう。
(gifアニメです)
検索を実行すると検索ワード領域の右側に
ヒット総数と現在位置が出るようになりました。
戻る側の検索にも対応しています。
実装の解説
前回は逐次検索していましたが、
それだと総数表示ができないため、
検索実行時にヒット箇所のリストを構築するようにしました。
private void CalcHitList(string text)
{
hitList.Clear();
currentIndex = 0;
// DataGridのセルの中身を片っ端から確認
for (int i = 0; i < MainDataGrid.Items.Count; i++)
{
var item = MainDataGrid.Items[i];
for (int j = 0; j < MainDataGrid.Columns.Count; j++)
{
var column = MainDataGrid.Columns[j];
var cellContent = column.OnCopyingCellClipboardContent(item)?.ToString();
if (cellContent?.Contains(text) == true)
{
hitList.Add((i, j));
}
}
}
lastSearchWord = text;
}
OnCopyingCellClipboardContentメソッドを使って、
セルに入力された値を取得しているところは
前回と同じですが、結果にすぐにフォーカスせずに
ここではリストに蓄えるだけにしています。
そして、検索ワードが同じである限り、
そのリストを使いまわしてインデックスのみを移動させています。
if (text == lastSearchWord)
{
if (hitList.Count == 0) { return; }
currentIndex++;
if (currentIndex >= hitList.Count)
{
currentIndex = 0;
}
FocusAndScroll(currentIndex);
return;
}
また、インデックスを移動するだけになったので、
戻る方向の検索の処理がシンプルな違いで済むようになりました。
リストの再構築は
検索ワードが変わった時
ただし、編集できるDataGridの場合、
DataGridの中身が変わった時にも
リストを再構築しないといけません。
あとは、フォーカス移動が発生する時に、
ついでに表示している現在位置を更新すれば
今回の実装は完了です。
残っているやりたい事
- Ctrl+Fで検索バーを表示
- 編集が発生した時にリストを再構築する
- ヒットした部分のハイライト
- 検索エリアの見た目を整える
- 機能をビヘイビアなどに切り出し
既存のDataGridに後付けできるようにする
難易度が高いのは太字の部分ですね。
ビヘイビア切り出しまで行けると
良い落としどころになると思います。
まとめ
まとめです。
- DataGridでのページ内検索実装の続き
- 戻る方向の移動に対応
- 総数と現在位置表示に対応
最後までお読みいただき、ありがとうございました。
シリーズ記事
DataGridページ内検索はシリーズ記事となっております。
他記事は以下のリンクからどうぞ。
- 【WPF】DataGridでページ内検索的なことがしたいその1
- 【WPF】DataGridでページ内検索的なことがしたいその2(今ここ)
コメント