DataGridViewのカスタム列タイプの作成はじめに私のところには、プログラミングに関するさまざまな質問が電子メールで送られてきます。できる限りすべての問題を解決するように努力していますが、ランダムな質問の多くはパブリックニュースグループの方へ回しています。それでも、自分で調べずにはいられない興味深い質問もあります。先日も、友人から次のようなメールが届きました。 「Webページに表形式で値を表示しているんだ。そのうちの1つの列に、表中の値に基づくシンプルな小さい横棒グラフを表示している。この横棒グラフは単色のビットマップで、グラフの長さはフィールドの値を示している。このグラフの横にはフィールド値も表示している。単純な表にこういう視覚情報を加えるのはすごく効果的だと思うんだ。これをDataGridViewコントロールで行うにはどうすればいいかな?」 私の予想に反して、少し調べてみると、DataGridViewに新しい列タイプを作成すればこれを実現できることがわかりました。Visual Studio 2003のGridViewコントロールで非常に苦労した経験から、初めは不安でした。ところが、.NETの世界ではよくあるように、以前は難しかったこの処理も今は簡単に実現できることがわかりました。私はすぐにサンプルを作成して友人へ送りました。今回は、それにもう少し手を加えたものを紹介します。 この記事では、DataGridViewコントロールのカスタム列タイプの作成方法を説明するために、横棒グラフを表示するセルタイプのサンプルを見ていきます。このサンプルの作成方法を習得すれば、今度はもっと複雑な列タイプも作成できるようになるでしょう。 以降では、図1のフォームのようなサンプルを作成します。このフォームには、SQL Server 2000 Northwindサンプルデータベースのデータを使用していますが、他のテーブルを使って作ってみることもできます。ただ、整数列が含まれるデータソースを使用してください。また、横棒グラフの長さは、セルの境界内に収まるように調整されます(個々の長さは列の最大値に基づいて決定します)。列幅を広げると、横棒グラフの長さも列幅に合わせて長くなります。図2は、このサンプルの列の動作を示すため、同じ列を3種類の列幅で表示したものです。 図1 完成したフォーム。列の1つに、単なる数字の代わりに横棒グラフが表示されている ![]() 図2 同じ列を3種類の列幅で表示した様子 ![]() DataGridViewのカスタム列タイプ作成の基本概念は単純です。DataGridViewCellクラスを継承する独自のセルクラス(またはDataGridViewCellクラスを継承するクラスを継承するクラス)を作成します。このセルクラスでは、基本クラスの多数のプロテクトメンバをオーバーライドできます。たとえば、この記事のサンプルのようにセルの内容の描画方法を変更する場合は、クラスの セルクラスを作成したら、次はDataGridViewColumnクラスを継承するクラスを作成します。このクラスでも多くのメンバをオーバーライドできますが、通常、その必要はありません。必要なのは、このクラスのコンストラクタ内でCellTemplateプロパティを設定することだけです。最後に、DataGridViewの設定で、目的の列に新しい列タイプを指定すれば完了です(かつては、GridViewの中にComboBoxコントロールを表示する列を作成するには何百行ものコードを書かなければなりませんでした。辛抱強く待った甲斐があるというものです)。 編集部注
本稿は、『CoDe Magazine』2007年7月/8月号に掲載された記事を、承諾を得て転載したものです。
サンプルの作成まず、Visual Studio 2005で新規の[Windowsアプリケーション]プロジェクトを作成します。次に、新しいデータソースを作成します([データ]メニューの[新しいデータソースの追加]をクリックします)。2、3個またはそれ以上の列を含むデータソースを作成します。そのうち1つの列は整数列を選択します。 ここで示すサンプルでは、Northwind ProductsテーブルのProductID、ProductName、UnitsInStockの3つのフィールドを使用します。[データソースの表示]ウィンドウが表示されている状態で(表示されていない場合は[データ]メニューの[データソースの表示]をクリックします)、プロジェクトの単一フォームをフォームデザイナで開き、テーブル全体をこのフォームへドラッグします。Visual Studioによって、フォームにDataGridViewコントロールが自動的に作成されます(DataSet、BindingSource、TableAdapterなど、Windowsフォーム標準のデータバインディング要素も設定されます)。DataGridViewコントロールのスマートタグで、[親コンテナにドッキングする]を選択します。最後に、プロジェクトを保存してフォームを実行し、連結したデータがDataGridViewコントロールにそのまま表示されることを確認します。 これで土台はできました。これ以降、図1と同じフィールドを使用しているものとして説明を進めます。最初の実作業として、まずプロジェクトにDataGridViewBarGraphColumnという新しいクラスを追加します。このクラスではDataGridViewColumnクラスを継承します。このクラスのコードを次のように変更して、後でカスタムセルタイプを作成できるようにスタブを追加しておきます。後でまたこのクラスに戻り、仕上げを行います。 Visual Basic
Public Class DataGridViewBarGraphColumn Inherits DataGridViewColumn Public MaxValue As Long Private needsRecalc As Boolean = True Public Sub CalcMaxValue() End Sub End Class C#
public class DataGridViewBarGraphColumn : DataGridViewColumn { public DataGridViewBarGraphColumn(){} public long MaxValue; private bool needsRecalc = true; public void CalcMaxValue() {} } 次に、今度はDataGridViewBarGraphCellという名前の新しいクラスをプロジェクトに追加します。今回のサンプルでは、標準のテキストボックスセルの動作をエミュレートする必要があるので、このクラスがDataGridViewTextBoxCellクラス(セルの背景や内容を描画する機能を含む)を継承するようにします。C#の場合は、次のコード例のように、ファイルの先頭に Visual Basic
Public Class DataGridViewBarGraphCell Inherits DataGridViewTextBoxCell End Class C#
// Add these to your file: using System.Drawing; using System.Windows.Forms; // Your class should look like this: public class DataGridViewBarGraphCell : DataGridViewTextBoxCell { } このクラスでは、基本クラスのメソッドのうち Visual Basic
Overrides Paint
C#
public override Paint これで、Visual Studioによって宣言の内容が自動的に補充されます。以下は、このページに収まるように編集し直したものです(コードを読みやすくするために、関係のない名前空間参照も削除しました)。 Visual Basic
Protected Overrides Sub Paint( _ ByVal graphics As Graphics, _ ByVal clipBounds As Rectangle, _ ByVal cellBounds As Rectangle, _ ByVal rowIndex As Integer, _ ByVal cellState As DataGridViewElementStates, _ ByVal value As Object, _ ByVal formattedValue As Object, _ ByVal errorText As String, _ ByVal cellStyle As DataGridViewCellStyle, _ ByVal advancedBorderStyle As DataGridViewAdvancedBorderStyle, _ ByVal paintParts As DataGridViewPaintParts) MyBase.Paint(graphics, clipBounds, cellBounds, _ rowIndex, cellState, value, _ formattedValue, errorText, cellStyle, _ advancedBorderStyle, paintParts) End Sub C#
protected override void Paint( Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts) { base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts); } このコードを見てもわかるように、グリッドの各セルの描画時には、DataGridViewコントロールからDataGridViewTextBoxCellクラスに(ひいてはこのクラスを継承するかカスタムセルクラスにも)大量の情報が渡されます。表1に、この 表1 DataGridViewTextBoxCellのPaintメソッドのパラメータ
このサンプルの場合、 このサンプルは比較的単純で、横棒グラフと書式設定された値を並べて表示するだけです。表1に示されたパラメータのうち、このサンプルで使用する必要があるのは まず、先ほど作成した Visual Basic
MyBase.Paint(graphics, clipBounds, cellBounds, _ rowIndex, cellState, _ value, "", errorText, cellStyle, _ advancedBorderStyle, paintParts) C#
base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, "", errorText, cellStyle, advancedBorderStyle, paintParts); 次に、セルの値を取得するコードを追加します。値がDBNullの場合は0として処理します。値が0の場合は1に変更します(このようにすることで、値が0でも1ピクセル幅の縦棒が表示されます)。このようにする必要がなければ、そのままの単純な変換にしてもかまいません。 Visual Basic
' Get the value of the cell: Dim cellValue As Decimal If IsDBNull(value) Then cellValue = 0 Else cellValue = CDec(value) End If ' If cell value is 0, you still want to ' show something, so set the value to 1. If cellValue = 0 Then cellValue = 1 End If C#
// Get the value of the cell: decimal cellValue = 0; if (Convert.IsDBNull(value)) cellValue = 0; else cellValue = Convert.ToDecimal(value); // If cell value is 0, you still want to // show something, so set the value to 1. if (cellValue == 0) cellValue = 1; 左端からのオフセット、および横棒グラフとテキストの間の幅を制御する2つの定数を追加します。この2つの値は、好みに合わせて変更できます。 Visual Basic
Const HORIZONTALOFFSET As Integer = 1 Const SPACER As Integer = 4 C#
const int HORIZONTALOFFSET = 1; const int SPACER = 4; 配置の処理このようなプログラミングには、当然ながら、細かい配置処理が伴います。最長の横棒と各横棒の長さの比率、最長の横棒に使用できるスペース、関連テキスト用に残すべきスペースなどを計算する必要があります。 この問題を解決するには、列のすべての値の最大値を取得する必要があります。これは親列で処理できます。親列への参照は、セルの Visual Basic
' Get the parent column and the maximum value: Dim parent As DataGridViewBarGraphColumn = _ CType(Me.OwningColumn, DataGridViewBarGraphColumn) parent.CalcMaxValue() Dim maxValue As Long = parent.MaxValue C#
// Get the parent column and the maximum value: DataGridViewBarGraphColumn parent = (DataGridViewBarGraphColumn)this.OwningColumn; parent.CalcMaxValue(); long maxValue = parent.MaxValue; ここで、列の Visual Basic
Dim fnt As Font = parent.InheritedStyle.Font C#
Font fnt = parent.InheritedStyle.Font; 編集部注
DataGridViewコントロールのセルと列のスタイルの継承は、非常に綿密にサポートされています。詳細については、MSDNヘルプトピック「WindowsフォームDataGridViewコントロールでのセルのスタイル」を参照してください。
各セルで使用可能なスペースの量を計算するには、最大値のテキストの幅を把握しておく必要があります。この幅がわかれば、各セルの横棒の最大サイズを計算できます。必要な値は Visual Basic
Dim maxValueSize As SizeF = _ graphics.MeasureString(maxValue.ToString, fnt) Dim availableWidth As Single = _ cellBounds.Width - maxValueSize.Width - _ SPACER - (HORIZONTALOFFSET * 2) C#
SizeF maxValueSize =
graphics.MeasureString(maxValue.ToString(), fnt);
float availableWidth =
cellBounds.Width - maxValueSize.Width -
SPACER - (HORIZONTALOFFSET * 2);
横棒に使用できる幅を計算したら、使用可能なスペースに収まるように、現在のセルの値と最大値の比率を使用して、現在のセルの横棒の幅(長さ)を計算できます。これを処理する次のコードを追加します。 Visual Basic
cellValue = CDec((cellValue / maxValue) * _
availableWidth)
C#
cellValue = Convert.ToDecimal( (Convert.ToDouble(cellValue) / maxValue) * availableWidth); これで内容を描画する準備ができました。次のコードを追加してください。このコードでは、横棒のサイズを計算し、横棒を赤いブラシで描画します。 Visual Basic
' Draw the bar, truncating to fit in the space ' you've got in the cell: Const VERTOFFSET As Integer = 4 Dim newRect As New RectangleF( _ cellBounds.X + HORIZONTALOFFSET, _ cellBounds.Y + VERTOFFSET, _ cellValue, _ cellBounds.Height - (VERTOFFSET * 2)) graphics.FillRectangle(Brushes.Red, newRect) C#
const int VERTOFFSET = 4; RectangleF newRect = new RectangleF( cellBounds.X + HORIZONTALOFFSET, cellBounds.Y + VERTOFFSET, Convert.ToSingle(cellValue), cellBounds.Height - (VERTOFFSET * 2)); graphics.FillRectangle(Brushes.Red, newRect); 次に、テキストを描画するコードを追加します。 Visual Basic
' Get the text to draw and calculate its width: Dim cellText As String = formattedValue.ToString() Dim textSize As SizeF = _ graphics.MeasureString(cellText, fnt) ' Calculate where text would start: Dim textStart As PointF = _ New PointF( _ HORIZONTALOFFSET + cellValue + SPACER, _ (cellBounds.Height - textSize.Height) / 2) ' Calculate the correct color: Dim textColor As Color = _ parent.InheritedStyle.ForeColor If (cellState And _ DataGridViewElementStates.Selected) = _ DataGridViewElementStates.Selected Then textColor = parent.InheritedStyle.SelectionForeColor End If ' Draw the text: Using brush As New SolidBrush(textColor) graphics.DrawString(cellText, fnt, brush, _ cellBounds.X + textStart.X, _ cellBounds.Y + textStart.Y) End Using C#
string cellText = formattedValue.ToString(); SizeF textSize = graphics.MeasureString(cellText, fnt); // Calculate where text would start: PointF textStart = new PointF( Convert.ToSingle(HORIZONTALOFFSET + cellValue + SPACER), (cellBounds.Height - textSize.Height) / 2); // Calculate the correct color: Color textColor = parent.InheritedStyle.ForeColor; if ((cellState & DataGridViewElementStates.Selected) == DataGridViewElementStates.Selected) { textColor = parent.InheritedStyle.SelectionForeColor; } // Draw the text: using (SolidBrush brush = new SolidBrush(textColor)) { graphics.DrawString(cellText, fnt, brush, cellBounds.X + textStart.X, cellBounds.Y + textStart.Y); } このコードでは、まずテキストの開始位置の計算と新しいFontオブジェクトの作成を行っています。このコードの次の部分は、新しいフォントでのテキストの幅を計算しています。 Visual Basic
Dim cellText As String = formattedValue.ToString() Dim textSize As SizeF = _ graphics.MeasureString(cellText, fnt) C#
string cellText = formattedValue.ToString();
SizeF textSize =
graphics.MeasureString(cellText, fnt);
次の部分は、テキストの先頭がセル内のどこになるかを計算しています。横棒の幅、各種オフセット、セルとテキストの高さから計算しています。 Visual Basic
Dim textStart As PointF = _ New PointF( _ HORIZONTALOFFSET + cellValue + SPACER, _ (cellBounds.Height - textSize.Height) / 2) C#
PointF textStart = new PointF(
Convert.ToSingle(HORIZONTALOFFSET +
cellValue + SPACER),
(cellBounds.Height - textSize.Height) / 2);
また、選択中のセルのテキストは標準のセルとは異なる表示にするため、セルが選択されているかどうかを検出し、ふさわしいテキストカラーにする必要があります。このコードの次の部分は、 Visual Basic
Dim textColor As Color = _ parent.InheritedStyle.ForeColor If (cellState And _ DataGridViewElementStates.Selected) = _ DataGridViewElementStates.Selected Then textColor = parent.InheritedStyle.SelectionForeColor End If C#
Color textColor = parent.InheritedStyle.ForeColor;
if ((cellState &
DataGridViewElementStates.Selected) ==
DataGridViewElementStates.Selected)
{
textColor = parent.InheritedStyle.SelectionForeColor;
}
最後に、テキストをセル内の適切な位置に描画します。 Visual Basic
Using brush As New SolidBrush(textColor) graphics.DrawString(cellText, fnt, brush, _ cellBounds.X + textStart.X, _ cellBounds.Y + textStart.Y) End Using C#
using (SolidBrush brush = new SolidBrush(textColor)) { graphics.DrawString(cellText, fnt, brush, cellBounds.X + textStart.X, cellBounds.Y + textStart.Y); } 列の完成 これでセル部分はできあがりです。この列の作成の仕上げとして、DataGridViewBarGraphColumnクラスに既定のコンストラクタを追加し、そのコードを編集して列の Visual Basic
Public Class DataGridViewBarGraphColumn Inherits DataGridViewColumn Public Sub New() Me.CellTemplate = _ New DataGridViewBarGraphCell() Me.ReadOnly = True End Sub Public MaxValue As Long Private needsRecalc As Boolean = True Public Sub CalcMaxValue() End Sub End Class C#
public class DataGridViewBarGraphColumn : DataGridViewColumn { public DataGridViewBarGraphColumn() { this.CellTemplate = new DataGridViewBarGraphCell(); this.ReadOnly = true; } public long MaxValue; private bool needsRecalc = true; public void CalcMaxValue(){} } 最後に、列の最大値の計算処理を作成します。列自体は親コントロールのイベントにフックできないので、簡単な方法として、セルの描画時に各セルから呼び出すプロシージャ(CalcMaxValue)を使用します。本当は各セルが描画されるごとに最大値を計算しなくてもよいのですが、すべてのデータが読み込まれるまで実行されないコードを記述する場所として、これ以外に単純明白なわかりやすい場所がないので、この方法にします(この問題を解決する別の方法もあるかもしれませんが、ここに示した方法は比較的無難であり、また正しく動作します)。 CalcMaxValueプロシージャを編集し、現在の列のすべての値を調べて最大値を追跡するようにします。プロシージャの最後でneedsRecalcフィールドをfalseに設定し、それ以降のプロシージャ呼び出しで最大値を再計算しないようにします。 Visual Basic
Public Sub CalcMaxValue() If needsRecalc Then Dim colIndex As Integer = Me.DisplayIndex For rowIndex As Integer = 0 To _ Me.DataGridView.Rows.Count - 1 Dim row As DataGridViewRow = _ Me.DataGridView.Rows(rowIndex) MaxValue = Math.Max(MaxValue, _ CLng(row.Cells(colIndex).Value)) Next needsRecalc = False End If End Sub C#
public void CalcMaxValue() { if (needsRecalc) { int colIndex = this.DisplayIndex; for (int rowIndex = 0; rowIndex < this.DataGridView.Rows.Count; rowIndex++) { DataGridViewRow row = this.DataGridView.Rows[rowIndex]; MaxValue = Math.Max(MaxValue, Convert.ToInt64(row.Cells[colIndex].Value)); } needsRecalc = false; } } needsRecalcフィールドをパブリックにして他の呼び出し元からリセットできるようにすることもできます。たとえば、ユーザーが列の値を変更できるようにする場合、最大値の再計算を強制的に実行できるようにする必要があります。親グリッドのセル変更イベントをトラップして、対象の列の値が変更された場合に再計算を強制実行できるようにすることも可能です。これは読者への宿題にしましょう。 これで完成です。この列のセルテンプレートにDataGridViewBarGraphCellクラスを使用することを指定し、この列が読み取り専用であることを指定し、列の最大値を計算するコードを追加しました。 フォームのデザインビューに戻り、DataGridViewコントロールの各列を設定します。図3のように、UnitsInStock列(または使用する他の数値列)に、列の種類としてDataGridViewBarGraphColumnを選択します。[OK]をクリックし、プロジェクトを保存して実行します。問題なく動作すれば、図1のように、DataGridViewに横棒グラフ列が表示されるはずです。 もちろん、もっといろいろなカスタマイズも可能です。Button、CheckBox、ComboBox、Image、Linkといったコントロールを表示する列など、さまざまなタイプを継承できます。適切なクラスを継承すれば、ほとんどの処理が自動的に行われます。また、各基本クラスにはオーバーライド可能なプロテクトメソッドが多数あり、これによって、求めるとおりのセルタイプを作成できます。このようなプロジェクトでは、System.Drawing.Drawing2D名前空間のクラスの知識は非常に役に立ちます。このサンプルで使用したクラスをこれまで使ったことがない場合は、この機会に調べてみてください。きっとまた利用することになるでしょう。 著者紹介Ken Getz(Ken Getz)
MCW Technologies社上級顧問。プログラミング、執筆、教育に携わる。専門はVisual Studio .NETおよびVisual Basicでのツール/アプリケーション開発。『CoDe Magazine』誌の好評コラム「.Finalize()」を担当。共著にベストセラーとなった『Access 2002 Developer's Handbooks』(Paul Litwin、Mike Gunderloy共著、Sybex刊)、『Visual Basic Language Developer's Handbook』(Mike Gilbert共著、Sybex刊)、『VBA Developer's Handbook』(Mike Gilbert共著、Sybex刊)など。Application Developer's Training Company社のVB.NET、ASP.NET、Access 2000、Access 97、Visual Basic 6、Visual Basic 5セミナーのトレーニングコースの共著者でもあり、同社のVB.NET、ASP.NET、VB6、Access 2000、Access 97のトレーニングビデオも作成。各種技術会議での講演多数、MicrosoftのTech-Edカンファレンスでも講演。Access-VB-SQL Advisor誌のテクニカルエディター、Informant Publicationsのasp.netPRO誌のコラムニストも務める。
|