Creating tree view check boxes manually: Themed check boxes


Originally, we created our check box state image list with Draw­Frame­Control. But that gives you the old and busted check boxes. What if you want the new hotness of visual styles?

Enter Draw­Theme­Background.

#include <uxtheme.h>
#include <vsstyle.h>

HIMAGELIST CreateTreeViewCheckBoxes(HWND hwnd, int cx, int cy)
{
  const int frames = 6;

  // Get a DC for our window.
  HDC hdcScreen = GetDC(hwnd);

  // Get a button theme for the window, if available.
  HTHEME htheme = OpenThemeData(hwnd, L"button");

  // If there is a theme, then ask it for the size
  // of a checkbox and use that size.
  if (htheme) {
    SIZE siz;
    GetThemePartSize(htheme, hdcScreen,
      BP_CHECKBOX, CBS_UNCHECKEDNORMAL, nullptr,
      TS_DRAW, &siz);
    cx = siz.cx;
    cy = siz.cy;
  }

  // Create a 32bpp bitmap that holds the desired number of frames.
  BITMAPINFO bi = { sizeof(BITMAPINFOHEADER), cx * frames, cy, 1, 32 };
  void* p;
  HBITMAP hbmCheckboxes = CreateDIBSection(hdcScreen, &bi,
    DIB_RGB_COLORS, &p, nullptr, 0);

  // Create a compatible memory DC.
  HDC hdcMem = CreateCompatibleDC(hdcScreen);

  // Select our bitmap into it so we can draw to it.
  HBITMAP hbmOld = SelectBitmap(hdcMem, hbmCheckboxes);

  // Set up the rectangle into which we do our drawing.
  RECT rc = { 0, 0, cx, cy };

  // Frame 0 is not used. Draw nothing.
  OffsetRect(&rc, cx, 0);

  if (htheme) {
    // Frame 1: Unchecked.
    DrawThemeBackground(htheme, hdcMem, BP_CHECKBOX,
      CBS_UNCHECKEDNORMAL, &rc, nullptr);
    OffsetRect(&rc, cx, 0);

    // Frame 2: Checked.
    DrawThemeBackground(htheme, hdcMem, BP_CHECKBOX,
      CBS_CHECKEDNORMAL, &rc, nullptr);
    OffsetRect(&rc, cx, 0);

    // Frame 3: Indeterminate.
    DrawThemeBackground(htheme, hdcMem, BP_CHECKBOX,
      CBS_MIXEDNORMAL, &rc, nullptr);
    OffsetRect(&rc, cx, 0);

    // Frame 4: Disabled, unchecked.
    DrawThemeBackground(htheme, hdcMem, BP_CHECKBOX,
      CBS_UNCHECKEDDISABLED, &rc, nullptr);
    OffsetRect(&rc, cx, 0);

    // Frame 5: Disabled, checked.
    DrawThemeBackground(htheme, hdcMem, BP_CHECKBOX,
      CBS_CHECKEDDISABLED, &rc, nullptr);

    // Done with the theme.
    CloseThemeData(htheme);
  } else {
    // Flags common to all of our DrawFrameControl calls:
    // Draw a flat checkbox.
    UINT baseFlags = DFCS_FLAT | DFCS_BUTTONCHECK;

    // Frame 1: Unchecked.
    DrawFrameControl(hdcMem, &rc, DFC_BUTTON,
      baseFlags);
    OffsetRect(&rc, cx, 0);

    // Frame 2: Checked.
    DrawFrameControl(hdcMem, &rc, DFC_BUTTON,
      baseFlags | DFCS_CHECKED);
    OffsetRect(&rc, cx, 0);

    // Frame 3: Indeterminate.
    DrawFrameControl(hdcMem, &rc, DFC_BUTTON,
      baseFlags | DFCS_CHECKED | DFCS_BUTTON3STATE);
    OffsetRect(&rc, cx, 0);

    // Frame 4: Disabled, unchecked.
    DrawFrameControl(hdcMem, &rc, DFC_BUTTON,
      baseFlags | DFCS_INACTIVE);
    OffsetRect(&rc, cx, 0);

    // Frame 5: Disabled, checked.
    DrawFrameControl(hdcMem, &rc, DFC_BUTTON,
      baseFlags | DFCS_INACTIVE | DFCS_CHECKED);
  }

  // The bitmap is ready. Clean up.
  SelectBitmap(hdcMem, hbmOld);
  DeleteDC(hdcMem);
  ReleaseDC(hwnd, hdcScreen);

  // Create an imagelist from this bitmap.
  HIMAGELIST himl = ImageList_Create(cx, cy, ILC_COLOR, frames, frames);
  ImageList_Add(himl, hbmCheckboxes, nullptr);

  // Don't need the bitmap any more.
  DeleteObject(hbmCheckboxes);

  return himl;
}

void OnThemeChange(HWND hwnd)
{
  // Rebuild the state images to match the new theme.
  HIMAGELIST himl = CreateTreeViewCheckBoxes(g_hwndChild,
    16, 16);
  ImageList_Destroy(TreeView_SetImageList(g_hwndChild,
                                     himl, TVSIL_STATE));
}

// WndProc
  case WM_THEMECHANGE:
    OnThemeChange(hwnd);
    break;

We spruce up the Create­Tree­View­Check­Boxes function by having it check whether visual styles are enabled for the window. If so, it uses the button theme to draw the check boxes in the various states. If not, then it falls back to our existing Draw­Frame­Control version.

We also respond to the WM_THEME­CHANGE message by creating a new state image list which match the new theme. We then exchange that state image list into place and destroy the previous (old and busted) state image list.

That's it. The rest is the same as before.

Next time, we'll engage in some historical speculation to help explain why the built-in tree view check boxes are so quirky.

Comments (6)
  1. Adrian says:

    I assume WM_SYSCOLORCHANGE be handled like WM_THEMECHANGED? If themes are not enabled, you still might need to redraw the checkboxes if the system colors change.

  2. Serhii says:

    But how about animations? You know, fading between images on mouse hover, for example? It seems like the change here is instantenious.

  3. Neil says:

    Won’t the caller of CreateTreeViewCheckBoxes get confused when it asks for 16×16 images but gets back an image list of images of a different size?

  4. Tihiy says:

    Consider adding per-display DPI support here.

    1. Pietro Gagliardi (andlabs) says:

      The way this sample is written, this particular change would be trivial, if not one-line.

  5. xtal256 says:

    Wow, a Men In Black reference. Raymond, you constantly surprise me.

Comments are closed.

Skip to main content