Application Security Testing: An Integral Part of DevOps
Environment: VC6 and .NET, IE5 (comctl32.dll version 5.80)
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 excellently 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.
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.
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 by using the ON_NOFIFY_REFLECT macros, which can be added automatically by Visual Studio. With 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.
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 scrollers 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 correct.
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 that 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 noticable 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).
Of course, there are many 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.
Author's Home Page
You may find the newest versions of my articles at www.mimec.w.pl.
DownloadsDownload demo project - 32 Kb
Download source - 5 Kb