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

An MFC-CListCtrl derived class that allows other ‘controls’ to be inserted into a particular cell

0.00/5 (No votes)
5 Jan 2014 11  
A class derived from CListCtrl that allows edit controls, combo boxes, check boxes, date pickers, and color pickers to be inserted into or removed from particular cells extremely easily. The inserted 'controls' are not CWnd-derived.

Introduction

A common need is an owner draw CListCtrl that allows a particular cell to contain another control such as an edit box, combo box, color picker etc... Such a control would be very useful in many applications, as it would provide a way to easily configure application specific properties such as background color etc...

This article is about code samples that do just that.

Background

Over the years, I have seen several code samples that allow a CComboBox or CEdit control to be inserted in a particular cell of a CListCtrl-derived control. I have also tried this out myself. With this solution, the control itself is not persistent, so you cannot see what type of control a particular cell is filled with unless it has focus. Also, usually you need to activate the control and then manipulate it, which is completely impractical: for instance, you would need to first click on a checkbox to activate it, and then check/uncheck it. Another issue is you do not know all the implementation details of the inserted control, meaning you might get completely undesired results, with no possibility of fixing. This is just what I obtained in my trial using Visual Studio 6.0.

This convinced me that what was needed was another completely new solution that did not make use of the inserted CWnd-derived controls.

There are system functions that allow you to paint the old style theme-less ‘classic’ look of buttons as these appeared in versions of Windows prior to XP. In addition, the OS has a whole collection of functions that enable you to handle themes: OpenThemeData etc…

Using these facilities, I have written this project in which, instead of inserting another CWnd-derived control into a cell of a CListCtrl, you paint the image of a combo box, checkbox etc.

I have tested this with Windows 7, Vista, and XP, tried out changing themes, and have obtained acceptable results. The biggest drawback is that changing the theme while the application with CConfigListCtrl is open does not produce the best results: MeasureItem is not called again as it should be, meaning the row height is not immediately recalculated. I have not found a way for forcing an instant recalculation of this. This is not too dramatic, as changing theme is presumably done very rarely by an average user.

There are other more minor faults, as would be expected: a small amount of flickering – which might be eliminated by improving the code.

A lot of work has been required for this project, spread in bits and pieces over the years (I started in 2006). There are still lots of features to add: no spin edit control, no editable combo, left/right/center text alignment to do...

Otherwise, all control types have a look and feel consistent with each other, including the color picker, which supports themes.

Overall view of design

The overall concept is really very simple: there is a main CConfigListCtrl class, and CCellCtrl derived classes inserted in cells. CConfigListCtrl calls CCellCtrl virtual functions, such as drawing, and passes messages to other CCellCtrl virtual functions, such as mouse clicking.

The main CConfigListCtrl has no knowledge of the implementation details of the inserted controls, and new control types can be implemented without touching the CConfigListCtrl source code. All that is required is that the inserted control must derive from CCellCtrl or CCellDropDown if it contains a handle to a drop down pop up. The drop down pop up must derive from CListCtrlCellWnd, which itself derives from CWnd.

Pointers to CCellCtrl-derived controls are stored in the m_CtrlMap member variable of CConfigListCtrl. This is just a DWORDCCellCtrl pointer map where the DWORD contains the locations of cells in the control.

Cleanup is done by traversing m_CtrlMap and deleting all the allocated memory.

Below is a diagram showing how all the classes interact. MFC classes are marked in orange. Abstract classes are marked in gray.

Evaluating samples

I have included two downloadable code samples:

  • A full demo project, which shows CConfigListCtrl integrated within a complete project. This is what is shown in the screenshots. I have done my development and testing within this. This should enable readers to rapidly assess the features of the control. I shall go over the functionality below.
  • A source code sample that only includes CConfigListCtrl and the relevant files. In the section 'Building a project that uses CConfigCtrl from scratch', I shall describe how to use this sample.

The value of any cell can be evaluated. The value of a CCellCtrl-filled cell has a format that is the same as the default value lpszDefaultTextValue parameter given in InsertCtrl, for instance, as described in the section 'Description of main functions'.

Tab and Shift+Tab are supported and focus moves to the relevant CCellCtrl within CConfigListCtrl.

F4 opens the pop up of a CCellDropDown-derived class, Esc closes it without setting the control to the currently selected item in the pop up. Enter closes it, but sets the control to the currently selected item. Arrow keys should allow the user to move around the items in the pop up. If there is not enough room to show the drop down downwards, then it is shown upwards instead.

It is possible to enable or disable a cell in a particular control. See below:

...Row and columns can also be inserted or removed:

...Controls can be inserted or removed in a particular cell. If inserting a control in a cell which already has a control, the previous control is automatically deleted.

...It is possible to insert, select, or remove items in a cell which contains a CCellComboBox:

...Supports theme change as already mentioned. If changed to 'Classic', you will get the following display. I suggest closing and reopening the application, as CListCtrl’s row height is not recalculated immediately after a theme change. This seems an MFC bug to me, but I may be mistaken.

Will also run correctly in XP. A screenshot is shown below with the olive theme:

Building a project that uses CConfigCtrl from scratch

This section describes how to build from scratch a project that uses CConfigListCtrl. To start with, create an MFC application using Visual Studio 2010. For simplicity, select Dialog based (not MDI or SDI). Ensure the ‘Common Control Manifest’ checkbox is checked and Finish.

Extract all of ConfigListCtrlSource into a folder which contains the .cpp and .h files. Include all the root files to the project. Add a new filter ‘CellCtrls’ and attach the two new filters ‘Header Files’ and ‘Source Files’. Include all the files in the subfolder CellCtrls here. See the image below. (I have called my project ‘MFCTestCConfigCtrl’, but you may call it something else, of course.)

Add a resource: right click on your *.rc file in the Resources tab and select ‘Add Resource…’. Select 'Bitmap' and click the button 'Import'. Navigate to the res\ folder and select checkbox.bmp. Rename the bitmap 'IDB_CHECKBOX'. (Open the bitmap's 'Properties' dialog box for that.)

In your project configuration properties, select Linker, Input, and add ‘UxTheme.lib’ to ‘Additional dependencies’:

In your dialog, add a list control using Toolbox. This list control must have the following properties set: ‘Owner Draw Fixed’ must be ‘True’. ‘View’ must be ‘Report’.

In ‘Class Wizard’, add a member variable, say m_ConfigListCtrl for the list control you just added. In the relevant xxxDlg.h header file, change the type of m_ConfigListCtrl from CListCtrl to CConfigListCtrl and add the #include "ConfigListCtrl.h" directive at the top.

Add the following code at the top of your xxxDlg.cpp file, so you can use CCellxxx controls:

#include "CellCtrls\CellEdit.h"
#include "CellCtrls\CellCheckBox.h"
#include "CellCtrls\CellComboBox.h"
#include "CellCtrls\CellDateCtrl.h"
#include "CellCtrls\CellColorCtrl.h"
#include "CellCtrls\CellTimeCtrl.h"

Override the PreTranslateMessage virtual function and add message handlers for WM_SIZING and WM_MOVE. Add the following code:

BOOL CMFCTestCConfigCtrlDlg::PreTranslateMessage(MSG* pMsg)
{
    if(pMsg->message==WM_KEYDOWN) 
        m_bKeyUp = FALSE;
    if(pMsg->message==WM_KEYUP) 
        m_bKeyUp = TRUE;
    if((pMsg->wParam == VK_ESCAPE || pMsg->wParam == VK_RETURN))
    {
        if (m_bKeyUp)
        {
            m_bKeyUp = FALSE;
            return m_ConfigListCtrl.OnEnterEsc(pMsg->wParam);
        }
        else
            return TRUE;
    }
    return CDialogEx::PreTranslateMessage(pMsg);
}

void CMFCTestCConfigCtrlDlg::OnMove(int x, int y)
{
    CDialog::OnMove(x, y);
    // Seems no way round this and have to explicitly call m_ConfigListCtrl this way.
    // Note: this is needed because when move, we want to either close any open
    // popups associated with the control or move these as well. It looks funny 
    // otherwise.
    m_ConfigListCtrl.OnParentMove(x, y);
}

void CMFCTestCConfigCtrlDlg::OnSizing(UINT fwSide, LPRECT pRect)
{
    CDialogEx::OnSizing(fwSide, pRect);
    m_ConfigListCtrl.OnSizing(fwSide, pRect);
}

You still need to add m_bKeyUp, which is a BOOL private member of your class instantiated to FALSE in the constructor.

...That is it! You have added a CConfigListCtrl to your project. For a bit of manipulation, you might add the following code in the "TODO: Add extra initialization here" area of OnInitDialog:

m_ConfigListCtrl.InsertColumn(0, _T("A column"));
m_ConfigListCtrl.SetColumnWidth(0, 160);
m_ConfigListCtrl.InsertItem(0, _T(""));
m_ConfigListCtrl.SetItem(0,0,new CCellComboBox, 
   _T("Selected Item\n1st item\n2nd item\nSelected Item"));

For more details on how to manipulate CCellCtrls, see the next section.

Description of the main functions

Of course, a function available within CListCtrl can still be used within CConfigListCtrl. The most useful probably are SetColumnWidth and GetItemText. I will only describe the new functions or functions that have been overloaded here.

int InsertColumn(int nCol, const LVCOLUMN* pColumn);
int InsertColumn(int nCol, LPCTSTR lpszColumnHeading, 
          int nFormat = LVCFMT_LEFT, int nWidth = -1, int nSubItem = -1);
BOOL DeleteColumn(int nCol);
int InsertItem(const LVITEM* pItem);
int InsertItem(int nItem, LPCTSTR lpszItem);
int InsertItem(int nItem, LPCTSTR lpszItem, int nImage);
BOOL DeleteItem(int nItem);
inline BOOL DeleteAllItems();

These functions are all overloaded. The functionality is same as for their CListCtrl equivalent, except that the underlying data placing CCellCtrls needs to be altered so that these remain in correct cells and are not out of phase. Also, if you insert a column at the beginning, all data in the cells needs to be programmatically shifted right, otherwise the content does not match the column headers.

In addition, there are the following specialized functions for insertion/deletion of controls in cells:

int InsertItem(int nItem, CCellCtrl *pCellCtrl, LPCTSTR lpszDefaultTextValue = _T("\0"));
BOOL SetItem(int nItem, int nSubItem, CCellCtrl *pCellCtrl, 
             LPCTSTR lpszDefaultTextValue = _T("\0"));
void InsertCtrl(int nItem, int nSubItem, CCellCtrl *pCellCtrl);
void DeleteCtrl(int nItem, int nSubItem);

pCellCtrl can be a pointer to any one of: CCellEdit, CCellCheckBox, CCellComboBox, CCellDateCtrl, or CCellColorCtrl. The pointer can be created by using the C++ new directive directly within the function call. No need to worry about corresponding delete statements as cleanup is done within the CConfigListCtrl class.

The default value lpszDefaultTextValue is used as follows:

  • CCellEdit: any text string with which you want to initialize your edit control.
  • CCellCheckBox: a text string prefixed with either ‘0’ (checkbox is unchecked) or ‘1’ (checked checkbox).
  • CCellComboBox: a string in the format: “sel string a\nstring b\nstring c\nsel string a”. This will populate the combobox with three items: “string b”, “string c”, and “sel string a”, with “sel string a” selected: that is if the first item in the list is repeated in the initial default value, then it is the selected item. If no repeat, the combo box displays an empty string.
  • CCellDateCtrl: This string must be in the standard format: “YYYYMMDD”, where YYYY is the 4 digit year, and MM and DD are respectively the 2-digit month and day.
  • CCellColorCtrl: Must be in the standard format: “0X00BBRRGG”, where BB, GG, and RR are two digit hexadecimal numbers representing the blue, green, and red values of a COLORREF.
  • CCellTimeCtrl: By default, will display the current time. If you want to overload this, you have to insert a string in format “HHmmss”.

To help manipulation, the following controls have been written:

BOOL IsOnCellCtrl(int iItem, int iSubItem, CCellCtrl **ppCellCtrl);
CCellCtrl *GetItemCellCtrl(int iItem, int iSubItem);

Returns the relevant cell control if present in the {iItem, iSubItem} location on CConfigListCtrl. IsOnCellCtrl returns TRUE if present, FALSE otherwise. GetItemCellCtrl returns NULL if no cell control is present.

CCellCtrl *GetActiveCellCtrl() const

Returns the cell control that currently has focus.

void EnableCtrl(int iItem, int iSubItem, BOOL Enable = TRUE);
BOOL IsCtrlEnabled(int iItem, int iSubItem);

EnableCtrl enables/disables a control in a particular cell if the cell has a control. IsCtrlEnabled returns whether the cell has an enabled or disabled control. If the cell does not have a control at all, the value returned is whether the instance of CConfigListCtrl is enabled.

BOOL SetCtrlAlignment(int iItem, int iSubItem, Alignment align);
BOOL GetCtrlAlignment(int iItem, int iSubItem, Alignment &align);

SetCtrlAlignment and GetCtrlAlignment will set or get the alignment of a cell if this contains a CCellCtrl. These functions return TRUE if the cell has a control, FALSE otherwise. Alignment only has an effect for CCellEdit, CCellCheckBox, and CCellComboBox. No effect for CCellDateCtrl, as not implemented. Alignment is not implemented on a standard MFC Date Picker control either (as presumably not very useful). Alignment has no meaning for a CCellColor control. Button stays on right hand side for a right aligned CCellComboBox. The standard MFC CComboBox displays button on left, with a small bug in Windows 7: the button’s corners are rounded on wrong side, which is strange since the right button style is available in themes.

It is sometimes useful to be able to determine the behaviour of a certain cell control type. This can be done preferably using the dynamic_cast directive as in, for example:

CCellComboBox * pComboCrtl = 
   dynamic_cast<CCellComboBox *>(m_ListCtrl.GetItemCellCtrl(iRow, iColumn));

if (pComboCrtl)
    pComboCrtl->SetSelectedItem(iIndex);

Below is a list of specialised CCellCtrl control functions:

BOOL CCellComboBox::InsertItem(int Idx, LPCTSTR strText);
BOOL CCellComboBox::RemoveItem(int Idx);

Inserts or removes an item in a combo box. The point at which insertion takes place is given by Idx. If Idx is out of the range of existing values, returns FALSE, and no action takes place. Otherwise, returns TRUE.

BOOL CCellComboBox::SetSelectedItem(LONG lSelectedItem);
LONG CCellComboBox::GetSelectedItem() const;

Sets or gets the currently selected item of a combo box.

LONG CCellComboBox::GetDisplayedRows() const; 
void CCellComboBox::SetDisplayedRows(LONG lDisplayedRows);

Sets or gets the number of selected rows shown when the drop down list of a combo box is open.

static void CCellDateCtrl::SetDateFormat(const CString & strDateFormat);

This static function will set the date format for all CCellDateCtrls within an application that uses CConfigListCtrl. By default, the date format is given by the short date format specified on your computer (usually DD/MM/YYYY in UK). The tags that strDateFormat can use are given by: Day, Month, Year, and Era Format Pictures.

void CCellDateCtrl::SetYMD(WORD Y, WORD M, WORD D);

Will set the year month and day values for the control.

void CCellTimeCtrl::SetTimeFormat(const CString& NewTimeFomat);

Will set the time format for the control. Tags that can be used are ‘H’, ‘HH’, ‘h’, ‘hh’, ‘m’, ‘mm’, ‘s’, ‘ss’, ‘t’ and ‘tt’.

COLORREF CCellColorCtrl::GetSelectedColor()

Will obtain the color as a COLORREF format

Limitations

I am aware of the following faults:

  • CListCtrl row height is not recalculated when changing the theme.
  • There does not seem to be a theme part identifier for painting a non-editable Windows Vista/7 combo box button which occupies the full length of the combo, but with arrow on the right hand side.
  • CMonthCalCtrl::GetMinReqRect does not always give the correct rectangle size. If you change the theme to Classic, the rectangle returned is wrong. If you comment the manifest dependency code in stdafx.h, the rectangle is correct again.
  • No editable combo box or spin edit control (with up/down buttons).
  • Should allow old style other colors dialog box in PopupColorBar. “Other” should be in a string table, as items in tooltips, so text can be translated when used in another country. The colours displayed should be configurable too. In short, more work needs to be done in CCellColor.
  • Maybe there should be a style so it does not highlight a row when clicking on a cell.

Acknowledgment

I started this project after having read 'Design Patterns' by Gamma, Helm, Johnson, and Vlissides, so was inspired by that book. Otherwise, all my sources have been within MSDN.

For the repeat effect of new CCellSpinCtrl, following article was very useful: Mouse Repeat.

History

  • 1st August 2011: Initial version.
  • 9th August 2011: Right and center alignment implemented and lots of embarrassing CCellEdit bugs fixed (copy, paste, scrolling...).
  • 25th November 2011: Added a CCellTimeCtrl as a user requested this. 
  • 5th January 2014: Bug fixes and added a CCellPushButton. Special thank you to Alexey Pismenny for supplying me code for pushbutton and for giving me improvements for RemoveCtrl.

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