Menu

List View Control tip and tricks

List View Control tips and tricks

The ListView control is one of the Window common controls, introduced way back with Windows 95. In OWLNext it is encapsulated by the TListViewCtrl class. It can display collection of items in different views - as icon and label, or as a grid with column headers and data cells.
It provides a lot of customization options, but some are not trivial to achieve. Following are examples of such advanced customizations:


Alternate row coloring


Alternate row coloring can be implemented by handling the NM_CUSTOMDRAW notification of list view control:

class TListViewWindow : public owl::TWindow
{
...
private:
  owl::TListViewCtrl List;
  owl::TColor oddLineColor, evenLineColor;

  int NmCustomDraw(owl::TNmCustomDraw& nm);
}

DEFINE_RESPONSE_TABLE1(TListViewWindow, TWindow)
...
EV_NM_CUSTOMDRAW(ListId, NmCustomDraw),
END_RESPONSE_TABLE;

TListViewWindow::TListViewWindow(TWindow* parent)
  : TWindow(parent),
  List(this, ListId, 0, 0, 0, 0),
  oddLineColor(TColor::White),
  evenLineColor(230, 230, 230)
{
...
  List.ModifyStyle(0, LVS_REPORT | LVS_SHOWSELALWAYS);
}
...
void TListViewWindow::SetupWindow() // override
{
  TWindow::SetupWindow();

  List.SendMessage(LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_FULLROWSELECT, LVS_EX_FULLROWSELECT);
  ...
}

int TListViewWindow::NmCustomDraw(owl::TNmCustomDraw& nm)
{
  TLvCustomDraw& lvnm = *((TLvCustomDraw*)&nm);
  int row = (int)nm.dwItemSpec;

  switch (nm.dwDrawStage)
  {
  case CDDS_PREPAINT:
    return CDRF_NOTIFYITEMDRAW;

  case CDDS_ITEMPREPAINT:
  {
    if (row % 2 == 1)
      lvnm.clrTextBk = oddLineColor;
    else
      lvnm.clrTextBk = evenLineColor;

    return CDRF_DODEFAULT;
  }

  case CDDS_SUBITEM | CDDS_ITEMPREPAINT:
    return CDRF_DODEFAULT;

  default:
    return CDRF_DODEFAULT;
  }
}

The full code is included in the Classes sample project in the 6.44 branch.

Tooltips

Adding tooltips to the main item of a list view (the first column in report view) is fairly easy - by adding the LVS_EX_INFOTIP extended style and handling the LVN_GETINFOTIP notification:

DEFINE_RESPONSE_TABLE1(TListViewWindow, TWindow)
...
EV_LVN_GETINFOTIP(ListId, LvnGetInfoTip),
END_RESPONSE_TABLE;

void TListViewWindow::SetupWindow() // override
{
  TWindow::SetupWindow();

  List.SendMessage(LVM_SETEXTENDEDLISTVIEWSTYLE, LVS_EX_INFOTIP, LVS_EX_INFOTIP);
  ...
}

void TListViewWindow::LvnGetInfoTip(TLvGetInfoTip& git)
{
  TLvItem item = List.GetItem(git.iItem);

  tostringstream sTooltip;
  sTooltip << _T("Tooltip for item #") << git.iItem << _T(" : ") << item.GetText();

  _tcscpy_s(git.pszText, git.cchTextMax, sTooltip.str().c_str());
}

It is more complicated to add tooltips to the column headers, but it can be very useful for situations when a column is too narrow to display the full text. In this case, the standard OWLNext tooltip support can help - for each column header, a tooltip for it's rectangular region can be added.

class TListViewWindow : public owl::TWindow
{
...
private:
 owl::TIPtrArray<owl::TToolInfo*> headerTooltips; // Hold the list of header tooltips
}

void TListViewWindow::SetupWindow() // override
{
  TWindow::SetupWindow();
  ...
  // Enable tooltips for the ListView control
  List.EnableTooltip(true); 

  // Get the header control height
  TRect headerRect;
  ::GetClientRect(List.GetHeaderCtrl(), &headerRect);

   // Determine the number of columns
  int columnCount = Header_GetItemCount(List.GetHeaderCtrl());
  int left = 0;
  for (int index = 0; index < columnCount; ++index)
  {
    TLvColumn column;
    if (List.GetColumn(index, column))
    {
      tostringstream sTooltip;
      sTooltip << _T("Tooltip for ") << column.GetText();

      // Create the tooltip for the column header
      TToolInfo* ti = new TToolInfo(List.GetHeaderCtrl(), TRect(left, 0, left + column.GetWidth(), headerRect.Height()), index, sTooltip.str());

      // Add the tooltip to the control
      List.GetTooltip()->AddTool(*ti);
      headerTooltips.Add(ti);

      left += column.GetWidth();
    }
  }

When the columns are resized, the tooltip rectanges needs to be updated. This can happen by handling the HDN_ITEMCHANGED notification. Since the notification is sent by the child header control, for which the id is unknown, it cannot be done with the EV_HDN_ITEMCHANGED macro. Instead, the generic EvNotify method must be overriden:

TResult TListViewWindow::EvNotify(uint id, TNotify & notifyInfo)
{
  if (notifyInfo.hwndFrom == List.GetHeaderCtrl())
  {
    // Check for both the Ansi and Unicode notification codes
    if ((int)notifyInfo.code == (int)HDN_ITEMCHANGEDW || (int)notifyInfo.code == (int)HDN_ITEMCHANGEDA)
    {
      // Get the header control height
      TRect headerRect;
      ::GetClientRect(List.GetHeaderCtrl(), &headerRect);

      int left = 0;
      for (int index = 0; index < headerTooltips.size(); ++index)
      {
        TLvColumn column;
        if (List.GetColumn(index, column))
        {
          //Update the tooltip rectangle
          headerTooltips[index]->SetRect(TRect(left, 0, left + column.GetWidth(), headerRect.Height()));
          List.GetTooltip()->NewToolRect(*headerTooltips[index]);
          left += column.GetWidth();
        }
      }
    }
  }

  return TWindow::EvNotify(id, notifyInfo);
}

A similar code can be used to add tooltips for all the subitems in report view. In this case, the tooltip rectangles may have to be recalculated when the list view is scrolled, resized, etc.

Custom column headers

The header of the list view is a separate control that is a child of the list view, and it has its own window ID. By default this is set to 0, which is not a valid ID for use in the response table. Hence you have to explicitly set a proper ID for the header. Having done that, you can then add entries to your response table to handle notification messages from the header, just as from the list view itself. See "FAQ | How do I handle notifications from the header of a List View? ".

Alternatively, notification messages from the header can be intercepted in the virtual EvNotify method:

TResult TListViewWindow::EvNotify(uint id, TNotify& notifyInfo)
{
  if (notifyInfo.hwndFrom == List.GetHeaderCtrl())
  {
        if ((int)notifyInfo.code == (int)NM_CUSTOMDRAW)
        {
            TNmCustomDraw& nm = *((TNmCustomDraw*)& notifyInfo);

            switch (nm.dwDrawStage)
            {
            case CDDS_PREPAINT:
                return CDRF_NOTIFYITEMDRAW;

            case CDDS_ITEMPREPAINT:
            {
                TDC dc(nm.hdc);

                TRect rect(nm.rc);
                dc.SelectObject(TPen(TColor::Gray));
                dc.SelectObject(TBrush(TColor::LtGray));
                dc.FillSolidRect(rect, TColor::LtGray);
                dc.Rectangle(rect);

                TLvColumn column;
                if (List.GetColumn(nm.dwItemSpec, column))
                {
                    tstring str(column.GetText());

                    //TSize size = dc.GetTextExtent(str, str.length());
                    dc.ExtTextOut(TPoint(nm.rc.left + 4, nm.rc.top + 2), ETO_CLIPPED, &rect, str);
                }

                return CDRF_SKIPDEFAULT;
            }

            default:
                return CDRF_DODEFAULT;
            }
        }
  }

  return TWindow::EvNotify(id, notifyInfo);
}

Custom group headers


List Views support grouping of items, which can be done by first creating the groups, and then setting the iGroupId field of each item:

  ListView_EnableGroupView(List.GetHandle(), TRUE);

  _USES_CONVERSION_A;   //Note: Header is always a wide char string, so need to convert when building without Unicode
  LVGROUP group;
  group.cbSize = sizeof(LVGROUP);
  group.state = LVGS_COLLAPSIBLE;
  group.mask = LVGF_HEADER | LVGF_GROUPID | LVGF_STATE;

  group.pszHeader = _A2W_A(_T("Group 1")); 
  group.iGroupId = 1;
  ListView_InsertGroup(List.GetHandle(), -1, &group);

  group.pszHeader = _A2W_A(_T("Group 2"));
  group.iGroupId = 2;
  ListView_InsertGroup(List.GetHandle(), -1, &group);

The group headers can be fully customized by implementing drawing in the NM_CUSTOMDRAW handler:

int TListViewWindow::NmCustomDraw(owl::TNmCustomDraw& nm)
{
  TLvCustomDraw& lvnm = *((TLvCustomDraw*)&nm);
  int row = (int)nm.dwItemSpec;

  if (lvnm.dwItemType == LVCDI_GROUP)
  {
    TDC dc(nm.hdc);
    dc.SelectObject(TPen(TColor::Sys3dShadow));
    dc.SelectObject(TBrush(TColor::SysInactiveCaption));

    int columnCount = Header_GetItemCount(List.GetHeaderCtrl());
    int left = 0;
    for (int index = 0; index < columnCount; ++index)
    {
      TLvColumn column;
      if (List.GetColumn(index, column))
      {
        tstring str(column.GetText());

        TRect rect(left, lvnm.rcText.top, left + column.GetWidth(), lvnm.rcText.bottom);
        dc.Rectangle(rect);
        //TSize size = dc.GetTextExtent(str, str.length()); // Can be used to draw it centered or right aligned
        dc.ExtTextOut(TPoint(left + 4, lvnm.rcText.top + 2), ETO_CLIPPED, &rect, str);

        left += column.GetWidth();
      }
    }

    return CDRF_DODEFAULT;
  }
  else if (lvnm.dwItemType == LVCDI_ITEM)
  ...

For example, the group headers can display totals for the items in the group.

Sort order indicator in column headers


When a List View column is sorted in ascending or descending order, it is helpful if the column header displays an indicator of the sort order. This can easily be achieved by setting the HDF_SORTUP or HDF_SORTDOWN format flag in the HDITEM structure. Here is a sample code how to set the initial sort indicator.

      auto header = GetHeaderCtrl();
      HDITEM hdItem;
      hdItem.mask = HDI_FORMAT;  // We need just the format flags
      Header_GetItem(header, SortedColumn, &hdItem);

      if (ShouldSortAscending)
      {
        hdItem.fmt |= HDF_SORTUP;
      }
      else
      {
        hdItem.fmt |= HDF_SORTDOWN;
      }

      Header_SetItem(header, SortedColumn, &hdItem);

In the case of user changing the sort order by clicking on a column header, the code may need to first get the item for the previous sort column, clear its indicator, and then set the indicator in the new column header.


Related

Wiki: Knowledge_Base
Wiki: Virtualized List View with grouping

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.