Owner drawn text table control
Introduction
This article describes a text table control with editing, checkboxes, cells merge, multiline and customizable appearance. Data binding is NOT supported.
Here is a screenshot:
Background
In one of my projects I needed simple table/grid control for text with minimal edit functions but with word wrap and cell merge in a row. My chief really hates standard datagrid and its appearance. Standard ListView doesn’t support word wrap in table mode. After some research I found some great controls (e.g. XPTable) but each of them lacked in some sides: word wrap or scrolling or speed… Of course there are commercial toolkits but I needed only small part of their functionality.
Having some experience in making owner drawn controls, I decided to make it myself. Hope it will be useful for someone else.
This sample shows how to instantiate a control and use it.
//
// bTable1
//
this.bTable1.BackColor = System.Drawing.Color.White;
this.bTable1.ColumnGridLines = false;
this.bTable1.DisabledColor = System.Drawing.Color.DarkGray;
this.bTable1.Dock = System.Windows.Forms.DockStyle.Fill;
this.bTable1.Font = new System.Drawing.Font("Microsoft Sans Serif", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(204)));
this.bTable1.GridColor = System.Drawing.Color.SlateGray;
this.bTable1.HeaderBackColore = System.Drawing.Color.SlateGray;
this.bTable1.HeaderFont = new System.Drawing.Font("Arial Narrow", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(204)));
this.bTable1.HeaderForeColore = System.Drawing.Color.White;
this.bTable1.HideDisabledRow = false;
this.bTable1.Location = new System.Drawing.Point(0, 0);
this.bTable1.MinimumRowHeight = 20;
this.bTable1.Name = "bTable1";
this.bTable1.RowGridLines = true;
this.bTable1.SelectedCell = null;
this.bTable1.SelectedRow = null;
this.bTable1.SelectionColor = System.Drawing.Color.AliceBlue;
this.bTable1.Size = new System.Drawing.Size(422, 470);
this.bTable1.TabIndex = 0;
this.bTable1.Text = "bTable1";
//
// Form1
//
//generate columns
BTable.Column col1 = new BTable.Column("№", 50);
BTable.Column col2 = new BTable.Column("Description", 200);
BTable.Column col3 = new BTable.Column("Check", 50);
BTable.Column col4 = new BTable.Column("Value", 100);
bTable1.Columns.Add(col1);
bTable1.Columns.Add(col2);
bTable1.Columns.Add(col3);
bTable1.Columns.Add(col4);
//add some data
for (int i = 1; i < 4; i++)
{
BTable.Row row1 = new BTable.Row(new string[] { i.ToString() + " Header"});
row1.Cells[0].Font = new Font("Arial", 16);
row1.Cells[0].Editable = false;
bTable1.Rows.Add(row1);
for (int j = 1; j < 5; j++)
{
BTable.Row row2 = new BTable.Row(new string[] { i.ToString() + "." + j.ToString(), "Some long description", "", "Some long text here" });
row2.Cells[1].Editable = true;
row2.Cells[2].CheckBox = true;
row2.Cells[2].Checked = true;
bTable1.Rows.Add(row2);
}
}
Solution
As we know table contains cells, rows, and columns. I have created simple classes for these objects with minimum amount of properties. Cell contains value, font, checkbox, rectangle and some other properties. Rectangle property presents the actual cell position on control. It refreshed every time when control is painted and used when user click on the control to find selected cell and its parent row.
Row class simply contains the list of cells, and Column has only name and width.
BTable class itself contains two docked onwer drawn custom controls presented by Header and Table classes. Table inherits scrollable control class which allows to implement easy scrolling.
In overridden OnPaint event of table control I draw all rows and cells using data from so named classes. Speaking about checkboxes, I decided to draw them myself also. I simply want them to look little bigger, so I took some code snippets from my earlier projects. Of course I use double buffering to avoid flickering and SmoothingMode.AntiAlias to make graphics (especially checkboxes) look better.
In overridden OnPaint event of Header class I paint column headers with gradient brushes to make control look more pleasant.
To implement editing I just create multiline textbox in selected cell rectangle area each time user double click on cell with enabled editable property and update cell value when enter is pressed or textbox loses focus.
Here is the overridden OnPaint event of the Table class where we get the final control appearance (except its header).
/// <summary>
/// Painting table
/// </summary>
/// <param name="e"></param>
protected override void OnPaint(PaintEventArgs e)
{
Pen GridPen = new Pen(BParent.GridColor);
DoubleBuffered = true;
if (AntiAliasText)
{
e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
}
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Matrix m = new Matrix();
m.Translate(this.AutoScrollPosition.X, this.AutoScrollPosition.Y, MatrixOrder.Append);
e.Graphics.Transform = m;
int topx = HeaderHeight;
//Loop through all rows
foreach (Row row in BParent.Rows)
{
//check if all cells are disabled and we hide them
if (BParent.HideDisabledRow)
{
bool alldisabled = true;
foreach (Cell cell in row.Cells)
{
if (cell.Enabled)
{
alldisabled = false;
break;
}
}
if (alldisabled)
{
continue;
}
}
//Find maximum height
int rowheight = 0;
int counter = 0;
int leftx = 0;
foreach (Cell cell in row.Cells)
{
int cellwidth = BParent.Columns[counter].ColumnWidth;
if (counter == row.Cells.Count-1)
{
cellwidth = Width - leftx - 15;
}
Font drawfont = this.Font;
if (cell.Font != null)
{
drawfont = cell.Font;
}
else
{
cell.Font = drawfont;
}
int currowheight = (int)e.Graphics.MeasureString(cell.Value.ToString(),drawfont,cellwidth).Height;
if (currowheight > rowheight)
{
rowheight = currowheight;
}
leftx += cellwidth;
counter++;
}
if (rowheight < BParent.MinimumRowHeight)
{
rowheight = BParent.MinimumRowHeight;
}
//Draw RowGridLine if needed
if (BParent.RowGridLines)
{
e.Graphics.DrawLine(GridPen, 0, topx + rowheight, Width, topx + rowheight);
}
//Generating cell rectangles
counter = 0;
leftx = 0;
foreach (Cell cell in row.Cells)
{
int cellwidth = BParent.Columns[counter].ColumnWidth;
if (counter == row.Cells.Count-1)
{
cellwidth = Width - leftx - 15;
}
//Draw ColumnGridLine if needed
if (BParent.ColumnGridLines)
{
e.Graphics.DrawLine(GridPen, leftx, topx, leftx, topx + rowheight);
}
Rectangle cellrectangle = new Rectangle(leftx, topx, cellwidth, rowheight);
cell.Rectangle = cellrectangle;
leftx += cellwidth;
counter++;
}
topx += rowheight;
}
//Painting selection background
if (BParent.SelectedRow != null)
{
foreach (Cell cell in BParent.SelectedRow.Cells)
{
e.Graphics.FillRectangle(new SolidBrush(BParent.SelectionColor), cell.Rectangle);
}
}
//Loop through all rows, Again)
foreach (Row row in BParent.Rows)
{
//check if all cells are disabled and we hide them
if (BParent.HideDisabledRow)
{
bool alldisabled = true;
foreach (Cell cell in row.Cells)
{
if (cell.Enabled)
{
alldisabled = false;
break;
}
}
if (alldisabled)
{
continue;
}
}
//At last painting all cells
foreach (Cell cell in row.Cells)
{
Brush ForeBrush = new SolidBrush(ForeColor);
Pen ForePen = new Pen(ForeColor);
Pen CheckPen = new Pen(ForeColor,2);
if (!cell.Enabled)
{
ForeBrush = new SolidBrush(BParent.DisabledColor);
ForePen = new Pen(BParent.DisabledColor);
CheckPen = new Pen(BParent.DisabledColor,2);
}
//if cell has checkbox
if (cell.CheckBox)
{
Rectangle checkrect = new Rectangle(cell.Rectangle.X + cell.Rectangle.Width / 2 - 7, cell.Rectangle.Y + cell.Rectangle.Height / 2 - 7, 15, 15);
e.Graphics.DrawRectangle(ForePen, checkrect);
if (cell.Checked)
{
Point[] check =
{
new Point( 2+checkrect.X, checkrect.Y+8),
new Point( 7+checkrect.X, checkrect.Y+12),
new Point(13+checkrect.X, checkrect.Y+1),
};
e.Graphics.DrawCurve(CheckPen, check);
}
}
e.Graphics.DrawString(cell.Value.ToString(), cell.Font, ForeBrush, cell.Rectangle);
}
}
AutoScrollMinSize = new Size(0, topx);
base.OnPaint(e);
}
Notes
Only vertical scrolling is now implemented, but horizontal scrolling can be added easily. In current version last column is expands to the all free space on the right. It’s not ideal but for now it suits my requirements.
Some properties and methods (e.g. Value2, SortbyValue2) were implemented specially for our working process and wouldn’t be useful for others.
There are many situations where error handlers need to be implemented. Especially in the moment of generating columns and rows, I’ll add them later.
History
Post Comment
lQCFAb Wow, great post.Much thanks again. Much obliged.
kku2g0 Really enjoyed this article post.Much thanks again. Keep writing.
WQATeT Thanks again for the blog.Really looking forward to read more. Will read on...
MB1bqk Thank you ever so for you post.Really thank you! Really Cool.