65.9K
CodeProject 正在变化。 阅读更多。
Home

CListCtrlExt 和 CListViewExt 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (22投票s)

2011年6月20日

CPOL

5分钟阅读

viewsIcon

87497

downloadIcon

8735

一个基于标准 CListCtrl MFC 类的增强型列表控件。

Sample Image

引言

MFC 中最重要和最有用的控件之一(而且不是唯一的一个)是列表控件。很长一段时间以来,我一直在寻找一个好的、完全兼容 MFC 的控件,但没有找到,我从几篇文章(和网站)收集了信息,并自己起草了一个,它

  • 不具备所有标准功能(GetItemDataSetItemData 等)
  • 完全兼容 MFC(继承自 CListCtrl
  • 可用于任何样式(LVS_ICONLVS_SMALLICONLVS_LISTLVS_REPORT
  • 可以对数据进行排序(并在不使用任何外部资源的情况下显示排序方向)
  • 可以为单元格、行、列着色文本和/或背景
  • 拥有完整的标题控件
  • 持久化列宽度、顺序、外观和排序
  • 可以具有网格行为
  • 可以插入到各种静态控件的单元格中(例如,CEditCComboBoxCOleDateTime 等)
  • 可以像 MFC 标准控件 CListView 一样使用(而不是 GetListCtrl()

所有这些只需要三个类(在 CListView 的情况下是四个)。

背景

如我上面所说,CListCtrlExt 继承自 CListCtrl MFC 类。这意味着你可以在使用标准 CListCtrl 的项目中无修改地使用它。对于新功能,你需要调用自定义方法(选择排序的列、网格行为等)。当然,上述许多功能仅在 LVS_REPORT 样式下可用。最重要的是,这个 CListCtrlExt 类可以像 CListView 控件一样使用。你需要在项目中添加一个额外的类:CChildCListCtrlExt 类。还有一点,作为一个标准控件,外观样式(控件的主题)可以像其他任何标准控件一样处理,没有任何复杂性(额外的样式类)。

使用代码

首先,你需要将六个文件(三个类)包含到你的项目中:ListCtrlExt.hListCtrlExt.cppHeaderCtrlExt.hHeaderCtrlExt.cppMsgHook.hMsgHook.cpp。假设你有一个基于 CView 的 SDI 应用程序,其视图类为 CMyView(在示例项目中为 CTestList6View)。我们可以在动态模式下创建我们的列表(在资源头文件 Resource.h 中,定义)

#define IDC_LIST 1001

然后声明一个 CListCtrlExt 变量

// 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);
}

在很多情况下,我们想对列表控件中的数据进行排序。没问题,在插入列之后,我们调用 SetColumnSorting(...) 如下:

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);
}

要为列表着色文本或背景,你可以使用以下方法来绘制单元格、行或列:SetCellColors(...)SetRowColors(...)SetColumnColors(...)

如果你在 LVS_REPORT 样式中使用此列表控件,你将拥有一个完整的标题控件:右键单击标题,可以选择哪些列可见,哪些不可见,但这仅在你将至少一列设置为不可移除时可用

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

要记住列宽度、列顺序、哪些列可见,甚至是最后一个排序的列,你需要调用两个方法:在第一次加载列表时调用 RestoreState(...),并在列表销毁处理程序中调用 SaveState(...)

如果你想要网格行为(在单个单元格中导航,在任何列中搜索),你必须做两件事:设置 LVS_EX_FULLROWSELECT 样式并调用 SetGridBehaviour()

此外,在动态模式下创建后,你还可以插入控件(CEditCComboBoxCDateTimeCtrl 等);在这种情况下,你需要实现两个静态方法:InitEditor(...)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;

这是实现代码

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

在这里,你可以在编辑控件的开头(InitEditor(...))或结尾(EndEditor(...))处理你的自定义操作。

使用 CListCtrlExt 的另一种可能性是将其用作 CListView 控件(你可以在第二个示例项目中看到这一点);在这种情况下,你需要将一个额外的类包含到你的项目中:CChildListCtrlExt

在这里,m_List 成为 CChildListCtrlExt 类成员(而不是 CListCtrlExt);在 CChildListCtrlExt 中将你的视图类声明为 friend,并在视图类中处理几个事件

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;
}

在需要调用 GetListCtrl() 的地方,改为键入 m_List。这里有一个小观察:要将父消息传递给子控件,你需要反射它们,如下面的代码所示

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

我从 这里 获取了 listview 实现的模型。

挑战

我随着时间的推移打磨了这个类,从中选择了几个文章的特性。我在这里只列出了最后一篇启发我显示如何在一个 CListView 类中控制一个派生 CListCtrl 的文章。几年后,Zafir Anjum 也说过我们不能 在一个 CListView 中使用派生的 CListCtrl。我不会反驳他,他在文章中的说法非常合乎逻辑。不过,第二个示例项目似乎运行良好,而且我已经在几个项目中将 CListCtrlExt 类用作 CListView 控件,并且到目前为止没有遇到任何问题……我将留给你自己去发现这个实现的任何问题。最后,但同样重要的是,我要感谢 codexpert 团队。

CListViewExt 类

CListViewExt 类继承自 CListView,并具有与 CListCtrlExt 相同的功能。它是如何工作的?视图类,在本例中为 CTestList6View,我们继承自 CListViewExt 类。如果我们只需要标准的 CListCtrl 方法,我们以正常方式获取 CListCtrl 指针:GetListCtrl()。如果我们只需要自定义 CListViewExt 方法(类似于 CListCtrlExt 方法),我们获取 CListViewExt 指针:GetListViewExt()。例如

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);

但是由于 CTestList6View 类继承自 CListViewExt 类,我们根本不需要获取 GetListCtrlExt() 指针

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);

我们所要做的就是在 CTestList6View::OnInitialUpdate() 的基类中调用 CListViewExt::OnInitialUpdate(); 方法。

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

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

有一个非常重要的注意事项:在 CListViewExt 中,你不能使用标准的 GetItemData()/SetItemData CListCtrl 方法。请改用自定义的 GetItemUserData()/SetItemUserData() CListViewExt 方法!!!请注意这个细节!无论如何,你还可以下载一个演示项目。

历史

  • 2011年10月11日:我修改了 PreSubclassWindow 方法,现在列表控件可以在任何样式下启动,并且仍然具有报表设置。
  • 2011年10月24日:我上传了 CListViewExt 类,它继承自 CListView 类,并具有与 CListCtrlExt 类相同的功能。
  • 2012年2月20日:添加了 CListCtrlExt::GetFocusCell()CListViewExt::GetFocusCell() 来获取焦点单元格的索引。修改了 BOOL CListCtrlExt::SaveState(LPCTSTR lpszListName); BOOL CListCtrlExt::RestoreState(LPCTSTR lpszListName); 以在注册表中设置列表视图名称。
© . All rights reserved.