Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Owner drawn text table control

4.58/5 (7 votes)
4 Jul 2011CPOL3 min read 36.1K   1.1K  
Owner drawn text table control with editing, checkboxes, cells merge, word wrap, and customizable appearance.

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:

BTableScreen.PNG

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:

C#
// 
// 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, 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):

C#
/// <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 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)