【WPF】DataGridでページ内検索的なことがしたいその2

C#

こんにちは、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ページ内検索はシリーズ記事となっております。
他記事は以下のリンクからどうぞ。

コメント

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