![]() |
Desktop Development »
List Controls »
ListView controls
Intermediate
XPTable - .NET ListView meets Java's JTableBy Mathew HallA fully customisable ListView style control based on Java's JTable. |
C#.NET1.1, Win2K, WinXP, Win2003, GDI, VS.NET2003, Dev
|
|
Advanced Search Add to IE Search |
|
|
||||||||||||||||||

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.
XPTable consists of the following components:
Table,
ColumnModel and its Columns,
TableModel and its Rows and Cells,
Renderers and
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.
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:
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.
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"));
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.
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.
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. A TableModel contains a collection of Rows that will be displayed in a Table.
A Row represents a row in a Table and contains a collection of Cells 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 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 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:

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.
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();
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);
}
}
}
}
}
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.
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();
}
}
}
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:

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 are the same as CellStyles, except that they are shared between Rows and don't have a CellPadding property.
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 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.
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);
}
}
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.
Below is a list of features that I would like to add to future versions:
LinkLabel cells
ListView style icon mode
Table causing a crash when an application is minimized.
DropDownCellEditor causes a crash when the dropdown portion is displayed.
Rows from a TableModel.
TableModels/Rows not updating Row/Cell indices when Row/Cells are added/removed causing drawing problems.
HideSelection bug where selected items were not drawn as selected even when the Table had focus.
Table overriding Cursors set by a CellRenderer.
InvalidateCell and InvalidateRow to the Table for convenience.
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 17 Sep 2005 Editor: Rinish Biju |
Copyright 2005 by Mathew Hall Everything else Copyright © CodeProject, 1999-2010 Web21 | Advertise on the Code Project |