本文作者:yonken
这篇文章同时也被发表在 CodeProject 上:http://www.codeproject.com/KB/MFC/CVComboBox.aspx
在上次写的那个非递归遍历指定文件夹下的所有文件及其子文件夹的文章的演示程序中我使用了虚拟列表控件来展示动态内容,然后我就开始想把这种虚拟列表的功能应用到ComboBox上。本来这是个很常用的功能,就是类似浏览器的地址栏(以及开始-运行 对话框里面出现的ComboBox),在edit control上输入字符的过程中,下拉列表同时根据所输入的关键字更新内容,这种应用往往会同时配合Auto Complete来实现更好的用户体验。
刚开始我在网上找了挺久,现成的可用代码就是通过实现COM的IAutoComplete来做,网上貌似也一大堆代码,不过我不熟悉COM。CodeProject上倒是有一篇很接近这个效果的例子,AdvComboBox,我看了一下代码发现是通过重新构造列表来实现的,严重的不geilivable啊,我也没去测试它在大量数据的情况下的效率如何,就先放弃使用了。
在找别人的实现的过程中,我自己也一直在思考实现的方法。由于ComboBox里面的下拉列表跟列表控件类似,对应于MFC,一个是CListBox,一个是CListCtrl。既然CListCtrl可以变成虚拟的,那么作为它“兄弟”的CListBox自然也不会少了这项技能。如果有办法可以让CListBox虚拟,并且又有办法可以subclass ComboBox里的CListBox,那么这个问题就等于解决了,可是没想到就这样一个看似简单的问题倒是让我花了不少时间。在实现了虚拟combobox之后,为了记录一下,我在CodeProject上post了下面这篇文章的英文版:
注意:下面的下载链接都是在 CodeProject.com 上的,我顺便打包了一份存在cnblogs(注意,此备份未必是最新版本,请以CodeProject上的为准),有需要的话可以猛击此处下载。
- 下载 bin.zip - 99.99 KB
- 下载 code_base.zip - 100.14 KB
- 下载 CVComboDemo_src.zip - 18.65 KB
- 下载 CustomControlDemo_src.zip - 28.88 KB
Source layout
+---code_base
| ControlAnchor.cpp
| ControlAnchor.h
| CustomDrawCommon.cpp
| CustomDrawCommon.h
| CustomDrawControl.cpp
| CustomDrawControl.h
| CustomDrawUtils.cpp
| CustomDrawUtils.h
| FileEnumerator.cpp
| FileEnumerator.h
| ObjInfoControl.cpp
| ObjInfoControl.h
| ObjInfoHolder.cpp
| ObjInfoHolder.h
| VComboBox.cpp
| VComboBox.h
|
+---CustomControlDemo
| | Source files of CustomControlDemo
|
+---CVComboDemo
| Source files of CVComboDemo
关于虚拟列表控件(ListCtrl/ListView) - Introduction to virtual list control
在 Windows 下,我们可以创建一个带有 LVS_OWNERDATA 风格的列表控件并相应 LVN_GETDISPINFO 消息来实现所谓的虚拟列表控件。LVS_OWNERDATA 这个风格暗示了列表控件本身不存储显示项目的数据,而是它的owner-通常是它的父窗口-来处理数据相关的事务。
虚拟列表控件在我们需要显示大量数据的时候会很有用,而且“增加”和“删除”列表的项目是非常快的(因为它根本就没有存储对应的数据,所谓的增删操作实际上只是通过外部调用 SetItemCount() 来告诉它当前要显示项目的数量,然后它转而向我们-实际是它的父窗口-发现一个特殊的消息 LVN_GETDISPINFO 来“询问”每一项对应要显示的内容),这很像是个回调的过程。
使用虚拟列表控件(ListCtrl/ListView)的原因 - The Reason of Using Virtual List Control
- 根据 MSDN 的说法,虚拟列表控件默认只能支持最大到一个整型数的极值,而在有的时候我们可能需要显示超过这个限制的内容。
- 要在列表控件上显示任何内容,就必须先把所有内容通过 InsertItem() 加上去,如果数据很多,这个过程必然很慢。
- 如果想要实现动态更新列表内容,比如说在一个edit control上输入一些关键字,然后对应的列表框上即时根据关键字显示过滤后的内容,普通的做法就是不断增加和删除项目,这也是个慢的过程。
在实现了虚拟列表控件之后,以上这些问题就可以全部得到解决:
- 虚拟列表控件可以支持的最大项目数量是
DWORD 的极值。
- 虚拟列表控件并不实际存有数据,在显示列表之前无需逐一添加项目内容。
- 正如前面提到的那样,要更新显示项目的数量,只需调用
SetItemCount() 即可,根本不需要手动添加或删除项目。
如果你还想了解更多关于虚拟列表控件的内容,可以参考:
Virtual List Controls (MSDN)
Using virtual lists (CP)
下面顺便贴一张在上一篇文章中用到的图片来演示一下一个虚拟列表控件的简单应用。
正如你所见,虚拟列表控件很酷,但是如果我们想要让ComboBox也有这样的功能可以吗?我们可以让 ComboBox 里面的那个列表控件也变成一个虚拟列表控件吗?当然这是可以的,看看你的浏览器的地址栏,在上面随便敲些东西你就知道已经被人家实现了。
预备知识 - Background
我们知道,ComboBox实际上是由两部分组成的:一个列表控件 (CListBox)和一个edit 控件。正如 CListCtrl 所有的 LVS_OWNERDATA 风格那样,CListBox 也有一个类似的风格,就是 LBS_NODATA。不妨先看看 MSDN 上的描述:
Specifies a no-data list box. Specify this style when the count of items in the list box will exceed one thousand. A no-data list box must also have the LBS_OWNERDRAWFIXED
style, but must not have the LBS_SORT
or LBS_HASSTRINGS
style.
简单而言就是说这个所谓的虚拟 CListBox 自己是不管数据的,带有这个 LBS_NODATA 风格的 CListBox 同时还必须要有 LBS_OWNERDRAWFIXED 风格,但又不能有 LBS_SORT 和 LBS_HASSTRINGS 这两个风格的任意一个。
虚拟列表控件(ListBox)的实现 - How a Virtual List Box Works
首先得要有一个继承与CListBox的类,比如CVListBox,然后通过调用 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;
}
剩下的事情就跟处理虚拟ListCtrl一样了,当要更新列表内容的时候,只需执行:
SendMessage(listbox.m_hWnd, LB_SETCOUNT, nCount, 0)
看起来似乎很简单,对吧?你可能想通过 ModifyStyle() 来给 ListBox 调整所需的风格,然而那是行不通的。并不是所有的风格都可以动态增加和去除,有些风格比如 LVS_OWNERDRAWFIXED/LBS_OWNERDRAWFIXED 就只能在创建窗口的时候指定(其后再增加是无效的),而
LBS_NODATA 也在此列,这样看来我们是没办法简单地使得ComboBox的ListBox成为虚拟了(很明显,在创建ComboBox的时候我们并没有机会去指定创建ListBox的风格)。
问题的解决 - Solution
这个问题的解决方案初看起来很简单:那就是我们自己来完完整整地实现一个山寨的ComboBox控件!这意味着我们要创建ListBox 和 edit 控件,然后处理各种琐碎的消息等等,这个工作量是很大的。
在 CodeProject 上,有个叫 Mathias Tunared 的人发了一篇 AdvComboBox 能一定程度实现“虚拟”的功能,可惜它并不是真的虚拟列表。
好消息是原来微软早在2007年就已经发了一个示例程序,叫 VCOMBOBX !可惜暂时找不到MFC版本的实现,看来我又要做这种工作了。
如何使用 - Using the code
虚拟 ComboBox 的用法就跟使用虚拟列表差不多,如果你已经很熟悉虚拟列表,那么就很容易上手。
关于 CVComboBox - Understanding the class CVComboBox
基本而言 CVComboBox
是代码的核心,这个类的实现在 VComboBox.h/cpp。
CVComboBox 是一个虚拟ComboBox的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();
我想这些函数是非常直观明了的,在此就不一一说明了,不过倒是可以简单提一下 CVComboListBox 和 CVComboEdit 这两个类:
CVComboListBox
封装了虚拟ListBox控件,它继承于CComboListBox,而CComboListBox最终继承于CListBox。
CVComboEdit
直接继承于 CEdit。
在对话框程序中使用 CVComboBox 的简单步骤 - The Basic Steps to use CVComboBox in a Dialog Based Application
1. 创建一个基于对话框的MFC项目,取名为 CVComboDemo。
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. 如果你用的是VC6,你还要在 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 的类,例如:
class CMyVComboBox : public CVComboBox { public: virtual CString GetItemText(UINT nItem) { CString strText; strText.Format(_T("Item %d"), nItem); return strText; } };
6. 打开对话框编辑器,增加一个自定义控件,如下图所示设置:
7. 增加一个 ComboBox 到对话框上,设置其ID 为 IDC_VCOMBO2。
8. 增加如下两个成员变量(当然你还要包含 VComboBox.h):
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. 编译并运行!
题外话 - Points of Interest
一开始我想到过用一个 WH_CBT 钩子来动态修改 ListBox 的风格来实现,可惜貌似在使用了 Common Control version 6 的情况下会失败。
我在实现的过程中发现custom draw 和 owner draw 还挺好玩的,后来还自己写了几个类来模拟 Win7 的样子,下面就是我写的这些类:
- CCustomDrawHeaderCtrl
- CSortHeaderCtrl
- CCustomDrawToolTipCtrl
- CCustomDrawListCtrl
- CCustomDrawListBox
- CCustomDrawTreeCtrl
- CTriCheckStateTreeCtrl
- CCustomDrawComboBox
这些类在XP 下可以一定程度模拟 Win7 的外观,而且这些代码兼容 VC6和VS2010。
时间关系,就不详细介绍这些类了,感兴趣的话你可以自己运行代码玩一下。
History
11/25/2010: Initial released.
P.S. 话说 Custom Draw 和 Owner Draw 都是些很古老的技术了,现在流行的都是啥 WPF 和 Windowless GUI(咳咳,就是 DirectUIHWND),俺落后了啊~
P.S.S. 很晚了,我是先把文章发的 CodeProject 上的,刚刚才翻译回中文,累si。
P.S.S.S 各路高人如果不小心看到此文,必是一笑了之,但如若能对小弟的coding(对,coding,俺只是个coder不是programmer)提出改进之处(必然是很多的),那真是要先在此拜谢啊!话说我开始觉得自己不太适合做coding啊,脑子不够使,注定是个垒代码的IT农民工啊,coding要只是个兴趣该多好呢。