Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Separator Combo-box

0.00/5 (No votes)
13 Jun 2004 1  
A CComboBox derived combo-box class

Introduction

Recently, I had a project requirement based on the current code to enhance a combobox with some separators, setting its items apart from different meanings, such as an example shown as below:

zsepcmb/sepcmb1.jpg

By searching websites, I saw many guys also looking for such an adapted combo box control, while I was not able to find an ideal match to my use exactly. In Code Project, the article A Custom Group Combo Box created by Brett R. Mitchell looks good, as it groups its items with distinguishing lines and headers. To fit my use, I might make a header empty and leave a full width line as a separator.

However, this custom combo box called CDropButton uses a CButton class as the main class, and contains a CWnd and a CListbox for data display, which is not a class derived from the standard CCombobox. Thus there is no way to combine it into our current code base as a genuine combo box, since we use CCombobox methods and events frequently. Also as no edit box in that custom control, it loses the editable feature.

Therefore, I decided to write a CCombobox derived class called CSeparatorComboBox to fit my requirement. By using it, I only need to change the class type in our current code base to maintain its functionality without touch others, and then add two separators with a new method like this:

m_ctrlCombo.SetSeparator(0);
m_ctrlCombo.SetSeparator(-1);

Where m_ctrlCombo is an object of CSeparatorComboBox and SetSeparator(0) adds a separator after the first item indexed by zero as after “All Fruits” in the figure. SetSeparator(-1) is for the second separator positioned one item before last. Its alternative can be SetSeparator(5).

Now any CCombobox event is available. When selecting the item “Apple”, I can receive ON_CBN_SELCHANGE and show the result here in my test program:

zsepcmb/sepcmb2.jpg

Editing it to “Apple-2” is responded by ON_CBN_EDITCHANGE with the result below:

zsepcmb/sepcmb3.jpg

Main Implementations

The main point for the separator combo box design is that the separator should not be selected either from UI or in the program logic. Generally, we may have two choices: First, let a separator occupy some space similar to an item but no actual selection to it. This is usually implemented in the owner draw style to manually draw both text and separator, just as what CDropButton did.

The second way is simply drawing a line between items in the dropdown list, with the same height for each item and no extra space for the separator. The owner draw in this case is not necessary. For my problem, this seems easier and enough.

To draw a line between items without the owner draw style, I have to decide which message handler is appropriate, and how to get a handle to the dropdown list and its device-context as well. Recommended by Microsoft KB article Q174667, I decide to intercept the WM_CTLCOLOR message to subclass CListBox inside CComboBox in my CSeparatorComboBox and do the drawing as follows:

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

Note that for subclassing to occur, the dialog box must be painted at least once. At the first time, by calling SubclassWindow(), I construct the m_listbox object alive and then call its GetItemRect() to retrieve an item coordinate specified by m_arySeparators, which is an integer array containing all the separator positions filled by the SetSeparator() function as mentioned earlier. Using pDC to draw a line is so simple. And in cleanup, don’t forget to call UnsubclassWindow() like this:

void CSeparatorComboBox::OnDestroy()
{
   if (m_listbox.GetSafeHwnd() !=NULL)
      m_listbox.UnsubclassWindow();
    
   CComboBox::OnDestroy();
}

Class Interface

The following is the CSeparatorComboBox class.

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

To consider visual effect on a separator between items, I provide a function to adjust the item height as follows. It increases an item height based on the current item’s relative height, the default parameter nInc is three pixels.

void CSeparatorComboBox::AdjustItemHeight(int nInc/*=3*/)
{
  SetItemHeight(0, nInc+ GetItemHeight(0));
}

When the first separator is set, SetSeparator() automatically adjusts the default item height for you. But you can call AdjustItemHeight() to overwrite the default. If no separator set, there is no change to the item height.

void CSeparatorComboBox::SetSeparator(int iSep)
{
   if (!m_arySeparators.GetSize())
      AdjustItemHeight();

   m_arySeparators.Add(iSep);
}

The rest of the five functions are used to set the separator attributes:

  • SetSepLineStyle() sets a separator’s line style (PS_SOLID=0, PS_DASH=1, PS_DOT=2, etc.), default to dot line.
  • SetSepLineColor() sets a separator’s color, default to dark gray.
  • SetSepLineWidth() sets a separator’s line width, default to 1 pixel.
  • SetBottomMargin() sets a separator’s bottom margin, default to 2 pixels.
  • SetHorizontalMargin() sets a separator’s horizontal margin, default to 2 pixels.

Using CSeparatorComboBox

To use CSeparatorComboBox, just copy two source files, SepComboBox.h and SepComboBox.cpp to your project and include SepComboBox.h wherever you want. Define a variable like:

CSeparatorComboBox m_ctrlCombo;

In an initialization procedure, add text strings using CComboBox methods:

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

Next, set separator positions like this:

m_ctrlCombo.SetSeparator(0);
m_ctrlCombo.SetSeparator(-1);

Optionally, set any attributes you want, such as:

m_ctrlCombo.SetSepLineStyle(PS_SOLID);
m_ctrlCombo.SetSepLineColor(0);
m_ctrlCombo.SetHorizontalMargin(1);

And set a current selection with SetCurSel() and use any CComboBox methods and event handlers. That’s all - what you see at the beginning of this article. For more details, please see CSepComboTestDlg in my demo package. Although the demo is programmed in VC7, it should work the same in the previous Visual C++, as long as MFC is available.

Points of Interest

Recall another way to implement a separator combo box I mentioned with the owner draw style, right? If you want a better UI visually, you may try that idea – no change the current text item height but make room for a separator similar to an item. Override the virtual function or MeasureItem() to manually set positions, draw texts and separators. This might need a bit more work, while a better separator combo box is for sure worthwhile..

History

  • 14th June, 2004: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here