This code sample was inspired from a question that Adam Cogan asked me about the DataGridView
: Suppose you wanted to show some kind of “bar graph” or a progress bar inside one of the columns of your grid. Here is an example:
In HTML, a common approach would be to use a 1px wide image, and have the width of the image bound to the value of the cell so that it would “stretch” to be a percentage of the total width available. This is a common approach you’ve probably seen on many web sites, but it isn’t so simple under Windows Forms.
The approach I suggested was very similar to my “Data Binding and lookup values” post. I created a custom DataGridView
Column and Cell combination, which now show in the designer:
The reasoning behind creating a custom cell/column combination is that it’s a lot cleaner than handling a bunch of Paint
events on the DataGridView
cells, and it’s something that can easily be reused.
To create my custom cell, I inherited from the DataGridViewImageCell
class. Now, when a DataGridViewCell
is being displayed, the cell’s GetFormattedValue
method is called, passing in the bound value (in our case, a number), and expecting to get a value that it can display in return. In the case of a DataGridViewTextBoxCell
, this would be a string, but in the case of a DataGridViewImageCell
, it needs an image (note: this is why if you try to bind an integer property to an Image cell you’ll get an exception).
To satisfy this need, I create a new Bitmap
in memory inside the GetFormattedValue
method, and measure out a rectangle that fills the right width of the cell. I then fill that with the chosen color, and return this new image to be displayed. The image is only drawn once this way (no matter how many times the form is “painted”) so the whole process is pretty efficient.
For completeness, the code is below:
... {
private DataGridViewCell _cellTemplate;
private Color _fillColor = Color.Red;
private double _maximumValue = 100.00;
public override DataGridViewCell CellTemplate
...{
get
...{
if (_cellTemplate != null)
...{
return _cellTemplate;
}
else
...{
return new ProgressCell(this.FillColor, this.MaximumValue);
}
}
set
...{
_cellTemplate = value;
}
}
public Color FillColor
...{
get ...{ return _fillColor; }
set ...{ _fillColor = value; }
}
public double MaximumValue
...{
get ...{ return _maximumValue; }
set ...{ _maximumValue = value; }
}
public override object Clone()
...{
ProgressColumn c = (ProgressColumn)base.Clone();
c.FillColor = this.FillColor;
c.MaximumValue = this.MaximumValue;
return c;
}
}
... {
private Color _fillColor;
private double _maximumValue = 100.00;
public ProgressCell()
...{
}
public ProgressCell(Color fillColor, double maximumValue)
...{
this.FillColor = fillColor;
this.MaximumValue = maximumValue;
}
public Color FillColor
...{
get ...{ return _fillColor; }
set ...{ _fillColor = value; }
}
public double MaximumValue
...{
get ...{ return _maximumValue; }
set ...{ _maximumValue = value; }
}
public override object Clone()
...{
ProgressCell c = (ProgressCell)base.Clone();
c.FillColor = this.FillColor;
c.MaximumValue = this.MaximumValue;
return c;
}
protected override object GetFormattedValue(
object value,
int rowIndex,
ref DataGridViewCellStyle cellStyle,
System.ComponentModel.TypeConverter valueTypeConverter,
System.ComponentModel.TypeConverter formattedValueTypeConverter,
DataGridViewDataErrorContexts context)
...{
if (value is int || value is double || value is long || value is short)
...{
double actualValue = Convert.ToDouble(value);
if (actualValue > this.MaximumValue)
...{
actualValue = this.MaximumValue;
}
// Convert the value we’re bound to (an integer for demo purposes) to an
// image which we will draw on.
Bitmap resultImage = new Bitmap(this.OwningColumn.Width,
this.OwningRow.Height);
// Figure out which color we should use to fill the cell
Color cellFillColor = this.FillColor;
if (this.Selected)
...{
// Fade the color slightly
cellFillColor = Color.FromArgb(100,
this.FillColor.R,
this.FillColor.G,
this.FillColor.B);
}
// Now draw the insides of the image
using (Graphics g = Graphics.FromImage(resultImage))
using (SolidBrush redFill = new SolidBrush(cellFillColor))
...{
// Figure out how wide the colored-in section should be (as a
// percentage of the width of the column).
int width = (int)((actualValue / this.MaximumValue) * resultImage.Width);
Rectangle r = new Rectangle(0, 0, width, resultImage.Height);
g.FillRectangle(redFill, r);
}
return resultImage;
}
else
...{
// It’s not a number so we have no idea how to display it.
// Let the image cell do the default action instead.
return base.GetFormattedValue(
value,
rowIndex,
ref cellStyle,
valueTypeConverter,
formattedValueTypeConverter,
context);
}
}
}
You could easily experiment with different brushes (try a LinearGradientBrush
for example) to achieve some cool GDI+ effects inside your cell.
Note: I managed to get the custom properties on the cell working by correctly implementing the CellTemplate
property on the column, and also by overriding the Clone
method properly, thanks to a comment left by Joo Lee on my previous blog entry. Cheers!