Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

XPTable - .NET ListView meets Java's JTable

0.00/5 (No votes)
17 Sep 2005 9  
A fully customisable ListView style control based on Java's JTable.

Contents

Introduction

For a project I'm working on I needed a highly customized ListView - one that would allow checkboxes and images in any column, ComboBoxes and NumericUpDowns for editing and it had to be easy to swap data in and out. Anyone who has tried customizing a ListView knows how painful it can be trying to bend it to your will, so I decided to create one from scratch. Having come from a Java background I decided to base it somewhat loosely on Java's JTable.

Features

  • Fully customizable visual appearance - from columns to rows and cells.
  • Supports Windows XP visual styles.
  • Powerful renderers that give cells the ability to act like controls.
  • Easy to add your own custom renderers and editors.
  • Columns can be hidden.
  • Rows, columns or individual cells can be disabled.
  • ToolTips for columns and cells.
  • Plus much more....

XPTable

XPTable consists of the following components:

  1. A Table,
  2. A ColumnModel and its Columns,
  3. A TableModel and its Rows and Cells,
  4. Renderers and
  5. Editors

I'm not going to go into much detail about the first three points and will only show the basics for points 4 and 5 as otherwise this article would be much larger than it already is. If you want more details on any of these topics then you should read the User Guide supplied with the documentation.

Using XPTable

Before using the XPTable, you need to add a reference to XPTable.dll in the References section of your project.

To add the XPTable.dll to the toolbox, you can either:

  1. Select Tools -> Add/Remove Toolbox Items from the menu, or
  2. Right click on the toolbox, select Add/Remove Items.

and browse for XPTable.dll and then press OK. You can then drag the controls onto your Form.

Note: If you recompile the source code you will need to re-sign XPTable.dll, as otherwise Visual Studio may throw an exception when you attempt to add it to the toolbox.

  1. Open up the VS .NET command prompt and change the directory to point to the XPTable\bin\Release directory.
  2. Then type "sn -R XPTable.dll ..\..\XPTable.snk" (without the quotes of course).

You should then be able to add it to the toolbox.

After that, all you need to do is drag a Table, ColumnModel and TableModel onto your form, set the Table's ColumnModel and TableModel properties, and add Columns to the ColumnModel and Rows and Cells to the TableModel.

or if you prefer code:

Table table = new Table();
ColumnModel columnModel = new ColumnModel();
TableModel tableModel = new TableModel();

// set the Table's ColumModel and TableModel

table.ColumnModel = columnModel;
table.TableModel = tableModel;

// add some Columns to the ColumnModel

columnModel.Columns.Add(new TextColumn("Text"));
columnModel.Columns.Add(new CheckBoxColumn("CheckBox"));
columnModel.Columns.Add(new ButtonColumn("Button"));

// add some Rows and Cells to the TableModel

tableModel.Rows.Add(new Row());
tableModel.Rows[0].Cells.Add(new Cell("Text 1"));
tableModel.Rows[0].Cells.Add(new Cell("CheckBox 1", true));
tableModel.Rows[0].Cells.Add(new Cell("Button 1"));
tableModel.Rows.Add(new Row());
tableModel.Rows[1].Cells.Add(new Cell("Text 2"));
tableModel.Rows[1].Cells.Add(new Cell("CheckBox 2", false));
tableModel.Rows[1].Cells.Add(new Cell("Button 2"));

Table

A Table is "simple" object in that it doesn't actually contain or know how to draw the data it will display. Instead it uses a ColumnModel to keep track of its Columns, a TableModel to keep track of its Rows and Cells, and Renderers and Editors to draw and edit its data. The Table's primary role is to manage the drawing operations and pass on events to the Renderers and Editors so that they can take the appropriate action.

ColumnModel

A ColumnModel contains a collection of Columns that will be displayed in a Table. It also keeps track of whether a CellRenderer or CellEditor has been created for a particular Column.

Columns

After thinking for a while about the best way to implement Columns I decided to use the same approach as a DataGrid - that is to have different types of Columns based on the type of data their Cells will contain. The following Column types are available:

  • Column - Base class for all Columns.
  • TextColumn - A Column whose Cells are displayed as strings.
  • ButtonColumn - A Column whose Cells are displayed as Buttons.
  • CheckBoxColumn - A Column whose Cells are displayed as CheckBoxes.
  • ImageColumn - A Column whose Cells are displayed as Images.
  • NumberColumn - A Column whose Cells are displayed as numbers.
  • ProgressBarColumn - A Column whose Cells are displayed as ProgressBars.
  • DropDownColumn - Base class for Columns that display a dropdown box for editing.
  • ComboBoxColumn - Represents a Column whose Cells are displayed as ComboBoxes.
  • DateTimeColumn - Represents a Column whose Cells contain DateTimes.
  • ColorColumn - Represents a Column whose Cells contain Colors.

TableModel

A TableModel contains a collection of Rows that will be displayed in a Table.

Rows

A Row represents a row in a Table and contains a collection of Cells that will be displayed in the Row.

Cells

A Cell contains a piece of data that will be displayed in a Table.

Renderers

As mentioned earlier, a Table doesn't know how to draw Cells or Column headers. Instead, it uses objects called Renderers to do all the drawing for it. The Java website describes a renderer as "a configurable ink stamp that the table uses to stamp appropriately formatted data onto each cell".

A Table uses two different types of Renderers: CellRenderers which draw the Cells, and HeaderRenderers which draw the Column headers

CellRenderers

CellRenderers are powerful objects in that they allow Cells to look and behave like Windows controls without consuming any extra resources.

The list below shows all the CellRenderers provided with XPTable:

  • ICellRenderer - Exposes common methods provided by Cell renderers.
  • CellRenderer - Base class for all Cell renderers.
  • TextCellRenderer - A CellRenderer that draws Cell contents as strings.
  • ButtonCellRenderer - A CellRenderer that draws Cell contents as Buttons.
  • CheckBoxCellRenderer - A CellRenderer that draws Cell contents as CheckBoxes.
  • ImageCellRenderer - A CellRenderer that draws Cell contents as Images.
  • NumberCellRenderer - A CellRenderer that draws Cell contents as numbers.
  • ProgressBarCellRenderer - A CellRenderer that draws Cell contents as a ProgressBar.
  • DropDownCellRenderer - Base class for CellRenderers that draw Cell contents like ComboBoxes.
  • ComboBoxCellRenderer - A CellRenderer that draws Cell contents as a ComboBox.
  • ColorCellRenderer - A CellRenderer that draws Cell contents as Colors.
  • DateTimeCellRenderer - A CellRenderer that draws Cell contents as a DateTime.

The image below shows the default output of each CellRenderer:

Creating a custom CellRenderer

If you want to create a custom CellRenderer you have two choices - subclass CellRenderer and override (at least) the OnPaint and OnPaintBackground methods (the easiest and preferred method) or implement ICellRenderer (a lot of work).

Below is the code for the Table's built in TextCellRenderer:

public class TextCellRenderer : CellRenderer
{
   protected override void OnPaint(PaintCellEventArgs e)
   {
      base.OnPaint(e);
 
      // don't bother going any further if the Cell is null

      if (e.Cell == null)
      {
         return;
      }
 
      // make sure we have some text to draw

      if (e.Cell.Text != null && e.Cell.Text.Length != 0)
      {
         // check whether the cell is enabled

         if (e.Enabled)
         {
            e.Graphics.DrawString(e.Cell.Text, base.Font, 
                        base.ForeBrush, base.ClientRectangle, 
                        base.StringFormat);
         }
         else
         {
            e.Graphics.DrawString(e.Cell.Text, base.Font, 
                    base.GrayTextBrush, base.ClientRectangle, 
                    base.StringFormat);
         }
      }
 
      // draw a focus rect around the cell if it is

      // enabled and has focus

      if (e.Focused && e.Enabled)
      {
         ControlPaint.DrawFocusRectangle(e.Graphics, 
                                       base.ClientRectangle);
      }
   }
}

For a more complex example, see the User Guide provided with the documentation.

HeaderRenderers

Unlike CellRenderers which are used on a per-column basis, a Table uses a single HeaderRenderer to draw all its Column headers.

The list below shows all the HeaderRenderers provided with XPTable:

  • IHeaderRenderer - Exposes common methods provided by Column header renderers.
  • HeaderRenderer - Base class for Renderers that draw Column headers.
  • XPHeaderRenderer - A HeaderRenderer that draws Windows XP themed Column headers.
  • GradientHeaderRenderer - A HeaderRenderer that draws gradient Column headers.
  • FlatHeaderRenderer - A HeaderRenderer that draws flat Column headers.

The image below shows the built in HeaderRenderers in action:

You can specify the HeaderRenderer that a Table will use by setting its HeaderRenderer property:

// get the table to use a FlatHeaderRenderer

// to draw the column headers

table.HeaderRenderer = new FlatHeaderRenderer();

Creating a custom HeaderRenderer

If you want to create a custom HeaderRenderer you have two choices - subclass HeaderRenderer and override (at least) the OnPaint and OnPaintBackground methods (the easiest and preferred method) or implement IHeaderRenderer (a lot of work).

Below is the code for the Table's built in XPHeaderRenderer:

public class XPHeaderRenderer : HeaderRenderer
{
   protected override void OnPaintBackground(PaintHeaderEventArgs e)
   {
      base.OnPaintBackground(e);

      if (e.Column == null)
      {
         ThemeManager.DrawColumnHeader(e.Graphics, e.HeaderRect, 
                                       ColumnHeaderStates.Normal);
      }
      else
      {
         ThemeManager.DrawColumnHeader(e.Graphics, e.HeaderRect, 
                         (ColumnHeaderStates) e.Column.ColumnState);
      }
   }


   protected override void OnPaint(PaintHeaderEventArgs e)
   {
      base.OnPaint(e);

      // don't bother if we don't have a column

      if (e.Column == null)
      {
         return;
      }

      Rectangle textRect = base.ClientRectangle;
      Rectangle imageRect = Rectangle.Empty;

      // check whether we can draw an image on the column header

      if (e.Column.Image != null)
      {
         imageRect = base.CalcImageRect();
         textRect.Width -= imageRect.Width;
         textRect.X += imageRect.Width;

         if (e.Column.ImageOnRight)
         {
            imageRect.X = base.ClientRectangle.Right - imageRect.Width;
            textRect.X = base.ClientRectangle.X;
         }

         // column headers that aren't themed and are pressed need

         // their contents shifted down and to the right by 1 pixel 

         if (!ThemeManager.VisualStylesEnabled && 
             e.Column.ColumnState == ColumnState.Pressed)
         {
            imageRect.X += 1;
            imageRect.Y += 1;
         }

         base.DrawColumnHeaderImage(e.Graphics, e.Column.Image, 
                                    imageRect, e.Column.Enabled);
      }

      // column headers that aren't themed and are pressed need

      // their contents shifted down and to the right by 1 pixel 

      if (!ThemeManager.VisualStylesEnabled && 
          e.Column.ColumnState == ColumnState.Pressed)
      {
         textRect.X += 1;
         textRect.Y += 1;
      }

      // check whether we need to draw a sort arrow

      if (e.Column.SortOrder != SortOrder.None)
      {
         // work out where to draw it

         Rectangle arrowRect = base.CalcSortArrowRect();
    
         // adjust the textRect to take the arrow into account

         arrowRect.X = textRect.Right - arrowRect.Width;
         textRect.Width -= arrowRect.Width;

         base.DrawSortArrow(e.Graphics, arrowRect, e.Column.SortOrder, 
                                                     e.Column.Enabled);
      }

      // check whether we have any text to draw

      if (e.Column.Text == null)
      {
         return;
      }

      if (e.Column.Text.Length > 0 && textRect.Width > 0)
      {
         if (e.Column.Enabled)
         {
            e.Graphics.DrawString(e.Column.Text, 
                           base.Font, base.ForeBrush,
                           textRect, base.StringFormat);
         }
         else
         {
            using (SolidBrush brush = 
                   new SolidBrush(SystemPens.GrayText.Color))
            {
               e.Graphics.DrawString(e.Column.Text, 
                                   base.Font, brush, 
                                   textRect, base.StringFormat);
            }
         }
      }
   }
}

Editors

XPTable contains five built-in editors:

  • ICellEditor - Exposes common methods provided by Cell editors.
  • CellEditor - Base class for Cell editors.
  • TextCellEditor - A class for editing Cells that contain strings.
  • NumberCellEditor - A class for editing Cells that contain numbers.
  • DropDownCellEditor - Base class for editing Cells that contain drop down buttons.
  • ComboBoxCellEditor - A class for editing Cells that look like a ComboBox.
  • ColorCellEditor - A class for editing Cells that contain Colors.
  • DateTimeCellEditor - A class for editing Cells that contain DateTimes.
  • IEditorUsesRendererButtons - Specifies that a CellEditor uses the buttons provided by its counter-part CellRenderer during editing.

Note: For more information about IEditorUsesRendererButtons see the User Guide provided with the documentation.

The image below shows the editors that use a drop-down control to edit Cell contents:

You can programmatically edit a Cell by using the table's EditCell method:

// start editing the cell at (0, 0)

table.EditCell(0, 0);

// stop editing the cell and commit any changes

table.StopEditing();

// or cancel editing and ignore any changes

table.CancelEditing();

Note: If you want to stop or cancel editing always use the table's StopEditing or CancelEditing methods (even when implementing a custom CellEditor). This gives the table a chance to do any work it needs to do before calling the CellEditor's StopEditing or CancelEditing methods.

Creating a custom CellEditor

If you want to create a custom CellEditor you have two choices - subclass CellEditor and override (at least) the SetEditValue, SetCellValue and SetEditLocation methods (the easiest and preferred method) or implement ICellEditor (a lot of work).

Below is the code for the Table's built in TextCellEditor:

public class TextCellEditor : CellEditor
{
   public TextCellEditor() : base()
   {
      TextBox textbox = new TextBox();
      textbox.AutoSize = false;
      textbox.BorderStyle = BorderStyle.None;

      base.Control = textbox;
   }


   // Sets the location and size of the CellEditor

   protected override void SetEditLocation(Rectangle cellRect)
   {
      this.TextBox.Location = cellRect.Location;
      this.TextBox.Size = new Size(cellRect.Width-1, 
                                       cellRect.Height-1);
   }


   // Sets the initial value of the 

   // editor based on the contents of 

   // the Cell being edited

   protected override void SetEditValue()
   {
      this.TextBox.Text = base.EditingCell.Text;
   }


   // Sets the contents of the Cell 

   // being edited based on the value 

   // in the editor

   protected override void SetCellValue()
   {
      base.EditingCell.Text = this.TextBox.Text;
   }


   // Starts editing the Cell

   public override void StartEditing()
   {
      this.TextBox.KeyPress += 
                 new KeyPressEventHandler(OnKeyPress);
      this.TextBox.LostFocus += 
                 new EventHandler(OnLostFocus);

      base.StartEditing();

      this.TextBox.Focus();
   }


   // Stops editing the Cell and commits any changes

   public override void StopEditing()
   {
      this.TextBox.KeyPress -= 
                 new KeyPressEventHandler(OnKeyPress);
      this.TextBox.LostFocus -= 
                 new EventHandler(OnLostFocus);
            
      base.StopEditing();
   }


   // Stops editing the Cell and ignores any changes

   public override void CancelEditing()
   {
      this.TextBox.KeyPress -= 
                   new KeyPressEventHandler(OnKeyPress);
      this.TextBox.LostFocus -= 
                   new EventHandler(OnLostFocus);
            
      base.CancelEditing();
   }


   // Gets the TextBox used to edit the Cells contents

   public TextBox TextBox
   {
      get
      {
         return base.Control as TextBox;
      }
   }


   // Handler for the editors TextBox.KeyPress event

   protected virtual void OnKeyPress(object sender, 
                                       KeyPressEventArgs e)
   {
      // check whether we nned to stop or cancel editing

      if (e.KeyChar == AsciiChars.CarriageReturn /*Enter*/)
      {
         if (base.EditingTable != null)
         {
            base.EditingTable.StopEditing();
         }
      }
      else if (e.KeyChar == AsciiChars.Escape)
      {
         if (this.EditingTable != null)
         {
            base.EditingTable.CancelEditing();
         }
      }
   }


   // Handler for the editors TextBox.LostFocus event

   protected virtual void OnLostFocus(object sender, 
                                           EventArgs e)
   {
      // if the textbox loses focus 

      // we should stop editing

      if (base.EditingTable != null)
      {
         base.EditingTable.StopEditing();
      }
   }
}

Visual styles

With XPTable, visual styles are inheritable - that is Rows and Cells will use the visual settings of their parent container (unless otherwise told). XPTable also provides style objects that can be shared between Rows and Cells which save system resources. The image below shows an example of this:

CellStyles

Cells have a CellStyle property which allows you to provide a consistent look and feel across multiple Cells while saving system resources. The CellStyle object provides four properties that control the appearance of a Cell:

  • BackColor - specifies the background color for the Cell.
  • ForeColor - specifies the foreground color for the Cell.
  • Font - specifies the font used by the Cell.
  • CellPadding - specifies the amount of space between the Cell's border and its contents.

Note: Setting one of these values on a Cell will override the same values inherited from its parent Row. Cells also have BackColor, ForeColor, Font and CellPadding properties that use the CellStyle property to store their values. Setting one of these properties on a Cell that shares its CellStyle with other Cells will affect all the other Cells as well.

RowStyles

RowStyles are the same as CellStyles, except that they are shared between Rows and don't have a CellPadding property.

Table styles

In this version Tables do not have a TableStyle property (although future versions will). Instead a Table has the following properties to control its appearance:

  • BackColor - specifies the background color for the Table.
  • ForeColor - specifies the foreground color for the Table.
  • Font - specifies the font used by the Table.
  • AlternatingRowColor - specifies the Table's alternating row background color.
  • SelectionBackColor - specifies the background color of selected Rows and Cells.
  • SelectionForeColor - specifies the foreground color of selected Rows and Cells.
  • UnfocusedSelectionBackColor - specifies the background color of selected Rows and Cells when the Table doesn't have focus.
  • UnfocusedSelectionForeColor - specifies the foreground color of selected Rows and Cells when the Table doesn't have focus.
  • HeaderFont - specifies the font used to draw the text in the Column headers.
  • GridColor - specifies the color of the grid lines.
  • GridLineStyle - specifies the line style of the grid lines.
  • SortedColumnBackColor - specifies the color of a sorted Column's background.

Note: Rows and Cells will inherit these values unless explicitly set.

The example below shows how CellStyles and Rowstyles can be shared:

// create a new CellStyle object

CellStyle cellStyle = new CellStyle();
cellStyle.BackColor = Color.Blue;
cellStyle.ForeColor = Color.Red;
cellStyle.Font = new Font("Tahoma", 8.25f, FontStyle.Bold);

// create a new RowStyle object

RowStyle rowStyle = new RowStyle();
rowStyle.BackColor = Color.Yello;
rowStyle.ForeColor = Color.Green;
rowStyle.Font = new Font("Arial", 8.25f, FontStyle.Italics);

for (int i=0; i<3; i++)
{
   tableModel.Rows[i].RowStyle = rowStyle;

   // only set the cellstyle for cells in the 3rd column

   tableModel[i, 2].CellStyle = cellStyle;
}

Sorting

Sorting a table is performed on a per-column basis, and can be initiated by clicking on a Column's header or through code.

There are six inbuilt comparers:

  • ComparerBase - Base class for Cell comparers.
  • TextComparer - for comparing Cells based on the Text property.
  • CheckBoxComparer - for comparing Cells based on the Checked property.
  • NumberComparer - for comparing Cells that contain numbers in the Data property.
  • ImageComparer - for comparing Cells based on the Image property.
  • ColorComparer - for comparing Cells that contain Colors in the Data property.
  • DateTimeComparer - for comparing Cells that contain DateTimes in the Data property.

There are also four inbuilt sorters:

  • InsertionSorter
  • MergeSorter
  • ShellSorter
  • HeapSorter

InsertionSort and MergeSort are considered to be stable sorts, whereas ShellSort and HeapSort are unstable. Also, InsertionSort and ShellSort are faster than MergeSort and HeapSort on smaller lists and slower on large lists. The actual algorithm used to sort a Column depends on the number of Rows in the Table and whether a stable sort is required.

For more information on sorting methods and stable/unstable sorting refer to this site.

You can programmatically sort a Column by calling one of the table's Sort methods:

// sort the currently sorted column in the opposite direction

// to its currnent sort order, or if no columns are sorted, the

// column that has focus in ascending order

table.Sort();

// sort the currently sorted column in the opposite direction

// to its currnent sort order, or if no columns are sorted, the

// column that has focus in ascending order using an unstable 

// sort method

table.Sort(false);

// sort the column at index 3 in the table's ColumnModel 

// opposite to its current sort order, or in ascending order 

// if the column is not sorted

table.Sort(3);

// sort the column at index 3 in the table's ColumnModel 

// opposite to its current sort order, or in ascending order 

//if the column is not sorted using a stable sort method

table.Sort(3, true);

// sort the column at index 3 in the table's ColumnModel

// in descending order

table.Sort(3, SortOrder.Descending);

// sort the column at index 3 in the table's ColumnModel

// in ascending order using an unstable sort method

table.Sort(3, SortOrder.Ascending, false);

Note: The Sort methods that don't supply an option for specifying a stable or unstable sort automatically use a stable sort.

You can disable Column sorting by setting the Column's Sortable property to false:

// disable sorting for a column

column.Sortable = false;

Note: Setting the Table's HeaderStyle property to NonClickable or None will stop column sorting from clicking on a column header, however the Column can still be sorted programmatically.

Creating a custom comparer

It is also possible to create a custom comparer for use by a Column by sub classing ComparerBase and overriding the Compare method:

public class TextComparer : ComparerBase
{
   // Compares two objects and returns a 

   // value indicating whether one is less

   // than, equal to or greater than the other

   public override int Compare(object a, object b)
   {
      Cell cell1 = (Cell) a;
      Cell cell2 = (Cell) b;
 
      // check for null cells

      if (cell1 == null && cell2 == null)
      {
         return 0;
      }
      else if (cell1 == null)
      {
         return -1;
      }
      else if (cell2 == null)
      {
         return 1;
      }
 
      // check for null data

      if (cell1.Text == null && cell2.Text == null)
      {
         return 0;
      }
      else if (cell1.Text == null)
      {
         return -1;
      }
 
      // now that we know both cells contain valid data,

      // use the frameworks built in string comparer

      return cell1.Text.CompareTo(cell2.Text);
   }
}

Selections

A Table provides two ways that selected Cells can be visualized - Grid style where the individual selected Cells are highlighted, or ListView style where only the Cell in the first visible Column is highlighted. The images below show an example of this:

Top: ListView style selection

Bottom: Grid style selection

This can be set using the table's SelectionStyle property:

// use grid style selection

table.SelectionStyle = SelectionStyle.Grid;

Note: With ListView style selection the highlighted Cell may not actually be selected.

The TableModel also provides a Selection object that you can use to programmatically select or deselect Cells.

Future features

Below is a list of features that I would like to add to future versions:

  • Word wrapping for cells and column headers
  • Autosizing rows and columns
  • Variable height rows
  • LinkLabel cells
  • RichTextFormat cells
  • Dialog based CellEditors
  • ListView style icon mode
  • RightToLeft support
  • Cut and paste support
  • Drag and drop support
  • Data binding
  • Column re-ordering
  • Printing support
  • Export to HTML and XML
  • Serialization
  • Other stuff I've forgotten or haven't thought of yet

History

  • 11th September, 2005 - Initial release.
  • 13th September, 2005 - Version 1.0.1.
    • Fixed Table causing a crash when an application is minimized.
    • Updated future features list.
  • 17th September, 2005 - Version 1.0.2
    • Fixed using a DropDownCellEditor causes a crash when the dropdown portion is displayed.
    • Fixed exception thrown when removing Rows from a TableModel.
    • Fixed TableModels/Rows not updating Row/Cell indices when Row/Cells are added/removed causing drawing problems.
    • Fixed HideSelection bug where selected items were not drawn as selected even when the Table had focus.
    • Fixed Table overriding Cursors set by a CellRenderer.
    • Added utility methods InvalidateCell and InvalidateRow to the Table for convenience.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here