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

VCOMBOBX in MFC - A Virtual Combo Box Control

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (31投票s)

2010 年 11 月 26 日

CPOL

8分钟阅读

viewsIcon

75599

downloadIcon

11507

A MFC based virtual combo box

源代码布局

  • code_base
    • VComboBox.cpp
    • VComboBox.h
    • CustomDrawCommon.cpp
    • CustomDrawCommon.h
    • CustomDrawControl.cpp
    • CustomDrawControl.h
    • CustomDrawUtils.cpp
    • CustomDrawUtils.h
    • ControlAnchor.cpp
    • ControlAnchor.h
    • FileEnumerator.cpp
    • FileEnumerator.h
    • ObjInfoControl.cpp
    • ObjInfoControl.h
    • ObjInfoHolder.cpp
    • ObjInfoHolder.h
  • CustomControlDemo
    • CustomControlDemo 的源文件
  • CVComboDemo
    • CVComboDemo 的源文件

您应该将源文件提取到您的计算机上,并保持与上面列出的相同的结构!

VCOMBO.gif

前言

在我们开始之前,我们可能需要先理解“虚拟”的概念。一个好的起点是虚拟列表控件,如果您已经熟悉它,可以跳过本文的背景部分。那么,让我们看看什么是虚拟列表控件。

虚拟列表控件简介

在Windows中,我们可以创建一个带有LVS_OWNERDATA样式的列表控件(这意味着列表控件本身不保留数据,而是由所有者/父窗口保留数据),并处理LVN_GETDISPINFO通知,这就是所谓的虚拟列表控件

当我们要显示大量项目时,虚拟列表控件很有用,更重要的是,“添加/删除”虚拟列表控件的项目速度很快(因为虚拟列表控件实际上不保留任何数据,所以不需要添加或删除任何东西,我们只需要告诉它应该显示多少项目 - 通过调用SetItemCount(),然后它会反过来询问我们 - 向父控件发送LVN_GETDISPINFO通知 - 来获取要显示的信息),您可以将其视为回调。

使用虚拟列表控件的原因

  1. 根据MSDN,列表控件的默认项目计数仅限于int,有时我们可能希望超出此限制。
  2. 列表控件必须在开始显示任何内容之前进行填充,一旦我们需要在其上添加大量项目,这将非常缓慢且难以忍受。
  3. 我们可能希望动态地向用户输入做出一些视觉反馈,例如,当用户在搜索字段中键入时,筛选出列表控件中的一些项目,您可以将其想象成Google Instant。这需要频繁地添加和删除项目,正如我们所知,这也是不可接受的慢。

借助虚拟列表控件,以上所有问题都可以得到解决

  1. 虚拟列表控件支持高达DWORD的项目计数。
  2. 虚拟列表控件没有任何数据,我们甚至不需要填充项目。
  3. 正如我在上一部分提到的,更改项目计数可以通过调用SetItemCount()来完成,我们不需要添加或删除任何东西。

如果您想了解更多关于虚拟列表控件的信息,您可以参考

作为使用虚拟列表控件的演示,这是我其他文章的演示应用程序的快照

QOF_SnapShot.gif

正如您所看到的,虚拟列表控件很棒,但是,如果我们想对组合框执行相同的操作呢?我们能否使组合框中的列表框部分虚拟化?这可能吗?当然,这是可能的,看看您的网络浏览器的地址栏,然后输入一些内容。

背景

众所周知,组合框实际上由列表框控件和一个(可选的,当它不是CBS_SIMPLE时)编辑控件组成。就像列表控件的LVS_OWNERDATA样式一样,如果我们使用LBS_NODATA样式创建列表框控件,它也可以变成虚拟的,请参阅MSDN上的描述。

指定一个无数据列表框。当列表框中的项目数超过一千时,请指定此样式。无数据列表框还必须具有LBS_OWNERDRAWFIXED样式,但不得具有LBS_SORTLBS_HASSTRINGS样式。

虚拟列表框的工作原理

首先,我们需要创建一个类似于CVListBox 的类,该类派生自CListBox,要创建它的实例,我们需要调用CWnd::Create(LBS_NODATA|LBS_OWNERDRAWFIXED, ...)

像这样覆盖DrawItem()/MeasureItem()/CompareItem()方法

void CVListBox::DrawItem( LPDRAWITEMSTRUCT lpDIS )
{
 if ( (int)lpDIS->itemID < 0 )
  return;

 CDC* pDC = CDC::FromHandle(lpDIS->hDC);
 int nSavedDC = pDC->SaveDC();
 
 CString strText = GetItemText(lpDIS->itemID); // retrieve the text to show for this item
 CRect rectItem(lpDIS->rcItem);
 pDC->DrawText(strText, rectItem, DT_SINGLELINE);
 
 pDC->RestoreDC(nSavedDC);
}

void CVListBox::MeasureItem( LPMEASUREITEMSTRUCT  )
{
 // nothing to do
}

int CVListBox::CompareItem( LPCOMPAREITEMSTRUCT  )
{
 return 0;
}

CString CVListBox::GetItemText( UINT nItem )
{
 CString str;
 // Here you can send a custom message (like WM_USER+0x100) to the parent window 
 // to ask/fetch the content for the specified item, that's like simulating the 
 // LVN_GETDISPINFO notification.
 // Of course you can also implement this in your own way.
 return str;
}

另一件事就像虚拟列表控件一样,当您想更新列表框时,只需尝试这样做

SendMessage(myListbox.m_hWnd, LB_SETCOUNT, nCount, 0)

这看起来有点容易,是吗?您可能还认为我们可以创建组合框,然后尝试GetComboBoxInfo()ModifyStyle()等操作,但遗憾的是,这是一条错误的道路!正如我们中的一些人可能已经经历过的,并非所有样式都可以动态设置/移除,某些样式如LVS_OWNERDRAWFIXED/LBS_OWNERDRAWFIXED只能在控件创建期间指定,LBS_NODATA样式也在此类中,并且我们不能在创建期间更改组合框中列表框的样式,因此似乎无法更改列表框的样式以使其虚拟化。

解决方案

答案一开始很简单:我们必须自己从头开始创建组合框!这意味着我们需要创建编辑控件和listbox 并设法使它们协同工作,换句话说,我们需要处理各种消息/通知来响应用户的输入,这就像重新发明轮子一样!

Mathias Tunared在他的文章AdvComboBox中发布了他的文章,这在某种程度上符合我的要求,但是,我发现他的实现并未利用虚拟列表框。

好消息是,微软提供了一个名为VCOMBOBX的示例代码!这篇文章发布于2007年,然后我在网上搜索,但找不到它的MFC版本(也许我错过了),所以我决定自己来做。

Using the Code

使用虚拟组合框就像使用虚拟列表框一样,如果您熟悉虚拟列表控件,您会发现它很容易使用。

理解CVComboBox类

基本上,CVComboBox类是此示例的核心,您可以在源文件VComboBox.h/cpp中找到它。

CVComboBox是一个MFC包装类,它封装了一个自定义组合框控件,该控件由一个虚拟列表框控件组成,其窗口类名为Virtual_ComboBox32

CVComboBox支持几乎所有普通CComboBox可以拥有的样式,但使用不同的宏(尽管它们的值相同),列出如下

#define VCBS_SIMPLE    0x0001L
#define VCBS_DROPDOWN   0x0002L
#define VCBS_DROPDOWNLIST  0x0003L
#define VCBS_AUTOHSCROLL  0x0010L
#define VCBS_OEMCONVERT   0x0020L
#define VCBS_DISABLENOSCROLL 0x0040L
#define VCBS_UPPERCASE   0x0100L
#define VCBS_LOWERCASE   0x0200L

#define VCBS_DEFAULT    (WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_CLIPSIBLINGS | \
          WS_CLIPCHILDREN | VCBS_AUTOHSCROLL)

#define VCBS_DEFAULT_SIMPLE   (VCBS_DEFAULT | VCBS_SIMPLE)
#define VCBS_DEFAULT_DROPDOWN  (VCBS_DEFAULT | VCBS_DROPDOWN)
#define VCBS_DEFAULT_DROPDOWNLIST (VCBS_DEFAULT | VCBS_DROPDOWNLIST)

CVComboBox提供了几乎所有CComboBox的方法,只不过它还额外提供了以下方法

virtual BOOL Create(DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID);

// Create a virtual combo box to replaced the control with the specified ID, 
// the newly created combo box will be placed at the same position as that control,
// after that, the control with the specified ID will be destroyed.
virtual BOOL CreateFromCtrl
	(CWnd* pParent, int nID, DWORD dwStyle = VCBS_DEFAULT_DROPDOWN); 
 
int    GetItemCount();
void   SetItemCount(int nCount);
void   SetDroppedVisibleItemCount(int nCount, BOOL bRepaint = TRUE);

virtual CVComboListBox&  GetVComboListBox();
virtual CComboListBox&  GetComboListBox();
virtual CVComboEdit&  GetComboEdit();

这些方法相当直接,我想您已经能够理解它们的用途了,这里我将仅对CVComboListBoxCVComboEdit类进行简单描述

CVComboListBox是一个封装虚拟列表框控件的类,它派生自CComboListBox,而CComboListBox又派生自CListBox

CVComboEdit只是一个派生自CEdit的类。

由于上述类被封装在CVComboBox类中,并且在大多数情况下,我们不需要关心它们,所以现在让我们看看如何使用CVComboBox类。

在基于对话框的应用程序中使用CVComboBox的基本步骤

以下是演示如何使用CVComboBox类的基本步骤

  1. 创建一个名为CVComboDemo的基于对话框的MFC项目。
  2. 将以下源文件(在code_base文件夹下)添加到您的工作区
    • CustomDrawCommon.cpp
    • CustomDrawCommon.h
    • CustomDrawControl.cpp
    • CustomDrawControl.h
    • CustomDrawUtils.cpp
    • CustomDrawUtils.h
    • VComboBox.cpp
    • VComboBox.h
  3. 打开预编译头文件,通常是StdAfx.h,将此行添加到其中
    #include "..\code_base\CustomDrawCommon.h"
  4. 如果您使用的是Visual Studio 6.0,您还需要将以下内容放在StdAfx.h的开头
    #pragma warning(disable: 4786)  // try to disable the annoying warning in VC6
    
    #ifndef WINVER
     #define WINVER 0x0501
    #endif // WINVER
    
    #ifndef _WIN32_WINNT
     #define _WIN32_WINNT 0x0501
    #endif // _WIN32_WINNT
  5. 声明一个派生自CVComboBox的自定义类,并覆盖必要的方法,通常是GetItemText(),例如像这样
    #include "..\code_base\VComboBox.h"
    
    class CMyVComboBox : public CVComboBox
    {
    public:
     virtual CString GetItemText(UINT nItem)
     {
      CString strText;
      strText.Format(_T("Item %d"), nItem);
      return strText;
     }
    };
  6. 使用资源编辑器打开对话框,从控件工具栏拖放一个自定义控件,并按如下所示填写属性
    CVComboDemo.PNG
  7. 将组合框控件拖放到您的对话框上,将其ID设置为IDC_VCOMBO2
  8. 向您的对话框类添加两个成员变量,如下所示,当然您需要添加#include指令以包含声明CMyVComboBox类的必要头文件
    CMyVComboBox  m_vComboBox1;
    CMyVComboBox  m_vComboBox2;
  9. 在您的对话框类的DoDataExchange()方法中添加下面粗体显示的行
    void CCVComboDemoDlg::DoDataExchange(CDataExchange* pDX)
    {
     CDialog::DoDataExchange(pDX);
     DDX_Control(pDX, IDC_VCOMBO1, m_vComboBox1);
    }
  10. 将此代码放在您的OnInitDialog()
    m_vComboBox1.SetItemCount(50);
    m_vComboBox1.SetDroppedVisibleItemCount(10);
    
    m_vComboBox2.CreateFromCtrl(this, IDC_VCOMBO2);
    
    m_vComboBox2.SetItemCount(10);
    m_vComboBox2.SetDroppedVisibleItemCount(5);
  11. 编译并运行应用程序!

关注点

起初,我试图使用WH_CBT钩子来拦截列表框的创建,然后修改其样式,但是,如果启用了Common Control版本6(当使用清单文件时),此解决方案似乎会失败,这就是我寻求虚拟列表框实现的原因。

当我编写组合框代码时,我发现自定义绘制控件很有趣,我想知道如何实现Vista/Win7中那种漂亮的控件,并将其引入WinXP等早期操作系统,所以我还编写了一些实现了自定义绘制的其他控件类。

您可以在CustomControlDemo演示项目中找到一些有趣/有用的控件类,如下所示

  • CCustomDrawHeaderCtrl
  • CSortHeaderCtrl
  • CCustomDrawToolTipCtrl
  • CCustomDrawListCtrl
  • CCustomDrawListBox
  • CCustomDrawTreeCtrl
  • CTriCheckStateTreeCtrl
  • CCustomDrawComboBox

这些类能够一些能力来表示/模拟Win7在WinXP下的漂亮外观,它们兼容VC6/VS2010。

出于某些原因,我无意详细介绍它们或发布另一篇文章来介绍它们,但是,您仍然可以自己探索源代码,并可以随心所欲地使用/修改它们。

这是演示应用程序的快照,希望您玩得开心

CustomControlDemo.png

历史

  • 2010/11/25: 初始发布
  • 2010/12/29: 改进绘制代码,修复了一些小错误
© . All rights reserved.