Multi-column tree view

Multi-column tree view

Introduction

Why another multi-column tree view? Because most of the solutions I've seen re-invent the wheel by creating a completely new control. My code is only 12 KB long and it works excellent if you don't need all those colors, fonts, sorting and whatever. You can use the CColumnTreeView class in place of CTreeView with almost no modifications.

This class uses a standard tree control and a standard header control and just does a few tricks for drawing multi-column text and to scroll the view horizontally if that's necessary. It doesn't use any custom data structures, and you can use normal functionality of the CTreeCtrl object returned by the GetTreeCtrl function. So you don't have to rewrite the existing code. Item text can be splitted into column by using the '\t' (tabulator) character. You can't directly change a single column's text, but it most cases this is not necessary.

Using in your application

Step 1: Add the ColumnTreeView.cpp/.h and ColumnTreeCtrl.cpp/.h files to your project.

Step 2: Change the base class of your view from CTreeView to CColumnTreeView. You will have to do a search & replace in the implementation file and to add the proper #include directive.

Step 3: Add some columns to the header control. You may do it at any time, the best place is in OnInitialUpdate:

   CHeaderCtrl& header = GetHeaderCtrl();
   HDITEM hditem;
   hditem.mask = HDI_TEXT | HDI_WIDTH | HDI_FORMAT;
   hditem.fmt = HDF_LEFT | HDF_STRING;
   hditem.cxy = 200;
   hditem.pszText = "First column";
   header.InsertItem(0, &hditem);
   hditem.cxy = 100;
   hditem.pszText = "Second column";
   header.InsertItem(1, &hditem);
   // ...
   UpdateColumns();

Note: After changing the columns in the header control, you have to call the UpdateColumns method to update the tree contents.

Step 4: Adjust the style flags of the tree control in OnInitialUpdate, for example:

   CTreeCtrl& tree = GetTreeCtrl();
   DWORD dwStyle = GetWindowLong(tree, GWL_STYLE);
   dwStyle |= TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_FULLROWSELECT;
   SetWindowLong(tree, GWL_STYLE, dwStyle);

Since version 1.4, the TVS_FULLROWSELECT style is supported. It works much better than in the standard tree control, because buttons and lines are drawn correctly and the width of the selection bar is adjusted to the columns of the view. Note that drag and drop is not available in the full-row selection mode.

Another change in version 1.4 is that the width of the column is automatically adjusted to fit its contents when the divider in the header control is double-clicked. A column's width may also be adjusted programatically using the AdjustColumnWidth method.

You may work with the tree control the same way as with CTreeView. For example, you may change the style, set an image list and add and remove items. Use '\t' to separate columns in item labels.

   CTreeCtrl& tree = GetTreeCtrl();
   hItem = tree.InsertItem("Label\tSecond column", 0, 0);
   // ...

An important difference between CColumnTreeView and a normal CTreeView is the way notifications are handled. In case of CTreeView, notifications are handled using the ON_NOFIFY_REFLECT macros, which can be added automatically by Visual Studio. With <code>CColumnTreeView you should use the ON_NOTIFY macros. Visual Studio won't be able to create them, but you can use a simple trick: temporarily change the base class to CTreeView, add the necessary handlers, restore the base class and change the generated macros to ON_NOTIFY. The identifiers for the tree control and the header control are TreeID and HeaderID. For example:

   ON_NOTIFY(TVN_SELCHANGED, TreeID, OnTvnSelChanged)
   ON_NOTIFY(HDN_ITEMCLICK, HeaderID, OnHdnItemClick)

If you are porting existing code from CTreeView, don't forget to change the message map.

Using in a dialog window

In version 1.3, a new class CColumnTreeWnd was added to the project. It is basically the same as CColumnTreeView, except that it's derived from CWnd so that it's easier to use it in a dialog window. It may be created in the OnInitInstance method. The following code demonstrates how to create the control in place of a placeholder control:

   // find the placeholder, get its position and destroy it
   CRect rcTreeWnd;
   CWnd* pPlaceholderWnd = GetDlgItem(IDC_COLUMNTREE);
   pPlaceholderWnd->GetWindowRect(&rcTreeWnd);
   ScreenToClient(&rcTreeWnd);
   pPlaceholderWnd->DestroyWindow();

   // create the multi-column tree window
   m_TreeWnd.CreateEx(WS_EX_CLIENTEDGE, NULL, NULL, WS_CHILD | WS_VISIBLE,
      rcTreeWnd, this, IDC_COLUMNTREE);

After you create the tree window, you should adjust the style of the tree control:

   DWORD dwStyle = GetWindowLong(m_TreeWnd.GetTreeCtrl(), GWL_STYLE);
   dwStyle |= TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_FULLROWSELECT;
   SetWindowLong(m_TreeWnd.GetTreeCtrl(), GWL_STYLE, dwStyle);

The CColumnTreeWnd class re-routes all notifications from the tree control to the dialog window. Notifications can be handled by adding normal ON_NOTIFY macros to the message map, using the identifier of the CColumnTreeWnd window. For example:

   ON_NOTIFY(TVN_SELCHANGED, IDC_COLUMNTREE, OnTreeSelchanged)

How does it work?

The first time OnInitUpdate is called, the tree view window creates two child windows: a tree control and a header control. The tree control is created with the TVS_NOHSCROLL, otherwise two horizontal scroll bars would be displayed sometimes. Note that this style requires version 5.80 of common controls (so at least IE5 is required). In case the Windows XP visual style is used (version 6.00 of common controls), the header control is a bit higher so it looks correctly.

Custom controls are generally very difficult to override, so I left the default functionality untouched wherever possible. The main thing that had to be changed is obviously painting the items. I let the control draw itself first, so all buttons (those with +/- signs), lines, icons and everything else is drawn correctly, according to the control's style, current visual settings and so on. So everything I changed was adding the CDDS_ITEMPOSTPAINT phase to the custom draw handler. The control draws the item's text as it is, with all columns and a square character instead of the tabulator mark. So the label has to be cleared and drawn again with only the first column's text. Then, the text of the following columns is drawn in correct position, depending on the columns' width. I also added drawing grid lines which make the control more legible.

If the columns are wider than the control, a vertical scroll bar is added by calling SetScrollInfo. When the window is scrolled, the controls are repositioned so that the correct part of them is visible. The controls also have to be resized when the view is resized. The CColumnTreeCtrl class, which is derived from CTreeCtrl, overrides the OnPaint method and uses a temporary bitmap in memory to avoid flickering (which would be very noticeable as the original labels are cleared and drawn again). This method works well with common controls, because we can pass a device context in the WM_PAINT message and the control's message handler will use it instead of calling BeginPaint.

The control uses the length of the full string with all columns to calculate the area of the item's label. The problem is that we cannot simply override the TVM_HITTEST message, because it's not used internally by the control. The solution is to capture mouse clicks (WM_LBUTTONDOWN and WM_LBUTTONDBLCLK) and discard these messages if the mouse cursor is outside the real item's label (i.e. the first column).

Final notes

Of course there are much more things that could be added to this control. I didn't do them, because I simply didn't need to, and I wanted to keep the code as simple as possible. You can use it as a starting point for creating a control that fully suits your needs. For example you can change the way the individual columns are drawn, change colors, text alignment, add icons and whatever.

Version history

Version 1.4 (July 7, 2005)

Version 1.3 (April 20, 2005)

Version 1.2 (November 29, 2003)

Version 1.1 (October 22, 2003)

License

Multi-Column Tree View, version 1.4 (July 7, 2005)
Copyright (C) 2003-2005 Michal Mecinski.

You may freely use and modify this code, but don't remove this copyright note.

THERE IS NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, FOR THIS CODE. THE AUTHOR DOES NOT TAKE THE RESPONSIBILITY FOR ANY DAMAGE RESULTING FROM THE USE OF IT.