Tree view check boxes: A sordid history


Tree view check boxes were not part of the original design of tree view controls, so people who needed check boxes rolled their own. At some point, somebody decided to do people a favor and move that code into the tree view, but did so in a way that preserved the way an external consumer would add check boxes, rather than integrating it with the tree view. I don't know whether this was due to simplicity of implementation (just taking existing code and moving it into the tree view control), or in order to preserve maximum compatibility with the existing code base of people using the external helper library (so you can tell people, "Just set this style and everything works the same as the library you are already using"). Or most likely both.

One part of the craziness enters the picture because the tree view needs to know how big to make the check boxes. If you have images associated with each item (which the tree view control calls the "normal image list"), then the check boxes should match the size of the images. But if you don't have any images, then the check boxes should match the system small icon size. The tree view cannot see into the future to know whether you are ever going to set the normal image list, so you need to give the tree view a signal to say "Okay, you can create the check boxes now."

Another part of the craziness is the fact that an item with a state image index of zero gets no state image. Since zero is the default state image index, it would mean that by default, even if you enable check boxes, you won't get any check boxes. Which is kind of unexpected. Therefore, the tree view control, as a courtesy, sets all the state image indices to 1 when it enables check boxes, thereby setting all the items into the unchecked state, which is a much more reasonable default than "no check box at all."

Given that the check boxes need to be created after the normal image list is set, it doesn't make sense to set the style at control creation time, because the control can't actually create the check boxes at creation time since you clearly haven't satisfied the prerequisites yet.

In retrospect, the use of a window style to trigger the creation of check boxes was a bad idea. It probably should have been a message like TVM_ACTIVATE­CHECK­BOXES. That makes it clearer that you need to prepare the tree view control, and then send the message to tell the control, "Okay, I'm all set. Go create the check boxes."

But it's a style, and we have to live with that. The tree view control responds to changes in the TVS_CHECK­BOXES style to initialize check box operations, because that's how things worked manually: When you manually created a tree view with check boxes, you created the tree view, and then added the check boxes.

That's why you have to set the style programmatically rather than doing so in the dialog template. If you set it in the dialog template, then the style is set at creation and no change is ever observed. If you were manually creating the tree view with check boxes, you had to add the check boxes later, and that's how the code was migrated into the tree view control.

And the act of showing the tree view control for the first time causes all the check boxes to reset because the rendering code has a special test that says, "If the TVS_CHECK­BOXES style is set, and I never observed it being set, then go and initialize the check boxes right now, before it's too late!" And one of steps in initializing the check boxes is setting all of the check box states get to unchecked. If it didn't do that, then there wouldn't be any check boxes at all, because the default state image for a tree view item is "blank".

But this means that when you add an item to the tree view, and you say that you don't want a state image, the tree view will say, "You said you wanted check boxes, but now you're adding an item with no check box at all. I'm going to assume you simply forgot to specify that you wanted an unchecked check box, so I'll give you an unchecked check box."

If you really did want to add an item with no check box at all, then you need to add it normally (whereupon it will be given an unchecked check box), and then clear the state image.

// First add the item normally.
// The tree view control will assign the unchecked check box
// state if you didn't specify a state or specified a state
// of zero.
HTREEITEM hti = TreeView_InsertItem(hwndTV, &tvis);

// Then explicitly remove the state image.
TreeView_SetItemState(hwndTV, hti,
                      INDEXTOSTATEIMAGEMASK(0),
                      TVIS_STATEIMAGEMASK);

There's another quirk of the TVS_CHECK­BOXES style: In versions prior to Windows Vista, the check boxes are created when the normal image list changes from null to non-null. And if you change it twice, then it creates the check boxes twice (and leaks the first one).

This leak was fixed in Windows Vista in version 6 of the common controls. From Windows Vista onward, if you use version 6 of the common controls, the check boxes will not be recreated if you set the normal image list more than once.

Given all these crazy behaviors, the best way to set up a tree view with check boxes is to do the following, in order:

  • Create the control without the TVS_CHECK­BOXES style.
  • Set the normal image list, if you want one.
  • Turn on the check boxes, either by setting the TVS_CHECK­BOXES style (if all you want is unchecked and checked) or setting one or more of the TVS_EX_XXX­CHECK­BOXES styles (if you want other states, too).
  • Do not touch any of the checkbox-related styles any more. You get one chance, and that's it.
  • If in the future you need to change the normal image list, change it from a non-null image list to another non-null image list. Do not change it to null, and then change it to non-null. This avoids the leak mentioned above.
  • Add your items.
  • If you need to add an item with no check box at all, first add it normally (whereupon it gets an empty check box assigned automatically), and then remove the check box manually, as noted above.

Wait, what are these TVS_EX_XXX­CHECK­BOXES extended styles? We'll look at those next time.

Comments (6)

  1. Frankly, this series could be renamed to “how not to add a feature to a control”. Given the care put by Microsoft in the design of the public interfaces (APIs, message model, etc.) of Windows and its user interface library, it’s surprising this got into production without raising some eyebrows.

    1. cheong00 says:

      Btw, I’d be interested to know how Microsoft staffs did their code review in the old days (at the time like Win2.X or DOS). Did they only get review when there’s some bad smell of code detected by seniors or was there routine sampled checks?

    2. pc says:

      Well, as Raymond says if you understand the history you can understand why the tradeoffs that were made at the time may have made sense at the time. First you don’t have a concept of “treeview with checkboxes”, but you do have “treeview with ability to put images next to items”. So, people build their own libraries to use checkboxes for those images, in the best way they can (without even using undocumented internal structures). Next, Microsoft when trying to figure out what features to add, decides that since everybody wants treeviews with checkboxes, to go ahead and add them. Then, they have to decide which way to do so: (1) In a way that’s easily backwards-compatible with the existing codebases and libraries, or (2) In a more “Windows-native” way that makes more sense with how other controls are created, meaning that anybody who wants to use it needs to rewrite their code.

      Microsoft has historically chosen option (1) whenever possible, as Raymond keeps on demonstrating. Engineering is all about understanding tradeoffs, and I’m sure that they made this one intentionally.

      And if you think there should be an option (3) Do Both, then, well, the feature would have taken at least twice as long and might not have been included in the first place.

      There are no good answers. There is merely understanding the tradeoffs one is making.

      1. viila says:

        I don’t see why (3) should take twice as long. There is no reason why the enhancement has to be backwards compatible, adding new proper way wouldn’t stop the original workaround from working and the workaround that existing software already uses is already in place. You don’t need to do anything, just let it be. (In fact, since it’s being used by software it has to remain working no matter what you do.) Then implement (2) in the proper way.

        Old software and people who like the old libraries continue to work as they always have. New software can take advantage of the new Right Way(tm) to do things.

        1. Realistically, there is no option 2. Option 1 was “Hey, our team made a nice routine to add checkboxes to the treeview last week. I can put it into common controls with an hour or two of work, maybe, save everyone the hassle in the future.” Option 3 involves tens of man-hours in meetings to design it robustly, then more hours to code it from scratch. Minimal effort won, in a way that it probably wouldn’t now.

  2. Yuhong Bao says:

    I wonder if it is possible to go through NULL when setting a new image list. The idea is ImageList_Destroy(TreeView_SetImageList(hwndTV, nullptr, TVSIL_STATE)); TreeView_SetImageList(hwndTV, newImageList, TVSIL_STATE);

Skip to main content