C#

【C#】DataGridViewのフィルタ機能にユーザ設定フィルタを追加

C#のアプリで「Excelのようなフィルター機能」が実装できることを前回確認しました。

【C#】DataGridViewにExcel風のフィルター機能を実装するメモ C#のアプリで「Excelのようにフィルター機能を使いたいっ!」といった お話をされることがあり、「そんなのこっちがやり方を教えてほし...

今回は、「Excelのようなユーザー設定フィルター」を追加してみたいと思います。

ユーザー設定フィルター機能の追加

今回は、一昔前のExcelにあった「ユーザー設定フィルター」の実装を目指します。

ユーザー設定フィルターの追加は、Microsoft様のWebサイトでダウンロードできる サンプルプロジェクトを使用します。

準備

DataGridViewにフィルター機能を追加する方法について記載された、Microsoft様のWebサイトを参考にしながら、ユーザー設定フィルター機能を追加していきます。

参考サイトは、以下になります。

DataGridView 列ヘッダー セルのDrop-Down フィルター リストの作成 | Microsoft Learn

また、コードサンプルは、以下からダウンロードできます。

Download Building a Drop-Down Filter List for a DataGridView Column Header Cell Sample Code from Official Microsoft Download Center

ダウンロードしたファイルを解凍して、「CS」フォルダにある「DataGridViewAutoFilter.sln」を起動します。

3つあるプロジェクトのうち、「DataGridViewAutoFilter」に処理を追記していきます。

また、ユーザー設定フィルターの動作確認は、「DesignerSetupDemo」を使用して行います。

実装内容

ユーザー設定フィルターの実装概要は、以下になります。

  1. ユーザー設定フィルター画面を追加する。
  2. フィルターのドロップダウンリストに「ユーザー設定フィルター」を追加する。
  3. ユーザー設定フィルターに基づく、フィルタ更新処理を追加する。

実際にプログラムを作成していきます。

ユーザー設定フィルター画面の追加

ドロップダウンリストで (ユーザー設定フィルタ) が選択された場合に表示する画面を作成します。

新規で画面を追加して、以下のようにコントロールを配置します。昔風のExcelのユーザー設定画面にしています。

最大2つの抽出条件を「AND」、または、「OR」で設定できるようにします。

フォーム名は、「formUserFilter」にしました。

  1. 抽出文字列1 コンボボックス(cmbSelect1)
  2. 抽出文字列2 コンボボックス(cmbSelect2)
  3. 抽出条件1 コンボボックス(cmbFilter1)
  4. 抽出条件2 コンボボックス(cmbFilter2)
  5. AND条件 ラジオボタン(radAnd)
  6. OR条件 ラジオボタン(radOr)
  7. OKボタン(btnOk)
  8. キャンセルボタン(btnCancel)

次に、検索情報のクラス「SearchInfo.cs」を追加します。

	/// <summary>
	/// 検索データ
	/// </summary>
	public class SearchInfo
	{
		public string search1;      //検索文字列1
		public string search2;      //検索文字列2
		public int filter1;         //検索フィルタ1
		public int filter2;         //検索フィルタ2
		public int sel;             //And Or選択

		/// <summary>
		/// コンストラクタ
		/// </summary>
		public SearchInfo()
		{
			filter1 = 1;
			filter2 = 0;
			sel = 0;
		}
	}

検索情報クラスは、前回検索した条件を「カラムごと」に保持しておくためのクラスになります。

ユーザー設定フィルターを選択した時に、カラムごとに前回検索した条件で画面が表示されるようにするためです。

クラスを追加したら、ユーザー設定フィルターのプログラム「formUserFilter.cs」を作成します。

以下のプログラムを記述します。ちょっと長いです。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace DataGridViewAutoFilter
{
	public partial class formUserFilter : Form
	{
		/// <summary>
		/// フィルタ条件
		/// </summary>
		private string[] filterCategory = new string[13] {
			"",
			"と等しい",
			"と等しくない",
			"より大きい",
			"以上",
			"より小さい",
			"以下",
			"で始まる",
			"で始まらない",
			"で終わる",
			"で終わらない",
			"を含む",
			"を含まない"
		};

		// コントロール配列(AND・OR ラジオボタン)
		private const int RAD_ZERO = 0;
		private const int RAD_ONE = 1;
		private const int RAD_BUTTON_NUM = 2;
		private RadioButton[] radSelect = new RadioButton[RAD_BUTTON_NUM];

		// 対象データ
		private System.Collections.Specialized.OrderedDictionary filters = new System.Collections.Specialized.OrderedDictionary();

		// 検索条件
		private SearchInfo sInfo;		// 検索条件
		private string column;			// 検索カラム名
		private string searchStr;		// 検索文字列

		/// <summary>
		/// コンストラクタ
		/// </summary>
		/// <param name="Filters">対象データ</param>
		/// <param name="SearcInfo">検索条件</param>
		/// <param name="Column">対象カラム</param>
		public formUserFilter(System.Collections.Specialized.OrderedDictionary Filters,
								SearchInfo SInfo,
								string Column)
		{
			InitializeComponent();

			filters = Filters;
			sInfo = SInfo;
			column = Column;
			searchStr = "";
		}

		/// <summary>
		/// フォームロード
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void formUserFilter_Load(object sender, EventArgs e)
		{
			SetCmbFilter();

			SetCmbSelect();

			SetControlArray();

			SetSearchInfo();
		}

		/// <summary>
		/// 前回検索情報設定
		/// </summary>
		private void SetSearchInfo()
		{
			try
			{
				cmbSelect1.Text = sInfo.search1;
				cmbSelect2.Text = sInfo.search2;

				cmbFilter1.SelectedIndex = sInfo.filter1;
				cmbFilter2.SelectedIndex = sInfo.filter2;

				radSelect[sInfo.sel].Checked = true;
			}
			catch (Exception e)
			{
			}
		}

		/// <summary>
		/// コントロール配列設定
		/// </summary>
		private void SetControlArray()
		{
			radSelect[RAD_ZERO] = radAnd;
			radSelect[RAD_ONE] = radOr;

			radSelect[RAD_ZERO].Checked = true;
		}

		/// <summary>
		/// コンボボックス(条件)設定
		/// </summary>
		private void SetCmbFilter()
		{
			cmbSelect1.BeginUpdate();
			cmbSelect2.BeginUpdate();

			cmbSelect1.Items.Add("");
			cmbSelect2.Items.Add("");

			foreach (string val in filters.Values)
			{
				if (val != null)
				{
					cmbSelect1.Items.Add(val);
					cmbSelect2.Items.Add(val);
				}
			}

			cmbSelect1.EndUpdate();
			cmbSelect2.EndUpdate();
		}

		/// <summary>
		/// コンボボックス(フィルタ)設定
		/// </summary>
		private void SetCmbSelect()
		{
			cmbFilter1.Items.AddRange(filterCategory);
			cmbFilter1.SelectedIndex = 1;

			cmbFilter2.Items.AddRange(filterCategory);
		}

		/// <summary>
		/// 検索文字列設定処理
		/// </summary>
		/// <param name="str"></param>
		/// <param name="indx"></param>
		/// <returns></returns>
		private string SetFilterCondition(string str, int indx)
		{
			string target = "";
			StringBuilder sb = new StringBuilder();
			sb.Append("[");
			sb.Append(column);
			sb.Append("]");

			target = str.Replace("'", "''");

			switch (indx)
			{
				case 1:     // と等しい
					sb.Append("=");
					sb.Append("'");
					sb.Append(target);
					sb.Append("'");
					break;
				case 2:     // と等しくない
					sb.Append("<>");
					sb.Append("'");
					sb.Append(target);
					sb.Append("'");
					break;
				case 3:     // より大きい
					sb.Append(">");
					sb.Append("'");
					sb.Append(target);
					sb.Append("'");
					break;
				case 4:     // 以上
					sb.Append(">=");
					sb.Append("'");
					sb.Append(target);
					sb.Append("'");
					break;
				case 5:     // より小さい
					sb.Append("<");
					sb.Append("'");
					sb.Append(target);
					sb.Append("'");
					break;
				case 6:     // 以下
					sb.Append("<=");
					sb.Append("'");
					sb.Append(target);
					sb.Append("'");
					break;
				case 7:     // で始まる
					sb.Append("LIKE '");
					sb.Append(target);
					sb.Append("%'");
					break;
				case 8:     // で始まらない
					sb.Append("NOT LIKE '");
					sb.Append(target);
					sb.Append("%'");
					break;
				case 9:     // で終わる
					sb.Append("LIKE ");
					sb.Append("'");
					sb.Append(target);
					sb.Append("%'");
					break;
				case 10:     // で終わらない
					sb.Append("NOT LIKE ");
					sb.Append("'%");
					sb.Append(target);
					sb.Append("'");
					break;
				case 11:    // を含む
					sb.Append("LIKE '%");
					sb.Append(target);
					sb.Append("%'");
					break;
				case 12:    // を含まない
					sb.Append("NOT LIKE '%");
					sb.Append(target);
					sb.Append("%'");
					break;
				default:
					break;
			}

			// 今回検索情報の設定
			sInfo.search1 = cmbSelect1.Text;
			sInfo.search2 = cmbSelect2.Text;
			sInfo.filter1 = cmbFilter1.SelectedIndex;
			sInfo.filter2 = cmbFilter2.SelectedIndex;
			
			for(int i = 0; i < radSelect.Length; i++)
			{
				if (radSelect[i].Checked)
				{
					sInfo.sel = i;
				}
			}

			return sb.ToString();
		}

		/// <summary>
		/// OKボタン押下処理
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void btnOk_Click(object sender, EventArgs e)
		{
			this.DialogResult = DialogResult.OK;

			if (cmbFilter1.Text != "")
			{
				searchStr = SetFilterCondition(cmbSelect1.Text, cmbFilter1.SelectedIndex);
			}

			if (cmbFilter2.Text != "")
			{
				// 条件が2つ以上の場合は、カッコ( )で囲む
				searchStr = "(" + searchStr;
				if (radSelect[RAD_ZERO].Checked)
				{
					searchStr += " AND ";
				}
				else if (radSelect[RAD_ONE].Checked)
				{
					searchStr += " OR ";
				}
				searchStr += SetFilterCondition(cmbSelect2.Text, cmbFilter2.SelectedIndex);
				searchStr += ")";
			}

			Close();
		}

		/// <summary>
		/// キャンセルボタン押下処理
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void btnCancel_Click(object sender, EventArgs e)
		{
			Close();
		}

		/// <summary>
		/// 検索文字列取得処理
		/// </summary>
		/// <returns></returns>
		public string GetSearchString()
		{
			return searchStr;
		}

		/// <summary>
		/// 検索情報
		/// </summary>
		/// <returns></returns>
		public SearchInfo GetSearchInfo()
		{
			return sInfo;
		}
	}
}

簡単にプログラムの説明を記載します。

16~30行目が、今回追加するフィルター条件です。条件は言葉通りの動作になります。

69~78行目のフォームロードでは、画面に配置されるコンボボックスのアイテム設定とラジオボタンの初期化になります。

コンボボックスの条件設定の「SetCmbFilter()」関数では、上位から渡されたデータ一覧をコンボボックスに追加しています。これは、DataGridViewの各カラムに設定されているドロップダウンリストのデータと同じものになります。

152~251行目の「SetFilterCondition()」関数は、ユーザー設定画面で選択された条件を基に、検索文字列を作成する関数になります。

例えば、「品名」カラムの「リンゴ」と等しい、といった条件では、以下のような文字列が作成されます。

 [品名]=’リンゴ’

価格が100円より大きい場合は、[価格]>’100’、文章に「本日」が含まれるといった場合は、[文章] LIKE ‘%本日%’ といった感じになります。

258~284行目のOKボタン クリックイベントで、フィルタ条件コンボボックス(画面右側のコンボボックス)が空白以外の場合に検索条件の文字列を作成します。

検索条件が2個以上の場合は、全体をカッコで囲む必要があるので要注意です。

※最終的に作成される検索条件が、以下の場合結果が異なってしまうため。

 ① [場所] = [東京] AND [品名] = [リンゴ] OR [価格] < 200

 ② [場所] = [東京] AND ( [品名] = [リンゴ] OR [価格] < 200 )

 東京でリンゴ、または、価格が200円未満のもの と条件でフィルターしても、①だと正常に表示されないのでカッコで囲む必要があります。

フィルターのドロップダウンリストに項目を追加

DataGridViewのフィルター機能で表示されるドロップダウンリストに「ユーザー設定フィルター」を追加します。

ここからは、「DataGridViewAutoFilterColumnHeaderCell.cs」の改造になります。

プログラムを追加する場所は、「PopulateFilters()」関数です。

この関数では、フィルターのドロップダウンリスト用のディクショナリデータを作成しています。

フィルターのディクショナリを作成した後に、特別なフィルターオプションとして、(All)、(Blanks)、(NonBlanks)を追加している処理があるので、同様に(ユーザー設定フィルタ)を追加します。

以下に プログラムを抜粋して 記載します。

/// <summary>
/// Populates the filters dictionary with formatted and unformatted string
/// representations of each unique value in the column, accounting for all 
/// filters except the current column's. Also adds special filter options. 
/// </summary>
private void PopulateFilters()
{
    // Continue only if there is a DataGridView.
    if (this.DataGridView == null)
    {
        return;
    }


	/// ~ 省略 ~ ///


    // Add special filter options to the filters dictionary
    // along with null values, since unformatted representations
    // are not needed. 
    filters.Insert(0, "(All)", null);
	filters.Insert(1, "(ユーザー設定フィルタ)", null);
    if (containsBlanks && containsNonBlanks)
    {
        filters.Add("(Blanks)", null);
        filters.Add("(NonBlanks)", null);
    }
}

この関数の修正箇所は1つだけです。

22行目で、(ユーザー設定フィルタ)を追加しています。

この部分の処理では、(All)が常にフィルタリストの最上位に追加され、リストデータに「NULL」や「空白」が含まれている場合は、リスト最下部に(Blnaks)と(NonBlanks)を追加しています。

ドロップダウンリストから (All)が選択されると、絞り込みが解除され、全てのデータが表示されます。

(Blanks)が選択されるとデータが「NULL」、または、「空白」だけ表示され、(NonBlanks)が選択されると「NULL」と「空白」以外のデータが表示されます。

これらの特別なフィルターオプションと同様に処理するため、(ユーザー設定フィルタ) の項目を最上位から2番目に追加しています。

filters.Insert(1, "(ユーザー設定フィルタ)", null);

フィルター更新処理を追加する

ユーザー設定フィルター画面を使ったフィルター更新処理について、プログラムを追記していきます。

こちらも、「DataGridViewAutoFilterColumnHeaderCell.cs」の改造になります。

まず、検索情報テーブルの定義を追加します。

/// <summary>
/// 検索情報テーブル
/// </summary>
private Dictionary<string, SearchInfo> sInfo = new Dictionary<string, SearchInfo>();

カラム名をキーとして、ディクショナリ形式で検索情報を保持することで、前回検索情報を各カラムごとに呼び出すことが可能になります。

次に、データソースのフィルター更新処理を行っている「UpdateFilter()」関数の処理を改造していきます。

この関数は、各カラムのドロップダウンリストから選択されたフィルター条件を使用して、データソースの絞り込みや解除を行っています。

追加する処理概要は、以下になります。

  • ドロップダウンリストから (ユーザー設定フィルタ) が選択された場合に、ユーザー設定フィルタ画面を表示します。
  • ユーザー設定フィルター画面で設定された検索条件をデーターソースのフィルター条件に追加します。

「UpdateFilter()」関数のプログラムを以下に記載します。

        /// <summary>
        /// Updates the BindingSource.Filter value based on a user selection
        /// from the drop-down filter list. 
        /// </summary>
        private void UpdateFilter()
        {
            // Continue only if the selection has changed.
            if (dropDownListBox.SelectedItem.ToString().Equals(selectedFilterValue) &&
				!dropDownListBox.SelectedItem.ToString().Equals("(ユーザー設定フィルタ)"))
            {
                return;
            }

            // Store the new selection value. 
            selectedFilterValue = dropDownListBox.SelectedItem.ToString();

            // Cast the data source to an IBindingListView.
            IBindingListView data = 
                this.DataGridView.DataSource as IBindingListView;

            Debug.Assert(data != null && data.SupportsFiltering,
                "DataSource is not an IBindingListView or does not support filtering");

            // If the user selection is (All), remove any filter currently 
            // in effect for the column. 
            if (selectedFilterValue.Equals("(All)"))
			{
				data.Filter = FilterWithoutCurrentColumn(data.Filter);
                filtered = false;
                currentColumnFilter = String.Empty;
                return;
            }

            // Declare a variable to store the filter string for this column.
            String newColumnFilter = null;

            // Store the column name in a form acceptable to the Filter property, 
            // using a backslash to escape any closing square brackets. 
            String columnProperty = 
                OwningColumn.DataPropertyName.Replace("]", @"\]");

            // Determine the column filter string based on the user selection.
            // For (Blanks) and (NonBlanks), the filter string determines whether
            // the column value is null or an empty string. Otherwise, the filter
            // string determines whether the column value is the selected value. 
            switch (selectedFilterValue)
            {
                case "(Blanks)":
                    newColumnFilter = String.Format(
                        "LEN(ISNULL(CONVERT([{0}],'System.String'),''))=0",
                        columnProperty);
                    break;
                case "(NonBlanks)":
                    newColumnFilter = String.Format(
                        "LEN(ISNULL(CONVERT([{0}],'System.String'),''))>0",
                        columnProperty);
                    break;
				case "(ユーザー設定フィルタ)":
					SearchInfo wkinfo = new SearchInfo();
					if (sInfo.ContainsKey(columnProperty))
					{
						wkinfo = sInfo[columnProperty];
					}
					formUserFilter fu = new formUserFilter(filters, wkinfo, columnProperty);
					if(fu.ShowDialog() == DialogResult.Cancel)
					{
						return;
					}
					newColumnFilter = fu.GetSearchString();
					SetLastSerchInfo(columnProperty, fu.GetSearchInfo());
					break;
				default:
                    newColumnFilter = String.Format("[{0}]='{1}'",
                        columnProperty,
                        ((String)filters[selectedFilterValue])
                        .Replace("'", "''"));  
                    break;
            }

            // Determine the new filter string by removing the previous column 
            // filter string from the BindingSource.Filter value, then appending 
            // the new column filter string, using " AND " as appropriate. 
            String newFilter = FilterWithoutCurrentColumn(data.Filter);
			String tempFilter = newFilter;
            if (String.IsNullOrEmpty(newFilter))
            {
                newFilter += newColumnFilter;
            }
            else
            {
                newFilter += " AND " + newColumnFilter;
            }


            // Set the filter to the new value.
            try
			{
				data.Filter = newFilter;
            }
            catch (InvalidExpressionException ex)
            {
				//throw new NotSupportedException(
				//    "Invalid expression: " + newFilter, ex);
			
				//フィルタ設定が不正の場合はすべて表示にする	
				MessageBox.Show("ユーザー設定フィルタ 入力値エラー",
								"設定エラー",
								MessageBoxButtons.OK,
								MessageBoxIcon.Warning);

				data.Filter = tempFilter;
				newFilter = tempFilter;
				filtered = false;
				currentColumnFilter = String.Empty;

				dropDownListBox.SelectedIndex = 0;
				selectedFilterValue = dropDownListBox.SelectedItem.ToString();

				return;
			}

            // Indicate that the column is currently filtered
            // and store the new column filter for use by subsequent
            // calls to the FilterWithoutCurrentColumn method. 
            filtered = true;
            currentColumnFilter = newColumnFilter;
        }

簡単にプログラムの説明を記載します。ハイライトで色が変わっているところが、変更した箇所になります。

9行目に、(ユーザー設定フィルタ)が前回検索値と一致した場合に、関数が終了しないように条件を追加しています。

8行目の処理は、ドロップダウンリストで前回選択した値と同じ値が再度選択された場合に、無駄な処理をしないように関数を終了する条件になります。ユーザー設定フィルターの場合は、選択されたら必ず画面を表示したいので、この処理を追加しています。

58~71行目は、ドロップダウンリストで (ユーザー設定フィルタ) が選択された場合に、ユーザー設定フィルタ画面を表示する処理になっています。

ユーザー設定フィルタ画面で設定された検索条件を「GetSearchString()」関数で取得して、データソースのフィルタ条件「newColumnFilter」に設定しています。

ドロップダウンリストで特別なフィルタ条件以外(All、Blanks、Non Blanks)の値を選択した場合は、「default」条件で処理されます。作成されるデータの例は以下になります。

 [カラム名] = ‘選択値’

上記で作成したフィルタ文字列を 85~92行目のフィルタ文字列に連結していき、最終的な絞り込み条件を作成しているようです。

ユーザー設定フィルタ画面で設定した検索条件は、カラム名をキーにして、「SetLastSerchInfo()」関数でディクショナリに保持しています。

「SetLastSerchInfo()」関数の処理内容は以下になります。

		/// <summary>
		/// 検索情報設定
		/// </summary>
		/// <param name="key"></param>
		/// <param name="si"></param>
		private void SetLastSerchInfo(string key, SearchInfo si)
		{
			if (sInfo.ContainsKey(key))
			{
				sInfo[key] = si;
			}
			else
			{
				sInfo.Add(key, si);
			}
		}

84行目は、作成したフィルター条件に問題があった場合のために、一時的に現在の条件を保持しています。

最後に102~119行目で、フィルター更新時にエラーが発生した場合の処理を追加しています。変更前の処理は、上位に例外をスローする感じになっていましたが、テストだけなので、メッセージボックスでのエラー表示に変更しました。

また、ドロップダウンリストの選択初期化や、フィルター条件をエラー発生前に戻しておくなどの処理を追加してみました。

プログラムの改造は以上になります。

動作確認

正常に動作するか確認してみます。

ユーザー設定フィルターの動作確認

まず、「DesignerSetupDemo」プロジェクトをVisual Studio上部のコンボボックスで選択して、デバッグ起動します。

画面が表示されたら、ドロップダウンリストを表示してみます。

(ユーザー設定フィルタ)が追加されていました。

(ユーザー設定フィルタ)を選択すると、条件設定画面が表示されます。

条件を設定して、「OK」ボタンを押すと条件と一致したデータがちゃんと表示されています。

とりあえず、ちゃんと動作したみたいで良かったです。