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

CListCtrlExt and CListViewExt controls

0.00/5 (No votes)
20 Feb 2012 1  
An enhanced list control based on the standard CListCtrl MFC class.

Sample Image

Introduction

One of the most important and useful controls in MFC (and not the only one) is the list control. For a long time, I searched for a good one that is fully MFC compatible, and not finding one, I gathered information from several articles (and sites) and I drafted myself one which:

  • Doesn't have all the standard capabilities (GetItemData, SetItemData, etc.)
  • Is fully MFC compatible (is derived from CListCtrl)
  • Can be used in any kind of style (LVS_ICON, LVS_SMALLICON, LVS_LIST, LVS_REPORT)
  • Can sort data (and show the sort direction without any external resources)
  • Can color text and/or background of cells, rows, columns
  • Has full header control
  • Persists column width, order, appearance, and sorting
  • Can have grid behaviour
  • Can be inserted in cells with various static controls (e.g., CEdit, CComboBox, COleDateTime, etc.)
  • Can be used like the MFC CListView standard control (instead of GetListCtrl())

and all that with only three classes (four in CListView's case).

Background

Like I said above, CListCtrlExt is derived from the CListCtrl MFC class. That means you can use it in projects which use the standard CListCtrl without any modifications. For the new functionality, you need to call custom methods (choose sorting columns, grid behaviour, etc.). Of course, many of the above features are available only in the LVS_REPORT style. And most importantly, this CListCtrlExt class can be used like the CListView control. You need one more class to include in your project: CChildCListCtrlExt class. And one more thing, being a standard control, appearance style (theme of control) can be handled like any other standard control without any complications (extra theme classes).

Using the code

First, you need to include in your project six files (three classes): ListCtrlExt.h, ListCtrlExt.cpp, HeaderCtrlExt.h, HeaderCtrlExt.cpp, MsgHook.h, and MsgHook.cpp. Let's say you have an SDI application with CMyView (CTestList6View in the sample project) based on CView. We could create our list in dynamic mode (in the resource header, Resource.h, define):

#define IDC_LIST 1001

and then declare a CListCtrlExt variable:

// TestList6View.h : interface of the CTestList6View class
//
class CTestList6View : public CView
{
...
...
protected:
    CListCtrlExt m_List;
};

// TestList6View.cpp : implementation of the CTestList6View class
//
int CTestList6View::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
    if(CView::OnCreate(lpCreateStruct) == -1)return -1;

    // TODO: Add your specialized creation code here

    DWORD dwStyle = WS_CHILD | WS_VISIBLE | WS_TABSTOP | CS_DBLCLKS | LVS_REPORT;

    BOOL bResult = m_List.Create(dwStyle,CRect(0,0,0,0),this,IDC_LIST);
    m_List.PreSubclassWindow();

    return bResult ? 0 : -1;

//    return 0;
}

void CTestList6View::OnSize(UINT nType, int cx, int cy) 
{
    CView::OnSize(nType, cx, cy);
    
    // TODO: Add your message handler code here

    if(::IsWindow(m_List.m_hWnd))m_List.MoveWindow(0,0,cx,cy,TRUE);
}

In a lot of situations, we'd want to sort data in a list control. No problem, after column inserts, we call SetColumnSorting(...) like:

void CTestList6View::OnInitialUpdate() 
{
    CView::OnInitialUpdate();

    // TODO: Add your specialized code here and/or call the base class

    if(m_List.GetHeaderCtrl()->GetItemCount() > 0)return;

    m_List.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | 
                            LVS_EX_HEADERDRAGDROP | LVS_EX_INFOTIP);

    m_List.InsertColumn(0, "Integer",        LVCFMT_LEFT, 100);
    m_List.InsertColumn(1, "String",        LVCFMT_LEFT, 100);
    m_List.InsertColumn(2, "List",            LVCFMT_LEFT, 100);
    m_List.InsertColumn(3, "DateTime",        LVCFMT_LEFT, 100);
    m_List.InsertColumn(4, "Amount",        LVCFMT_LEFT, 100);


    m_List.SetColumnSorting(0, CListCtrlExt::Auto, CListCtrlExt::Int);
    m_List.SetColumnSorting(1, CListCtrlExt::Auto, CListCtrlExt::String);
    m_List.SetColumnSorting(2, CListCtrlExt::Auto, CListCtrlExt::StringNoCase);
    m_List.SetColumnSorting(3, CListCtrlExt::Auto, CListCtrlExt::Date);
    m_List.SetColumnSorting(4, CListCtrlExt::Auto, CListCtrlExt::StringNoCase);
}

To color the text or background of a list, you have the following methods to paint a cell, row, or column: SetCellColors(...), SetRowColors(...), SetColumnColors(...).

If you use this list control in LVS_REPORT style, you have a full header control: on right click on the header, you can choose which column to be visible and which not, but this feature is available only if you set at least one column to be irremovable:

m_List.GetHeaderCtrl()->SetRemovable(0,FALSE);

To memorize the column width, column order, which column to be visible, even the last column which was sorted, you have call two methods: RestoreState(...) after you load the list the very first time, and SaveState(...) in the list destroy handler.

In case you want to have a grid behaviuor (navigate on individual cells, search on any column), you must do two things: set the LVS_EX_FULLROWSELECT style on and call SetGridBehaviour().

Also, you can insert controls (CEdit, CComboBox, CDateTimeCtrl, etc. ) after you create it in dynamic mode; in this case, you need to implement two static methods: InitEditor(...) and EndEditor(...).

// TestList6View.h : interface of the CTestList6View class
//
protected:
    CListCtrlExt m_List;
    CComboBox m_Combo;
    CDateTimeCtrl m_DT;
    static BOOL EndEditor(CWnd** pWnd, int nRow, int nColumn, CString &strSubItemText, 
                          DWORD_PTR dwItemData, void* pThis, BOOL bUpdate);
    static BOOL InitEditor(CWnd** pWnd, int nRow, int nColumn, CString &strSubItemText, 
                           DWORD_PTR dwItemData, void* pThis, BOOL bUpdate);

private:
    CFont* m_pFont;

and here is the implementation code:

// TestList6View.cpp : implementation of the CTestList6View class
//
void CTestList6View::OnInitialUpdate() 
{
    CView::OnInitialUpdate();

    // TODO: Add your specialized code here and/or call the base class

    m_pFont = m_List.GetFont();
    CRect Rect(CPoint(0,0),CSize(100,500));
    m_DT.Create(WS_CHILD | WS_TABSTOP, Rect, this, IDC_DATE);
    m_Combo.Create(WS_CHILD | WS_TABSTOP | CBS_DROPDOWNLIST | 
      CBS_HASSTRINGS | CBS_SORT | CBS_AUTOHSCROLL,Rect,this,IDC_COMBO);

    m_Combo.AddString("Test 1");
    m_Combo.AddString("Test 2");
    m_Combo.AddString("Test 3");
    m_Combo.AddString("Test 4");
    m_Combo.AddString("Test 5");
    m_Combo.AddString("Test 6");
    m_Combo.AddString("Test 7");
    m_Combo.AddString("Test 8");
    m_Combo.AddString("Test 9");

    m_Combo.SetFont(m_pFont);

    m_List.SetColumnEditor(2, &CTestList6View::InitEditor, 
                           &CTestList6View::EndEditor, &m_Combo);
    m_List.SetColumnEditor(3, &CTestList6View::InitEditor, 
                           &CTestList6View::EndEditor, &m_DT);
}

BOOL CTestList6View::InitEditor(CWnd** pWnd, int nRow, int nColumn, 
     CString &strSubItemText, DWORD_PTR dwItemData, void* pThis, BOOL bUpdate)
{
    ASSERT(*pWnd);
    switch(nColumn)
    {
    case 2:
        {
            CComboBox* pCmb = reinterpret_cast<CComboBox*>(*pWnd);
            pCmb->SelectString(0, strSubItemText);
        }
        break;
    case 3:
        {
            CDateTimeCtrl* pDTC = reinterpret_cast<CDateTimeCtrl*>(*pWnd);
            COleDateTime dt;
            if(dt.ParseDateTime(strSubItemText))pDTC->SetTime(dt);
        }
        break;
    }

    return TRUE;
}

BOOL CTestList6View::EndEditor(CWnd** pWnd, int nRow, int nColumn, 
     CString &strSubItemText, DWORD_PTR dwItemData, void* pThis, BOOL bUpdate)
{
    ASSERT(pWnd);
    switch(nColumn)
    {
    case 2:
        {
            CComboBox* pCmb = reinterpret_cast<CComboBox*>(*pWnd);
            int index = pCmb->GetCurSel();
            if(index >= 0) pCmb->GetLBText(index, strSubItemText);
        }
        break;
    case 3:
        {
            CDateTimeCtrl* pDTC = reinterpret_cast<CDateTimeCtrl*>(*pWnd);
            COleDateTime dt;
            pDTC->GetTime(dt);
            strSubItemText = dt.Format();
        }
        break;
    }

    return TRUE;
}

Here you can handle your custom action at the beginning of the edit control (InitEditor(...)) or at the ending of the edit control (EndEditor(...)).

Another possibility to use CListCtrlExt is like the CListView control (you can see this in the second sample project); in this case, you need to include in your project one more class: CChildListCtrlExt.

Here, m_List becomes the CChildListCtrlExt class member (not CListCtrlExt); declare as friend your view class in CChildListCtrlExt and handle a few events in the view class:

BOOL CTestList6View::PreTranslateMessage(MSG* pMsg) 
{
    // TODO: Add your specialized code here and/or call the base class

    if(! CListView::PreTranslateMessage(pMsg))
        return m_List.PreTranslateMessage(pMsg);

    return FALSE;
}

BOOL CTestList6View::OnChildNotify(UINT message, WPARAM wParam, 
                                   LPARAM lParam, LRESULT* pLResult) 
{
    // TODO: Add your specialized code here and/or call the base class

    if(! CListView::OnChildNotify(message, wParam, lParam, pLResult))
        return m_List.OnChildNotify(message, wParam, lParam, pLResult);

    return FALSE;
}

LRESULT CTestList6View::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) 
{
    // TODO: Add your specialized code here and/or call the base class

    LRESULT lResult = 0;

    if(! CListView::OnWndMsg(message, wParam, lParam, &lResult))
    {
        if(! m_List.OnWndMsg(message, wParam, lParam, &lResult))
        {
            lResult = DefWindowProc(message, wParam, lParam);
        }
    }

    return lResult;
}

and where you need to call GetListCtrl(), type m_List instead. A little observation here: to reach parent messages to child, you need to reflect them, like in the code below:

// TestList6View.h : interface of the CTestList6View class
//
// Generated message map functions
protected:
    //{{AFX_MSG(CTestList6View)
    afx_msg LRESULT OnColumnclick(NMHDR* pNMHDR, LRESULT* pResult);
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()

// TestList6View.cpp : implementation of the CTestList6View class
//

BEGIN_MESSAGE_MAP(CTestList6View, CListView)
    //{{AFX_MSG_MAP(CTestList6View)
    ON_NOTIFY_REFLECT_EX(LVN_COLUMNCLICK, OnColumnclick)
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

...
...

LRESULT CTestList6View::OnColumnclick(NMHDR* pNMHDR, LRESULT* pResult) 
{
    NM_LISTVIEW* phdr = reinterpret_cast<NM_LISTVIEW*>(pNMHDR);
    // TODO: Add your control notification handler code here

    m_nColumnSort = phdr->iSubItem;
    *pResult = 0;

    return *pResult;
}

I got the model for the listview implementation from here.

The challenge

I polished this class over time, picking features from several articles. I listed here only the last article from where I was inspired that show the way a derived CListCtrl can be controlled in a CListView class. The same Zafir Anjum said after few years that we can not use a derived CListCtrl with a CListView. I will not contradict him, what he says in his article is very logical. Still, the second sample project seems to work well and I have used the CListCtrlExt class like the CListView control in a few projects by now without problems... I will let you discover any problems of this implementation. Last, but not the least, I want to thank the codexpert team.

CListViewExt class

The CListViewExt class is derived from CListView and has the same functionality as CListCtrlExt. How does it function? The view class, CTestList6View in our case, we derive from the CListViewExt class. If we need standard CListCtrl methods, we get a CListCtrl pointer the normal way: GetListCtrl(). If we need custom CListViewExt methods (similar to CListCtrlExt methods), we get a CListViewExt pointer: GetListViewExt(). For example:

GetListCtrl().InsertColumn(0, "Integer",    LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(1, "String", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(2, "List", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(3, "DateTime", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(4, "Random", LVCFMT_LEFT, 100);

GetListCtrlExt().SetColumnSorting(0, CListViewExt::Auto, CListViewExt::Int);
GetListCtrlExt().SetColumnSorting(1, CListViewExt::Auto, CListViewExt::StringNoCase);
GetListCtrlExt().SetColumnSorting(2, CListViewExt::Auto, CListViewExt::StringNoCase);
GetListCtrlExt().SetColumnSorting(3, CListViewExt::Auto, CListViewExt::Date);
GetListCtrlExt().SetColumnSorting(4, CListViewExt::Auto, CListViewExt::StringNoCase);

but as the CTestList6View class is derived from the CListViewExt class, we don't need to get the GetListCtrlExt() pointer at all:

GetListCtrl().InsertColumn(0, "Integer",    LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(1, "String", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(2, "List", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(3, "DateTime", LVCFMT_LEFT, 100);
GetListCtrl().InsertColumn(4, "Random", LVCFMT_LEFT, 100);

SetColumnSorting(0, CListViewExt::Auto, CListViewExt::Int);
SetColumnSorting(1, CListViewExt::Auto, CListViewExt::StringNoCase);
SetColumnSorting(2, CListViewExt::Auto, CListViewExt::StringNoCase);
SetColumnSorting(3, CListViewExt::Auto, CListViewExt::Date);
SetColumnSorting(4, CListViewExt::Auto, CListViewExt::StringNoCase);

All we have to do is call the CListViewExt::OnInitialUpdate(); method in the CTestList6View::OnInitialUpdate() base.

void CTestList6View::OnInitialUpdate()
{
    CListViewExt::OnInitialUpdate();

    // TODO: You may populate your ListView with items by directly accessing
    //  its list control through a call to GetListCtrl().
    ....
    ....
}

There is one very important note: in CListViewExt, you can not use the standard GetItemData()/SetItemData CListCtrl methods. Use the custom GetItemUserData()/SetItemUserData() CListViewExt methods instead !!! Be aware of this detail! Anyway, you have a demo project attached.

History

  • 11 Oct. 2011: I changed the PreSubclassWindow method in such a way that now the list control can be started in any style and will still have report settings.
  • 24 Oct. 2011: I uploaded the CListViewExt class, derived from the CListView class and with same functionality like the CListCtrlExt class.
  • 20 Feb. 2012: Added CListCtrlExt::GetFocusCell(), CListViewExt::GetFocusCell() to get index of focused cell. Modified BOOL CListCtrlExt::SaveState(LPCTSTR lpszListName); BOOL CListCtrlExt::RestoreState(LPCTSTR lpszListName); to setup a listview name in Registry.

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