C#のWindows フォーム アプリケーションにはDataGridViewという便利なコントロールがあります。
下のように、行と列からなる表を表現するコントロールです。
実行環境
本記事で紹介する実行画面は次の環境のものです。
- OS
- Windows 10 Home バージョン 20H2
- Visual Studio バージョン
- Visual Studio 2017
- .NET Framework バージョン
- .NET Framework 2.0
基本
プロパティのおすすめ設定
DataGridViewのプロパティのおすすめ設定は次のとおりです。
セルの指定方法
DataGridViewのセルの指定方法はいくつかあります。
Rows
プロパティとそのCells
プロパティを使用する方法
//dgvListはDataGridViewコントロールthis.dgvList.Rows[0].Cells[0].Value
- DataGridViewのインデクサを使用する方法
//dgvListはDataGridViewコントロールthis.dgvList[0,0].Value
よく使うプロパティ
DataGridView関連でよく使用するプロパティの例を挙げます。
//dgvListはDataGridViewコントロール//セルの値を取得したり変更したりするthis.dgvList[0,0].Value//セルの文字色を取得したり変更したりするthis.dgvList[0,0].Style.ForeColor//セルの背景色を取得したり変更したりするthis.dgvList[0,0].Style.BackColor//セルのエラーアイコンとメッセージを取得したり変更したりするthis.dgvList[0,0].ErrorText//セルのフォントを取得したり変更したりするthis.dgvList[0,0].Style.Font//行と紐付いているバインドオブジェクトを取得します。this.dgvList.Rows[0].DataBoundItem
DataGridViewのバインド
DataGridViewには、「バインド」という機能があります。
この機能は、オブジェクトのデータを簡単にコントロールに反映する便利な機能です。
バインドを行うには、DataGridViewに次のように指定します。
//dgvListはDataGridViewコントロール//カスタムクラスのバインドリストををバインドする場合、AutoGenerateColumnsプロパティをfalseにしないと勝手に列が追加されてしまうthis.dgvList.AutoGenerateColumns=false;this.dgvList.DataSource=[バインドするオブジェクト];
バインドの準備
DataGridViewにオブジェクトをバインドするにはDataTable
などいくつか方法がありますが、今回はカスタムクラスを使用した方法をご紹介します。
SortableBindingList
クラスを用意する
System.Windows.Forms
名前空間にはBindingSource
というクラスがありますが、そのままだとソートを行うことができません。そのため、次のリンクにあるSortableBindingList
クラスを作成してください。
https://garafu.blogspot.com/2016/09/cs-sorablebindinglist.html
- カスタムクラスの基底クラスを用意する
カスタムクラスを使用するには、共通で使用できる基底クラスがあると便利です。
コードの例を次に挙げます。
usingSystem;usingSystem.Collections.Generic;usingSystem.Text;usingSystem.ComponentModel;namespaceDataGridViewBindingSample.Datas.DataGridView.Rows{/// <summary>/// DataGridViewのカスタムクラスの基底クラスです。/// </summary>publicclassDataGridViewDataClassBase:INotifyPropertyChanged{/// <summary>/// プロパティ変更イベント/// </summary>publiceventPropertyChangedEventHandlerPropertyChanged;/// <summary>/// プロパティ変更イベントを発生させます。/// </summary>/// <param name="name"></param>publicvoidOnPropertyChanged(stringname){if(this.PropertyChanged!=null){this.PropertyChanged(this,newPropertyChangedEventArgs(name));}}/// <summary>/// プロパティ変更イベントを発生させながらプロパティに関連するフィールドに値をセットします。/// </summary>/// <typeparam name="T">データ型</typeparam>/// <param name="field">フィールド変数</param>/// <param name="value">セットする値</param>/// <param name="propName">プロパティ名(イベントの発生に必要)</param>publicvoidSetValue<T>(outTfield,Tvalue,stringpropName){field=value;this.OnPropertyChanged(propName);}/// <summary>/// 入力操作をした場合プロパティ変更イベントを発生させながらプロパティに関連するフィールドを整数に変換して値をセットします。/// </summary>/// <typeparam name="T">データ型</typeparam>/// <param name="field">フィールド変数</param>/// <param name="value">セットする値</param>/// <param name="propName">プロパティ名(イベントの発生に必要)</param>publicvoidSetValueInputInt(refint?field,stringvalue,stringpropName){intintValue;if(int.TryParse(value,outintValue)){//整数を入力した場合field=intValue;}elseif(value==null||value.Trim().Length==0){//何も入力しない場合field=null;}else{//文字や記号を含めて入力した場合//何もしない}this.OnPropertyChanged(propName);}}}
実は、カスタムクラスといっても、プロパティに値をセットしてもすぐにはDataGridViewに反映されません。
そのため、DataGridViewに通知をする仕組みが必要です。
その仕組みが、INotifyPropertyChanged
インターフェイス・PropertyChanged
イベント・OnPropertyChanged
メソッドです。PropertyChanged
イベントをプロパティ名を指定して実行すれば、DataGridViewに通知され、表示が反映されるという仕組みになっています。
カスタムクラスの実装
先述で紹介したカスタムクラスの基底クラスを継承して実装します。
各プロパティがDataGridViewの列の1つに紐付けされます。
usingSystem;usingSystem.Collections.Generic;usingSystem.Text;namespaceDataGridViewBindingSample.Datas.DataGridView.Rows.Sample{/// <summary>/// カスタムクラスサンプル1/// </summary>publicclassDgvRowSample1:DataGridViewDataClassBase{privatestringstrName;privateint?intKyoka1Point;privateint?intKyoka2Point;privateint?intKyoka3Point;privateint?intKyoka4Point;privateint?intKyoka5Point;/// <summary>/// 名前/// </summary>publicstringName{get{returnthis.strName;}set{this.SetValue(outthis.strName,value,"Name");}}/// <summary>/// 国語の点数/// </summary>publicint?Kyoka1Point{get{returnthis.intKyoka1Point;}set{this.SetValue(outthis.intKyoka1Point,value,"Kyoka1Point");}}/// <summary>/// 数学の点数/// </summary>publicint?Kyoka2Point{get{returnthis.intKyoka2Point;}set{this.SetValue(outthis.intKyoka2Point,value,"Kyoka2Point");}}/// <summary>/// 理科の点数/// </summary>publicint?Kyoka3Point{get{returnthis.intKyoka3Point;}set{this.SetValue(outthis.intKyoka3Point,value,"Kyoka3Point");}}/// <summary>/// 社会の点数/// </summary>publicint?Kyoka4Point{get{returnthis.intKyoka4Point;}set{this.SetValue(outthis.intKyoka4Point,value,"Kyoka4Point");}}/// <summary>/// 英語の点数/// </summary>publicint?Kyoka5Point{get{returnthis.intKyoka5Point;}set{this.SetValue(outthis.intKyoka5Point,value,"Kyoka5Point");}}/// <summary>/// 国語の点数(表示・入力用)/// </summary>publicstringKyoka1PointDisplay{get{returnthis.intKyoka1Point.ToString();}set{this.SetValueInputInt(refthis.intKyoka1Point,value,"Kyoka1PointDisplay");}}/// <summary>/// 数学の点数(表示・入力用)/// </summary>publicstringKyoka2PointDisplay{get{returnthis.intKyoka2Point.ToString();}set{this.SetValueInputInt(refthis.intKyoka2Point,value,"Kyoka2PointDisplay");}}/// <summary>/// 理科の点数(表示・入力用)/// </summary>publicstringKyoka3PointDisplay{get{returnthis.intKyoka3Point.ToString();}set{this.SetValueInputInt(refthis.intKyoka3Point,value,"Kyoka3PointDisplay");}}/// <summary>/// 社会の点数(表示・入力用)/// </summary>publicstringKyoka4PointDisplay{get{returnthis.intKyoka4Point.ToString();}set{this.SetValueInputInt(refthis.intKyoka4Point,value,"Kyoka4PointDisplay");}}/// <summary>/// 英語の点数(表示・入力用)/// </summary>publicstringKyoka5PointDisplay{get{returnthis.intKyoka5Point.ToString();}set{this.SetValueInputInt(refthis.intKyoka5Point,value,"Kyoka5PointDisplay");}}}}
列とカスタムクラスのプロパティの紐付け
カスタムクラスをバインドしても、列にどのプロパティを適用するかわからないため、そのままでは表示することができません。
そのため、バインドするプロパティを設定する必要があります。
デザイナ画面のDataGridViewの右上の三角をクリックして、「DataGridView タスク」を開き、「列の編集…」を開きます。DataPropertyName
プロパティに表示対象のプロパティ名を設定し、OKを押します。
バインドリスト・カスタムクラスの基本操作
先ほど紹介したSortableBindingList
はList
クラスとほぼ同じように(一部使えないメソッドもありますが)追加・挿入・削除・クリア操作ができます。
コードの例を次に挙げます。
//※sbListはSortableBindingList//DataGridViewの行を追加するthis.sbList.Add(【カスタムクラス】);//DataGridViewの1行目に行を挿入するthis.sbList.Insert(1,【カスタムクラス】);//DataGridViewの1行目を削除するthis.sbList.RemoveAt(1);//DataGridViewの行をすべてクリアするthis.sbList.Clear();//DataGridViewのセルの値は基本的に次のように参照することができます。this.dgvList[0,0].Value//ですが、カスタムクラスを使用すると、次のようにも参照することができます。//※rowBoundはカスタムクラスrowBound.Name
応用
入力可能列の制御
整数を入力する場合、単純にカスタムクラスのプロパティの型を整数にしてしまうと、文字列にしたときにエラーが発生してしまいます。
それを防ぐためのコードの例を挙げます。
/// <summary>/// 入力操作をした場合プロパティ変更イベントを発生させながらプロパティに関連するフィールドを整数に変換して値をセットします。/// </summary>/// <typeparam name="T">データ型</typeparam>/// <param name="field">フィールド変数</param>/// <param name="value">セットする値</param>/// <param name="propName">プロパティ名(イベントの発生に必要)</param>publicvoidSetValueInputInt(refint?field,stringvalue,stringpropName){intintValue;if(int.TryParse(value,outintValue)){//整数を入力した場合field=intValue;}elseif(value==null||value.Trim().Length==0){//何も入力しない場合field=null;}else{//文字や記号を含めて入力した場合//何もしない}this.OnPropertyChanged(propName);}
カスタムクラスの基底クラスの例にある上のコードは派生クラスのプロパティのset
で呼び出します。
/// <summary>/// 国語の点数(表示・入力用)/// </summary>publicstringKyoka1PointDisplay{get{returnthis.intKyoka1Point.ToString();}set{this.SetValueInputInt(refthis.intKyoka1Point,value,"Kyoka1PointDisplay");}}
実はDataGridViewでデータを入力すると、プロパティのset
が呼び出されます。
その性質を利用して、文字列で入力したものを整数に変換できるか確かめて、変換出来たら整数として格納し、空文字列の場合はnull
を格納し(null
許容型でフィールド変数を作成)、文字が入力されていたら格納しないようにします。
また、そのままではデータの表示が反映されないため、OnPropertyChanged
メソッドを呼び出してPropertyChanged
イベントを発生させます。
文字色・背景色の制御
DataGridViewのカスタムクラスでは直接色を紐付けすることはできません。
ただし、工夫次第でカスタムクラスで色をコントロールすることができます。
背景色を変更するコードの例を挙げます。
/// <summary>/// DataGridViewセルフォーマット時/// </summary>/// <param name="sender"></param>/// <param name="e"></param>privatevoiddgvList_CellFormatting(objectsender,DataGridViewCellFormattingEventArgse){DataGridViewdgv=senderasDataGridView;if(dgv==null)return;if(e.RowIndex<0||e.ColumnIndex<0||e.RowIndex>=dgv.Rows.Count||e.ColumnIndex>=dgv.Columns.Count)return;DataGridViewCellcell=dgv[e.ColumnIndex,e.RowIndex];if(cell==null)return;DataGridViewRowrow=cell.OwningRow;if(row==null)return;DataGridViewColumncol=cell.OwningColumn;if(col==null)return;DgvRowSample1rowBound=row.DataBoundItemasDgvRowSample1;if(rowBound==null)return;if(e.ColumnIndex==this.colName.Index){//名前if(rowBound.Name==null||rowBound.Name.Trim().Length==0){//未入力cell.Style.BackColor=Color.LightPink;}else{//入力有cell.Style.BackColor=Color.White;}}elseif(e.ColumnIndex==this.colKyoka1.Index){//国語の点数if(rowBound.Kyoka1Point!=null&&rowBound.Kyoka1Point.Value<30){//赤点cell.Style.BackColor=Color.LightPink;}else{//合格点cell.Style.BackColor=Color.White;}}elseif(e.ColumnIndex==this.colKyoka2.Index){//数学の点数if(rowBound.Kyoka2Point!=null&&rowBound.Kyoka2Point.Value<30){//赤点cell.Style.BackColor=Color.LightPink;}else{//合格点cell.Style.BackColor=Color.White;}}elseif(e.ColumnIndex==this.colKyoka3.Index){//理科の点数if(rowBound.Kyoka3Point!=null&&rowBound.Kyoka3Point.Value<30){//赤点cell.Style.BackColor=Color.LightPink;}else{//合格点cell.Style.BackColor=Color.White;}}elseif(e.ColumnIndex==this.colKyoka4.Index){//社会の点数if(rowBound.Kyoka4Point!=null&&rowBound.Kyoka4Point.Value<30){//赤点cell.Style.BackColor=Color.LightPink;}else{//合格点cell.Style.BackColor=Color.White;}}elseif(e.ColumnIndex==this.colKyoka5.Index){//英語の点数if(rowBound.Kyoka5Point!=null&&rowBound.Kyoka5Point.Value<30){//赤点cell.Style.BackColor=Color.LightPink;}else{//合格点cell.Style.BackColor=Color.White;}}}
DataGridViewにはCellFormatting
というイベントがあります。
そのイベントを使用して、上のようにプロパティを設定すると背景色を変更できます。
文字色を変更する場合は、ForeColor
プロパティを使用します。
エラーアイコン・メッセージの制御
少しトリッキーな方法ですが、Timer
クラスを利用する方法があります。
一定間隔でTick
イベントを発生させて、エラーチェックをし、エラーの場合はエラーアイコンとメッセージを表示させます。
コードの例を次に挙げます。
/// <summary>/// 表示更新用タイマー/// </summary>/// <param name="sender"></param>/// <param name="e"></param>privatevoidtmUpdateDisplay_Tick(objectsender,EventArgse){foreach(DataGridViewRowrowinthis.dgvList.Rows){DgvRowSample1rowBound=row.DataBoundItemasDgvRowSample1;if(rowBound==null)continue;foreach(DataGridViewColumncolinthis.dgvList.Columns){DataGridViewCellcell=this.dgvList[col.Index,row.Index];if(col.Index==this.colName.Index){//名前if(rowBound.Name==null||rowBound.Name.Trim().Length==0){//未入力cell.ErrorText="名前が未入力です。";}else{//入力有cell.ErrorText="";}}elseif(col.Index==this.colKyoka1.Index){//国語の点数if(rowBound.Kyoka1Point!=null&&rowBound.Kyoka1Point.Value<30){//赤点cell.ErrorText="国語が赤点です。";}else{//合格点cell.ErrorText="";}}elseif(col.Index==this.colKyoka2.Index){if(rowBound.Kyoka2Point!=null&&rowBound.Kyoka2Point.Value<30){//赤点cell.ErrorText="数学が赤点です。";}else{//合格点cell.ErrorText="";}}elseif(col.Index==this.colKyoka3.Index){//理科の点数if(rowBound.Kyoka3Point!=null&&rowBound.Kyoka3Point.Value<30){//赤点cell.ErrorText="理科が赤点です。";}else{//合格点cell.ErrorText="";}}elseif(col.Index==this.colKyoka4.Index){//社会の点数if(rowBound.Kyoka4Point!=null&&rowBound.Kyoka4Point.Value<30){//赤点cell.ErrorText="社会が赤点です。";}else{//合格点cell.ErrorText="";}}elseif(col.Index==this.colKyoka5.Index){//英語の点数if(rowBound.Kyoka5Point!=null&&rowBound.Kyoka5Point.Value<30){//赤点cell.ErrorText="英語が赤点です。";}else{//合格点cell.ErrorText="";}}}}}
ただし、重い処理やデータベースにアクセスするような処理はTimerイベントに記述することは望ましくないでしょう。
カスタムクラスの階層化
カスタムクラスを使用すればリストを選択すると子リストも変わる処理を簡単に実装することができます。
まず最初に親リストのカスタムクラスを作成します。
usingSystem;usingSystem.Collections.Generic;usingSystem.Text;namespaceDataGridViewBindingSample.Datas.DataGridView.Rows.Sample{/// <summary>/// カスタムクラスサンプル2-1/// </summary>publicclassDgvRowSample21:DataGridViewDataClassBase{privatestringstrName;privateSortableBindingList<DgvRowSample22>sbChild=newSortableBindingList<DgvRowSample22>();/// <summary>/// 名前/// </summary>publicstringName{get{returnthis.strName;}set{this.SetValue(outthis.strName,value,"Name");}}/// <summary>/// 教科の点数リスト(子バインドリスト)/// </summary>publicSortableBindingList<DgvRowSample22>ChildList{get{returnthis.sbChild;}set{this.sbChild=value;}}}}
注目すべき点は、親リストのカスタムクラスにバインドリスト(SortableBindingList
)がある点です。
このバインドリストの使い方は後述で述べます。
次に、子リストのカスタムクラスを作成します。
usingSystem;usingSystem.Collections.Generic;usingSystem.Text;namespaceDataGridViewBindingSample.Datas.DataGridView.Rows.Sample{/// <summary>/// カスタムクラスサンプル2-2/// </summary>publicclassDgvRowSample22:DataGridViewDataClassBase{privatestringstrKyoka;privateint?intPoint;/// <summary>/// 教科名/// </summary>publicstringKyoka{get{returnthis.strKyoka;}set{this.SetValue(outthis.strKyoka,value,"Kyoka");}}/// <summary>/// 点数/// </summary>publicint?Point{get{returnthis.intPoint;}set{this.SetValue(outthis.intPoint,value,"Point");}}/// <summary>/// 点数/// </summary>publicstringPointDisplay{get{returnthis.intPoint.ToString();}set{this.SetValueInputInt(refthis.intPoint,value,"PointDisplay");}}}}
フォーム上では、最初にDataGridViewに次のようにプロパティに設定します。
//親リストにデータソースを追加したとき勝手に列が追加されないようにするthis.dgvList.AutoGenerateColumns=false;//子リストにデータソースを追加したとき勝手に列が追加されないようにするthis.dgvListChild.AutoGenerateColumns=false;//親リストのデータソースを設定するthis.dgvList.DataSource=this.sbList;
DataGridViewにはセルの選択変更時に発生するSelectionChanged
というイベントがあります。
親リストのセルを選択変更したときにこのバインドリストを子リストのデータソースに設定します。
/// <summary>/// DataGridView選択変更時/// </summary>/// <param name="sender"></param>/// <param name="e"></param>privatevoiddgvList_SelectionChanged(objectsender,EventArgse){this.dgvListChild.DataSource=null;DataGridViewdgv=senderasDataGridView;if(dgv==null)return;if(dgv.SelectedCells.Count==0)return;DataGridViewCellcell=dgv.SelectedCells[0];if(cell==null)return;DataGridViewRowrow=cell.OwningRow;if(row==null)return;DataGridViewColumncol=cell.OwningColumn;if(col==null)return;DgvRowSample21rowBound=row.DataBoundItemasDgvRowSample21;if(rowBound==null)return;//子リストのデータソースをセットする。this.dgvListChild.DataSource=rowBound.ChildList;}
最後に
DataGridViewのカスタムクラスは「クラス」なので、いろいろな処理を実装することができます。
メソッドを追加して計算処理をしたり、文字列の加工処理をしたり、型の変換処理をすることもできます。
また、入力に不備がないかチェックするメソッドを実装することもできます。
本記事では色を挙げましたが、画像(Image
クラスなど)などいろいろ格納することができます。
バインドにカスタムクラスを使用すれば、実装の可能性が広がります。
いろいろ試行錯誤をして、DataGridViewを使用した実装をしてみてはいかがでしょうか?
最後までお読みいただきありがとうございました。