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 a simple table/grid control for text, with minimal edit functions, but with word wrap and cell merge in a row. My chief really hates the standard DataGrid
and its appearance. The 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 a 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.
Using the code
This sample shows how to instantiate a control and use it:
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";
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);
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, a table contains cells, rows, and columns. I have created simple classes for these objects with minimum amount of properties. A cell contains value, font, checkbox, rectangle, and some other properties. The rectangle property presents the actual cell position on the control. It is refreshed every time a control is painted, and used when the user clicks on the control to find the selected cell and its parent row.
The Row
class simply contains the list of cells, and Column
has only name and width.
The BTable
class itself contains two docked owner drawn custom controls presented by the Header
and Table
classes. Table
inherits a scrollable control class which allows implementing easy scrolling.
In the overridden OnPaint
event of the Table
control, I draw all rows and cells using data from the named classes. Speaking about checkboxes, I decided to draw them myself. I simply want them to look a 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 the graphics (especially the checkboxes) look better.
In the overridden OnPaint
event of the Header
class, I paint the column headers with gradient brushes to make the control look more pleasant.
To implement editing, I just create a multiline textbox in the selected cell rectangle area each time the user double clicks on a cell with the enabled editable property, and update the cell value when Enter is pressed or the textbox loses focus.
Here is the overridden OnPaint
event of the Table
class where we get the final control appearance (except its header):
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;
foreach (Row row in BParent.Rows)
{
if (BParent.HideDisabledRow)
{
bool alldisabled = true;
foreach (Cell cell in row.Cells)
{
if (cell.Enabled)
{
alldisabled = false;
break;
}
}
if (alldisabled)
{
continue;
}
}
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;
}
if (BParent.RowGridLines)
{
e.Graphics.DrawLine(GridPen, 0, topx + rowheight, Width, topx + rowheight);
}
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;
}
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;
}
if (BParent.SelectedRow != null)
{
foreach (Cell cell in BParent.SelectedRow.Cells)
{
e.Graphics.FillRectangle(
new SolidBrush(BParent.SelectionColor), cell.Rectangle);
}
}
foreach (Row row in BParent.Rows)
{
if (BParent.HideDisabledRow)
{
bool alldisabled = true;
foreach (Cell cell in row.Cells)
{
if (cell.Enabled)
{
alldisabled = false;
break;
}
}
if (alldisabled)
{
continue;
}
}
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.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 the current version, the last column is expanded to all the 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 at the time of generating columns and rows; I'll add them later.
History
- July 04, 2011: Fixed some bugs, and added some new features to the control:
- Horizontal scrolling
- DateTime columns
- Column sorting