Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Multi-line List Control

0.00/5 (No votes)
5 Sep 2006 1  
A custom control which provides a multi-column list of items with multi-line rows.

MultilineListControl1.gif

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).

/**
* Sets the overall geometry of the grid. When reducing the size using this
* method, anything which is then outside the new size is deleted. You must call
* this before anything else to ensure the size you need is available.
* @param nCols Desired number of columns (which are numbered [0...N-1])
* @param nRows Desired number of rows (which are numbered [0...N-1])
*/
void SetSize(int nCols, int nRows);

/**
* Gets the current overall geometry of the grid.
* @param nCols will be initialized with the number
*        of columns (which are numbered [0...N-1])
* @param nRows will be initialized with 
*        the number of rows (which are numbered [0...N-1])
*/
void GetSize(int & nCols, int & nRows);

/**
* Sets the text shown in the heading above the specified column.
* @param col the column number [0...N-1]
* @param heading the text to be displayed
*/
void SetColHeading(int col, LPCTSTR heading);

/**
* Sets the width of the specified column in pixels. Note that the user cal
* also change this themselves by dragging the column separators in the
* header control.
* @param col the column number [0...N-1]
* @param width desired width of the column in pixels
*/
void SetColWidth(int col, int width);

/**
* Sets the text for the specified cell. The text may contain '\n' which
* will cause line breaks and heightening of the row as needed. The text
* will also be word-wrapped to ensure it fits within the column width.
* @param col the column number [0...N-1]
* @param row the row number [0...N-1]
* @param text the desired text for the cell
*/
void SetCellText(int col, int row, LPCTSTR text);

/**
* Gets the current text in the specified cell.
* @param col the column number [0...N-1]
* @param row the row number [0...N-1]
* @return text in the specified cell
*/
CString GetCellText(int col, int row);

/**
* changes the current selection to the specified row. changing the selection
* programmatically using this method will not trigger a WM_COMMAND notification
* message to be sent to the parent window.
* @param row the row number to select [0...N-1]
*/
void SetSelRow(int row);

/**
* Gets the row number [0...N-1] of the currently selected row.
* @return the currently selected row number
* [0...N-1] or -1 if no row is currently selected
*/
int GetSelRow();

/**
* Gets the row which is displayed at the specified client co-ords in the window
* @param pt the point in client coords
* @return the row number [0...N-1] which the specified
* point is over (or -1 if there is no row at that point)
*/
int GetRowFromPoint(CPoint pt);

/**
* Ensure the specified row is visible
* @param row the row number [0...N-1]
*/
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.

// create a list with 3 columns and 5 rows

m_items.SetSize(3,5);

// first two columns 100 pixels wide and the third column 200 pixels wide

m_items.SetColWidth(0,100);
m_items.SetColWidth(1,100);
m_items.SetColWidth(2,200);

// set the column heading text

m_items.SetColHeading(0,_T("Name"));
m_items.SetColHeading(1,_T("Quantity"));
m_items.SetColHeading(2,_T("Description"));

// populate the list data

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:

MultilineListControl2.gif

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)
{
   // IDC_ITEMS is the ID assigned to the control in this dialog

   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

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here