Шаг 384 - Множественный выбор в ComboBox'е

Какую возможность дает нам 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 имел следующие свойства:

384_1.gif (3573 b)

Вот собственно и вся теория, а теперь практика. Создаем приложение на базе диалогового окна с именем TestCC. Кидаем на форму ComboBox и идем в его свойства: даем ему имя IDC_COMBO и стиль как на рисунке выше. Далее нам нужно создать класс, реализующий все описанное выше. Идем в ClassWizard, для чего нажимаем Ctrl+W. Дальше стандартная процедура: Add Class -> New Class. Даем имя новому классу ComboClass. И устанавливаем базовый для него класс: CComboBox.

В результате у вас должно быть что-то вроде этого:

384_2.gif (4789 b)

Не выходя из 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)


Загрузить проект | Предыдущий Шаг | Следующий Шаг | Оглавление
Автор Каев Артем.