Contents
Introduction
CQuickList
is another owner draw CListCtrl
derived control. The major difference between this control and other owner draw list controls at CodeProject is that this is a virtual list. This means that you don't insert items in the list. Instead, the list control will ask the parent when it needs information. So, you could make a large complex list very fast without using a lot of memory. If you haven't had used a virtual list before, it might be a good idea to have a look on the article "Virtual list" where I try to explain the idea.
Background
For a while ago, I needed a list where I could have images in subitems. If it was possible to edit subitems, that would be great too. But the most important for me was that the list was virtual, and since I didn't find something like that, I started to create my own. And this is the result, so far.
However, even if I didn't find what I searched for, I found other controls, which have been a great help for me. So, thanks to all other authors at CodeProject :-).
Features
The most important features of CQuickList
are:
- Subitem editing.
- Images in subitems.
- Buttons in list (like checkboxes, radio buttons).
- Progress bar.
- Customizing colors.
- Tooltips.
- Column navigation.
- Bold/italic text.
- Show message if the list is empty.
- Automatic handling of the
LVN_ODFINDITEM
message.
- Small code. Use
#define
to remove unused features.
- Unicode support.
- Support for themes in Windows XP.
Creating a CQuickList
Creating a CQuickList
is quite simple. Add a list control in the resource editor and add a CListCtrl
variable to this control. Replace "CListCtrl
" to "CQuickList
" in the header file, and you are done.
Make sure that you have checked the style "Owner data" and have the view in Report mode. Make also sure that "Owner draw fixed" is not checked.
Add items to the list
Let's say m_list
is the control variable for the list. Normally, you add data to the list like this:
m_list.InsertItem(0, _T("Hello world"));
But in a virtual list (like CQuickList
), this will not work. Instead, it is up to you to handle the data. Instead of adding, you change the number of elements the list is showing:
m_list.SetItemCount(100);
If you set the item count to 100 or 1,000,000, it doesn't matter, the time to run this command will still be practically zero. In a non-virtual list, adding a million elements could take hours.
Handling the WM_QUICKLIST_GETLISTITEMDATA message
A normal virtual list sends LVN_GETDISPINFO
to the parent when it needs information. This message is also sent when you are using CQuickList
, but that message isn't important. Instead, you should handle the WM_QUICKLIST_GETLISTITEMDATA
message. Add this in the header file:
afx_msg LRESULT OnGetListItem(WPARAM wParam, LPARAM lParam);
Add a message handler in the message map:
BEGIN_MESSAGE_MAP(CMyListCtrlDlg, CDialog)
ON_MESSAGE(WM_QUICKLIST_GETLISTITEMDATA, OnGetListItem)
END_MESSAGE_MAP()
And finally, add the function:
LRESULT CMyListCtrlDlg::OnGetListItem(WPARAM wParam, LPARAM lParam)
{
ASSERT( (HWND)wParam == m_list.GetSafeHwnd() );
CQuickList::CListItemData* data =
(CQuickList::CListItemData*) lParam;
int item = data->GetItem();
int subItem = data->GetSubItem();
return 0;
}
CQuickList::CListItemData
CQuickList::CListItemData
is a very simple, but important class. In this class, there are several member variables. These variables are used to draw the item. The public part in this class is:
class CQuickList::CListItemData
{
public:
CListItemData();
int GetItem() const;
int GetSubItem() const;
bool IsSelected() const;
bool IsHot() const;
CString m_text;
CString m_tooltip;
bool m_noSelection;
bool m_allowEdit;
struct CListTextStyle
{
bool m_bold;
bool m_italic;
UINT m_textPosition;
struct CListImage
{
int m_imageID;
CImageList* m_imageList;
bool m_noSelection;
bool m_center;
int m_blend;
} m_image;
struct CListButton
{
int m_style;
bool m_draw;
bool m_center;
bool m_noSelection;
} m_button;
struct CListProgressbar
{
int m_maxvalue;
int m_value;
COLORREF m_fillColor;
COLORREF m_fillTextColor;
UINT m_edge;
} m_progressBar;
struct CListColors
{
COLORREF m_textColor;
COLORREF m_backColor;
COLORREF m_hotTextColor;
COLORREF m_selectedTextColor;
COLORREF m_selectedBackColor;
COLORREF m_selectedBackColorNoFocus;
COLORREF m_navigatedTextColor;
COLORREF m_navigatedBackColor;
} m_colors;
};
As you see, there are several settings to use. However, you will probably use only a few of them. I will try to explain most of the settings in the following text. data
in the following text is a pointer to a CQuickList::CListItemData
object.
Text
The simplest setting is m_text
. This is the text to be drawn:
data->m_text = _T("Hello world");
Here the text Hello world will be drawn for the current item.
Tool tip
Tool tips could sometimes be useful. If you place the mouse cursor above an item, the text you set here will be shown as a tool tip. Example:
data->m_tooltip = _T("Tip: Hello world");
Here "Tip: Hello world" will be shown.
Note 1: To activate tooltips, you must call EnableToolTips(TRUE)
.
Note 2: If you don't use this feature, you could define QUICKLIST_NOTOOLTIP
to make the application a little bit smaller.
Note 3: Unfortunately, there are some problems when tooltips are used. See points of interest.
Draw no selection
As default, an item will be drawn as selected if it is selected. However, if you want to draw an item as unselected even if it is selected, then do this:
data->m_noSelection = false;
The item will not now be drawn as selected even if it is. But what is this good for? This may seem like a useless feature, but sometimes this could be pretty nice to use. For example, if you have several columns in a list, some columns could be drawn as not selected. In the image, all items in column 1 are drawn as not selected even if they are selected.
Allow edit
If you want to allow an item to be edited, do this:
data->m_allowEdit = true;
Note 1: You must handle some messages as well to make editing possible. I discuss this later.
Note 2: If you don't use this feature, you could define QUICKLIST_NOEDIT
to make the application a little bit smaller.
Text style
Sometimes, it's useful to draw the text as bold or italic. This is simple to do:
data->m_textStyle.bold = true;
data->m_textStyle.italic = true;
You could also specify in which position the text should be drawn. Example:
data->m_textStyle.m_textPosition =
DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS;
data->m_textStyle.m_textPosition =
DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS;
But you can do much more settings than this. Look in MSDN about CDC::DrawText
.
Note: If you will not use this feature, you could define QUICKLIST_NOTEXTSTYLE
to make the application a little bit smaller.
Images
Images are useful to have. If you have an image list connected to the list (called SetImageList
), you only have to do this:
data->m_image.m_imageID = 2;
Then the default image list will be used. However, you could specify which image list you want to use, like this:
data->m_image.m_imageList = &m_mySecondImagelist;
If the item is selected, the image will not be drawn as selected, as default. But you can change this:
data->m_image.m_noSelection = false;
As you see on the image, the selected images are a little bit more blue than the images that are not selected. If you don't want this, you could change m_blend
setting:
data->m_image.m_blend = 0;
The image will be drawn to the left as default. But if you don't have anything else than an image, why not center it?
data->m_image.m_center = true;
Note: If you will not use this feature, you could define QUICKLIST_NOIMAGE
to make the application a little bit smaller.
Button
If you want to draw a check box or a radio button, you could use the m_button
variable:
data->m_button.m_draw = true;
data->m_button.m_style = DFCS_BUTTONCHECK
data->m_button.m_style = DFCS_BUTTONCHECK|DFCS_CHECKED;
data->m_button.m_style = DFCS_BUTTONRADIO
data->m_button.m_style = DFCS_BUTTONRADIO|DFCS_CHECKED;
Just like images, buttons are not drawn as selected. They could also be centered:
data->m_button.m_noSelection = false;
data->m_button.m_center = true;
Note 1: Buttons aren't drawn with themes in XP as default. To solve this, you should call SetThemeManager()
. Read here.
Note 2: If you will not use this feature, you could define QUICKLIST_NOBUTTON
to make the application a little bit smaller.
Progress bar
Progress bars are rarely used in list controls, but they could be useful. To use it, first specify the max value:
data->m_progressBar.m_maxvalue = 100;
The minimum value is 0. Then specify which value the progress bar has:
data->m_progressBar.m_value = 50;
You can also change the edge. The default value for the edge is EDGE_SUNKEN
. If you don't want any edge, set the value to 0.
data->m_progressBar.m_edge = 0;
See CDC::DrawEdge
in MSDN for more settings.
You could also specify which fill color and text color to use. The default for these settings are DEFAULTCOLOR
, which means that Windows should decide which color to use.
data->m_progressBar.m_fillColor = RGB(255,0,0);
data->m_progressBar.m_fillColor = RGB(255,255,255);
Note 1: If you have specified any text in m_text
, the text will be drawn in the progress bar.
Note 2: If you will not use this feature, you could define QUICKLIST_NOPROGRESSBAR
to make the application a little bit smaller.
Colors
As default, Windows colors will be used, but you can change this with the m_colors
:
data->m_colors.m_textColor = RGB(0,255,0);
data->m_colors.m_backColor = RGB(0,0,0);
data->m_colors.m_hotTextColor = RGB(0,255,255);
data->m_colors.m_selectedTextColor = RGB(255,255,255);
data->m_colors.m_selectedBackColor = RGB(0,128,0);
data->m_colors.m_selectedBackColorNoFocus = RGB(64,64,64);
data->m_colors.m_navigatedTextColor = RGB(255,0,0);
data->m_colors.m_navigatedBackColor = RGB(0,0,128);
When you set the background color, you could use a transparent color by using TRANSPARENTCOLOR
. This is useful if you have a background image.
Navigation between subitems
In a normal list, you could select items with the mouse or keyboard. But it's not possible to select a subitem, which may be quite useful. However, in CQuickList
, that is possible :-). To specify which column is currently selected, call:
m_list.EnableColumnNavigation(true);
m_list.SetNavigationColumn(2);
OK, that's easy. But let's say you have three columns, and you don't want it to be possible to navigate to column 2. To solve this, add a message handler for the message WM_QUICKLIST_NAVIGATIONTEST
(add a function in the header, and connect to it in the message handler). Then, write the function like this:
LRESULT CMyListCtrlDlg::OnNavigationTest(WPARAM wParam, LPARAM lParam)
{
ASSERT( (HWND)wParam == m_list.GetSafeHwnd() );
CQuickList::CNavigationTest* test =
(CQuickList::CNavigationTest*) lParam;
if(test->m_newColumn == 2)
test->m_allowChange = false;
return 0;
}
Now, it will not be possible to navigate to column 2.
Note: If you will not use this feature, you could define QUICKLIST_NONAVIGATION
to make the application a little bit smaller.
The message LVN_ODFINDITEM
As you might know, it's possible to find in item in a normal list by writing in the list (read here for more information). To make this possible in virtual list, you have to handle the LVN_GETDISPINFO
message. But when you are using CQuickList
that is not necessary, the list will handle this for you. But you can specify which column the list will search in when it tries to find an item. Example:
m_list.SetKeyfindColumn(1);
You can use KEYFIND_CURRENTCOLUMN
to search in the current navigated column. If you want the parent to handle this message, use KEYFIND_DISABLED
.
Note: If you will not use this feature, you could define QUICKLIST_NOKEYFIND
to make the application a little bit smaller.
Click on image/button
When you click on a check box, you expect that it will toggle. But in a virtual list, the list can't change the value. To solve this, the list sends a message to the parent that has to do the work. Add a message handler for the message WM_QUICKLIST_CLICK
(add a function in the header, and connect to it in the message handler). Then, write the function like this:
LRESULT CMyListCtrlDlg::OnListClick(WPARAM wParam, LPARAM lParam)
{
ASSERT( (HWND)wParam == m_list.GetSafeHwnd() );
CQuickList::CListHitInfo *hit=
(CQuickList::CListHitInfo*) lParam;
if(hit->m_onButton)
{
m_list.RedrawCheckBoxs( hit->m_item,
hit->m_item,
hit->m_subitem);
}
else
if(hit->m_onImage)
{
m_list.RedrawImages(hit->m_item,
hit->m_item,
hit->m_subitem);
}
return 0;
}
As you can see, it's possible to see if an image was hit.
Note: Another way to solve this is to handle the NM_LCLICK
message. Call then CQuickList::HitTest
to see if a button or image was hit.
Empty list
If the list is empty, it could be nice, so show a little message in the list. This is easy to do:
m_list.SetEmptyMessage(_T("Hello world"));
Note: If you will not use this feature, you could define QUICKLIST_NOEMPTYMESSAGE
to make the application a little bit smaller.
Right click on column header
As far as I know, there is no easy way to catch a right click in the column header in CListCtrl
. This is pretty sad since it would be a great way to show the user a context menu where he, for example, could hide a menu. When you are using CQuickList
, the list will send WM_QUICKLIST_HEADERRIGHTCLICK
to the parent when a right click appears in the column header. WPARAM
is a handle to the list, and LPARAM
is a pointer to a CQuickList::CHeaderRightClick
object. That object includes the mouse position (m_mousePos
) and which column was clicked (m_column
).
A function that pops up a menu would look something like this:
LRESULT CMyListCtrlDlg::OnHeaderRightClick(WPARAM wParam, LPARAM lParam)
{
ASSERT( (HWND)wParam == m_list.GetSafeHwnd() );
CQuickList::CHeaderRightClick *hit=
(CQuickList::CHeaderRightClick*) lParam;
CMenu menu;
VERIFY(menu.LoadMenu(IDR_HEADERMENU));
CMenu* popup = menu.GetSubMenu(0);
popup->TrackPopupMenu( TPM_LEFTALIGN | TPM_RIGHTBUTTON,
hit->m_mousePos.x,
hit->m_mousePos.y,
this);
return 0;
}
Edit subitems
Editing subitems in CQuickList
doesn't differ much from a CListCtrl
. Before editing starts, the message OnBeginlabeleditList
is sent to the parent. Unless you want to specify another text than you specify in m_text
, you can ignore this message. When editing is done, the message LVN_ENDLABELEDIT
is sent. You must add a handler for this message if you want to save the text. A function will look something like this:
void CMyListCtrlDlg::OnEndlabeleditList(NMHDR* pNMHDR, LRESULT* pResult)
{
LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
if(pDispInfo->item.pszText != NULL)
{
}
*pResult = 0;
}
You could call CQuickList::GetLastEndEditKey
to see which key was pressed when editing ended. For example, if the user pressed enter (VK_RETURN
), it might be a good idea to start editing the next item in the list.
The edit box is closed when it is losing focus. If you call CQuickList::SetEndEditOnLostFocus(false)
, it will not close when it is losing focus. Instead, the parent will receive the message WM_QUICKLIST_EDITINGLOSTFOCUS
. (I guess this feature is strange, but I need it in one of my programs, so I added this :-)).
Editing will start when F2 or ENTER is pressed, or when the user double clicks on the item. You could change this by calling the functions SetEditOnEnter
, SetEditOnF2
, and SetEditOnDblclk
. You can also call EditSubItem
to start editing an item.
Themes in XP
CQuickList
supports themes in Window XP. Themes are used when drawing buttons (check button, radio buttons...). If you don't use this, you could ignore this.
To enable themes, you should call SetThemeManager()
with a pointer to a CTheme
object. If you don't do this, buttons will be drawn in the traditional way.
In the demo project is a CTheme
class. I have based this on the article XP Style CBitmapButton (CHoverBitmapButton). The good thing about this is that programs run fine on other systems than Windows XP. Unfortunately, we must add some code in the main window. Look in the demo project to see how to do this. Make sure to define "USEXPTHEMES
" in StdAfx.h. You will need a pretty new version of Platform SDK to compile the code with theme support.
Note: If you don't use this feature, you could define QUICKLIST_NOXPTHEME
to make the application a little bit smaller.
Acknowledgments
Since I haven't done any similar work before, I'm very glad there have been other projects that have been a great help for me. I have looked on and even copied some code from other projects. The most useful project for me has been "XListCtrl - A custom-draw list control with subitem formatting". But I also want to thank:
Another nice control is Virtual Grid Control. It's quite similar to CQuickList
and worth to look at.
To do
CQuickList
works well, but there are some things I want to be fixed/implemented:
- Fix tool tip problems (see Points of interest).
- I want to set the
LVS_OWNERDATA
setting when the CQuickList
control is created. But neither Create
nor OnCreate
is called. Suggestion, someone?
- Specify height of items.
- When you double click in the column header between two columns, the column width should be set to the widest item. This doesn't work perfectly, especially if buttons or images are used.
- Support for drag and drop. I have tried to make a function that creates a drag image (
CreateDragImageEx
), but that doesn't work at all.
Points of Interest
CQuickList
is mostly designed with full row select, but it works also when you don't use this. However, it might be some minor drawing problems, so my recommendation is to use full row select.
When I used the list in Windows XP with a manifest file, I had some problems. One was that the "hot item" was changed when the mouse pointer was over an item. The solution to this was to handle the LVN_HOTTRACK
message. Another problem was that the list was drawn over the edit box when the mouse pointer was moved over the list, I solved this by handling the message WM_MOUSEMOVE
.
Another strange behavior in XP is that there are some drawing problems when tooltips are used. When you use tooltips, you may notice that the list is flickering a little bit. The problem is in OnToolHitTest
. This functions calls ListView_SubItemHitTest
, and for some very, very strange reason, this forces the list to make some redrawing (probably only in the first column). If you move the mouse pointer over the column header, the header will temporarily disappear and you will see the item under it, very weird. I haven't figured out why this happens. If you have this problem, the simplest way to solve it is to not use tool tips.
History
- 22 January, 2006 - Version 1.01. Fixed problem when
LVS_EX_HEADERDRAGDROP
is used.
- 10 September, 2004 - Version 1.0. Solved the "hot item" problem in Windows XP. Added support for themes. Added message when user right clicks on the column header.
- 28 August, 2004 - Version 0.9. Initial version.