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

CGridListCtrlEx - Grid Control Based on CListCtrl

4.91/5 (138 votes)
16 Mar 2015CPOL12 min read 263   66.9K  
A custom draw CListCtrl with subitem editing and formatting

Introduction 

Microsoft's CListCtrl has support for displaying data in a grid using the report style, but we have to make several changes to implement features like: 

This article demonstrates how to use CGridListCtrlEx, which implements all the above features while maintaining the Windows XP/Vista look.

Image 1

The GitHub - CGridListCtrlEx can be used if wanting Git/Subversion access, and there is also Doxygen Documentation.

Background

There are lots of advanced grid controls that extend the CListCtrl, and one of those is the Enhanced List Control (CGfxListCtrl). This wonderful control provides all the above features, but fails to handle Windows XP and Vista. Finding a good replacement for this control is not very easy:

  • MFC Grid Control - Doesn't inherit from CListCtrl so it is not restricted by it, but it will not benefit from any improvements that Microsoft adds to the CListCtrl.
  • Ultimate Grid - Like MFC Grid Control, it doesn't inherit from CListCtrl. Originally one had to buy it, but now it is free to use.
  • CQuickList - Very close to being a perfect replacement, but hard to add new ways to display data, and it requires LVS_OWNERDATA that makes sorting a little harder.
  • XListCtrl - Also a very complete CListCtrl, but hard to add new ways to display data, and it fails to support LVS_OWNERDATA. One now has to buy a license, to get the latest version.
  • Another Report List Control - Simple and easy to use, but lacks other means to edit data besides using CEdit, and also misses subitem navigation.
  • CListCtrlEx - Implements lots of feature and is well documented. Originally required LVS_OWNERDRAWFIXED, and now evolved into using custom draw. The combination of both custom draw and owner draw causes the code to be a little complicated, and it also doesn't support LVS_OWNERDATA.

The CGridListCtrlEx inserts an abstract layer called column traits that handles the cell drawing and editing. In case Microsoft extends their CListCtrl again, then hopefully, the core of CGridListCtrlEx will continue to function.

How to Use the CGridListCtrlEx

The CGridListCtrlEx tries to stay true to the CListCtrl, and doesn't try to replace anything the CListCtrl already provides. This means we can replace a CListCtrl with CGridListCtrlEx without needing to do anything more.

It is recommended that we don't use the CGridListCtrlEx directly, but create a new class that inherits/derives from CGridListCtrlEx. This will make it easier to migrate any updates there will be to the CGridListCtrlEx class later on.

Editing Cells/Subitems

By default, when inserting columns in the CGridListCtrlEx, they will be configured as read-only, without the ability to be edited. By using CGridListCtrlEx::InsertColumnTrait(), we can provide a CGridColumnTrait class which specifies what type of editor it should use.

C++
CGridColumnTrait* pTrait = new CGridColumnTraitEdit;
m_ListCtrl.InsertColumnTrait(nCol, title.c_str(), LVCFMT_LEFT, 100, nCol, pTrait);

When having edited an item, a standard LVN_ENDLABELEDIT message will be sent to the CListCtrl. When the CGridListCtrlEx receives this message, it will automatically call the virtual method CGridListCtrlEx::OnEditComplete(), allowing a derived class to validate the input and maybe update an underlying data model.

Editing Cells/Subitems with a Combo-box

By using CGridListCtrlEx::InsertColumnTrait(), we can also provide a CGridColumnTrait class which works as a CComboBox.

C++
CGridColumnTraitCombo* pTrait = new CGridColumnTraitCombo;
pTrait->AddItem(0, "Hello");
pTrait->AddItem(1, "Goodbye");
m_ListCtrl.InsertColumnTrait(nCol, title.c_str(), LVCFMT_LEFT, 100, nCol, pTrait);

We can specify the items of the CComboBox when inserting the columns (as shown above). If we want to provide the CComboBox items dynamically, then we can override the CGridListCtrlEx::OnEditBegin(). Use dynamic_cast<> to either call the column trait method CGridColumnTraitCombo::LoadList(), or to work on the returned CComboBox-editor directly.  

If wanting to get the itemdata of the CComboBox-item selected, then one can override CGridListCtrlEx::OnEditComplete() and check the parameter value pLVDI->item.lParam. One is required to save the itemdata elsewhere, as it cannot be stored inside the local datamodel of CListCtrl.

Sorting Rows

By default, the GridListCtrlEx will have sorting enabled for all columns, where it will perform a simple text-comparison. It is possible through the column traits to implement custom sorting by overriding CGridColumnTrait::OnSortRows().

To configure a column trait to sort using number comparison:

C++
CGridColumnTraitEdit* pTrait = new CGridColumnTraitEdit;
pTrait->SetSortFormatNumber(true);	// Numeric column
m_ListCtrl.InsertColumnTrait(nCol, title.c_str(), LVCFMT_LEFT, 100, nCol, pTrait);

The column trait CGridColumnTraitDateTime will automatically attempt to sort using date comparison.

We also have the option to override the CGridListCtrlEx::SortColumn() method. Then, it is just a matter of choosing the right way to perform the sorting. See also CListCtrl and Sorting Rows.

Showing Tooltip

By default, the CGridListCtrlEx will just display the cell contents as tooltip. If we want to display something different in the tooltip, then we can override the CGridListCtrlEx::OnDisplayCellTooltip() method.

Formatting Cells/Subitems

If we want to change the foreground/background color or the font style (bold, italic, underline), then we can override the methods CGridListCtrlEx::OnDisplayCellColor() and CGridListCtrlEx::OnDisplayCellFont().

C++
bool MyGridCtrl::OnDisplayCellColor(int nRow, int nCol, COLORREF& textColor, COLORREF& backColor)
{
   if (nRow == 3 && nCol == 3)
   {
      textColor = RGB(0,255,0);
      backColor = RGB(0,0,255);
      return true;  // I want to override the color of this cell
   }
   return false;  // Use default color
}

Displaying Cell/Subitem Images

The CGridListCtrlEx enables the extended style LVS_EX_SUBITEMIMAGES by default, but one is still required to attach a CImageList using CListCtrl::SetImageList().

After having attached the images, one can bind a cell/subitem with an index in the CImageList. This can be done with CGridListCtrlEx::SetCellImage(), or if using I_IMAGECALLBACK then return the image index by overriding CGridListCtrlEx::OnDisplayCellImage().

The CGridListCtrlEx also enables the extended style LVS_EX_GRIDLINES by default, which can cause subitem images to overlap the grid border. This can be solved by making sure that the image only uses 15 of the 16 pixels (first pixel transparent).

When using subitem images and running the application on Windows XP or using classic style, it will show a white background when a row is selected. This can be fixed by using CGridRowTraitXP:

C++
m_ListCtrl.SetDefaultRowTrait(new CGridRowTraitXP);

Checkbox Support

CListCtrl supports checkboxes for the label column out of the box. Just apply the extended style LVS_EX_CHECKBOXES:

C++
m_ListCtrl.SetExtendedStyle(m_ListCtrl.GetExtendedStyle() | LVS_EX_CHECKBOXES);

Remember not to use InsertHiddenLabelColumn() as it will hide the label column and its checkbox. One can use GetCheck() / SetCheck() to retrieve/modify the checkbox value.

If wanting to have checkboxes for multiple columns, then one can use CGridColumnTraitImage (and its specializations):

C++
// Appends the unchecked/checked state images to the list control image list
int nStateImageIdx = CGridColumnTraitImage::AppendStateImages(m_ListCtrl, m_ImageList);
m_ListCtrl.SetImageList(&m_ImageList, LVSIL_SMALL);
// Creates an image column, that can switch between the 2 images
CGridColumnTrait* pTrait = new CGridColumnTraitImage(nStateIdx, 2);
m_ListCtrl.InsertColumnTrait(nCol, title.c_str(), LVCFMT_LEFT, 20, nCol, pTrait);
for(int i=0; i < m_ListCtrl.GetItemCount(); ++i)
	m_ListCtrl.SetCellImage(i, nCol, nStateImageIdx);	// Uncheck item

The label column will automatically display the image column when assigning an CImageList using SetImageList(). It is not possible to disable this behavior, but one can hide the label column using InsertHiddenLabelColumn().

CGridColumnTraitImage uses the cell image to draw the checkbox, so it is not possible to have both cell image and checkbox in the same column. To get and set the checked / unchecked state, one can use GetCellImage() / SetCellImage().

CGridColumnTraitImage supports sorting according to whether the checkbox is enabled. Use CGridColumnTraitImage::SetSortImageIndex() to enable this.

CGridColumnTraitImage also supports toggling of checkboxes for all selected rows. Use CGridColumnTraitImage::SetToggleSelection() to enable this.

Hyperlink support

Hyperlink columns can display the cell contents as links, which can be clicked and will launch an external applications like the default web-browser (http) or email-client (mailto).

The CGridColumnTraitHyperLink allows one to supply a prefix (and suffix) for the celltext, which allows one to add extra protocol details without it being displayed:

C++
CGridColumnTraitHyperLink* pHyperLinkTrait = new CGridColumnTraitHyperLink;
pHyperLinkTrait->SetShellFilePrefix(_T("http://en.wikipedia.org/wiki/UEFA_Euro_"));
m_ListCtrl.InsertColumnTrait(nCol, title.c_str(), LVCFMT_LEFT, 100, nCol, pHyperLinkTrait);

It is also possible with CGridColumnTraitHyperLink::SetShellApplication() to specify a custom application to be launched, instead of just the default application launched based on the protocol prefix.

The hyperlink can also be used to simulate a button. When clicked it will send a LVN_ENDLABELEDIT notification, which one can treat as a button clicked notification in the parent view.

Changing Row Height

The CGridListCtrlEx uses customdraw, so there are only these available solutions:

  • Assign a CImageList where the image has the height wanted for the row.
  • Change the font of the grid control, and the row height will follow. CGridListCtrlEx::SetCellMargin() uses this trick to increase the font of the grid control, while keeping the row font intact.

Changing the Empty Markup Text

When the CGridListCtrlEx doesn't contain any items, it will display markup text to indicate the list is empty.

Use CGridListCtrlEx::SetEmptyMarkupText() to change this markup text. If providing empty text, then it will behave like a normal CListCtrl.

If using CGridListCtrlGroups, it will instead react to LVN_GETEMPTYMARKUP if running on Windows Vista.

Load and Save Column Width and Position

CViewConfigSectionWinApp provides the ability to store the width, position and whether columns are shown. After having added all available columns to the CGridListCtrlEx, assign an instance of CViewConfigSectionWinApp using CGridListCtrlEx::SetupColumnConfig() and it will restore the last saved column configuration through CWinApp.

C++
m_ListCtrl.SetupColumnConfig(new CViewConfigSectionWinApp("MyList"));

If using CGridListCtrlEx several places in your application, then one should ensure creation of a unique CViewConfigSectionWinApp for each place.

OLE Drag and Drop

CGridListCtrlEx can both work as an OLE drag source and an OLE drop target. This allows drag operations between the CGridListCtrlEx and other windows and applications.

CGridListCtrlEx supports reordering of rows when doing internal drag and drop. This is implemented using a special sort operation, so items are not deleted/inserted.

To implement your own special behavior for a drop operation, either override OnDropSelf() or OnDropExternal() depending on what situation you want to handle.

To control what is placed in the drag-source when a drag is initiated, override OnDisplayToDragDrop().

How Does the CGridColumnTrait Work

CGridListCtrlEx tries to keep away from all the nasty details about how to display and edit data. These things are instead handled by the CGridColumnTrait class, and if we want to modify how data is displayed, then it is "just" a matter of creating a new CGridColumnTrait class.

When inserting a column, we can assign a CGridColumnTrait to the column. The CGridListCtrlEx will activate the appropriate CGridColumnTrait when we need to draw a cell in that column, or edit a cell in the column.

The CGridColumnTrait includes some special members known as meta-data. These members can be used by your own class when it derives from CGridListCtrlEx, so we can easily add extra properties to a column.

When inheriting from CGridColumnTrait, we must consider the following:

  • If performing custom drawing, we must also handle the selection and focus coloring.
  • If performing editing, we must ensure that the editor closes when it loses focus, and also sends a LVN_ENDLABELEDIT message when the edit is complete.

How Does the CGridRowTrait Work

It is based on the same idea as CGridColumnTrait but operates at row level instead of column level. This is useful for situations where one has to modify the display behavior of all columns.

Using the Code

The source code includes the following classes:

  • CGridListCtrlEx - The specialized CListCtrl
  • CGridListCtrlGroups - CGridListCtrlEx extended with support for grouping
  • CGridColumnTrait - Specifies the interface of a column-trait
    • CGridColumnTraitText - Implements cell formatting
      • CGridColumnTraitImage - Implements cell editing by switching between images (can mimic a checkbox)
        • CGridColumnTraitEdit - Implements cell editing with CEdit
        • CGridColumnTraitCombo - Implements cell editing with CComboBox
        • CGridColumnTraitDateTime - Implements cell editing with CDateTimeCtrl
        • CGridColumnTraitHyperLink - Implements cell behavior as hyperlinks
  • CGridRowTrait - Specifies the interface of a row trait
    • CGridRowTraitText - Implements row formatting
    • CGridRowTraitXP - Implements drawing of subitem image background when using classic- or XP-style
  • CViewConfigSection - Abstract interface for persisting column setup
    • CViewConfigSectionWinApp - Implements the interface and can switch between multiple column setups.

Things To Do

The CGridListCtrlEx tries to stay away from performing any drawing itself. This means that the following features/bugs will not get that much attention:

  • Support for progress bar - Requires a CGridColumnTrait class that draws the entire cell.

Implementing a CGridColumnTrait class that draws the entire cell could probably be done by stealing/borrowing some code from ListCtrl - A WTL List Control with Windows Vista Style Item Selection.

Contributions to this project are very welcome.

History

  • Version 1.0 (2008-09-04) First release
  • Version 1.1 (2008-09-18)
    • Added support for grouping with CGridListCtrlGroups
    • Added CDateTimeCtrl editor
    • Fixed drawing bugs when using Classic- and XP-style
      • Fixed image background color for selected subitems (no longer white)
      • Fixed grid border disappearing when scrolling right and left
    • Indicate the list is empty when it contains no items
    • Extended CComboBox editor, so it automatically resizes dropdown width to its contents
  • Version 1.2 (2008-09-24)
    • Replaced the CGridListCtrlXP with CGridRowTraitXP
    • Fixed some reported bugs
  • Version 1.3 (2008-10-09)
    • Fixed extended style when used in CView
    • Fixed positioning of context-menu when using keyboard shortcut (SHIFT+F10)
    • Fixed compiler errors that appeared when using Visual Studio 6 (VC6)
  • Version 1.4 (2008-11-07)
    • Added clipboard support for copying the contents of the selected cell / rows
    • Renamed the "Callback"-functions to "OnDisplay", as it resembles the MFC naming convention
    • Fixed some reported bugs
  • Version 1.5 (2009-03-29)
    • Added column manager CGridColumnManager
    • Added support for groups on VC6 with platform SDK
    • Added sample projects for the different versions of Visual Studio
    • Improved documentation through Doxygen comments
  • Version 1.6 (2009-09-13)
    • Added OLE drag and drop support
    • Added support for checkbox style LVS_EX_CHECKBOX when using LVS_OWNERDATA
    • Added better support for keyboard search with LVS_OWNERDATA
    • Fixed several bugs
  • Version 1.7 (2009-12-12)
    • Added CGridColumnTraitImage, that can mimic checkbox editing for any column
    • Renamed OnTraitEditBegin() to OnEditBegin()
    • Renamed OnTraitEditComplete() to OnEditComplete()
    • Renamed OnTraitCustomDraw() to OnCustomDrawCell()
    • Fixed several bugs (Mostly row and cell coloring)
  • Version 1.8 (2010-10-01)
    • Made CGridColumnTraitImage a base class for all editor column traits, so they all can mimic checkbox support
    • Implemented multiple selection checkbox support, so checkboxes are flipped for all selected rows
    • Implemented min and max column width through the base column trait
    • Fixed several bugs
  • Version 1.9 (2011-05-30)
    • Changed CGridColumnTrait::OnSortRows to take an LVITEM as argument instead of character strings
    • Renamed CGridColumnConfig to CViewConfigSection
    • Removed CGridColumnManager and moved LoadState/SaveState into CGridListCtrlEx (breaking change)
  • Version 2.0 (2012-05-01)
    • Added copy/paste support for CDateTimeCtrl editor (DTS_APPCANPARSE)
    • When having grouped by column, then column header click now sorts instead of grouping by column
    • Changed the row sorting to be case insensitive by default
    • Fixed bug where cell editor discarded changes performed using the mouse (copy/paste using context menu)
    • Fixed several compiler warnings, and small bugs
  • Version 2.1 (2012-06-20)
    • Fixed bug introduced in 2.0, where grouping of items no longer worked on WinXP.
    • Improved performance when sorting item groups, especially on Vista/Win7+.
    • Added new column trait CGridColumnTraitHyperLink, that can display cell text as weblinks. Updated demo application to display the new column type.
    • Added new column trait CGridColumnTraitMultilineEdit, that can edit cell text that contains newlines (still displayed in a single line).
    • Fixed several small bugs
  • Version 2.2 (2012-11-11)
    • CGridColumnTraitCombo now has the option to display dropdown on edit begin (SetShowDropDown)
    • All column trait editors now has the option to start editor on first mouse click (SetSingleClickEdit)
    • CGridColumnTraitHyperLink will sent LVN_ENDLABELEDIT notification to parent on mouse click (Simulates button click)  
    • Checkbox state icon used by subitems has been improved on Windows 7/8 by avoiding image scaling 
    • Fixed several small bugs
  • Version 2.3 (2015-03-17)
    • Moved project to GitHub, since Google Code is closing down.
    • Implemented support for IDropSource::GiveFeedback to allow changing mouse cursor during dragdrop
    • Inverted cell focus coloring is now disabled by default. Call SetInvertCellSelection(true) to enable.
    • Fixed several small bugs

License

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