Introduction
This article provides a custom control which displays a multi-column list of items, with support for multi-line text in individual cells. Cell text can contain embedded '\n' characters which will force line breaks. In addition, word-wrap is performed as needed to keep the text within the specified column width.
The utility of this is that it allows you to display a list of items which can contain multi-line text. You and/or the user sets the column widths, and the control dynamically picks the row heights to support the cell text. The standard list control does not support multi-line text (or varying row heights which are needed to support multi-line text).
The demo project linked above provides the source, and an executable for an app which populates the control as shown in the image above. The demo app also handles notification of selection changes by updating the title bar to indicate which item has been selected.
The code was compiled with Visual Studio .NET 2003 with SP1 applied (Visual C++ v7.1).
Background
The need to display information in a tabular format is usually satisfied by using the standard list control in report mode. A limitation of this solution is the lack of support for multi-line text in the cells.
This custom control was created with the goal of addressing this limitation. The idea was to allow the control user to set the column widths and have the control dynamically adjust the individual row heights as needed to fit the text within those widths. The control should word-wrap the text as needed, and should honor any embedded '\n' characters by forcing line breaks.
In designing and coding this control, the focus was solely on removing the above limitation, as opposed to creating the ultimate grid control (see the References section for some complete grid control articles). As a result, this control does not do everything the standard list control does, and does far less than many higher-end grid controls. The idea was to provide textual multi-column data in a tabular format such that multi-line text would just work as expected. The limited scope was aimed at providing a very simple, easy to use solution to this particular limitation of the standard list control.
Having said that, this control is not derived from the standard list control since it didn't seem feasible to modify the existing control to remove the single-line limitation. Therefore, the interface isn't a drop-in replacement for the standard list control. See the remainder of the article for details on how to use the control.
Using the code
Adding the control to your project
To use the control in your code, add the CMultilineList.h and CMultilineList.cpp files to your project.
Creating the control
For some background on creating and using custom controls, you may wish to read Chris Maunder's Creating Custom Controls[^] article.
If you're using the Visual Studio dialog editor, you can create a custom control and set the Class
string in the properties editor to CMultilineList
. You can then right-click on the custom control and select "Add Variable", and add a control variable of type CMultilineList
.
If you'd instead like to dynamically create the control yourself, just create an instance of the CMultilineList
class and then call the Create
method.
Populating the list
The first thing you'll want to do is set the overall size of the list (# columns and # rows). Once you've done this, you can setup the column widths, specify text for the column headings, and then fill in the cells with text. The public methods used to perform these tasks (and a few others) are shown below.
The row and column indices are zero-based, and there is no item/sub-item distinction as with the standard list control (since only a report/grid-style view is supplied).
void SetSize(int nCols, int nRows);
void GetSize(int & nCols, int & nRows);
void SetColHeading(int col, LPCTSTR heading);
void SetColWidth(int col, int width);
void SetCellText(int col, int row, LPCTSTR text);
CString GetCellText(int col, int row);
void SetSelRow(int row);
int GetSelRow();
int GetRowFromPoint(CPoint pt);
void EnsureRowIsVisible(int row);
A brief example which illustrates using some of the above methods is shown below. In the example below, m_items
is a variable of type CMultilineList
.
m_items.SetSize(3,5);
m_items.SetColWidth(0,100);
m_items.SetColWidth(1,100);
m_items.SetColWidth(2,200);
m_items.SetColHeading(0,_T("Name"));
m_items.SetColHeading(1,_T("Quantity"));
m_items.SetColHeading(2,_T("Description"));
m_items.SetCellText(0,0,_T("Coffee Beans"));
m_items.SetCellText(1,0,_T("12"));
m_items.SetCellText(2,0,
_T("An essential part of the daily diet for ensuring " )
_T("productivity is at required levels.\n\nNOTE: Decaf is for wimps!"));
m_items.SetCellText(0,2,_T("Water"));
m_items.SetCellText(1,2,_T("10"));
m_items.SetCellText(2,2,_T("Listed as a dependency of the coffee beans module."));
The results of the above sample code are shown in the image below:
Handling notifications from the list
When the user clicks in the control to select an item, a notification is sent to the control's parent window. For the sake of simplicity, the list box LBN_SELCHANGE
message is used (instead of using the list control notification message). This is sent as a WM_COMMAND
message to the parent, just as with a list box. The below code excerpt is taken from the demo application, and illustrates responding to a selection change by updating the titlebar text.
BOOL CMultilineListDemoDlg::OnCommand(WPARAM wParam, LPARAM lParam)
{
if ((HIWORD(wParam) == LBN_SELCHANGE) &&
(LOWORD(wParam) == IDC_ITEMS))
{
int selRow = m_items.GetSelRow();
if (selRow < 0)
{
SetWindowText(_T("MultilineListDemo"));
else
{
CString s;
s.Format(_T("MultilineListDemo (you have selected %s)"),
m_items.GetCellText(0,selRow));
SetWindowText(s);
}
}
return CDialog::OnCommand(wParam, lParam);
}
Points of interest
Although the goal was to add multi-line text, creating a custom control to do that brought in a bunch of other requirements. There are a number of things which are taken for granted in the standard controls which must be handled in a custom control. For example, maintaining the current font, figuring out how to scale/position everything and respond to events appropriately in an attempt to make the control look and behave like a "normal" list control, and other sneaky things which would pop up during initial testing.
One of the major impacts from supporting multi-line text was the varying row heights. When rendering the visible area of the list, it is of course necessary to compute the row heights for those rows. But it becomes necessary to know all the row heights when, for example, getting the scroll bars to work as expected and supporting item selection. The class uses a std::map
which maps integer row numbers to pixel row heights. The assumption is that any height in the map is valid. Row heights are invalidated by removing them from the map. So, for example, when the text of a cell is changed, the corresponding row height is invalidated. When column widths are dragged by the user (or set programmatically), all the row heights are invalidated. The CalculateRowHeights()
method is then used to calculate any row heights not already in the map. Many other sections of the code then rely on being able to have this map of row heights to do their calculations.
Finally, a disclaimer is probably necessary. I'm sure I missed a number of "standard" behaviors which might be expected from the standard controls, although hopefully I didn't manage to miss them all. There isn't complete support for styles and heavy customization etc., and the full suite of notifications isn't yet provided. So, consider this an initial/draft release.
References
- "Creating Custom Controls", by Chris Maunder[^] - Provides background on creating your own custom control and on using someone else's custom control in your code.
- "Changing Row Height in an Owner Drawn Control", by Uwe Keim[^] - Illustrates changing row heights for a list view control-derived class, but with uniform row heights.
- "The Grid Control", by Chris Maunder, Ken Bertelson, Mario Zucca, and Fred Ackers[^] - Very well done grid control with tons of functionality; a bit more than what I wanted to do, and multiline cell text didn't appear to be a primary feature. The grid control comments section has an explanation of how you could do multiline text, but it seemed more involved than what I wanted. My goal was to be able to just set the text and let the control figure it out for me.
- "Report control - with category", by Andre Arpin[^] - Supports multi-line text, but it also did way more than what I wanted to do, there was limited documentation, and the interface seemed more involved. I was looking for a very simple interface to handle multi-line text without all the other bells and whistles.
- "CodeProject Article Writer Helper", by Jason Henderson[^] - Great tool for writing CodeProject articles.
License
The LGPL open source license[^] was selected for the code, since that does not restrict how you can use the code in your own projects or enforce any licensing model on your projects which use it (unlike GPL). It only asks that if you modify/improve/fix the code that those modifications/improvements/fixes are also contributed back to the community.
History
- September 4, 2006 - initial release.