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

C#

こんにちは、C#プログラマーのさんさめです。

本記事のサンプルは書きかけです。
参考にできるところはあるかもしれませんが、
基本的にこのままでは使いにくい
です

DataGridで、
いわゆるページ内検索のような
挙動を実現したいと思いました。

この挙動には大きく2つの要件仕様が混在しています。

  • 検索ワードを入れると
    • ヒットしたセルに次々と移動できる
    • セル内のヒットした部分はハイライトされる

また、
ページ内検索なので、
ViewModelが絡んでくる必要はありません。
極力Viewだけで完結させたいです。

今回は、
ヒットしたセルに次々と移動できる部分の実装を
お試しで作ってみました。

スポンサーリンク

サンプルコード

まずは、サンプルコードだけ載せます。
繰り返しになりますが、書きかけです。

<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" />
            <DockPanel
                x:Name="SearchBox"
                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="SearchTextBox" MinWidth="80" />
            </DockPanel>
        </Grid>
    </DockPanel>
</Window>

次に、コードビハインドです。
冒頭にも述べましたが、
Viewだけで完結する機能ですので、
ViewModel層はありません。

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 lastHitRowIndex;
    private int lastHitColumnIndex = -1;
    private string lastSearchWord;

    private void OnClickSearchNextButton(object sender, RoutedEventArgs e)
    {
        var text = SearchTextBox.Text;
        if (string.IsNullOrEmpty(text))
        {
            return;
        }
        if (text != lastSearchWord)
        {
            lastHitRowIndex = 0;
            lastHitColumnIndex = -1;
        }

        var currentLastHitColumnIndex = lastHitColumnIndex;
        // 前回ヒットが最終列だった場合のケア
        if (currentLastHitColumnIndex == MainDataGrid.Columns.Count - 1)
        {
            lastHitRowIndex++;
            currentLastHitColumnIndex = -1;
        }

        // DataGridのセルの中身を片っ端から確認
        for (int i = lastHitRowIndex; i < MainDataGrid.Items.Count; i++)
        {
            var item = MainDataGrid.Items[i];
            for (int j = currentLastHitColumnIndex + 1; j < MainDataGrid.Columns.Count; j++)
            {
                var column = MainDataGrid.Columns[j];
                var cellContent = column.OnCopyingCellClipboardContent(item)?.ToString();
                if (cellContent?.Contains(text) == true)
                {
                    var cellInfo = new DataGridCellInfo(item, column);
                    MainDataGrid.SelectedCells.Clear();
                    MainDataGrid.SelectedCells.Add(cellInfo);
                    MainDataGrid.CurrentCell = cellInfo;
                    MainDataGrid.ScrollIntoView(item, column);
                    lastHitRowIndex = i;
                    lastHitColumnIndex = j;
                    goto searchEnd;
                }
            }
            // 2行目以降は0からスタートするように
            currentLastHitColumnIndex = -1;
        }

        searchEnd:

        lastSearchWord = text;
    }

    private void OnClickSearchPrevButton(object sender, RoutedEventArgs e)
    {
        // 後で考える
    }
}

public class Person
{
    public string Name { get; set; }

    public int Age { get; set; }

    public string Address { get; set; }
}

挙動の確認

動いているところを見てみましょう。
(gifアニメです)

「→」ボタンを押すたびに、
検索ワードにヒットしたセルに移り変わっています。

フォーカスしていない時の
選択セルのハイライトが弱いので分かりづらいですね。

また、全体でいくつヒットしたのか分からないので
どこまで移動できるのか不安があります。

実装の解説

一番のキモはやはり、
セルの値を取得する部分でしょう。

var cellContent = column.OnCopyingCellClipboardContent(item)?.ToString();

OnCopyingCellClipboardContentというメソッドを使って、
セルに入力された値を取得しています。

通常のDataGridTextColumnならば、
コピー処理を呼び出すことでセルの値を取得できます。

また、仮想化をしていても
問題なく取得することができます。

引数はDataContextなので、
DataGrid.Itemsの中身を渡せばOKです。

次々にセルに移動するという部分では、
最後にヒットした行と列のインデックスを
記憶することで実現しています。

lastHitRowIndex = i;
lastHitColumnIndex = j;

同じワードで検索されたときには、
このインデックスから検索を開始することで、
次のセルに移動という挙動を実現しています。

残っているやりたい事

やりたい事だらけです。

  • Ctrl+Fで表示
  • 全体でいくつヒットしたか表示
  • 戻る方向の検索に対応
  • 最後のヒットだった時に最初に戻れるようにする
    検索結果が1周した時に通知する
  • ヒットした部分のハイライト
  • 検索エリアの見た目を整える
  • 機能をビヘイビアなどに切り出し
    後付けできるようにする

ページ内検索の挙動自体は、
参考資料が沢山あるので理想を設定しやすいですね。

まとめ

まとめです。

  • WPFのDataGridでページ内検索のようなことがしたくなった
  • セルを次々に選択する部分をお試し実装
  • まだまだやりたい事だらけ

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

シリーズ記事

DataGridページ内検索はシリーズ記事となっております。
他記事は以下のリンクからどうぞ。

関連記事

DataGridでよくやりたくなることの1つに
複数セル選択して一括入力というものがあります。

【WPF】DataGridで複数セルにまとめて入力できるようにするには
で2通りの実現方法について解説しています。

DataGridはそのまま扱うには
あまりにクセのあるコントロールです。

【WPF】DataGridをMVVMで使うなら変えておくべきプロパティ
にて、MVVMとして使う上で
必ず見ておきたいプロパティを解説しています。

コメント

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