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

CQuickList

0.00/5 (No votes)
24 Jan 2006 1  
A custrom-draw virtual list control. Support for subitem editing, images, button (checkboxes, radio buttons), custom colors, and "column navigation".

Sample Image - quicklist.gif

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:

//"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; 

    //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

Tooltip sample

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

No selection sample. Column 1 is not selected.

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

Text style sample

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

Image sample

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

Button sample

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

Progressbar sample

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

Default colors Specified 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

Navigation sample

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

Empty list sample

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

Right click sample

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:

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.

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