Introduction
I recently wanted to display some HTML content in a DataGridView cell. However, after searching the web I wasn't able to find anything that fitted my needs (i didn't find anything). So I decided to investigate what was involved in writing a custom DataGridView Cell.
Basically to create your own custom cell you need to create / derive from three base classes to do with the DataGridView control. These are:
- Derive a class from DataGridViewColumn
- Derive a class from DataGridViewCell
- Derive a class that implements the IDataGridViewEditingControl interface
Below is a screen shot of the demo application.
The source code / sample application for this project can be found on GitHub here: https://github.com/OceanAirdrop/DataGridViewHTMLCell
HTML Renderer
To render the HTML content in the cell I will be using the HTMLRenderer library. This is a fantastic framework. It is lightweight, includes its own rendering engine (no external dependencies) and includes only 2 .DLL's. Check it out here: https://htmlrenderer.codeplex.com/ for more information.
To add HTMLRenderer to your project either use nuget to add the references automatically or download the .dll's from codeplex/github and reference them in your C# project manually.
Deriving from DataGridViewColumn (Step 1 of 3)
Let's first look at the 1st of the 3 classes that need to be derived from: The DataGridViewColumn class. MSDN states that the DataGridViewColumn
class "represents a logical column in a DataGridView control". This class is needed and provides scaffolding for the DataGridView to work. It lets the DataGridView know what type of cell is in that column.
Here is the class in its entirety:
Code Snippet 1: Setting up the DataGridViewHTMLColumn
public class DataGridViewHTMLColumn : DataGridViewColumn
{
public DataGridViewHTMLColumn() : base(new DataGridViewHTMLCell())
{
}
public override DataGridViewCell CellTemplate
{
get
{
return base.CellTemplate;
}
set
{
if (!(value is DataGridViewHTMLCell))
throw new InvalidCastException("CellTemplate must be a DataGridViewHTMLCell");
base.CellTemplate = value;
}
}
}
Short and sweet eh? Not much to see here. - Basically we have created a class named: DataGridViewHTMLColumn
. Notice in the constructor we are new-ing up a DataGridViewHTMLCell
(marked in red). We are also overriding the CellTemplate
property. The CellTemplate
property "sets the template used to create new cells."
Deriving from DataGridViewCell (Step 2 of 3)
This is where most of our custom code will live. The DataGridViewCell
class represents an individual cell in the DataGridView control. The bulk of our code will reside in this class.
Let's take a look at the Paint method:
Code Snippet 2: DataGridViewCell Paint Method
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, null, null, errorText, cellStyle, advancedBorderStyle, paintParts);
Image img = GenerateHTMLImage(rowIndex, value, base.Selected);
if (img != null)
{
graphics.DrawImage(img, cellBounds.Left, cellBounds.Top);
}
}
The purpose/output of the Paint function is to draw the HTML on screen. The line: graphics.DrawImage(img, cellBounds.Left, cellBounds.Top);
actually does the drawing but the main bulk of the work is sub-contracted out to another function: GenerateHTMLImage()
. This function generates the HTML image which will be painted to screen in the location of the cell.
Let's take a closer look at the GenerateHTMLImage()
function:
Code Snippet 3: GenerateHTMLImage Function
private Image GenerateHTMLImage(int rowIndex, object value, bool selected)
{
Size cellSize = GetSize(rowIndex);
if (cellSize.Width < 1 || cellSize.Height < 1)
return null;
m_editingControl.Size = GetSize(rowIndex);
SetHTMLPanelText(m_editingControl, Convert.ToString(value));
if (m_editingControl != null)
{
Image htmlImage = null; Size fullImageSize;
if ( RenderHTMLImage( Convert.ToString(value), cellSize, out htmlImage, out fullImageSize ) == false )
{
Console.WriteLine("Failed to Generate HTML image");
return null;
}
if ( fullImageSize.Height > cellSize.Height )
{
htmlImage = AddElipsisToImage(htmlImage);
}
return htmlImage;
}
return null;
}
Following along with the red comments in the code snippet above, the 1st thing we do is get the size of the cell. If the width or height is less than 1 we simply return null as there is nothing worth painting
In step 2, we then set the HTML's ".Text" property (so it knows what HTML to render). The "value" variable is the actual html markup.
At step 3, we call the RenderHTMLImage()
function. This returns an image representation of the HTML to be displayed.
Finally, at step 4, we check to see if we need to add an ellipsis to the image. More on this later on.
Let's now take a look at the RenderHTMLImage()
function:
Code Snippet 4: Actually render the HTML image
private bool RenderHTMLImage(string htmlText, Size cellSize, out Image htmlImage, out Size fullImageSize)
{
bool bResult = true;
htmlImage = null;
fullImageSize = new System.Drawing.Size();
try
{
if (string.IsNullOrEmpty(htmlText) == true)
htmlText = "This cell has a null value!";
htmlImage = HtmlRender.RenderToImage(htmlText, maxWidth: cellSize.Width);
fullImageSize.Height = htmlImage.Height;
fullImageSize.Width = htmlImage.Width;
htmlImage = HtmlRender.RenderToImage(htmlText, new Size(cellSize.Width, cellSize.Height));
m_editingControl.Text = htmlText;
}
catch(Exception ex)
{
bResult = false;
}
return bResult;
}
The main bits of code to pick out here are in steps 3 and 5. Essentially we are calling through to the 3rd party HTMLRenderer library to render the HTML as an image.
htmlImage = HtmlRender.RenderToImage(htmlText, new Size(cellSize.Width, cellSize.Height));
The above line is where we are leveraging the functionality from the HTMLRenderer library.
Implementing the IDataGridViewEditingControl interface (Step 3 of 3)
At first I did not implement this but it meant that users could not interact with this control. The cell didn't feel natural. The user could not copy the text in the cell to the clipboard. I then (originally) played with using a simple TextBox control but this displayed the full markup text (which is something I suspect you don't want your users to see). Let's take a look at the code:
Code Snippet 5: Setting up the DataGridView Editing Control
public class DataGridViewHTMLEditingControl : HtmlLabel, IDataGridViewEditingControl
{
private DataGridView m_dataGridView;
private int m_rowIndex;
private bool m_valueChanged;
public DataGridViewHTMLEditingControl()
{
this.BorderStyle = BorderStyle.None;
AutoSize = false;
}
}
In a nutshell the code for this class looks like the code snippet above (I have left out the overridden interface methods for brevity). The important thing to note is that this class is-a HtmlLabel. We derive from the HtmlLabel class. Therefore DataGridViewHTMLEditingControl
is-a HtmlLabel. To satisfy the requirements of being an editable DataGridView control we also need to implement the methods in the interface IDataGridViewEditingControl
. There is nothing exciting in these interface methods which is the reason I have not displayed them here. You can view them over at the github page referenced above.
All done!
At this point we are all done. That's it. We have implemented all 3 classes needed to create a custom cell in the DataGridView control. It nicely displays html content. But before wrapping up I just wanted to discuss the ellipsis code mentioned above.
Extra: Adding an Ellipsis (...) to bottom right of Cell
I noticed, when playing around with the HTML cell that, if there was more text to display the cell didn't indicate anything back to the user to let them know "there was more". I then created a simple text cell and filled it with random text and noticed it displayed an ellipsis (...) to tell the user: "Hey! There is more text for me to display if you care to resize me". This is where the ellipsis comes in.
In the code snippet 4 above you may have noticed that I was rendering the html image twice and thought to yourself "why would you do that?". Well, the first time I render the image I let the HTMLRenderer render the full text. This produces an image with the full HTML in. I then render the HTML a 2nd time but this time limit the image size to the cell width and height. Now, we can check if the full image size height is greater than the cell image size height. If it is, we know there is more HTML to display and can add an ellipsis to the cell.
The code that adds the ellipsis is as follows:
Code Snippet 6: Adding an elipsis to the cell
Image AddElipsisToImage(Image img)
{
Graphics g = Graphics.FromImage(img);
g.DrawImage(Resources.elipsis, new Point(img.Width - Resources.elipsis.Width, img.Height - Resources.elipsis.Height));
return img;
}
The only caveat I have found with this is that when the cell is in edit mode the control is painted by itself which means (obviously) the ellipsis is not displayed. But that is to be expected.
Conclusion
This was a brief walk-through of the steps involved in creating a custom HTML DataGridView cell using the fantastic 3rd party library 'HTMLRenderer'. The code for this project is available at Github which means you are free to Add, Edit & Modify this code as you see fit.
Resources
HTML Renderer
| https://htmlrenderer.codeplex.com/
|
Article on CodeProject.com "RichTextBox Cell in a DataGridView" by MrWisdom
| http://www.codeproject.com/Articles/31823/RichTextBox-Cell-in-a-DataGridView
|
How to: Customize Cells and Columns in the Windows Forms DataGridView Control by Extending Their Behaviour and Appearance
| https://msdn.microsoft.com/en-us/library/vstudio/7fb61s43(v=vs.100).aspx
|
Code for this article
| https://github.com/OceanAirdrop/DataGridViewHTMLCell/
|