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:
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:
Editing it to “Apple-2
” is responded by ON_CBN_EDITCHANGE
with the result below:
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)
{
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