WPF DataGridでCtrl+F・F3検索を実装する方法|MVVMとBehaviorでブラウザ風インクリメンタルサーチ

WPFのDataGridで大量データを扱っていると、ユーザーからほぼ必ず「ブラウザみたいに Ctrl+F と F3 で検索したい」と言われます。この記事では、コードビハインドにべったり依存せず、MVVM+ICollectionView+Attached Behavior で、DataGridにブラウザ風のインクリメンタル検索&F3移動を実装する手順を具体的なコード付きで解説します。

目次

WPF DataGridでブラウザ風「Ctrl+F/F3」検索を実現する全体像

まずゴールを整理します。目指す挙動は、一般的なブラウザの検索バーとほぼ同じです。

操作挙動
Ctrl + FDataGridヘッダー(または上部)の検索TextBoxにフォーカスし、文字列入力を開始できる
入力中入力するたびに検索語が更新され、一致する行のリストを再計算する
F3次の一致へ移動(該当が末尾なら先頭へループ)
Shift + F3前の一致へ移動(先頭なら末尾へループ)
該当なし検索TextBoxを赤枠にしてユーザーに「0件」を明示

これを MVVM で実現するにあたり、責務を以下のように分けます。

レイヤー役割
Model従業員など、1行分のデータ(プロパティ+INotifyPropertyChanged)
ViewModelObservableCollectionとICollectionView、SearchText、Matches、Next/Prevロジック、件数・現在位置の管理
View (XAML)DataGrid本体、検索TextBox、赤枠スタイル、件数表示、IsSynchronizedWithCurrentItem
Attached BehaviorCtrl+F / F3 / Shift+F3 のキー入力を捕捉し、ViewModelのメソッドや検索TextBoxフォーカスへ橋渡し。ScrollIntoViewを呼ぶ

ポイントは以下の2つです。

  • 検索ロジックと件数カウントはすべてViewModel側(データ側)で行う
  • ビュー側は「選択の移動」と「スクロール」と「見た目のハイライト」に専念する

これにより、DataGridの仮想化とMVVMの両方を崩さずに、軽快な検索UIを実装できます。

データ側実装:ICollectionViewと検索結果リスト

まずは ViewModel の設計からです。ここでは従業員リストを例にします。

モデルクラスの例


public class Employee : INotifyPropertyChanged
{
    private int _id;
    private string _name;
    private string _title;
    private bool _isMatch;

    public int Id
    {
        get => _id;
        set { _id = value; OnPropertyChanged(nameof(Id)); }
    }

    public string Name
    {
        get => _name;
        set { _name = value; OnPropertyChanged(nameof(Name)); }
    }

    public string Title
    {
        get => _title;
        set { _title = value; OnPropertyChanged(nameof(Title)); }
    }

    /// <summary>
    /// 行単位ハイライト用。「検索にヒットしているかどうか」。
    /// </summary>
    public bool IsMatch
    {
        get => _isMatch;
        set { _isMatch = value; OnPropertyChanged(nameof(IsMatch)); }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

IsMatch プロパティは行ハイライト用のオプションですが、入れておくと後で便利です(なくても基本検索は動きます)。

ICollectionViewと検索プロパティ

一覧データは ObservableCollection<Employee> をベースにし、ビュー側の選択・移動を楽にするために ICollectionView にラップします。


public class EmployeeListViewModel : INotifyPropertyChanged
{
    private readonly ObservableCollection&lt;Employee&gt; _source;
    private readonly ICollectionView _view;

    private string _searchText;
    private List&lt;Employee&gt; _matches = new List&lt;Employee&gt;();
    private int _currentIndex;

    public EmployeeListViewModel()
    {
        _source = new ObservableCollection&lt;Employee&gt;();
        _view = CollectionViewSource.GetDefaultView(_source);
        _view.CurrentChanged += (s, e) =&gt; OnPropertyChanged(nameof(CurrentItem));

        // サンプルデータ
        _source.Add(new Employee { Id = 1, Name = "Tanaka", Title = "Manager" });
        _source.Add(new Employee { Id = 2, Name = "Suzuki", Title = "Developer" });
        _source.Add(new Employee { Id = 3, Name = "Yamada", Title = "Sales" });
    }

    public ICollectionView View =&gt; _view;

    /// &lt;summary&gt;
    /// 検索文字列。TextBoxから双方向バインドする。
    /// &lt;/summary&gt;
    public string SearchText
    {
        get =&gt; _searchText;
        set
        {
            if (_searchText == value) return;
            _searchText = value;
            OnPropertyChanged(nameof(SearchText));
            UpdateMatches();   // 入力のたびに一致リストを更新
        }
    }

    public int CurrentIndex
    {
        get =&gt; _currentIndex;
        private set
        {
            if (_currentIndex == value) return;
            _currentIndex = value;
            OnPropertyChanged(nameof(CurrentIndex));
            OnPropertyChanged(nameof(CurrentPositionText));
        }
    }

    /// &lt;summary&gt;
    /// 検索結果件数。0なら該当なし。
    /// &lt;/summary&gt;
    public int TotalMatches =&gt; _matches?.Count ?? 0;

    /// &lt;summary&gt;
    /// 「3 / 12」のようなUI用表示文字列。
    /// &lt;/summary&gt;
    public string CurrentPositionText
        =&gt; TotalMatches == 0
            ? "0 / 0"
            : $"{CurrentIndex + 1} / {TotalMatches}";

    /// &lt;summary&gt;
    /// Viewから見た現在の選択アイテム。
    /// &lt;/summary&gt;
    public Employee CurrentItem =&gt; _view.CurrentItem as Employee;

    /// &lt;summary&gt;
    /// ViewからScrollIntoViewを呼んでもらうためのイベント。
    /// &lt;/summary&gt;
    public event Action&lt;object&gt; RequestScrollIntoView;

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
        =&gt; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

検索ロジック:MatchesリストとMoveCurrentTo

SearchText が変更されたら、 LINQ で一致行のリストを作ります。


private void UpdateMatches()
{
    var text = SearchText ?? string.Empty;

    // 全件を一旦クリア
    foreach (var e in _source)
    {
        e.IsMatch = false;
    }

    if (string.IsNullOrWhiteSpace(text))
    {
        _matches = new List&lt;Employee&gt;();
        CurrentIndex = 0;
        OnPropertyChanged(nameof(TotalMatches));
        return;
    }

    // 大小無視+全角半角を吸収したい場合はNormalizeなどを挟む
    string normalized = text.Trim();

    _matches = _source
        .Where(e =&gt; ContainsIgnoreCase(e, normalized))
        .ToList();

    // 行単位ハイライト用フラグを立てる
    foreach (var e in _matches)
    {
        e.IsMatch = true;
    }

    CurrentIndex = 0;
    GoTo(CurrentIndex);

    OnPropertyChanged(nameof(TotalMatches));
    OnPropertyChanged(nameof(CurrentPositionText));
}

/// &lt;summary&gt;
/// 複数列検索の例。Id/Name/Titleのどれかに含まれていたらヒットとみなす。
/// &lt;/summary&gt;
private static bool ContainsIgnoreCase(Employee e, string text)
{
    if (string.IsNullOrEmpty(text)) return false;

    var candidates = new[]
    {
        e.Id.ToString(),
        e.Name,
        e.Title
    };

    return candidates.Any(v =&gt;
        !string.IsNullOrEmpty(v) &amp;&amp;
        v.IndexOf(text, StringComparison.OrdinalIgnoreCase) &gt;= 0);
}

現在位置の移動と dataGrid.ScrollIntoView(…) をお願いする部分です。


public void Next()
{
    if (TotalMatches == 0) return;

    CurrentIndex = (CurrentIndex + 1) % TotalMatches;
    GoTo(CurrentIndex);
}

public void Prev()
{
    if (TotalMatches == 0) return;

    CurrentIndex = (CurrentIndex - 1 + TotalMatches) % TotalMatches;
    GoTo(CurrentIndex);
}

private void GoTo(int index)
{
    if (TotalMatches == 0) return;
    if (index &lt; 0 || index &gt;= TotalMatches) return;

    var target = _matches[index];

    // ICollectionViewのカレントを移動
    _view.MoveCurrentTo(target);

    // Viewに行スクロールしてもらう
    RequestScrollIntoView?.Invoke(target);

    OnPropertyChanged(nameof(CurrentItem));
    OnPropertyChanged(nameof(CurrentPositionText));
}

ここまでで、ViewModel側の中核である「検索語の変更 → Matches再計算 → CurrentIndexの移動 → MoveCurrentTo+ScrollIntoView要求」が揃いました。

入力のデバウンスを入れる場合

文字を一文字打つたびに LINQ を回すのが重い場合、SearchText の setter で直接 UpdateMatches を呼ばず、DispatcherTimer でデバウンスするのが実務的です。


private readonly DispatcherTimer _searchTimer;

public EmployeeListViewModel()
{
    _source = new ObservableCollection&lt;Employee&gt;();
    _view = CollectionViewSource.GetDefaultView(_source);

    _searchTimer = new DispatcherTimer
    {
        Interval = TimeSpan.FromMilliseconds(200)
    };
    _searchTimer.Tick += (s, e) =&gt;
    {
        _searchTimer.Stop();
        UpdateMatches();
    };
}

public string SearchText
{
    get =&gt; _searchText;
    set
    {
        if (_searchText == value) return;
        _searchText = value;
        OnPropertyChanged(nameof(SearchText));

        // タイピングから200ms経過したら検索実行
        _searchTimer.Stop();
        _searchTimer.Start();
    }
}

これで長い文字列をタイプしても無駄な再計算が減り、体感速度がかなり改善されます。

ビュー側実装:DataGridと検索TextBox

次に XAML 側です。ここでは「DataGridのすぐ上に検索ボックスを置く」パターンで説明します。ヘッダーに埋め込む場合も、基本的な Binding の考え方は同じです。

画面レイアウトの例


&lt;Window x:Class="SampleApp.Views.EmployeeListView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
        xmlns:local="clr-namespace:SampleApp"
        mc:Ignorable="d"
        Title="Employee List" Height="450" Width="800"&gt;

    &lt;Grid Margin="8"&gt;
        &lt;Grid.RowDefinitions&gt;
            &lt;RowDefinition Height="Auto" /&gt;
            &lt;RowDefinition Height="*" /&gt;
        &lt;/Grid.RowDefinitions&gt;

        &lt;StackPanel Orientation="Horizontal" Margin="0,0,0,4"&gt;
            &lt;TextBlock Text="検索" VerticalAlignment="Center" Margin="0,0,4,0" /&gt;

            &lt;TextBox x:Name="SearchBox"
                     Width="200"
                     Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"&gt;
                &lt;TextBox.Style&gt;
                    &lt;Style TargetType="TextBox"&gt;
                        &lt;Setter Property="BorderBrush" Value="Gray" /&gt;
                        &lt;Setter Property="BorderThickness" Value="1" /&gt;
                        &lt;Style.Triggers&gt;
                            &lt;DataTrigger Binding="{Binding TotalMatches}" Value="0"&gt;
                                &lt;Setter Property="BorderBrush" Value="Red" /&gt;
                                &lt;Setter Property="BorderThickness" Value="2" /&gt;
                            &lt;/DataTrigger&gt;
                        &lt;/Style.Triggers&gt;
                    &lt;/Style&gt;
                &lt;/TextBox.Style&gt;
            &lt;/TextBox&gt;

            &lt;TextBlock Margin="8,0,0,0"
                       VerticalAlignment="Center"
                       Text="{Binding CurrentPositionText}" /&gt;
        &lt;/StackPanel&gt;

        &lt;DataGrid x:Name="EmployeesGrid"
                  Grid.Row="1"
                  ItemsSource="{Binding View}"
                  AutoGenerateColumns="False"
                  IsReadOnly="True"
                  IsSynchronizedWithCurrentItem="True"&gt;

            &lt;DataGrid.Columns&gt;
                &lt;DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="80" /&gt;
                &lt;DataGridTextColumn Header="氏名" Binding="{Binding Name}" Width="*" /&gt;
                &lt;DataGridTextColumn Header="役職" Binding="{Binding Title}" Width="200" /&gt;
            &lt;/DataGrid.Columns&gt;

            &lt;i:Interaction.Behaviors&gt;
                &lt;local:DataGridFindBehavior
                    SearchBox="{Binding ElementName=SearchBox}" /&gt;
            &lt;/i:Interaction.Behaviors&gt;
        &lt;/DataGrid&gt;
    &lt;/Grid&gt;
&lt;/Window&gt;

ポイントは以下の通りです。

  • TextBox の Style で TotalMatches == 0 のときだけ赤枠にする
  • CurrentPositionText をすぐ右に置き、「3 / 12」のような表示にする
  • DataGrid の IsSynchronizedWithCurrentItem="True" により、ICollectionView の CurrentItem と選択行を同期する
  • Attached Behavior に TextBox を渡すため、Behavior の SearchBox プロパティに ElementName でバインドする

行ハイライト用スタイル(オプション)

検索にヒットした行全体をうっすら色付けしたい場合は、RowStyle を使います。先ほどの Employee.IsMatch を利用します。


&lt;DataGrid.RowStyle&gt;
    &lt;Style TargetType="DataGridRow"&gt;
        &lt;Style.Triggers&gt;
            &lt;DataTrigger Binding="{Binding IsMatch}" Value="True"&gt;
                &lt;Setter Property="FontWeight" Value="Bold" /&gt;
            &lt;/DataTrigger&gt;
        &lt;/Style.Triggers&gt;
    &lt;/Style&gt;
&lt;/DataGrid.RowStyle&gt;

最初は「検索結果のうち1件だけ(現在位置)を強調」、慣れてきたら「ヒットした行を全部薄い色に」「現在位置だけ濃い色に」といった形で段階的に強化すると実装しやすいです。

Attached BehaviorでCtrl+F/F3を処理する

MVVMを崩さずにキーボードショートカットを扱うには、コードビハインドより Attached Behavior を使う方が責務を分離しやすくなります。

Behaviorクラスの実装


public class DataGridFindBehavior : Behavior&lt;DataGrid&gt;
{
    public static readonly DependencyProperty SearchBoxProperty =
        DependencyProperty.Register(
            nameof(SearchBox),
            typeof(TextBox),
            typeof(DataGridFindBehavior),
            new PropertyMetadata(null));

    /// &lt;summary&gt;
    /// Ctrl+Fでフォーカスを当てる検索ボックス。
    /// &lt;/summary&gt;
    public TextBox SearchBox
    {
        get =&gt; (TextBox)GetValue(SearchBoxProperty);
        set =&gt; SetValue(SearchBoxProperty, value);
    }

    private EmployeeListViewModel ViewModel =&gt;
        AssociatedObject?.DataContext as EmployeeListViewModel;

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.Loaded += OnLoaded;
        AssociatedObject.PreviewKeyDown += OnPreviewKeyDown;

        if (ViewModel != null)
        {
            ViewModel.RequestScrollIntoView += OnRequestScrollIntoView;
        }
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        AssociatedObject.Loaded -= OnLoaded;
        AssociatedObject.PreviewKeyDown -= OnPreviewKeyDown;

        if (ViewModel != null)
        {
            ViewModel.RequestScrollIntoView -= OnRequestScrollIntoView;
        }
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        // 画面表示直後にDataGridにフォーカスを当てておくと、
        // 「セルをクリックしていないとCtrl+Fが効かない」問題を避けやすい
        AssociatedObject.Focus();
    }

    private void OnPreviewKeyDown(object sender, KeyEventArgs e)
    {
        // Ctrl+F で検索ボックスにフォーカス
        if (Keyboard.Modifiers == ModifierKeys.Control &amp;&amp; e.Key == Key.F)
        {
            FocusSearchBox();
            e.Handled = true;
            return;
        }

        // F3 / Shift+F3 で前後の一致に移動
        if (e.Key == Key.F3)
        {
            if (ViewModel == null) return;

            if (Keyboard.Modifiers == ModifierKeys.Shift)
            {
                ViewModel.Prev();
            }
            else
            {
                ViewModel.Next();
            }

            e.Handled = true;
        }
    }

    private void FocusSearchBox()
    {
        if (SearchBox == null) return;

        SearchBox.Focus();
        SearchBox.SelectAll();
    }

    private void OnRequestScrollIntoView(object item)
    {
        if (item == null) return;

        AssociatedObject.ScrollIntoView(item);
    }
}

Behavior のポイントは次の通りです。

  • Behavior<DataGrid> を継承し、DataGridにだけ貼り付けられるようにする
  • SearchBox を DependencyProperty にして、XAMLから ElementName で渡せるようにする
  • PreviewKeyDown で Ctrl+F / F3 / Shift+F3 を捕捉し、ViewModel のメソッドを呼び出す
  • ViewModel の RequestScrollIntoView イベントを購読し、DataGrid.ScrollIntoView を呼ぶ
  • OnDetaching でイベントをきちんと解除し、メモリリークを防ぐ

XAMLでBehaviorをアタッチする

先ほどの XAML にすでに書いていますが、BehaviorをDataGridに付ける部分だけ抜き出します。


&lt;DataGrid x:Name="EmployeesGrid"
          Grid.Row="1"
          ItemsSource="{Binding View}"
          IsSynchronizedWithCurrentItem="True"&gt;

    &lt;i:Interaction.Behaviors&gt;
        &lt;local:DataGridFindBehavior
            SearchBox="{Binding ElementName=SearchBox}" /&gt;
    &lt;/i:Interaction.Behaviors&gt;
&lt;/DataGrid&gt;

nuget で Microsoft.Xaml.Behaviors.Wpf を参照に追加し、名前空間 xmlns:i="http://schemas.microsoft.com/xaml/behaviors" を宣言しておくのを忘れないようにしてください(古い System.Windows.Interactivity は非推奨です)。

「セルをクリックしていないとCtrl+Fを拾えない」問題への対処

よくあるハマりポイントが、「画面を開いた直後、どこにもフォーカスが無くて Ctrl+F を押しても何も起きない」というものです。対処方法はいくつかあります。

DataGridに初期フォーカスを与える

Behaviorの OnLoaded で AssociatedObject.Focus() を呼ぶ方法はすでに紹介しました。XAMLだけで完結させたい場合は、Window側で FocusManager を使う手もあります。


&lt;Window x:Class="SampleApp.Views.EmployeeListView"
        ...
        FocusManager.FocusedElement="{Binding ElementName=EmployeesGrid}"&gt;

こうしておけば、画面表示直後から DataGrid にキーボードフォーカスがあり、Ctrl+F を押したときに Behavior の PreviewKeyDown が呼ばれるようになります。

WindowレベルのInputBindingsで拾う方法

フォーカス位置に依存させたくない場合は、Window.InputBindings でグローバルにキーを拾う方法もあります。この場合はコマンドと組み合わせるのが自然です。


&lt;Window.InputBindings&gt;
    &lt;KeyBinding Key="F"
                Modifiers="Control"
                Command="{Binding FocusSearchCommand}" /&gt;
    &lt;KeyBinding Key="F3"
                Command="{Binding FindNextCommand}" /&gt;
    &lt;KeyBinding Key="F3"
                Modifiers="Shift"
                Command="{Binding FindPrevCommand}" /&gt;
&lt;/Window.InputBindings&gt;

ViewModel側に RelayCommand などでコマンドを用意し、Behavior の代わりにコードビハインド最小限で ScrollIntoView だけを行う、という構成も有力です。この記事では Behaviorパターンにフォーカスしましたが、既存プロジェクトの方針に合わせて選択してください。

件数カウント/スクロール移動/仮想化の落とし穴

DataGridは VirtualizingStackPanel を使った仮想化が有効になっていることが多く、可視領域外の行やセルはそもそもビジュアルツリーに存在しません。そのため、次のようなアプローチは避けるべきです。

  • VisualTreeHelper を使って DataGridRow や TextBox を総なめし、文字列検索を行う
  • 「全TextBoxを列挙して、そのTextプロパティから件数をカウントする」といったUI側ベースの検索

これをやると、スクロールするタイミングによってヒット件数が変わる・パフォーマンスが悪化するなど、トラブルの元になります。

この記事のようにデータ側(ObservableCollection)に対して件数を計算し、Matches リストを持ち回ると、仮想化の有無に関係なく安定して動作します。

仮想化とScrollIntoViewの関係

DataGrid.ScrollIntoView(item) は、仮想化が効いていても必要な行を自動的に実体化してくれます。つまり、

  1. ViewModel で MoveCurrentTo(target) を呼ぶ
  2. Behavior が ScrollIntoView(target) を呼ぶ

という流れさえ守れば、ビジュアルツリーを直接触る必要はありません。どうしても「現在位置のセルにフォーカスしたい」などの要件がある場合だけ、ItemContainerGenerator から DataGridRow を取得してフォーカスを動かすような処理を検討してください。

文字単位のハイライトをしたい場合

ブラウザのように「セル内の該当部分だけ黄色に塗る」表現をしたい場合は、少し工夫が必要です。

  • ModelやViewModelに IsMatch や MatchText を持たせる
  • TextBlock の Inlines を生成する IMultiValueConverter を用意し、検索語とセルの内容から Run を分割・色付けする
  • パフォーマンスに注意しながら、仮想化を前提に「必要なときだけコンバータで Inlines を構築」する

検索語や件数の管理自体はこの記事の実装そのまま利用し、見た目だけを高度化する、というイメージです。

実務で役立つ細かい工夫

複数列検索と検索オプション

先ほどの ContainsIgnoreCase を少し拡張すると、柔軟な検索ができます。

  • 対象列を Config や列挙型で管理し、「氏名だけ」「氏名と役職」「全列」など切り替え
  • 完全一致/部分一致の切り替え
  • 前方一致(StartsWith)や正規表現を使った高度な検索

あまり機能を盛り込みすぎるとUIが複雑になるので、実務では「部分一致・大小無視・複数列検索」くらいまでに抑えておくと、ユーザーにとっても開発者にとってもバランスが良いです。

全角半角とローマ字/かなの扱い

日本語圏では、「全角・半角」や「ひらがな・カタカナ」の揺れが現実問題としてよく発生します。簡易的には String.Normalize(NormalizationForm.FormKC) を使って全角・半角をならすだけでも、ヒット率がかなり改善します。


private static string NormalizeForSearch(string text)
{
    if (string.IsNullOrEmpty(text)) return string.Empty;
    return text
        .Normalize(NormalizationForm.FormKC); // 全角半角・一部の互換文字を統一
}

private static bool ContainsIgnoreCase(Employee e, string text)
{
    var normalized = NormalizeForSearch(text);
    var candidates = new[]
    {
        NormalizeForSearch(e.Id.ToString()),
        NormalizeForSearch(e.Name),
        NormalizeForSearch(e.Title),
    };

    return candidates.Any(v =&gt;
        !string.IsNullOrEmpty(v) &amp;&amp;
        v.IndexOf(normalized, StringComparison.OrdinalIgnoreCase) &gt;= 0);
}

アクセシビリティとショートカットのガイド

Ctrl+F / F3 自体は便利ですが、ユーザーに伝わらなければ宝の持ち腐れです。次のような工夫をしておくと親切です。

  • 検索ボックスの近くに「Ctrl+Fで検索、F3で次を検索」のようなヒントテキストを小さく出す
  • Alt+N などの AccessKey を付けておき、マウスが使えない環境でも操作しやすくする
  • 「N件中K件目」を見れば現在位置が分かるようにしておく(すでに CurrentPositionText で実現)

まとめ:検索はデータで、ビューは見た目と操作に集中させる

この記事で紹介した構成をざっと振り返ります。

  • 一覧データは ObservableCollection を ICollectionView にラップし、CurrentItem と選択行を同期させる
  • 検索語 SearchText を ViewModel に持ち、変更時に LINQ で Matches リストを作る
  • Next / Prev メソッドで CurrentIndex をループ移動させ、MoveCurrentTo + ScrollIntoView で該当行へジャンプする
  • 該当件数 TotalMatches を公開し、0件のときは TextBox スタイルで赤枠にする
  • Ctrl+F / F3 / Shift+F3 は Attached Behavior または Window.InputBindings で捕捉し、フォーカス移動と ViewModel 呼び出しを行う
  • 仮想化された DataGrid ではビジュアルツリーを直接さわらず、あくまでデータ側で一致判定や件数管理を行う

ブラウザ風の検索UIは、一度パターン化してしまえば他の画面にも簡単に横展開できます。この記事の ViewModel と Behavior を少し一般化して、「検索可能な DataGrid 用の共通コンポーネント」としてライブラリ化しておくと、後々の開発効率がぐっと上がります。

MVVM と仮想化のルールさえ守れば、WPFのDataGridでも快適な Ctrl+F/F3 検索を実現できます。ぜひ自分のプロジェクト用にカスタマイズしてみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次