Creating tree view check boxes manually: Responding to clicks


Last time, we added state images to the tree view, but we didn't provide any way for the user to click on them. Let's add the code so that the user can click on the check box to change the value. For simplicity, we will just cycle through the state images. In real life, you would probably have more complex logic for deciding what to do when the user clicks on the check box, such as ignoring clicks on disabled check boxes.

Take our program from last time and make these additions:

void CycleStateImage(HWND hwndTV, HTREEITEM hti)
{
  UINT oldState = TreeView_GetItemState(hwndTV, hti,
                                        TVIS_STATEIMAGEMASK);
  int stateIndex = (int)(oldState & TVIS_STATEIMAGEMASK) >> 12;

  stateIndex = stateIndex + 1;
  if (stateIndex >= ImageList_GetImageCount(
                TreeView_GetImageList(hwndTV, TVSIL_STATE)))
  {
    // We ran out of states. Wrap around to the first state.
    stateIndex = 1;
  }

  TreeView_SetItemState(hwndTV, hti,
      INDEXTOSTATEIMAGEMASK(stateIndex), TVIS_STATEIMAGEMASK);
}

To cycle to the next state image, we request the state image mask from the current state, isolate the state image mask, and increment it, wrapping around to 1 if we were on the last state. (Note that we wrap around to 1 rather than to 0, because we saw last time that state image zero is not used; if you ask for state image zero, that means that you want no state image at all.)

LRESULT OnNotifyTVClicked(HWND hwndTV)
{
  TVHITTESTINFO tvhti;
  DWORD dwPos = GetMessagePos();
  tvhti.pt.x = GET_X_LPARAM(dwPos);
  tvhti.pt.y = GET_Y_LPARAM(dwPos);
  MapWindowPoints(HWND_DESKTOP, hwndTV, &tvhti.pt, 1);
  HTREEITEM hti = TreeView_HitTest(hwndTV, &tvhti);
  if (tvhti.flags & TVHT_ONITEMSTATEICON)
  {
    CycleStateImage(hwndTV, hti);
    return TRUE; // handled
  }
  return FALSE; // not handled
}

Frustratingly, the tree view control notifies us when it receives a click, but it doesn't tell us where the click was. We have to fetch it ourselves by calling Get­Message­Pos(), and then converting screen coordinates to client coordinates. Once we have those coordinates, we ask the tree view what is at those coordinates, and if it says "Oh, no big deal, just the state image¹ for an item," then we get all excited and cycle the state image.

LRESULT OnNotifyTVKeyDown(HWND hwndTV, NMTVKEYDOWN* ptvkd)
{
  switch (ptvkd->wVKey)
  {
  case VK_SPACE:
    HTREEITEM hti = TreeView_GetNextItem(hwndTV, nullptr,
                                         TVGN_CARET);
    if (hti)
    {
      CycleStateImage(hwndTV, hti);
      return TRUE; // handled
    }
  }
  return FALSE; // not handled
}

For keyboard accessibility, we use the space bar as an equivalent to clicking on the state image.

LRESULT OnNotify(HWND hwnd, int idFrom, NMHDR* pnm)
{
  if (pnm->hwndFrom == g_hwndChild)
  {
    switch (pnm->code)
    {
    case NM_CLICK:
      return OnNotifyTVClicked(pnm->hwndFrom);

    case TVN_KEYDOWN:
      return OnNotifyTVKeyDown(pnm->hwndFrom,
         CONTAINING_RECORD(pnm, NMTVKEYDOWN, hdr));
    }
  }
  return 0; // unhandled
}

// Add to WndProc
  HANDLE_MSG(hwnd, WM_NOTIFY, OnNotify);

Here is our WM_NOTIFY handler. If the notification is coming from the tree view control, then dispatch click notifications to On­Notify­TV­Clicked and dispatch key-down notifications to On­Notify­TV­Key­Down. The TVN_KEY­DOWN notification comes with a custom notification structure, so we convert our generic NMHDR pointer to a NMTV­KEY­DOWN pointer.

And there we have it. We manually implemented tree view check boxes. This code is effectively equivalent² to what you get when you turn on TVS_CHECK­BOXES and it's this code that was moved into the tree view control.

Next time, we'll bring tree view check boxes into the 21st century.

¹ Note the inconsistent terminology. Normally, the state image is called a "state image", but here, it's called a "state icon".

² Plus a few more quirks we'll learn about later.

Comments (3)
  1. Neil says:

    Normally I’d use the the % operator for anything that deliberately wraps around (e.g. November + 3 = February) although the usage would be a little awkward in this case.

    1. IInspectable says:

      That pattern is fine, if you are using the full range of values (i.e. [0 .. N)). In this case, however, index 0 is unused. Applying the modulo-operator will make the expression more complex, and harder to comprehend and debug.

      1. ErikF says:

        The operation isn’t that hard (it’s just [0..N-1] => [1..N], so it would look like “(index % (N-1)) + 1)”), but it’s sufficiently obtuse that I’d have to comment it really thoroughly. I have to assume that compilers nowadays are smart enough to find these kinds of operations and optimize them, so there’s less need for the fancy math.

Comments are closed.

Skip to main content