XPTable - .NET ListView meets Java's JTable






4.97/5 (441 votes)
Sep 11, 2005
12 min read

7014576

55356
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, ComboBox
es and NumericUpDown
s 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:
- A
Table
, - A
ColumnModel
and itsColumn
s, - A
TableModel
and itsRow
s andCell
s, Renderer
s andEditor
s
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:
- Select Tools -> Add/Remove Toolbox Items from the menu, or
- 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.
- Open up the VS .NET command prompt and change the directory to point to the XPTable\bin\Release directory.
- 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 Column
s to the ColumnModel
and Row
s and Cell
s 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 Column
s, a TableModel
to keep track of its Row
s and Cell
s, 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 Column
s 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 Column
s I decided to use the same approach as a DataGrid
- that is to have different types of Column
s based on the type of data their Cell
s will contain. The following Column
types are available:
Column
- Base class for allColumn
s.TextColumn
- AColumn
whoseCell
s are displayed as strings.ButtonColumn
- AColumn
whoseCell
s are displayed asButton
s.CheckBoxColumn
- AColumn
whoseCell
s are displayed asCheckBox
es.ImageColumn
- AColumn
whoseCell
s are displayed asImage
s.NumberColumn
- AColumn
whoseCell
s are displayed as numbers.ProgressBarColumn
- AColumn
whoseCell
s are displayed asProgressBar
s.DropDownColumn
- Base class forColumn
s that display a dropdown box for editing.ComboBoxColumn
- Represents aColumn
whoseCell
s are displayed asComboBox
es.DateTimeColumn
- Represents aColumn
whoseCell
s containDateTime
s.ColorColumn
- Represents aColumn
whoseCell
s containColor
s.
TableModel
A TableModel
contains a collection of Row
s that will be displayed in a Table
.
Rows
A Row
represents a row in a Table
and contains a collection of Cell
s 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 Cell
s 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
: CellRenderer
s which draw the Cell
s, and HeaderRenderer
s which draw the Column
headers
CellRenderers
CellRenderer
s are powerful objects in that they allow Cell
s to look and behave like Windows controls without consuming any extra resources.
The list below shows all the CellRenderer
s provided with XPTable
:
ICellRenderer
- Exposes common methods provided byCell
renderers.CellRenderer
- Base class for allCell
renderers.TextCellRenderer
- ACellRenderer
that drawsCell
contents as strings.ButtonCellRenderer
- ACellRenderer
that drawsCell
contents asButton
s.CheckBoxCellRenderer
- ACellRenderer
that drawsCell
contents asCheckBox
es.ImageCellRenderer
- ACellRenderer
that drawsCell
contents asImage
s.NumberCellRenderer
- ACellRenderer
that drawsCell
contents as numbers.ProgressBarCellRenderer
- ACellRenderer
that drawsCell
contents as aProgressBar
.DropDownCellRenderer
- Base class forCellRenderer
s that drawCell
contents likeComboBox
es.ComboBoxCellRenderer
- ACellRenderer
that drawsCell
contents as aComboBox
.ColorCellRenderer
- ACellRenderer
that drawsCell
contents asColor
s.DateTimeCellRenderer
- ACellRenderer
that drawsCell
contents as aDateTime
.
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 CellRenderer
s 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 HeaderRenderer
s provided with XPTable
:
IHeaderRenderer
- Exposes common methods provided byColumn
header renderers.HeaderRenderer
- Base class forRenderer
s that drawColumn
headers.XPHeaderRenderer
- AHeaderRenderer
that draws Windows XP themedColumn
headers.GradientHeaderRenderer
- AHeaderRenderer
that draws gradientColumn
headers.FlatHeaderRenderer
- AHeaderRenderer
that draws flatColumn
headers.
The image below shows the built in HeaderRenderer
s 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 byCell
editors.CellEditor
- Base class forCell
editors.TextCellEditor
- A class for editingCell
s that contain strings.NumberCellEditor
- A class for editingCell
s that contain numbers.DropDownCellEditor
- Base class for editingCell
s that contain drop down buttons.ComboBoxCellEditor
- A class for editingCell
s that look like aComboBox
.ColorCellEditor
- A class for editingCell
s that containColor
s.DateTimeCellEditor
- A class for editingCell
s that containDateTime
s.IEditorUsesRendererButtons
- Specifies that aCellEditor
uses the buttons provided by its counter-partCellRenderer
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 Row
s and Cell
s will use the visual settings of their parent container (unless otherwise told). XPTable
also provides style objects that can be shared between Row
s and Cell
s which save system resources. The image below shows an example of this:
CellStyles
Cell
s have a CellStyle
property which allows you to provide a consistent look and feel across multiple Cell
s while saving system resources. The CellStyle
object provides four properties that control the appearance of a Cell
:
BackColor
- specifies the background color for theCell
.ForeColor
- specifies the foreground color for theCell
.Font
- specifies the font used by theCell
.CellPadding
- specifies the amount of space between theCell
's border and its contents.
Note: Setting one of these values on a Cell
will override the same values inherited from its parent Row
. Cell
s 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 Cell
s will affect all the other Cell
s as well.
RowStyles
RowStyle
s are the same as CellStyle
s, except that they are shared between Row
s and don't have a CellPadding
property.
Table styles
In this version Table
s 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 theTable
.ForeColor
- specifies the foreground color for theTable
.Font
- specifies the font used by theTable
.AlternatingRowColor
- specifies theTable
's alternating row background color.SelectionBackColor
- specifies the background color of selectedRow
s andCell
s.SelectionForeColor
- specifies the foreground color of selectedRow
s andCell
s.UnfocusedSelectionBackColor
- specifies the background color of selectedRow
s andCell
s when theTable
doesn't have focus.UnfocusedSelectionForeColor
- specifies the foreground color of selectedRow
s andCell
s when theTable
doesn't have focus.HeaderFont
- specifies the font used to draw the text in theColumn
headers.GridColor
- specifies the color of the grid lines.GridLineStyle
- specifies the line style of the grid lines.SortedColumnBackColor
- specifies the color of a sortedColumn
's background.
Note: Row
s and Cell
s will inherit these values unless explicitly set.
The example below shows how CellStyle
s and Rowstyle
s 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 forCell
comparers.TextComparer
- for comparingCell
s based on theText
property.CheckBoxComparer
- for comparingCell
s based on theChecked
property.NumberComparer
- for comparingCell
s that contain numbers in theData
property.ImageComparer
- for comparingCell
s based on theImage
property.ColorComparer
- for comparingCell
s that containColor
s in theData
property.DateTimeComparer
- for comparingCell
s that containDateTime
s in theData
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 Row
s 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 Cell
s can be visualized - Grid
style where the individual selected Cell
s 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 Cell
s.
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.
- Fixed
- 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
Row
s from aTableModel
. - Fixed
TableModel
s/Row
s not updatingRow
/Cell
indices whenRow
/Cell
s are added/removed causing drawing problems. - Fixed
HideSelection
bug where selected items were not drawn as selected even when theTable
had focus. - Fixed
Table
overridingCursor
s set by aCellRenderer
. - Added utility methods
InvalidateCell
andInvalidateRow
to theTable
for convenience.
- Fixed using a