Этот материал ранее был опубликован в виде статьи на http://www.codeproject.com (на английском языке) после обсуждения на форуме http://sql.ru/forum/C++/.
Здесь фактически воспроизводится итоговый материал обсуждения из того же форума:
Июнь 2009 года
Решена проблема «деформации» заголовка и его местоположения, продемонстрировано изменение высоты строк списка и форматирование текста в ячейках контрола.
Загрузить демонстрационные файлы с www.codeproject.com (требуется регистрация (бесплатная) на этом сайте):
TypicalListCtrl.zip - 51.58 Kb
ModifiedListCtrl.zip - 60.98 Kb
Загрузить демонстрационные файлы с sql.ru (сначала просто зайдите на этот сайт, затем воспользуйтесь ссылками):
TypicalListCtrl.zip - 51.58 Kb
ModifiedListCtrl.zip - 60.98 Kb
Загрузить те же файлы отсюда (переименуйте расширение в zip):
TypicalListCtrl.001 - 51.58 Kb
ModifiedListCtrl.001 - 60.98 Kb
Формулировка проблемы и способов ее решения
Не знаю, чем можно объяснить такой факт, что заголовок CHeaderCtrl / SysHeader32 стандартного списка Windows CListCtrl / SysListView32 рисуется с некоторым искажением, которое сразу бросается в глаза. Это портит впечатление от программ использующих эти контролы. Обычно линии заголовка отстают на 1 пиксел от линий списка и сам заголовок почему-то на 2 пиксела короче, чем следовало бы. Чтобы скрыть этот эффект, часто линии списка убирают совсем. Это хорошо видно на рис. 1-3.
Помимо решения вопроса «деформации» заголовка нам еще будет интересно произвольно менять его высоту и высоту строк списка, независимо от размеров текущего шрифта и иконки присутствующей в колонках или рядах списка. Причем, мы желаем решить эти вопросы наиболее элегантным способом.
Мы также желаем использовать заданные шрифты и форматирование текста в ячейках заголовка и списка. Кроме этого, демонстрационный пример, для типичного списка, мы будем заполнять данными, обычным способом, т.е. посредством InsertItem / SetItem, а для модифицированного списка будем использовать виртуальный режим (стиль LVS_OWNERDATA), через SetItemCount и обработкой сообщения LVN_GETDISPINFO.
Результат нашей работы представлен на рис. 4. Заметим, что хотя мы желали для типичного CListCtrl’a выравнивание первого столбца «Ind. No.» по правому краю, этот контрол проигнорировал наше «распоряжение» и выровнял столбец по левому краю (рис. 1-3). И только для модифицированного контрола мы смогли добиться нужного выравнивания (рис. 4). Кроме того, мы показали, как можно изменять форматирование данных по умолчанию.
Если мы меняем высоту заголовка за счет увеличения размеров шрифта или иконок, то указанная «деформация» тем не менее, сохраняется. Очевидно, что это «свойство» не самого MFC класса CListCtrl а контрола Windows SysListView32.
Чтобы ликвидировать «деформацию» заголовка, нужно дополнительно сместить его вправо на 1 пиксел и увеличить его высоту на 2 пиксела, желательно без явного привлечения таких функций как MoseWindow / SetWindowPos, а также без необходимости делать собственную перерисовку CHeaderCtrl / CListCtrl в обработчиках типа OnPaint. Кроме этого, мы не хотим использовать функции типа SetFont не по назначению (только для увеличения размеров заголовка, с последующим переопределением его шрифта). То же самое касается и иконок, которые мы желаем использовать исключительно по назначению, а не для изменения только размеров заголовка или строк (скажем, за счет использования тонких иконок заданной высоты и не отличимых от фона). Есть еще вариант пересоздавать заголовок на базе CHeaderCrtrl за счет функции Create, который допускает явное указание размеров и его местоположения, но этот вариант нас также не устраивает как слишком громоздкий.
Таким образом, у нас остается фактически один вариант для изменения высоты и местоположения заголовка (за счет обработки сообщения HDM_LAYOUT) и один вариант изменения высоты строк списка (за счет обработки сообщения WM_MEASUREITEM). Способ с HDM_LAYOUT интересен еще и потому, что очень часто программисты используют его не совсем корректно, что иногда приводит к избыточному коду или неверной работе программы. Заодно мы покажем, как можно форматировать текст в ячейках списка и его заголовка.
Обработка сообщения HDM_LAYOUT
Чтобы получить и обработать сообщение заголовка таблицы HDM_LAYOUT необходимо определить обработчик:
afx_msg LRESULT OnLayout(WPARAM wparam, LPARAM lparam);
в файле заголовка типа HeaderCtrlEx.h. Затем в соответствующем файле HeaderCtrlEx.cpp указать в карте сообщений макрос:
ON_MESSAGE(HDM_LAYOUT, OnLayout)
и определить соответствующую функцию OnLayout:
/////////////////////////////////////////////////////////////////////////
// OnLayout
/////////////////////////////////////////////////////////////////////////
LRESULT CHeaderCtrlEx::OnLayout(WPARAM, LPARAM lParam) {
LPHDLAYOUT pHL = reinterpret_cast<LPHDLAYOUT>(lParam);
//*** The table list rectangle
RECT *pRect = pHL->prc;
//*** The table header rectangle
WINDOWPOS *pWPos = pHL->pwpos;
//*** Here's equivalent code for the code which follows after
/*
pWPos->hwndInsertAfter = NULL;
//*** Moves the table header to the righ
pWPos->x = pRect->left + m_nHdrWidthDefect;
pWPos->y = pRect->top;
pWPos->cx = pRect->right - pRect->left;
//*** New table header height
pWPos->cy = m_nHdrHeight + m_nHdrHeightDefect;
pWPos->flags = SWP_NOACTIVATE|SWP_NOZORDER;
//*** Decreases the table list height on the table header height
pRect->top += m_nHdrHeight;
return TRUE;
*/
//*** Sends HDM_LAYOUT message to the base class
int nRet = CHeaderCtrl::DefWindowProc(HDM_LAYOUT, 0, lParam);
//*** Moves the table header to the right
pWPos->x += m_nHdrWeightDefect;
//*** New table header height
pWPos->cy = m_nHdrHeight + m_nHdrHeightDefect;
//*** Decreases the table list height on the table header height
pRect->top = m_nHdrHeight;
return nRet;
} // OnLayout
Фактически, в ней показаны два варианта изменения размеров и местоположения заголовка класса CHeaderCtrlEx без явного привлечения функций типа SetFont / MoveWindow / SetWindowPos / CHeaderCtrlEx::Create или использования псевдо иконок (для достижения тех же целей). Не смотря на кажущуюся простоту этой функции, многие программисты допускают ошибки в обработчике сообщения HDM_LAYOUT. Возможно, это вызвано недостаточно четким описанием структуры LPHDLAYOUT в MSDN.
Обработка сообщения WM_MEASUREITEM
Аналогично предыдущему, определяем обработчик:
afx_msg void MeasureItem(LPMEASUREITEMSTRUCT pMIS);
в файле заголовка типа ListCtrlEx.h. Затем в соответствующем файле ListCtrlEx.cpp указываем в карте сообщений макрос:
ON_WM_MEASUREITEM_REFLECT()
и определяем соответствующую функцию MeasureItem:
/////////////////////////////////////////////////////////////////////////
// MeasureItem
/////////////////////////////////////////////////////////////////////////
void CListCtrlEx::MeasureItem(LPMEASUREITEMSTRUCT pMIS) {
//*** The table list height
pMIS->itemHeight = m_nListHeight;
} // MeasureItem
Форматирование текста
Немногим более сложно определяется форматирование текста заголовка по нашему усмотрению. Вот код CHeaderCtrlEx::DrawItem :
////////////////////////////////////////////////////////////////////////
// DrawItem
////////////////////////////////////////////////////////////////////////
void CHeaderCtrlEx::DrawItem(LPDRAWITEMSTRUCT pDIS) {
HDITEM hDI;
TCHAR szBuf[MAXITEMTEXT];
hDI.mask = HDI_TEXT;
hDI.pszText = szBuf;
hDI.cchTextMax = MAXITEMTEXT;
GetItem(pDIS->itemID, &hDI);
CDC *pDC;
HDC hDC = pDIS->hDC; // Handle to device context
pDC = CDC::FromHandle(hDC);
//*** Selects necessary font
pDC->SelectObject(m_pFont);
//pDC->SelectObject(GetStockObject(DEFAULT_GUI_FONT));
int x = 0; // x-coordinate of reference point
int y = 0; // y-coordinate of reference point
UINT nOptions = 0; // Text output options ETO_CLIPPED|ETO_OPAQUE
RECT *pIRect = NULL; // Optional clipping and/or opaquing rectangle
pIRect = &pDIS->rcItem;
SIZE Size = {0};
//*** Gets the header cell sizes
if(!GetTextExtentPoint(hDC, szBuf, wcslen(szBuf), &Size)) {
_M("Failed to call GetTextExtentPoint for table header!");
return;
}
//*** x-coordinate of reference point
x = (pIRect->left + pIRect->right - Size.cx)/2 - 1;
x = (x < pIRect->left + 2) ? pIRect->left + 2 : x;
//*** y-coordinate of reference point
y = (pIRect->bottom - pIRect->top - Size.cy)/2 - 1;
//*** Specifies that the current background color fills the rectangle
// pIRect
nOptions |= ETO_CLIPPED;
//*** Decreases the text border to the right
pIRect->right -= 4;
//*** Writes the text in the (x, y) – coordinates
pDC->ExtTextOut(x, y, nOptions, pIRect, szBuf, wcslen(szBuf), NULL);
//*** Restores system font
pDC->SelectStockObject(SYSTEM_FONT);
} // DrawItem
И наиболее существенный код для форматирования текста списка:
///////////////////////////////////////////////////////////////////////
// SetColItemText
///////////////////////////////////////////////////////////////////////
void CListCtrlEx::SetColItemText(CDC *pDC, CString& stColText, CRect&
TextRect, UINT nJustify) {
int x = 0; // x-coordinate of reference point
int y = 0; // y-coordinate of reference point
UINT nOptions = 0; // Text-output options ETO_CLIPPED|ETO_OPAQUE
int nTextLen = stColText.GetLength();
HDC hDC = pDC->m_hDC;
SIZE Size = {0};
if(!GetTextExtentPoint(hDC, stColText, nTextLen, &Size)) {
_M("Failed to call GetTextExtentPoint for table list!");
return;
}
// Align the text in the whole table
CRect TmpRect(TextRect);
//*** x-coordinate of reference point
x = (TextRect.left + TextRect.right - Size.cx)/2 - 1;
x = (x < TextRect.left + 2) ? TextRect.left + 2 : x;
//*** y-coordinate of reference point
y = (TextRect.bottom - TextRect.top - Size.cy)/2 - 1;
//*** Specifies that the current background color fills the rectangle
nOptions |= ETO_OPAQUE;
//*** Draw the background fast
pDC->ExtTextOut(TextRect.left, TextRect.top, nOptions, TextRect, NULL,
0, NULL);
TmpRect.left++; // Cosmetic
TmpRect.top += y; // y-coordinate of reference point
TmpRect.InflateRect(-3, 0); // Text does not touch borders
UINT nFormat = 0;
switch(nJustify & LVCFMT_JUSTIFYMASK) {
case LVCFMT_LEFT:
nFormat = DT_LEFT;
break;
case LVCFMT_RIGHT:
nFormat = DT_RIGHT;
break;
case LVCFMT_CENTER:
nFormat = DT_CENTER;
break;
default:
_M("CListCtrlEx: Error of the text formatting!");
return;
}
//*** Writes the text in the TmpRect
::DrawText(hDC, stColText, nTextLen, TmpRect, nFormat);
} // SetColItemText
Примеры реализации виртуального режима хорошо известны, поэтому мы не будем акцентировать на этом внимание.