Какую возможность дает нам ComboBox? Правильно - выбор единственного элемента из списка. А если необходимо сделать множественный выбор. То приходится использовать ListBox с его неудобным выбором нескольких пунктов. Но как решить эту проблему, если на форме еще и необходимо экономить место. Вот один из способов решить это - ComboBox с множественным выбором.
Я решил поместить в ComboBox ListBox'ы (коряво сказал:). Тут возникла проблема. Суть ее вот в чем: я не мог получить указатель на выпадающее окно ComboBox'а. В распоряжении был только указатель на сам ComboBox, что мне никак не могло помочь.
Фишка заключается в следующем: мы отлавливаем сообщение WM_CTLCOLORLISTBOX. Когда посылается это сообщение, то lParam содержит нужный нам указатель.
Вот как это реализовано:
BEGIN_MESSAGE_MAP(CComboClass, CComboBox) //{{AFX_MSG_MAP(CComboClass) ...... ON_MESSAGE(WM_CTLCOLORLISTBOX, OnCtlColorListBox) ...... //}}AFX_MSG_MAP END_MESSAGE_MAP() ...... LRESULT CComboClass::OnCtlColorListBox(WPARAM wParam, LPARAM lParam) { // если еще не получили указатель, то получаем его... if (m_hListBox == 0) { HWND hWnd = (HWND)lParam; if (hWnd != 0 && hWnd != m_hWnd) { // сохраняем наш указатель m_hListBox = hWnd; // впариваем CheckBox'ы m_pWndProc = (WNDPROC)GetWindowLong(m_hListBox, GWL_WNDPROC); SetWindowLong(m_hListBox, GWL_WNDPROC, (LONG)ComboBoxListBoxProc); } } return DefWindowProc(WM_CTLCOLORLISTBOX, wParam, lParam); }
В ComboBoxListBoxProc() реализовано выделение и ... не выделение элементов. Для прорисовки всего этого надо перегрузить функцию DrawItem().
Важно, чтобы ComboBox имел следующие свойства:
Вот собственно и вся теория, а теперь практика. Создаем приложение на базе диалогового окна с именем TestCC. Кидаем на форму ComboBox и идем в его свойства: даем ему имя IDC_COMBO и стиль как на рисунке выше. Далее нам нужно создать класс, реализующий все описанное выше. Идем в ClassWizard, для чего нажимаем Ctrl+W. Дальше стандартная процедура: Add Class -> New Class. Даем имя новому классу ComboClass. И устанавливаем базовый для него класс: CComboBox.
В результате у вас должно быть что-то вроде этого:
Не выходя из ClassWizard'a идем на вкладку Member Variables. Выбираем наш ComboBox (IDC_COMBO), далее Add Variable.
Даем имя переменной m_Combo, тип Control и выбираем базовый класс CComboClass. С формальностями покончено, переходим к реализации.
Открываем файл ComboClass.h. Добавляем:
Public: BOOL Create(DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID); INT SetCheck(INT nIndex, BOOL bFlag); BOOL GetCheck(INT nIndex); void SelectAll(BOOL bCheck = TRUE); // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CComboClass) virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct); virtual void MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct); //}}AFX_VIRTUAL // Implementation public: virtual ~CComboClass(); void RecalcText(); HWND m_hListBox; CString m_strText; BOOL m_bTextUpdated; BOOL m_bItemHeightSet; // Generated message map functions protected: //{{AFX_MSG(CComboClass) // NOTE - the ClassWizard will add and remove member functions here. afx_msg LRESULT OnCtlColorListBox(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnGetText(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnGetTextLength(WPARAM wParam, LPARAM lParam); afx_msg void OnDropDown(); //}}AFX_MSG DECLARE_MESSAGE_MAP() };
Теперь реализация этих функций в ComboBox.cpp. Добавляем сразу после включения заглавочных файлов:
static WNDPROC m_pWndProc = 0; static CComboClass *m_pComboBox = 0;
Затем в конструкторе:
m_hListBox = 0; m_bTextUpdated = FALSE; m_bItemHeightSet = FALSE;
Карта сообщений:
BEGIN_MESSAGE_MAP(CComboClass, CComboBox) //{{AFX_MSG_MAP(CComboClass) // NOTE - the ClassWizard will add and remove mapping macros here. ON_MESSAGE(WM_CTLCOLORLISTBOX, OnCtlColorListBox) ON_MESSAGE(WM_GETTEXT, OnGetText) ON_MESSAGE(WM_GETTEXTLENGTH, OnGetTextLength) ON_CONTROL_REFLECT(CBN_DROPDOWN, OnDropDown) //}}AFX_MSG_MAP END_MESSAGE_MAP()
Теперь самая главная функция:
extern "C" LRESULT FAR PASCAL ComboBoxListBoxProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam) { switch (nMsg) { case WM_RBUTTONDOWN: { #if FALSE if (m_pComboBox != 0) { INT nCount = m_pComboBox->GetCount(); INT nSelCount = 0; for (INT i = 0; i < nCount; i++) { if (m_pComboBox->GetCheck(i)) nSelCount++; } m_pComboBox->SelectAll(nSelCount != nCount); InvalidateRect(hWnd, 0, FALSE); m_pComboBox->GetParent()->SendMessage(WM_COMMAND, MAKELONG(GetWindowLong(m_pComboBox->m_hWnd, GWL_ID), CBN_SELCHANGE), (LPARAM)m_pComboBox->m_hWnd); } #endif break; } case LB_GETCURSEL: { return -1; } case WM_CHAR: { if (wParam == VK_SPACE) { INT nIndex = CallWindowProcA(m_pWndProc, hWnd, LB_GETCURSEL, wParam, lParam); CRect rcItem; SendMessage(hWnd, LB_GETITEMRECT, nIndex, (LONG)(VOID *)&rcItem); InvalidateRect(hWnd, rcItem, FALSE); m_pComboBox->SetCheck(nIndex, !m_pComboBox->GetCheck(nIndex)); m_pComboBox->GetParent()->SendMessage(WM_COMMAND, MAKELONG(GetWindowLong(m_pComboBox->m_hWnd, GWL_ID), CBN_SELCHANGE), (LPARAM)m_pComboBox->m_hWnd); return 0; } break; } case WM_LBUTTONDOWN: { CRect rcClient; GetClientRect(hWnd, rcClient); CPoint pt; pt.x = LOWORD(lParam); pt.y = HIWORD(lParam); if (PtInRect(rcClient, pt)) { INT nItemHeight = SendMessage(hWnd, LB_GETITEMHEIGHT, 0, 0); INT nTopIndex = SendMessage(hWnd, LB_GETTOPINDEX, 0, 0); INT nIndex = nTopIndex + pt.y / nItemHeight; CRect rcItem; SendMessage(hWnd, LB_GETITEMRECT, nIndex, (LONG)(VOID *)&rcItem); if (PtInRect(rcItem, pt)) { InvalidateRect(hWnd, rcItem, FALSE); m_pComboBox->SetCheck(nIndex, !m_pComboBox->GetCheck(nIndex)); m_pComboBox->GetParent()->SendMessage(WM_COMMAND, MAKELONG(GetWindowLong(m_pComboBox->m_hWnd, GWL_ID), CBN_SELCHANGE), (LPARAM)m_pComboBox->m_hWnd); } } break; } case WM_LBUTTONUP: { return 0; } } return CallWindowProc(m_pWndProc, hWnd, nMsg, wParam, lParam); }
Идем далее:
BOOL CComboClass::Create(DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID) { dwStyle &= ~0xF; dwStyle |= CBS_DROPDOWNLIST; dwStyle |= CBS_OWNERDRAWVARIABLE; dwStyle |= CBS_HASSTRINGS; return CComboBox::Create(dwStyle, rect, pParentWnd, nID); } LRESULT CComboClass::OnCtlColorListBox(WPARAM wParam, LPARAM lParam) { if (m_hListBox == 0) { HWND hWnd = (HWND)lParam; if (hWnd != 0 && hWnd != m_hWnd) { m_hListBox = hWnd; m_pWndProc = (WNDPROC)GetWindowLong(m_hListBox, GWL_WNDPROC); SetWindowLong(m_hListBox, GWL_WNDPROC, (LONG)ComboBoxListBoxProc); } } return DefWindowProc(WM_CTLCOLORLISTBOX, wParam, lParam); } void CComboClass::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { HDC dc = lpDrawItemStruct->hDC; CRect rcBitmap = lpDrawItemStruct->rcItem; CRect rcText = lpDrawItemStruct->rcItem; CString strText; INT nCheck = 0; if ((LONG)lpDrawItemStruct->itemID < 0) { RecalcText(); strText = m_strText; nCheck = 0; } else { GetLBText(lpDrawItemStruct->itemID, strText); nCheck = 1 + (GetItemData(lpDrawItemStruct->itemID) != 0); TEXTMETRIC metrics; GetTextMetrics(dc, &metrics); rcBitmap.left = 0; rcBitmap.right = rcBitmap.left + metrics.tmHeight + metrics.tmExternalLeading + 6; rcBitmap.top += 1; rcBitmap.bottom -= 1; rcText.left = rcBitmap.right; } if (nCheck > 0) { SetBkColor(dc, GetSysColor(COLOR_WINDOW)); SetTextColor(dc, GetSysColor(COLOR_WINDOWTEXT)); UINT nState = DFCS_BUTTONCHECK; if (nCheck > 1) nState |= DFCS_CHECKED; DrawFrameControl(dc, rcBitmap, DFC_BUTTON, nState); } if (lpDrawItemStruct->itemState & ODS_SELECTED) { SetBkColor(dc, GetSysColor(COLOR_HIGHLIGHT)); SetTextColor(dc, GetSysColor(COLOR_HIGHLIGHTTEXT)); } else { SetBkColor(dc, GetSysColor(COLOR_WINDOW)); SetTextColor(dc, GetSysColor(COLOR_WINDOWTEXT)); } ExtTextOut(dc, 0, 0, ETO_OPAQUE, &rcText, 0, 0, 0); DrawText(dc, ' ' + strText, strText.GetLength() + 1, &rcText, DT_SINGLELINE|DT_VCENTER|DT_END_ELLIPSIS); if ((lpDrawItemStruct->itemState & (ODS_FOCUS|ODS_SELECTED)) == (ODS_FOCUS|ODS_SELECTED)) DrawFocusRect(dc, &rcText); } void CComboClass::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) { CClientDC dc(this); CFont *pFont = dc.SelectObject(GetFont()); if (pFont != 0) { TEXTMETRIC metrics; dc.GetTextMetrics(&metrics); lpMeasureItemStruct->itemHeight = metrics.tmHeight + metrics.tmExternalLeading; lpMeasureItemStruct->itemHeight += 2; if (!m_bItemHeightSet) { m_bItemHeightSet = TRUE; SetItemHeight(-1, lpMeasureItemStruct->itemHeight); } dc.SelectObject(pFont); } } void CComboClass::OnDropDown() { m_pComboBox = this; } void CComboClass::SelectAll(BOOL bCheck) { INT nCount = GetCount(); for (INT i = 0; i < nCount; i++) SetCheck(i, bCheck); } LRESULT CComboClass::OnGetText(WPARAM wParam, LPARAM lParam) { RecalcText(); if (lParam == 0) return 0; lstrcpyn((LPSTR)lParam, m_strText, (INT)wParam); return m_strText.GetLength(); } LRESULT CComboClass::OnGetTextLength(WPARAM, LPARAM) { RecalcText(); return m_strText.GetLength(); } void CComboClass::RecalcText() { if (!m_bTextUpdated) { CString strText; INT nCount = GetCount(); TCHAR szBuffer[10] = {0}; GetLocaleInfo(LOCALE_USER_DEFAULT, LOCALE_SLIST, szBuffer, sizeof(szBuffer)); CString strSeparator = szBuffer; if (strSeparator.GetLength() == 0) strSeparator = ';'; strSeparator.TrimRight(); strSeparator += ' '; for (INT i = 0; i < nCount; i++) { if (GetItemData(i)) { CString strItem; GetLBText(i, strItem); if (!strText.IsEmpty()) strText += strSeparator; strText += strItem; } } m_strText = strText; m_bTextUpdated = TRUE; } } INT CComboClass::SetCheck(INT nIndex, BOOL bFlag) { INT nResult = SetItemData(nIndex, bFlag); if (nResult < 0) return nResult; m_bTextUpdated = FALSE; Invalidate(FALSE); return nResult; } BOOL CComboClass::GetCheck(INT nIndex) { return GetItemData(nIndex); }
Вот собственно и все. Осталось только добавить заглавочный файл в TestCCDlg.h и кинуть что-то в наш элемент управления. Делаем это в функции BOOL CTestCCDlg::OnInitDialog():
m_Combo.AddString("Один"); m_Combo.AddString("Два"); m_Combo.AddString("Три"); m_Combo.SetCheck(0, TRUE);
Шаг прислал Дрон (Miroslav_IF@mail.ru)