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

分隔符组合框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.51/5 (14投票s)

2004年6月9日

CPOL

5分钟阅读

viewsIcon

144989

downloadIcon

1867

一个派生自CCboBox的组合框类

引言

最近,我的一个项目要求在现有代码的基础上增强一个组合框,使其包含一些分隔符,从而区分具有不同含义的项,效果如图所示。

zsepcmb/sepcmb1.jpg

通过搜索网站,我看到许多人也在寻找这样一种定制的组合框控件,但我找不到完全符合我需求的。在 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_ctrlComboCSeparatorComboBox 的一个对象,SetSeparator(0) 在第一个项(索引为零)之后添加一个分隔符,就像图中“All Fruits”后面的那个。SetSeparator(-1) 用于将第二个分隔符放置在倒数第二项之前。它的替代方法可以是 SetSeparator(5)

现在,任何 CCombobox 事件都可用。当选择“Apple”项时,我可以接收 ON_CBN_SELCHANGE 并将结果显示在我的测试程序的此处。

zsepcmb/sepcmb2.jpg

将其编辑为“Apple-2”会通过 ON_CBN_EDITCHANGE 响应,结果如下:

zsepcmb/sepcmb3.jpg

主要实现

分隔符组合框设计的要点是,分隔符不应该从 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.hSepComboBox.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 日:初始版本
© . All rights reserved.