A few weeks
ago, a Business Analyst who works for a client of mine handed me requirements
for a WinForms project. One of the screenshots that the client painstakingly
created using some graphics program consisted of a grid that included a checkbox
column. The jarring thing about this checkbox column was that each cell in the
column also included some text. Since the DataGridView control doesn't
come "out of the box" with a combination Checkbox/Text type column, my initial
reflex reaction was to suggest that we either change our approach, or look into
third party products. However, before going down that road, I decided to try to
create one. So I tried, it worked, and we all lived happily ever after. Here's
the approach:
Since I was going to all this trouble (which
actually turned out to be pretty easy), I decided I should make this thing as
configurable as possible. Rather than hard-code it according to my client's
specific requirements, I decided to include the following properties: "Text",
"Color", "Font", and "Enabled", so that it would be flexible enough should this
client's needs change (and so I could use it in other projects for other clients
in the future).
To start out with, we need to extend not
only the DataGridViewCheckBoxCell class, but theDataGridViewCheckBoxColumn class
as well. I called my new classesDataGridViewCheckAndTextColumn and DataGridViewCheckAndTextCell.
Here's how we'll start:
public class
DataGridViewCheckAndTextColumn :
DataGridViewCheckBoxColumn
{
public DataGridViewCheckAndTextColumn()
{
this.CellTemplate =
new
DataGridViewCheckAndTextCell();
}
}
public class
DataGridViewCheckAndTextCell :
DataGridViewCheckBoxCell
{
public DataGridViewCheckAndTextCell()
{
this.Enabled =
true;
}
private bool
enabled;
public bool
Enabled
{
get
{
return enabled;
}
set
{
enabled = value;
this.ReadOnly = !enabled;
}
}
private string
text;
public string
Text
{
get {
return text; }
set { text =
value; }
}
private System.Drawing.Color
color;
public System.Drawing.Color
Color
{
get
{
return color;
}
set { color =
value; }
}
private System.Drawing.Font
font;
public System.Drawing.Font
Font
{
get {
return font; }
set { font =
value; }
}
}
Note that the DataGridViewCheckAndTextColumn class contains a constructor
which tells it that its cells will be of typeDataGridViewCheckAndTextCell.
And, the DataGridViewCheckAndTextCell class itself contains the
properties I mentioned earlier, in addition to a constructor which sets the
Enabled property to True by default.
Next, we're going to override
the DataGridViewCheckBoxCell's Paint() method. Within our version
of the Paint() method, we will first call the base class's (DataGridViewCheckBoxCell's) Paint() method,
and then do some work of our own. We call the base class's Paint() method
so that it can do whatever other stuff it normally does (we don't necessarily
need to know what that "other stuff" might consist of isn't inheritance
wonderful?), and then we do what we need to do to make our class work the way we
want it to. Here's the code. Explanations follow:
protected
override void
Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds,
int rowIndex, DataGridViewElementStates
elementState, object value,
object formattedValue,
string errorText, DataGridViewCellStyle
cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle,
DataGridViewPaintParts paintParts)
{
base.Paint(graphics, clipBounds, cellBounds,
rowIndex, elementState, value,
formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts);
if (this.Font
== null)
this.Font = cellStyle.Font;
if (!this.Enabled)
this.Color = Color.Gray;
else if (this.Color.IsEmpty)
this.Color = cellStyle.ForeColor;
CheckBoxState state;
bool val = this.Value
== null || !Convert.ToBoolean(this.Value)
? false : Convert.ToBoolean(this.Value);
if (this.enabled
&& val)
state = CheckBoxState.CheckedNormal;
else if (this.enabled
&& !val)
state = CheckBoxState.UncheckedNormal;
else if (!this.enabled
&& val)
state = CheckBoxState.CheckedDisabled;
else
state = CheckBoxState.UncheckedDisabled;
Point loc = new
Point(cellBounds.X + 2, cellBounds.Y + 2);
CheckBoxRenderer.DrawCheckBox(graphics, loc, state);
Rectangle contentBounds = this.GetContentBounds(rowIndex);
Point stringLocation = new Point();
stringLocation.Y = cellBounds.Y + 2;
stringLocation.X = cellBounds.X + contentBounds.Right + 2;
graphics.DrawString(this.Text,
this.Font, new
SolidBrush(this.Color), stringLocation);
}
The first thing you'll notice (after the call to base.Paint() ) is that
we're checking the values of our properties, and taking appropriate actions.
First, we check to see whether the calling code has set a value for this.Font.
If that property is null (the property hasn't been set), we'll take the default
Font (passed in to the Paint() method by the DataGridView control)
and use that as our font. Next, we check to see if the Enabled property
is set to true or false. If Enabled is false, we're going to set this.Color to
gray, no matter what the Color property says. Otherwise, if the property
has not been set, we'll use the default color passed in to the Paint() method
by the DataGridView control. And lastly, if the property was set by the
calling code and Enabled is true, we do nothing at this point, and use this.Color as
the calling code intended.
Next we set the state of the
checkbox. There are four possible values from the CheckBoxState enumerator
for our purposes:CheckedNormal, UncheckedNormal, CheckedDisabled,
and UncheckedDisabled. We set the state variable based on the underlying
value of the data in the cell, combined with the this.Enabled property
value.
Now that all of our properties and variables
have been set, we'll put them to work. First, we determine our starting point,
based on the cell boundaries. We pass the cellBounds variable passed into
the Paint() method to instantiate a Point object (loc),
adding 2 pixels top and left to create a little margin. Once we have our
starting location, we can use it, along with our state variable
and thegraphics object passed in to Paint(), to render a checkbox
exactly where we want it within the cell (all the way to the left so that
there's room for the text we're going to add).
Now we're ready to draw the text. First, we
get a Rectangle object, contentBounds, based on the rowIndex which
is also passed in to the Paint() method. ( I told you this was easy!)
Next, we instaniate a Point object (stringLocation). Its X coordinate
will be 2 pixels to the right of the checkbox, and its Y coordinate will
be 2 pixels down from the top of the cell. Then we draw a string beginning at
that location. We do that by calling the DrawString() method on the graphics object.
The method takes four parameters; the string to draw (for which we use the value
in this.Text which gets set by the calling code), the font (this.Font),
the color (this.Color), and of course, the location (stringLocation).
Believe it or not, that's all there is to
it. You can use your new masterpiece just like you would use any other type of DataGridViewcolumn.
It'll even show up in the designer for you:
And the calling code will look something
like this:
private
void grid1_DataBindingComplete(object
sender, DataGridViewBindingCompleteEventArgs e)
{
DataGridViewCheckAndTextCell ctCell = null;
foreach (DataGridViewRow row
in grid1.Rows)
{
if (row != null)
{
ctCell = row.Cells["CheckAndText"] as
DataGridViewCheckAndTextCell;
if (ctCell !=
null)
{
ctCell.Enabled = true;
ctCell.Text = "Hello!";
ctCell.Color = Color.Blue;
}
}
}
}
Here's a screenshot
of the final product:
That's all there is to it. Oh,
and if you're wondering how I got that column header to span multiple columns,
check out my article entitled, Creating
A Multiple Column Header Within DataGridView.
Enjoy!
Dave Verschleiser