Contents
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
.
- 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
consists of the following components:
- A
Table
,
- A
ColumnModel
and its Column
s,
- A
TableModel
and its Row
s and Cell
s,
Renderer
s and
Editor
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.
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();
table.ColumnModel = columnModel;
table.TableModel = tableModel;
columnModel.Columns.Add(new TextColumn("Text"));
columnModel.Columns.Add(new CheckBoxColumn("CheckBox"));
columnModel.Columns.Add(new ButtonColumn("Button"));
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"));
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.
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
.
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 all Column
s.
TextColumn
- A Column
whose Cell
s are displayed as strings.
ButtonColumn
- A Column
whose Cell
s are displayed as Button
s.
CheckBoxColumn
- A Column
whose Cell
s are displayed as CheckBox
es.
ImageColumn
- A Column
whose Cell
s are displayed as Image
s.
NumberColumn
- A Column
whose Cell
s are displayed as numbers.
ProgressBarColumn
- A Column
whose Cell
s are displayed as ProgressBar
s.
DropDownColumn
- Base class for Column
s that display a dropdown box for editing.
ComboBoxColumn
- Represents a Column
whose Cell
s are displayed as ComboBox
es.
DateTimeColumn
- Represents a Column
whose Cell
s contain DateTime
s.
ColorColumn
- Represents a Column
whose Cell
s contain Color
s.
A TableModel
contains a collection of Row
s that will be displayed in a Table
.
A Row
represents a row in a Table
and contains a collection of Cell
s that will be displayed in the Row
.
A Cell
contains a piece of data that will be displayed in a Table
.
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
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 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 Button
s.
CheckBoxCellRenderer
- A CellRenderer
that draws Cell
contents as CheckBox
es.
ImageCellRenderer
- A CellRenderer
that draws Cell
contents as Image
s.
NumberCellRenderer
- A CellRenderer
that draws Cell
contents as numbers.
ProgressBarCellRenderer
- A CellRenderer
that draws Cell
contents as a ProgressBar
.
DropDownCellRenderer
- Base class for CellRenderer
s that draw Cell
contents like ComboBox
es.
ComboBoxCellRenderer
- A CellRenderer
that draws Cell
contents as a ComboBox
.
ColorCellRenderer
- A CellRenderer
that draws Cell
contents as Color
s.
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);
if (e.Cell == null)
{
return;
}
if (e.Cell.Text != null && e.Cell.Text.Length != 0)
{
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);
}
}
if (e.Focused && e.Enabled)
{
ControlPaint.DrawFocusRectangle(e.Graphics,
base.ClientRectangle);
}
}
}
For a more complex example, see the User Guide provided with the documentation.
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 by Column
header renderers.
HeaderRenderer
- Base class for Renderer
s 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 HeaderRenderer
s in action:
You can specify the HeaderRenderer
that a Table
will use by setting its HeaderRenderer
property:
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);
if (e.Column == null)
{
return;
}
Rectangle textRect = base.ClientRectangle;
Rectangle imageRect = Rectangle.Empty;
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;
}
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);
}
if (!ThemeManager.VisualStylesEnabled &&
e.Column.ColumnState == ColumnState.Pressed)
{
textRect.X += 1;
textRect.Y += 1;
}
if (e.Column.SortOrder != SortOrder.None)
{
Rectangle arrowRect = base.CalcSortArrowRect();
arrowRect.X = textRect.Right - arrowRect.Width;
textRect.Width -= arrowRect.Width;
base.DrawSortArrow(e.Graphics, arrowRect, e.Column.SortOrder,
e.Column.Enabled);
}
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);
}
}
}
}
}
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 Cell
s that contain strings.
NumberCellEditor
- A class for editing Cell
s that contain numbers.
DropDownCellEditor
- Base class for editing Cell
s that contain drop down buttons.
ComboBoxCellEditor
- A class for editing Cell
s that look like a ComboBox
.
ColorCellEditor
- A class for editing Cell
s that contain Color
s.
DateTimeCellEditor
- A class for editing Cell
s that contain DateTime
s.
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:
table.EditCell(0, 0);
table.StopEditing();
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;
}
protected override void SetEditLocation(Rectangle cellRect)
{
this.TextBox.Location = cellRect.Location;
this.TextBox.Size = new Size(cellRect.Width-1,
cellRect.Height-1);
}
protected override void SetEditValue()
{
this.TextBox.Text = base.EditingCell.Text;
}
protected override void SetCellValue()
{
base.EditingCell.Text = this.TextBox.Text;
}
public override void StartEditing()
{
this.TextBox.KeyPress +=
new KeyPressEventHandler(OnKeyPress);
this.TextBox.LostFocus +=
new EventHandler(OnLostFocus);
base.StartEditing();
this.TextBox.Focus();
}
public override void StopEditing()
{
this.TextBox.KeyPress -=
new KeyPressEventHandler(OnKeyPress);
this.TextBox.LostFocus -=
new EventHandler(OnLostFocus);
base.StopEditing();
}
public override void CancelEditing()
{
this.TextBox.KeyPress -=
new KeyPressEventHandler(OnKeyPress);
this.TextBox.LostFocus -=
new EventHandler(OnLostFocus);
base.CancelEditing();
}
public TextBox TextBox
{
get
{
return base.Control as TextBox;
}
}
protected virtual void OnKeyPress(object sender,
KeyPressEventArgs e)
{
if (e.KeyChar == AsciiChars.CarriageReturn )
{
if (base.EditingTable != null)
{
base.EditingTable.StopEditing();
}
}
else if (e.KeyChar == AsciiChars.Escape)
{
if (this.EditingTable != null)
{
base.EditingTable.CancelEditing();
}
}
}
protected virtual void OnLostFocus(object sender,
EventArgs e)
{
if (base.EditingTable != null)
{
base.EditingTable.StopEditing();
}
}
}
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 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
. 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 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 Row
s and Cell
s.
SelectionForeColor
- specifies the foreground color of selected Row
s and Cell
s.
UnfocusedSelectionBackColor
- specifies the background color of selected Row
s and Cell
s when the Table
doesn't have focus.
UnfocusedSelectionForeColor
- specifies the foreground color of selected Row
s and Cell
s 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: 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:
CellStyle cellStyle = new CellStyle();
cellStyle.BackColor = Color.Blue;
cellStyle.ForeColor = Color.Red;
cellStyle.Font = new Font("Tahoma", 8.25f, FontStyle.Bold);
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;
tableModel[i, 2].CellStyle = cellStyle;
}
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 Cell
s based on the Text
property.
CheckBoxComparer
- for comparing Cell
s based on the Checked
property.
NumberComparer
- for comparing Cell
s that contain numbers in the Data
property.
ImageComparer
- for comparing Cell
s based on the Image
property.
ColorComparer
- for comparing Cell
s that contain Color
s in the Data
property.
DateTimeComparer
- for comparing Cell
s that contain DateTime
s 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 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:
table.Sort();
table.Sort(false);
table.Sort(3);
table.Sort(3, true);
table.Sort(3, SortOrder.Descending);
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
:
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
{
public override int Compare(object a, object b)
{
Cell cell1 = (Cell) a;
Cell cell2 = (Cell) b;
if (cell1 == null && cell2 == null)
{
return 0;
}
else if (cell1 == null)
{
return -1;
}
else if (cell2 == null)
{
return 1;
}
if (cell1.Text == null && cell2.Text == null)
{
return 0;
}
else if (cell1.Text == null)
{
return -1;
}
return cell1.Text.CompareTo(cell2.Text);
}
}
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:
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.
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
- 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
Row
s from a TableModel
.
- Fixed
TableModel
s/Row
s not updating Row
/Cell
indices when Row
/Cell
s 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 Cursor
s set by a CellRenderer
.
- Added utility methods
InvalidateCell
and InvalidateRow
to the Table
for convenience.