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 DWORD
– CCellCtrl
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);
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 CCellCtrl
s, 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 CCellCtrl
s
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 CCellDateCtrl
s 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
.