Extending The DataGridViewCheckBoxCell To Include Text

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, 
 
  1. public class DataGridViewCheckAndTextColumn: DataGridViewCheckBoxColumn {    
  2.        public DataGridViewCheckAndTextColumn() {    
  3.            this.CellTemplate = new DataGridViewCheckAndTextCell();    
  4.        }    
  5.    }    
  6.   public class DataGridViewCheckAndTextCell: DataGridViewCheckBoxCell {    
  7.        public DataGridViewCheckAndTextCell() {    
  8.            this.Enabled = true;    
  9.        }    
  10.        private bool enabled;    
  11.        public bool Enabled {    
  12.            get {    
  13.                return enabled;    
  14.            }    
  15.            set {    
  16.                enabled = value;    
  17.                this.ReadOnly = !enabled;    
  18.            }    
  19.        }    
  20.        private string text;    
  21.        public string Text {    
  22.            get {    
  23.                return text;    
  24.            }    
  25.            set {    
  26.                text = value;    
  27.            }    
  28.        }    
  29.        private System.Drawing.Color color;    
  30.        public System.Drawing.Color Color {    
  31.            get {    
  32.                return color;    
  33.            }    
  34.            set {    
  35.                color = value;    
  36.            }    
  37.        }    
  38.        private System.Drawing.Font font;    
  39.        public System.Drawing.Font Font {    
  40.            get {    
  41.                return font;    
  42.            }    
  43.            set {    
  44.                font = value;    
  45.            }    
  46.        }    
  47.    }   
 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,
 
  1. 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) {    
  2.        base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts);    
  3.        if (this.Font == nullthis.Font = cellStyle.Font;    
  4.        if (!this.Enabled) this.Color = Color.Gray;    
  5.        elseif(this.Color.IsEmpty)    
  6.        this.Color = cellStyle.ForeColor;    
  7.        CheckBoxState state;    
  8.        bool val = this.Value == null || !Convert.ToBoolean(this.Value) ? false : Convert.ToBoolean(this.Value);    
  9.        if (this.enabled && val) state = CheckBoxState.CheckedNormal;    
  10.        elseif(this.enabled && !val)    
  11.        state = CheckBoxState.UncheckedNormal;    
  12.        elseif(!this.enabled && val)    
  13.        state = CheckBoxState.CheckedDisabled;    
  14.        else state = CheckBoxState.UncheckedDisabled;    
  15.        Point loc = new Point(cellBounds.X + 2, cellBounds.Y + 2);    
  16.        CheckBoxRenderer.DrawCheckBox(graphics, loc, state);    
  17.        Rectangle contentBounds = this.GetContentBounds(rowIndex);    
  18.        Point stringLocation = new Point();    
  19.        stringLocation.Y = cellBounds.Y + 2;    
  20.        stringLocation.X = cellBounds.X + contentBounds.Right + 2;    
  21.        graphics.DrawString(this.Text, this.Font, new SolidBrush(this.Color), stringLocation);    
  22.    }   
 
 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,

 
 
 
  1. private void grid1_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e) {    
  2.       DataGridViewCheckAndTextCell ctCell = null;    
  3.       foreach(DataGridViewRow row in grid1.Rows) {    
  4.           if (row != null) {    
  5.               ctCell = row.Cells["CheckAndText"as DataGridViewCheckAndTextCell;    
  6.               if (ctCell != null) {    
  7.                   ctCell.Enabled = true;    
  8.                   ctCell.Text = "Hello!";    
  9.                   ctCell.Color = Color.Blue;    
  10.               }    
  11.           }    
  12.       }    
  13.   }   
 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!