CQuickList
A custrom-draw virtual list control. Support for subitem editing, images, button (checkboxes, radio buttons), custom colors, and "column navigation".
Contents
- Introduction
- Background
- Features
- Creating a CQuickList
- Add items to the list
- Handling the WM_QUICKLIST_GETLISTITEMDATA message
- CQuickList::CListItemData
- Navigation between subitems
- The message LVN_ODFINDITEM
- Click on image/button
- Right click on column header
- Empty list
- Edit subitems
- Themes in XP
- Acknowledgments
- To do
- Points of Interest
- History
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:
//"Add" 100 elements 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)
//...other messages here...
ON_MESSAGE(WM_QUICKLIST_GETLISTITEMDATA, OnGetListItem)
END_MESSAGE_MAP()
And finally, add the function:
LRESULT CMyListCtrlDlg::OnGetListItem(WPARAM wParam, LPARAM lParam) { //wParam is a handler to the list //Make sure message comes from list box ASSERT( (HWND)wParam == m_list.GetSafeHwnd() ); //lParam is a pointer to the data that //is needed for the element CQuickList::CListItemData* data = (CQuickList::CListItemData*) lParam; //Get which item and subitem that is asked for. int item = data->GetItem(); int subItem = data->GetSubItem(); //...insert information that is needed in "data"... 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(); //Some obvius functions int GetItem() const; int GetSubItem() const; bool IsSelected() const; bool IsHot() const; //The item text CString m_text; //Tool tip text. Note: Don't forget to call EnableToolTips() //to enable tool tips. CString m_tooltip; //Set this to true if you don't want to draw a selection mark //even if this item is selected. //Default value: false bool m_noSelection; //Set this to true if the item is available for editing //Default value: false bool m_allowEdit; //Information about which text style that should be used. struct CListTextStyle { //Default value: false bool m_bold; //Default value: false bool m_italic; //Default value: // DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS //See CDC:DrawText in MSDN UINT m_textPosition; } m_textStyle; //Information about the image struct CListImage { //The image position in the image list. //-1 if no image. //Default value: -1 int m_imageID; //The image list where the image is. //Default value: A pointer to the image list in the list //control that is used small images (LVSIL_SMALL) CImageList* m_imageList; //Set true if you don't want to draw selection mark if the //item is selection //Default value: true bool m_noSelection; //Center the image. Useful if no text. //Default value: false; bool m_center; //Blend if the image is selected. Use ILD_BLEND25 or //ILD_BLEND50, or 0 if you don't want to use this feature. //Default value: ILD_BLEND25 int m_blend; } m_image; //Information about the button struct CListButton { //The style to use to draw the control. //Default value: DFCS_BUTTONCHECK //Use DFCS_CHECKED to draw the check mark. //Use DFCS_BUTTONRADIO for radio button, DFCS_BUTTONPUSH //for push button. //See CDC::DrawFrameControl for details. int m_style; //If you want to draw a button, set this to true //Default value: false bool m_draw; //Center the check box is the column. Useful if no text //Default value: false bool m_center; //Set this to true if you don't want to draw selection //mark under the control. //Default value: true bool m_noSelection; } m_button; //Information about the progress bar struct CListProgressbar { //Note: The m_text member specifies the text in the //progress bar //The max value of progress bar. Use -1 to disable //progress bar. The min value is supposed to be 0. //Default value: -1 int m_maxvalue; //The value the progress bar has. The width of the //progress bar is calculated with use m_value and //m_maxvalue. //Default value: 0 int m_value; //The color the progress bar should be drawn with. //Default value: DEFAULTCOLOR COLORREF m_fillColor; //The color of the text on the progress bar //Default value: DEFAULTCOLOR COLORREF m_fillTextColor; //How to draw the edge. Use 0 for no edge. //See CDC::DrawEdge for different styles. //Default value: EDGE_SUNKEN UINT m_edge; } m_progressBar; //Information about the colors to use struct CListColors { //Default value for all: DEFAULTCOLOR COLORREF m_textColor; COLORREF m_backColor; COLORREF m_hotTextColor; COLORREF m_selectedTextColor; COLORREF m_selectedBackColor; COLORREF m_selectedBackColorNoFocus; //These colors are used to draw selected items in //the "navigation column" 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:
//Left align text: data->m_textStyle.m_textPosition = DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS; //Center text: 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:
//Use image 2 in the list 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:
//Use another image list
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:
//Don't "blend" the image: data->m_image.m_blend = 0; //Other possible values are ILD_BLEND25 and ILD_BLEND50 (default).
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?
//Center image 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:
//We want to draw a button data->m_button.m_draw = true; //Check box, not checked: data->m_button.m_style = DFCS_BUTTONCHECK //Check box, checked: data->m_button.m_style = DFCS_BUTTONCHECK|DFCS_CHECKED; //Radio button, not checked: data->m_button.m_style = DFCS_BUTTONRADIO //Radio button, checked: data->m_button.m_style = DFCS_BUTTONRADIO|DFCS_CHECKED;
Just like images, buttons are not drawn as selected. They could also be centered:
//Draw as selected data->m_button.m_noSelection = false; //Center 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:
//Max value is 100 data->m_progressBar.m_maxvalue = 100;
The minimum value is 0. Then specify which value the progress bar has:
//Fill half the progress bar: 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.
//No edge: 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.
//Red fill color data->m_progressBar.m_fillColor = RGB(255,0,0); //White text color 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
:
//Green text data->m_colors.m_textColor = RGB(0,255,0); //Black background data->m_colors.m_backColor = RGB(0,0,0); //If the item is "hot", use purple color data->m_colors.m_hotTextColor = RGB(0,255,255); //If the item is selected, the text should //be drawn in white color data->m_colors.m_selectedTextColor = RGB(255,255,255); //If the item is selected, the background should //be drawn in green color data->m_colors.m_selectedBackColor = RGB(0,128,0); //If the item is selected but the list hasn't focus, //the background should be drawn in gray color data->m_colors.m_selectedBackColorNoFocus = RGB(64,64,64); //If the item is "navigated", text will be drawn in red data->m_colors.m_navigatedTextColor = RGB(255,0,0); //If the item is "navigated", background will be drawn in blue 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:
//Enable navigation (it is enabled as default) m_list.EnableColumnNavigation(true); //Set column 2 as "navigated". 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) { //Make sure message comes from list box ASSERT( (HWND)wParam == m_list.GetSafeHwnd() ); CQuickList::CNavigationTest* test = (CQuickList::CNavigationTest*) lParam; //The previous column is in test->m_previousColumn. //Don't allow navigation to column 2 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:
//Search in column 1: 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) { //Make sure message comes from list box ASSERT( (HWND)wParam == m_list.GetSafeHwnd() ); CQuickList::CListHitInfo *hit= (CQuickList::CListHitInfo*) lParam; //Item: hit->m_item //Subitem: hit->m_subitem //Hit button? if(hit->m_onButton) { //...toggle check box in the database... //Redraw check box m_list.RedrawCheckBoxs( hit->m_item, hit->m_item, hit->m_subitem); } else //Hit image? if(hit->m_onImage) { //... toggle image ... //Redraw image 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:
//Show "Hello world" is the list is empty 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) { //Make sure message comes from list box ASSERT( (HWND)wParam == m_list.GetSafeHwnd() ); CQuickList::CHeaderRightClick *hit= (CQuickList::CHeaderRightClick*) lParam; //Load menu CMenu menu; VERIFY(menu.LoadMenu(IDR_HEADERMENU)); //Pop up sub menu 0 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 pszText is NULL, editing was canceled if(pDispInfo->item.pszText != NULL) { //Item: pDispInfo->item.iItem //Subitem: pDispInfo->item.iSubItem //... save the text ... } *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:
- "Neat Stuff to do in List Controls Using Custom Draw", Michael Dunn.
- "XListCtrl - A custom-draw list control with subitem formatting", Hans Dietrich.
- "SuperGrid - Yet Another listview control", Allan Nielsen.
- "Easy Navigation Through an Editable List View", Lee Nowotny.
- "Time to Complete Progress Control", Craig Henderson.
- "XP Style CBitmapButton (CHoverBitmapButton)", Rail Jon Rogut.
- "Determining right click on the header control (Codeguru)", Zafir Anjum.
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 theCQuickList
control is created. But neitherCreate
norOnCreate
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.