Этот материал ранее был опубликован в виде статьи на 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
Примеры реализации виртуального режима хорошо известны, поэтому мы не будем акцентировать на этом внимание.