组组合框
介绍 我需要一个类似于Microsoft Word产品中的字体组合框的控件:以显示最近使用的项目列表和以标题为界的单独组中的所有可用项目列表。最近使用的项必须按时间顺序排序,最近的项放在前面。另一方面,所有可用项目的列表必须按字母顺序排序。我还没有找到满足这些要求的任何实现,所以我决定创建一个。 绘制标题的先决条件是使用所有者绘制的组合框,以便在视觉上区别于其他项。但是,还有一些额外的问题需要解决(比如标题项必须是不可选择的)。本文将对这些问题进行探讨并提出解决方案。 内容 所有者绘制的组合框组合框项目层次化项目分类组组合框实现其他问题 项目选择问题跳过标题自动选择列表上匹配的项目下拉菜单自动完成输入文本取消选择鼠标点击事件下拉列表调整列表宽度 可变高度业主绘制的组合框 适应下拉列表高度 关于使用代码的演示应用程序 的机制组合框 所有者绘制的组合框允许不同的绘图每个单独的项目。从概念上讲,一个新类必须从CComboBox类派生,并在派生类中重写几个方法。如果使用CBS_OWNERDRAWFIXED样式创建组合框,则所有项的高度相等,只需要覆盖DrawItem()方法。对于下拉列表中显示的每个项,依次调用DrawItem()方法。如果使用CBS_SORTED标志集创建控件,那么也需要覆盖CompareItem()方法,以便提供正确的条目排序。 在本文和附带的源代码示例中,CGroupComboBox是从CComboBox派生出来的类,下面讨论的问题都是在CComboBox中实现的。 值得注意的是,机制组合框控件将显示比它在对话框中设置资源高:在资源编辑器中默认的组合框的高度是12像素,但是在运行的应用程序,它将显示2像素高,可见从下面的截图。此外,下拉列表中各个项目的高度增加了2个像素。甚至一些标准的组合框控件也显示得更高,正如最右边的CMFCComboBox控件的截图所示。 控件和项高度不一致是不正确计算字体高度的结果。在第一次绘制控件时,将用于显示内容的实际字体是未知的,这将导致无效的指标。一个可能的解决办法是实现WM_MEASUREITEM消息处理程序在父对话框和计算控制高度基于字体使用的对话框: 隐藏,复制Code
void CParentDlg::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct) { switch (nIDCtl) { case IDC_COMBO_OWNERDRAWN: CDC* dc = GetDC(); CFont* font = GetFont(); CFont* fontOld = dc->SelectObject(font); TEXTMETRIC tm; dc->GetTextMetrics(&tm); lpMeasureItemStruct->itemHeight = tm.tmHeight; // if invoked for control itself (i.e. for edit control), // must include control border if (lpMeasureItemStruct->itemID == -1) { int border = ::GetSystemMetrics(SM_CYBORDER); lpMeasureItemStruct->itemHeight += 2 * border; } dc->SelectObject(fontOld); ReleaseDC(dc); break; } CDialogEx::OnMeasureItem(nIDCtl, lpMeasureItemStruct); }
这个处理程序在第一次绘制控件之前被调用,在WM_INITDIALOG消息被发送到父对话框之前。它对每个组合框调用两次:第一次用于捕获控件本身的大小(更准确地说,是它的编辑控件部分),第二次用于捕获下拉列表中项的维度。编辑控件大小包括控件周围的边框,因此必须区分两次调用。在上面的OnMeasureItem()处理程序中,检查MEASUREITEMSTRUCT结构中itemID成员的值的if语句区分了两个调用。对于编辑控件,该成员的值为-1,而对于下拉列表中的项,该成员的值为对应的项索引。如果调用没有区分,项目会比“intrinsic”组合框中显示的更高。当比较最左边的普通组合框和下面截图中间的所有者绘制的组合框时,就可以看到这一点。由于该处理程序是为对话框上的每个控件调用的,因此必须更正高度的控件将由其id过滤掉。 一个更好的选择是,如果高度是由控件本身设置的,那么不需要在组合框类之外更改代码。在显示控件之前必须调整高度,其中一个选项是覆盖PreSubclassWindow()函数: 隐藏,复制Code
void CGroupComboBox::PreSubclassWindow() { CDC* dc = GetDC(); CFont* font = GetFont(); CFont* fontOld = dc->SelectObject(font); TEXTMETRIC tm; dc->GetTextMetrics(&tm); int border = ::GetSystemMetrics(SM_CYBORDER); // height of edit control SetItemHeight(-1, tm.tmHeight + 2 * border); // height of items in drop-down list SetItemHeight(0, tm.tmHeight); dc->SelectObject(fontOld); ReleaseDC(dc); CComboBox::PreSubclassWindow(); }
组合框项层次结构 为了使在组合框中添加各种项目变得简单,绘制项目并提供其高度的责任已经交给了项目本身。因此,创建了一个基于抽象CGroupComboBoxItem类和纯虚拟方法Draw()和GetSize()的层次结构(见下图)。派生项类必须实现这两个方法。CComboBox派生类持有指向项对象的指针,它将简单地从被重写的DrawItem()和MeasureItem()方法中调用对应的Draw()和GetSize(): 隐藏,复制Code
void CGroupComboBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CDC dc;
dc.Attach(lpDrawItemStruct->hDC);
CGroupComboBoxItem* item = reinterpret_cast<CGroupComboBoxItem*>(lpDrawItemStruct->itemData);
item->Draw(&dc, lpDrawItemStruct);
dc.Detach();
}
由于CMemDC类的存在,附加代码中的实际实现略有不同用于减少闪烁。 对于一个最简单的组合框项,Draw()方法的实现如下: 隐藏,复制Code
void CGroupComboBoxSimpleItem::Draw(CDC* dc, LPDRAWITEMSTRUCT lpDrawItemStruct) { COLORREF crOldTextColor = dc->GetTextColor(); COLORREF crOldBkColor = dc->GetBkColor(); RECT rect = lpDrawItemStruct->rcItem; if ((lpDrawItemStruct->itemAction | ODA_SELECT) && (lpDrawItemStruct->itemState & ODS_SELECTED)) { dc->SetTextColor(::GetSysColor(COLOR_HIGHLIGHTTEXT)); dc->SetBkColor(::GetSysColor(COLOR_HIGHLIGHT)); dc->FillSolidRect(&rect, ::GetSysColor(COLOR_HIGHLIGHT)); } else dc->FillSolidRect(&rect, crOldBkColor); rect.left += GCB_TEXT_MARGIN; { CShellDlgFont dlgFont(dc); dc->DrawText(GetCaption(), &rect, DT_LEFT | DT_SINGLELINE | DT_VCENTER); } dc->SetTextColor(crOldTextColor); dc->SetBkColor(crOldBkColor); }
在上面的代码中,CShellDlgFont是一个实用工具类,它自动选择默认对话框字体进入设备上下文,并在范围退出时释放资源: 隐藏,收缩,复制Code
class CShellDlgFont { public: CShellDlgFont(CDC* dc, LONG fontWeight = FW_NORMAL) : m_dc(dc) { LOGFONT lf = { 0 }; lf.lfHeight = -MulDiv(8, ::GetDeviceCaps(*dc, LOGPIXELSY), 72); lf.lfWidth = 0; lf.lfWeight = fontWeight; lf.lfCharSet = DEFAULT_CHARSET; lf.lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE; _tcscpy_s(lf.lfFaceName, _T("MS Shell Dlg 2")); m_font.CreateFontIndirect(&lf); m_oldFont = dc->SelectObject(&m_font); } ~CShellDlgFont() { m_dc->SelectObject(m_oldFont); m_font.DeleteObject(); } private: CDC* m_dc; CFont m_font; CFont* m_oldFont; };
尽管MSDN文档状态而不是GetStockObject()函数(与DEFAULT_GUI_FONT或SYSTEM_FONT参数传递),SystemParametersInfo()函数与SPI_GETNONCLIENTMETRICS应该用于检索对话框默认字体,没有NONCLIENTMETRICS结构成员包含正确的字体度量。正确的字体度量不仅对于所有者绘制的变量组合框来说是必要的,而且对于固定高度组合框来说也是必要的,如果下拉列表的宽度必须适应最宽的项,本文后面将对此进行描述。为了说明,下面屏幕截图中左边的组合框使用默认字体(由客户区域设备上下文提供)来计算项目宽度,而右边的组合框使用上面的实用程序类。即使在这两种情况下,项目都是用正确的字体绘制的,但默认字体会导致下拉列表比实际需要的更宽。 由于目前只考虑固定的所有者绘制的组合框,因此MeasureItem()方法没有任何效果,可以暂时忽略。 CGroupComboBoxItem类也包含m_caption字符串成员,指向其标题项的指针(用于快速查找)和大小写不敏感比较的方法: 隐藏,复制Code
int CGroupComboBoxItem::CompareCaption(LPCWSTR lpStringOther, int cchCount) const { return ::CompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE, m_caption, cchCount, lpStringOther, cchCount) - CSTR_EQUAL; }
请注意如何使用WinAPI CompareString()方法来确保对本地化应用程序进行正确的比较。 标题项由CGroupComboBoxHeader类表示,该类也派生自CGroupComboBoxItem。由于标题项不能被选中,所以它不会突出显示,并且Draw()方法的实现更简单: 隐藏,复制Code
void CGroupComboBoxHeader::Draw(CDC* dc, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
COLORREF crOldTextColor = dc->GetTextColor();
COLORREF crOldBkColor = dc->GetBkColor();
RECT rect = lpDrawItemStruct->rcItem;
dc->FillSolidRect(&rect, ::GetSysColor(COLOR_MENUBAR));
rect.left += GCB_HEADER_INDENT;
{
CShellDlgFont dlgFont(dc, FW_BOLD);
dc->DrawText(GetCaption(), &rect, DT_LEFT | DT_SINGLELINE | DT_VCENTER);
}
dc->SetTextColor(crOldTextColor);
dc->SetBkColor(crOldBkColor);
}
上面的代码将在灰色背景上以粗体字体绘制标题。 您必须注意,派生的组合框持有指向项数据的指针(而不仅仅是对应的标题),因此在创建时必须不带CBS_HASSTRINGS标志。这样做的原因将在稍后我们将转换为变量所有者绘制的组合框时被阐明。 物品分类 如前所述,其中一个要求是每个组中的项都是可选的、单独的排序。不能设置组合框的CBS_SORT标志;否则,它将调用被覆盖的CompareItem()方法,在我们的示例中,实现该方法将非常复杂(请记住,必须为组合框控件设置CBS_HASSTRINGS样式,以便存储指向项对象的指针)。由于排序算法对于每个组都是特定的,因此将其存储到标题项数据中是合乎逻辑的,从而使标题项负责显示其排序后的子项目。建议的解决方案是在CGroupComboBoxHeader中包含项目比较类实例,并使用它对项目进行排序。因此,CGroupComboBoxHeader类负责显示已排序的项目。CComboBoxItemCompare类是用于项比较的基类,它按词法比较项标题。因此,如果在相应的头构造函数中将sort标志设置为true,则默认情况下将按字母顺序排序。 为了允许对任何组进行简单的自定义排序,CGroupComboBoxHeader类构造函数重载了一个版本,该版本接受对比较器类的引用。要应用自定义排序,只需要定义一个从CComboBoxItemCompare类派生的类,重写Compare()方法,并将对派生类实例的引用传递给CGroupComboBoxHeader类构造函数。构造函数创建这个类的一个副本,并将其存储到其成员中以供以后使用,因此当它超出作用域时,不会丢失对原始对象的引用。 组组合框实现 CGroupComboBox派生自CComboBox类。项目被添加到组合框作为指向CGroupComboBoxItem实例的指针。通过AddGroup()方法添加CGroupComboBoxHeader项,该方法将标题追加到内部列表: 隐藏,复制Code
int CGroupComboBox::AddGroup(CGroupComboBoxHeader* groupHeader) { m_headers.push_back(groupHeader); return m_headers.size() - 1; }
请注意,标题总是被附加到内部列表中,所以它们必须按照它们应该出现的顺序添加。不能显式删除组标头-所有标头都在控件的析构函数中删除。如果组不包含任何项,则不显示相应的标题。AddGroup()方法返回标题的索引,以后可以将其用作要添加的新项的引用。 可以通过AddItem()方法添加项目,该方法将项目追加到对应组的末尾,或者根据头定义的排序将项目插入到对应的位置: 隐藏,复制Code
int CGroupComboBox::AddItem(CGroupComboBoxItem* item, int groupIndex) { CGroupComboBoxHeader* groupHeader = m_headers[groupIndex]; groupHeader->AssignItem(item); // if group doesn't have items, it's header is not shown yet if (FindItem(groupHeader) == CB_ERR) ShowGroupHeader(groupHeader); CGroupBounds gb = GetGroupBounds(groupHeader); int index = (groupHeader->IsSorted()) ? GetInsertionIndex(groupHeader, gb.FirstIndex, gb.GroupEnd, item) : gb.GroupEnd; return SendMessage(CB_INSERTSTRING, index, LPARAM(item)); }
无论定义的排序是什么,InsertItem()方法都会将项直接插入到相应头的下面标题: 隐藏,复制Code
int CGroupComboBox::InsertItem(CGroupComboBoxItem* item, int groupIndex) { CGroupComboBoxHeader* groupHeader = m_headers[groupIndex]; groupHeader->AssignItem(item); // if group doesn't have items, it's header is not shown yet if (FindItem(groupHeader) == CB_ERR) ShowGroupHeader(groupHeader); int insertIndex = FindItem(groupHeader) + 1; return SendMessage(CB_INSERTSTRING, insertIndex, LPARAM(item)); }
这两种方法都接受项目所属的对应CGroupComboBoxHeader的索引作为第二个参数。上面代码片段中出现的FindItem()、ShowGroupHeader()、GetGroupBounds()和GetInsertionIndex()是在CGroupComboBox类中定义的实用程序方法。 需要注意的是,项目是通过CB_INSERTSTRING消息添加到组合框中的。由于组合框的定义没有使用CBS_HASSTRINGS标志,因此将存储指向项数据的指针,并且组合框负责在删除项时将其释放。这是在WM_DELETEITEM消息处理程序中完成的: 隐藏,复制Code
void CGroupComboBox::OnDeleteItem(int nIDCtl, LPDELETEITEMSTRUCT lpDeleteItemStruct) { CGroupComboBoxItem* item = reinterpret_cast<CGroupComboBoxItem*>(lpDeleteItemStruct->itemData); // only non-header items must be deallocated // (headers are deallocated in the destructor of control) if (item->IsGroupHeader() == false) delete item; CComboBox::OnDeleteItem(nIDCtl, lpDeleteItemStruct); }
其他问题 组合框与上述实现仍有一些缺陷。它们将在下面的部分中进行处理。 项目选择问题 当从下拉列表中选择一个项目时(通过鼠标或键盘),所选项目不会放置到组合框的编辑框部分。这很容易通过添加CBN_SELCHANGE通知处理程序来修复: 隐藏,复制Code
void CGroupComboBox::OnCbnSelchange() { int index = GetCurSel(); if (index >= 0) { CGroupComboBoxItem* item = GetComboBoxItem(index); if (item->IsGroupHeader() == false) { SetWindowText(item->GetCaption()); SetEditSel(0, -1); } } }
跳过头 在滚动下拉列表时,最好跳过标题项。尽管当前描述的实现头不能被选中,也没有突出显示,但是在上下滚动下拉列表时直接跳过它们会更好。这可以通过覆盖PreTranslateMessage()方法实现——如果下一个突出显示的项目是标题项目,只需移动选择: 隐藏,复制Code
BOOL CGroupComboBox::PreTranslateMessage(MSG* pMsg) { switch (pMsg->message) { case WM_KEYDOWN: // preprocess Up and Down arrow keys to avoid selection of group headers switch (pMsg->wParam) { case VK_UP: if (PreProcessVkUp()) return TRUE; break; case VK_DOWN: if (PreprocessVkDown()) return TRUE; break; } break; } return CComboBox::PreTranslateMessage(pMsg); }
PreprocessVkUp()和PreprocessVkDown()方法检查接下来要选择的项是否为标题,并在这样的实例中跳过它。如果选择了最顶层组中的第一项,则所选内容不会向上移动。但是,如果最上面的标题不可见,下拉列表框将滚动显示它。 PreprocessVkUp()和PreprocessVkDown()方法使用SelectItem()和ChangeSelection()方法来避免过多的下拉列表滚动: 隐藏,复制Code
void CGroupComboBox::ChangeSelection(int newSelection, int top) { COMBOBOXINFO cbi = { sizeof(COMBOBOXINFO) }; GetComboBoxInfo(&cbi); ::SendMessage(cbi.hwndList, WM_SETREDRAW, FALSE, 0); SetCurSel(newSelection); SetTopIndex(top); ::SendMessage(cbi.hwndList, WM_SETREDRAW, TRUE, 0); ::RedrawWindow(cbi.hwndList, NULL, NULL, RDW_ERASE | RDW_FRAME | RDW_INVALIDATE | RDW_ALLCHILDREN); } void CGroupComboBox::SelectItem(int index) { if (GetDroppedState()) { int top = GetTopIndex(); if (top > index) top = index; else if (index > GetBottomForItem(top)) top = GetTopForItem(index); ChangeSelection(index, top); } else SetCurSel(index); }
GetBottomForItem()和GetTopForItem()是实用方法,它们分别计算底部项和顶部项的索引(如果所选内容位于视图的顶部或底部)。如果使用SetCurSel()方法而不是SelectItem(),那么下拉列表框通常会滚动以将所选项目定位到视图的顶部,即使该项目已经在视图中。 需要指出的是,必须在CGroupComboBox类中重新定义FindString()和FindStringExact()方法。特别地,如果组合框没有CBS_HASSTRINGS样式集,那么CComboBox类中定义的这两个方法的实现不会搜索文本内容,而是搜索项数据指针。重新定义的版本比较项目标题,跳过标题项目: 隐藏,收缩,复制Code
int CGroupComboBox::FindString(int nStartAfter, LPCTSTR lpszString) const { int strLen = _tcslen(lpszString); int index = nStartAfter + 1; for (int i = 0; i < GetCount(); ++i) { CGroupComboBoxItem* item = GetComboBoxItem(index); if (item->IsGroupHeader() == false) { if (item->GetCaption().GetLength() >= strLen) { if (item->CompareCaption(lpszString, strLen) == 0) return index; } } ++index; if (index >= GetCount()) index = 0; } return CB_ERR; } int CGroupComboBox::FindStringExact(int nIndexStart, LPCTSTR lpszFind) const { int index = nIndexStart + 1; for (int i = 0; i < GetCount(); ++i) { CGroupComboBoxItem* item = GetComboBoxItem(index); if (item->IsGroupHeader() == false) { if (item->CompareCaption(lpszFind) == 0) return index; } ++index; if (index >= GetCount()) index = 0; } return CB_ERR; }
上面代码中的GetComboBoxItem()是一个实用方法: 隐藏,复制Code
CGroupComboBoxItem* CGroupComboBox::GetComboBoxItem(int i) const { return reinterpret_cast<CGroupComboBoxItem*>(CComboBox::GetItemData(i)); }
同样,分页键击和分页键击也必须分开处理。这是通过PreprocessVkPageDown()和preprocessvkpageup()方法实现的,这两个方法是从被覆盖的PreTranslateMessage()方法调用的。这里将不详细讨论这些方法,但读者可以在附带的源代码中浏览一下实现。 自动选择匹配项目的列表下拉列表 如果在编辑控件中已经输入了文本,当通过点击下拉箭头或F4键打开下拉列表时,具有匹配标题开始的项目将自动放入编辑控件中。这可以在CBN_DROPDOWN通知处理器中完成,它在列表被下拉列表之前被调用: 隐藏,复制Code
// code below provides only a partial solution (see text below) void CGroupComboBox::OnCbnDropdown() { // find and copy matching item if (GetWindowTextLength() > 0) FindStringAndSelect(); } bool CGroupComboBox::FindStringAndSelect() { CString text; GetWindowText(text); int index = FindStringExact(-1, text); if (index == CB_ERR) index = FindString(-1, text); if (index == CB_ERR) return false; SelectItem(index); SetWindowText(GetComboBoxItem(index)->GetCaption()); SetEditSel(0, -1); return true; }
但是,这不会选择下拉列表中对应的项。调用带有已评估索引的SetCurSel()方法没有任何效果,因为在发送CBN_DROPDOWN通知时,下拉列表还不可见,因此不能将任何项标记为selected。 因此,WM_CTLCOLORLISTBOX消息处理程序(当绘制下拉列表时调用)必须实现: 隐藏,复制Code
BEGIN_MESSAGE_MAP(CGroupComboBox, CComboBox) // ... ON_MESSAGE(WM_CTLCOLORLISTBOX, &CGroupComboBox::OnCtlColorListbox) END_MESSAGE_MAP() LRESULT CGroupComboBox::OnCtlColorListbox(WPARAM wParam, LPARAM lParam) { if (GetWindowTextLength() > 0) { // check is required to prevent recursion (SetCurSel triggers new WM_CTLCOLOR message) if (GetCurSel() == CB_ERR) FindStringAndSelect(); } return 0; }
通过这些更改,上面的OnCbnDropdown()实现可以省略。必须指出的是,每次对SetCurSel()方法(从SelectItem()方法调用)的调用都会再次发送WM_CTLCOLORLISTBOX消息,因此检查当前选择是否发生了变化是非常重要的,以防止无限递归调用处理程序。 上面的代码将匹配的项目文本复制到编辑控件中,当用户按F4按钮时,下拉列表中的项目被选中。如果按下组合框箭头按钮,也会发生同样的情况。但是,一旦按钮被释放,列表框就会滚动到顶部,最终选中的项目会移出视图。这样做的原因是,当按钮被按下时,它有焦点;当按钮被释放时,下拉列表获得焦点并被重新绘制。尽管项目是仍然显示选中,光标向下键将开始从最上面的项目滚动。要解决这个问题,一个WM_LBUTTONUP处理程序需要被覆盖: 隐藏,复制Code
void CGroupComboBox::OnLButtonUp(UINT nFlags, CPoint point) { // store selected item index int index = GetCurSel(); int top = GetTopIndex(); // prevent list-box update and resulting flickerring COMBOBOXINFO cbi = { sizeof(COMBOBOXINFO) }; GetComboBoxInfo(&cbi); ::SendMessage(cbi.hwndList, WM_SETREDRAW, FALSE, 0); CComboBox::OnLButtonUp(nFlags, point); if (GetDroppedState() && index != CB_ERR) ChangeSelection(index, top); ::SendMessage(cbi.hwndList, WM_SETREDRAW, TRUE, 0); }
自动完成文本输入 当打开下拉列表并在编辑控件中输入文本时,应自动选择匹配的项目,并将编辑控件中的文本填充为项目文本,如下面的截图所示。这可以在重写的PreTranslateMessage()方法中执行: 隐藏,收缩,复制Code
BOOL CGroupComboBox::PreTranslateMessage(MSG* pMsg) { switch (pMsg->message) { // ...here comes already described code for VK_UP and VK_DOWN case WM_CHAR: if (GetDroppedState()) { if (_istprint(pMsg->wParam)) { // fill-up the text in edit control with matching item CString text; GetWindowText(text); DWORD sel = GetEditSel(); int start = LOWORD(sel); int end = HIWORD(sel); if (start != end) text.Delete(start, end - start); text.AppendChar(TCHAR(pMsg->wParam)); if (FindAndAutocompleteItem(text)) return TRUE; } } break; } return CComboBox::PreTranslateMessage(pMsg); }
如果输入一个字符,修改后的图案将被搜索并复制到编辑框中,并进行适当的选择: 隐藏,复制Code
bool CGroupComboBox::FindAndAutocompleteItem(const CString& text) { int start = GetCurSel(); if (start < 0) start = 0; int index = FindString(start - 1, text); if (index == CB_ERR) return false; SelectItem(index); SetWindowText(GetComboBoxItem(index)->GetCaption()); // select only auto-filled text so that user can continue typing SetEditSel(text.GetLength(), -1); return true; }
取消选择 微软Word中的字体组合框提供了一个通过Esc键取消选择的选项。要实现此功能,只需在弹出下拉列表时保存编辑控件的内容,并在用户按Esc键时检索该内容: 隐藏,复制Code
void CGroupComboBox::OnCbnDropdown() { GetWindowText(m_previousString); // follows the code already discussed... // ... } BOOL CGroupComboBox::PreTranslateMessage(MSG* pMsg) { switch (pMsg->message) { // ... case WM_CHAR: if (GetDroppedState()) { if (TCHAR(pMsg->wParam) == VK_ESCAPE) { SetWindowText(m_previousString); SetEditSel(0, -1); } } break; } return CComboBox::PreTranslateMessage(pMsg); }
m_previousString是CString类型的类数据成员。 鼠标点击下拉列表上的事件 当用鼠标单击下拉列表中的某个项目时,相应的项目文本将放置到编辑控件中。这是通过前面提到的OnCbnSelchange()处理程序实现的。单击标题项即可关闭下拉列表。为了模仿微软Word中的字体组合框的行为,点击标题没有效果,也就是让下拉列表打开,有必要处理鼠标事件。 不幸的是,组合框不会接收任何传递给下拉列表的鼠标消息——解决这个问题的唯一方法是动态地子类化列表框: 这个类的一个实例(m_listBox)成为CGroupComboBox类的数据成员;在WM_CTLCOLORLISTBOX消息处理程序中,列表框被子类化(原来的列表框被一个CGroupListBox实例代替): 隐藏,复制Code
LRESULT CGroupComboBox::OnCtlColorListbox(WPARAM wParam, LPARAM lParam) { // subclass list box control if (m_listBox.GetSafeHwnd() == NULL) m_listBox.SubclassWindow(reinterpret_cast<HWND>(lParam)); // the rest is same as above... }
显然,列表框在第一次弹出之前就被子类化了。 不要忘记取消CGroupListBox实例的子类,最好的地方是WM_DESTROY消息处理程序: 隐藏,复制Code
void CGroupComboBox::OnDestroy() { if (m_listBox.GetSafeHwnd() != NULL) m_listBox.UnsubclassWindow(); CComboBox::OnDestroy(); }
如果应用程序在上述更改之后启动,则在下拉列表显示时,断言将失败。现在,子类化的列表框必须进行项目绘制:DrawItem()方法必须简单地从CGroupComboBox转移到CGroupListBox类。 最后,可以注意标题的点击问题:PreTranslateMessage()在CGroupListBox类中被覆盖,以防止关闭列表框: 隐藏,复制Code
BOOL CGroupListBox::PreTranslateMessage(MSG* pMsg) { switch (pMsg->message) { case WM_LBUTTONDOWN: case WM_LBUTTONDBLCLK: CPoint pt(GET_X_LPARAM(pMsg->lParam), GET_Y_LPARAM(pMsg->lParam)); BOOL bOutside; int index = ItemFromPoint(pt, bOutside); // if user clicks on group header item, list should remain open if (bOutside == FALSE && reinterpret_cast<CGroupComboBoxItem*>(GetItemData(index))->IsGroupHeader()) return TRUE; break; } return CListBox::PreTranslateMessage(pMsg); }
调整列宽 一个不错的功能是调整下拉列表的宽度,使每个项目都是完全可见的。这是通过AdjustDropdownListWidth()方法实现的,注意下拉列表不会掉出桌面区域: 隐藏,收缩,复制Code
void CGroupComboBox::AdjustDropdownListWidth() { COMBOBOXINFO cbi = { sizeof(COMBOBOXINFO) }; GetComboBoxInfo(&cbi); RECT rect; ::GetWindowRect(cbi.hwndList, &rect); int maxItemWidth = GetMaxItemWidth() + ::GetSystemMetrics(SM_CXEDGE) * 2; // extend drop-down list to right if (maxItemWidth > rect.right - rect.left) { rect.right = rect.left + maxItemWidth; // reserve place for vertical scrollbar if (GetCount() > GetMinVisible()) rect.right += ::GetSystemMetrics(SM_CXVSCROLL); // check if extended drop-down list fits the desktop HMONITOR monitor = MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST); MONITORINFO mi; mi.cbSize = sizeof(mi); GetMonitorInfo(monitor, &mi); // it doesn't fit, move it left if (mi.rcWork.right < rect.right) { int delta = rect.right - mi.rcWork.right; rect.left -= delta; rect.right -= delta; } ::SetWindowPos(cbi.hwndList, NULL, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOZORDER); } }
该方法是从WM_CTLCOLORLISTBOX消息处理程序调用的。 可变高度业主绘制的组合框 尽管使用CBS_OWNERDRAWFIXED样式的组合框能够满足大多数需求,但在项目绘制和上述所有讨论问题得到解决之后,乍一看,对于一个可变高度的所有者绘制的组合框,只需要一个小的额外步骤来实现MeasureItem()。可变高度组合框提供了额外的灵活性和更容易的可扩展性。在附加的代码中,变量高度组合框是通过CGroupComboBoxVariable类实现的,该类直接派生自本文第一部分中描述的CGroupComboBox。 由于使用CBS_OWNERDRAWVARIABLE样式创建的组合框可以显示具有可变高度的项目,因此会为每个项目调用MeasureItem(),派生类中的重写实现负责提供相应的项目高度: 隐藏,复制Code
void CGroupComboBoxVariable::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) { CGroupComboBoxItem* item = reinterpret_cast<CGroupComboBoxItem*>(lpMeasureItemStruct->itemData); CClientDC dc(this); lpMeasureItemStruct->itemHeight = item->GetSize(dc).cy; }
注意,当通过任何相应的方法将一个项添加到组合框中时,会立即调用MeasureItem()方法。因此,它是不切实际的执行单独的插入字符串(即标题出现在组合框)由AddString()或InsertString()方法,然后附加额外的数据项高度信息称SetItemData()或SetItemDataPtr()方法(注意,后者两个方法实际上是等价的,设置值发送CB_SETITEMDATA消息)。尽管可以使用CComboBox类的SetItemHeight()方法,但在一个步骤中插入整个项目要简单得多,从而避免了后续调用SetItemHeight()方法的需要。 但转型并不那么简单。让我们讨论一下切换到所有者绘制的变量组合框后出现的一些问题。 适应下拉列表高度 在组合框应用了CBS_OWNERDRAWVARIABLE样式之后,首先要注意的可能是糟糕的下拉列表没错,如下面的截图所示。对于固定高度组合框控件,下拉列表的高度在默认情况下被调整为同时显示30个项目,但对于具有可变高度项目的下拉列表,显然必须明确地计算和调整列表的高度。 在揭示所提议的实现之前,必须考虑一些副作用和工件。 自项变量的总高度和下拉列表显示项目完全顶部,很明显,下面列表中的项必被剪除在大多数情况下,是不可能适应高度列表项高度每次滚动列表。但是,如果最后一项没有对齐到列表的底部(或者更糟的是,被切断),就不合适了。下拉列表必须具有非整数高度,才能显示最后对齐的项。这个选项可以在设计器中通过将组合框的“无积分高度”属性设置为true来更改。为了避免手动修改每个控件的这个选项,可以在代码中设置,例如在重写的PreSubclassWindow()方法中: 隐藏,复制Code
void CGroupComboBoxVariable::PreSubclassWindow() { ModifyStyle(0, CBS_NOINTEGRALHEIGHT, SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); CGroupComboBox::PreSubclassWindow(); }
下拉列表的高度必须通过对必须适合最底部视图的最后项的高度求和来计算。另外,必须注意下拉列表不能脱离工作区边界。例如,如果组合框控件放置在屏幕的底部,下拉列表将在其上方弹出。这些考虑是在CalculateDropDownListRect()方法中实现的: 隐藏,收缩,复制Code
RECT CGroupComboBoxVariable::CalculateDropDownListRect() { // get workspace area RECT rect; GetWindowRect(&rect); HMONITOR monitor = MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST); MONITORINFO mi; mi.cbSize = sizeof(mi); GetMonitorInfo(monitor, &mi); // check if drop-down box fits below edit box int availableHeight = mi.rcWork.bottom - rect.bottom; bool showBelow = true; // sum the heights of last m_itemsOnLastPage items int listHeight = 0; // last item int nBottom = GetCount() - 1; // item that should be on the top of the last page int nTop = max(nBottom - m_itemsOnLastPage + 1, 0); while ((nBottom >= nTop) && (listHeight + GetItemHeight(nBottom) < availableHeight)) { listHeight += GetItemHeight(nBottom); --nBottom; } // if cannot display requested number of items below and there is more space above // check how many items can be displayed when list is above if ((nBottom > nTop) && (availableHeight < rect.top - mi.rcWork.top)) { availableHeight = rect.top - mi.rcWork.top; showBelow = false; while (nBottom >= nTop && (listHeight + GetItemHeight(nBottom) < availableHeight)) { listHeight += GetItemHeight(nBottom); --nBottom; } } listHeight += ::GetSystemMetrics(SM_CYEDGE); if (showBelow) { rect.top = rect.bottom; rect.bottom += listHeight; } else { rect.bottom = rect.top; rect.top -= listHeight; } return rect; }
下拉列表的大小必须在显示之前进行调整,并且在CBN_DROPDOWN通知处理程序中有一个合适的位置: 隐藏,复制Code
void CGroupComboBoxVariable::OnCbnDropdown()
{
CGroupComboBox::OnCbnDropdown();
RECT rect;
GetWindowRect(&rect);
CRect listRect = CalculateDropDownListRect();
rect.bottom += listRect.Height();
GetParent()->ScreenToClient(&rect);
MoveWindow(&rect);
}
实际的实现略有不同,因为CalculateDropDownListRect()方法非常耗时。只要组合框的内容没有改变,就没有必要在每次下拉列表时调用它。 关于演示应用程序 演示应用程序包含五个组合框: 普通组合框(仅用于比较);所有者绘制的固定大小的组合框,只包含文本项;两个所有者绘制的固定大小的组合框,包含文本和图像项;所有者绘制的可变大小组合框文本和图像项。 组合框3和4使用图像存储在全局可用图像列表(这是有用的,如果几项使用相同的图像)或使用图标与相应资源定义的每个条目id。组合框2和4有相关联的控件来展示所述的一些功能。可以通过公共方法EnableDropdownListAutoWidth()、EnableAutocomplete()和EnableSelectionUndoByEscKey()来打开和关闭相应的选项。 免责声明:字体枚举程序在演示应用程序中仅用于演示目的,不是推荐的方法。 使用的代码 要在您的项目中使用上述代码,以下准备步骤必须作出: 包括GroupComboBox。h, GroupComboBox。cpp, ShellDlgFont.h和MemDC.h文件到您的项目中。定义从CGroupComboBoxItem派生的类并实现Draw()方法;对于所有者绘制的可变高度组合框,还必须实现GetSize()方法。创建从CGroupComboBox或CGroupComboBoxVariable类派生的类,并实现AddItem()和InsertItem()方法。这个步骤是可选的,只需要使添加/插入步骤(2)中定义的项更容易。如果需要另一个项目排序顺序,可以选择使用覆盖的Compare()方法创建CComboBoxItemCompare派生类。 演示应用程序中的CGroupComboBoxWithIcons、CFontGroupComboBox和CFontGroupComboBoxVariable类已经完成了这些步骤,以便读者可以检查源代码,如果还有一些不明确的地方。 完成这些步骤后,只需使用DDX_Control()方法将对话框中的组合框资源附加到相应的类实例。 历史 2012年12月24日-初始版本。2013年11月19日—为了允许其他控件(包括父对话框)处理通知,CBN_SELCHANGE和cbn_下拉通知的ON_CONTROL_REFLECT宏被更改为ON_CONTROL_REFLECT_EX。 本文转载于:http://www.diyabc.com/frontweb/news178.html