分隔符组合框






4.51/5 (14投票s)
一个派生自CCboBox的组合框类
引言
最近,我的一个项目要求在现有代码的基础上增强一个组合框,使其包含一些分隔符,从而区分具有不同含义的项,效果如图所示。
通过搜索网站,我看到许多人也在寻找这样一种定制的组合框控件,但我找不到完全符合我需求的。在 Code Project 上,由 Brett R. Mitchell 创建的文章 A Custom Group Combo Box 看起来不错,因为它用分隔线和标题来区分其项。为了适应我的需求,我可以将标题设置为空,只留下完整的横线作为分隔符。
然而,这个名为 CDropButton
的自定义组合框,以 CButton
类作为主类,并包含一个 CWnd
和一个 CListbox
用于显示数据,它不是从标准的 CCombobox
派生的。因此,无法将其作为真正的组合框集成到我们现有的代码库中,因为我们频繁使用 CCombobox
的方法和事件。此外,那个自定义控件中没有编辑框,所以它失去了可编辑的功能。
因此,我决定编写一个派生自 CCombobox
的类,名为 CSeparatorComboBox
,以满足我的需求。通过使用它,我只需要更改现有代码库中的类类型,就能在不触及其他代码的情况下维护其功能,然后通过一个新方法添加两个分隔符,如下所示:
m_ctrlCombo.SetSeparator(0);
m_ctrlCombo.SetSeparator(-1);
其中 m_ctrlCombo
是 CSeparatorComboBox
的一个对象,SetSeparator(0)
在第一个项(索引为零)之后添加一个分隔符,就像图中“All Fruits”后面的那个。SetSeparator(-1)
用于将第二个分隔符放置在倒数第二项之前。它的替代方法可以是 SetSeparator(5)
。
现在,任何 CCombobox
事件都可用。当选择“Apple
”项时,我可以接收 ON_CBN_SELCHANGE
并将结果显示在我的测试程序的此处。
将其编辑为“Apple-2
”会通过 ON_CBN_EDITCHANGE
响应,结果如下:
主要实现
分隔符组合框设计的要点是,分隔符不应该从 UI 或程序逻辑中被选中。通常,我们有两种选择:第一,让分隔符占用一些空间,类似于一个项,但实际上不被选中。这通常在所有者绘制样式中实现,手动绘制文本和分隔符,就像 CDropButton
所做的那样。
第二种方法是简单地在下拉列表中的项之间绘制一条线,每个项的高度相同,分隔符没有额外的空间。在这种情况下,所有者绘制是不必要的。对于我的问题,这似乎更容易,也足够了。
要在没有所有者绘制样式的情况下绘制一条线,我必须确定哪个消息处理程序是合适的,以及如何获取下拉列表及其设备上下文的句柄。根据 Microsoft KB 文章 Q174667 的建议,我决定在我的 CSeparatorComboBox
中拦截 WM_CTLCOLOR
消息来对 CComboBox
内部的 CListBox
进行子类化,并按如下方式进行绘制:
HBRUSH CSeparatorComboBox::OnCtlColor(CDC* pDC,
CWnd* pWnd, UINT nCtlColor)
{
if (nCtlColor == CTLCOLOR_LISTBOX)
{
if (m_listbox.GetSafeHwnd() ==NULL)
{
m_listbox.SubclassWindow(pWnd->GetSafeHwnd());
}
CRect r;
int nIndex, n = m_listbox.GetCount();
CPen pen(m_nPenStyle, m_nSepWidth, m_crColor), *pOldPen;
pOldPen = pDC->SelectObject(&pen);
for (int i=0; i< m_arySeparators.GetSize(); i++)
{
nIndex = m_arySeparators[i];
if (nIndex<0) nIndex += n-1;
if (nIndex < n-1)
{
m_listbox.GetItemRect(nIndex, &r);
pDC->MoveTo(r.left+m_nHorizontalMargin,
r.bottom-m_nBottomMargin);
pDC->LineTo(r.right-m_nHorizontalMargin,
r.bottom-m_nBottomMargin);
}
}
pDC->SelectObject(pOldPen);
}
return CComboBox::OnCtlColor(pDC, pWnd, nCtlColor);
}
请注意,要发生子类化,对话框必须至少绘制一次。第一次,通过调用 SubclassWindow()
,我使 m_listbox
对象保持活动状态,然后调用其 GetItemRect()
来检索由 m_arySeparators
指定的项坐标。m_arySeparators
是一个整数数组,包含所有分隔符位置,这些位置由前面提到的 SetSeparator()
函数填充。使用 pDC
绘制一条线非常简单。在清理时,不要忘记调用 UnsubclassWindow()
,如下所示:
void CSeparatorComboBox::OnDestroy()
{
if (m_listbox.GetSafeHwnd() !=NULL)
m_listbox.UnsubclassWindow();
CComboBox::OnDestroy();
}
类接口
以下是 CSeparatorComboBox
类。
class CSeparatorComboBox : public CComboBox
{
DECLARE_DYNAMIC(CSeparatorComboBox)
CListBox m_listbox;
CArray<int> m_arySeparators;
int m_nHorizontalMargin;
int m_nBottomMargin;
int m_nSepWidth;
int m_nPenStyle;
COLORREF m_crColor;
public:
CSeparatorComboBox();
virtual ~CSeparatorComboBox();
void SetSeparator(int iSep);
void AdjustItemHeight(int nInc=3);
void SetSepLineStyle(int iSep) { m_nPenStyle = iSep; }
void SetSepLineColor(COLORREF crColor) { m_crColor = crColor; }
void SetSepLineWidth(int iWidth) { m_nSepWidth = iWidth; }
void SetBottomMargin(int iMargin) { m_nBottomMargin = iMargin; }
void SetHorizontalMargin(int iMargin) { m_nHorizontalMargin = iMargin; }
protected:
DECLARE_MESSAGE_MAP()
public:
afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
afx_msg void OnDestroy();
};
为了考虑分隔符项之间的视觉效果,我提供了一个函数来调整项的高度,如下所示。它会根据当前项的相对高度增加项的高度,默认参数 nInc
是三个像素。
void CSeparatorComboBox::AdjustItemHeight(int nInc/*=3*/)
{
SetItemHeight(0, nInc+ GetItemHeight(0));
}
当设置第一个分隔符时,SetSeparator()
会自动为您调整默认的项高度。但您可以调用 AdjustItemHeight()
来覆盖默认值。如果没有设置分隔符,项的高度将不会改变。
void CSeparatorComboBox::SetSeparator(int iSep)
{
if (!m_arySeparators.GetSize())
AdjustItemHeight();
m_arySeparators.Add(iSep);
}
其余五个函数用于设置分隔符属性。
SetSepLineStyle()
设置分隔符的线型(PS_SOLID=0, PS_DASH=1, PS_DOT=2
等),默认为点线。SetSepLineColor()
设置分隔符的颜色,默认为深灰色。SetSepLineWidth()
设置分隔符的线宽,默认为 1 像素。SetBottomMargin()
设置分隔符的下边距,默认为 2 像素。SetHorizontalMargin()
设置分隔符的水平边距,默认为 2 像素。
使用 CSeparatorComboBox
要使用 CSeparatorComboBox
,只需将两个源文件SepComboBox.h 和 SepComboBox.cpp 复制到您的项目中,并在您需要的地方包含 SepComboBox.h。定义一个变量,例如:
CSeparatorComboBox m_ctrlCombo;
在初始化过程中,使用 CComboBox
方法添加文本字符串:
m_ctrlCombo.AddString("All Fruits");
m_ctrlCombo.AddString("Banana");
m_ctrlCombo.AddString("Orange");
m_ctrlCombo.AddString("Apple");
m_ctrlCombo.AddString("Pear");
m_ctrlCombo.AddString("Watermelon");
m_ctrlCombo.AddString("*Add/Edit Fruit");
接下来,设置分隔符位置,如下所示:
m_ctrlCombo.SetSeparator(0);
m_ctrlCombo.SetSeparator(-1);
可选地,设置任何您想要的属性,例如:
m_ctrlCombo.SetSepLineStyle(PS_SOLID);
m_ctrlCombo.SetSepLineColor(0);
m_ctrlCombo.SetHorizontalMargin(1);
并使用 SetCurSel()
设置当前选择,并使用任何 CComboBox
方法和事件处理程序。就是这样 - 就是您在本文开头看到的效果。有关更多详细信息,请参阅我演示包中的 CSepComboTestDlg
。虽然演示是在 VC7 中编程的,但只要 MFC 可用,它在以前的 Visual C++ 中应该工作相同。
关注点
回想一下我提到的实现分隔符组合框的另一种方法,即使用所有者绘制样式,对吗?如果您想要更好的视觉效果,可以尝试这个想法 - 不改变当前文本项的高度,而是为类似项的分隔符腾出空间。覆盖 virtual
函数 MeasureItem()
以手动设置位置,绘制文本和分隔符。这可能需要更多的工作,但一个更好的分隔符组合框绝对是值得的。
历史
- 2004 年 6 月 14 日:初始版本