Skip to content

Commit e481c96

Browse files
committed
Add dark checkboxes support to tree view, enable new dark mode theme on Windows 11 24H2
1 parent a2308e1 commit e481c96

5 files changed

Lines changed: 162 additions & 45 deletions

File tree

dmlib_demo/dmlib_dll_helper.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ namespace DarkMode
357357
inline void DummySetDarkListView(HWND) {}
358358
inline void DummySetDarkListViewCheckboxes(HWND) {}
359359

360+
inline void DummySetDarkTreeViewCheckboxes(HWND) {}
361+
360362
inline void DummySetDarkRichEdit(HWND) {}
361363

362364
inline void DummySetDarkWndSafeEx(HWND, bool) {}
@@ -817,6 +819,9 @@ namespace DarkMode
817819
using fnSetDarkListViewCheckboxes = void (*)(HWND hWnd);
818820
inline fnSetDarkListViewCheckboxes setDarkListViewCheckboxes = nullptr;
819821

822+
using fnSetDarkTreeViewCheckboxes = void (*)(HWND hWnd);
823+
inline fnSetDarkTreeViewCheckboxes setDarkTreeViewCheckboxes = nullptr;
824+
820825
using fnSetDarkRichEdit = void (*)(HWND hWnd);
821826
inline fnSetDarkRichEdit setDarkRichEdit = nullptr;
822827

include/DarkModeSubclass.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,8 @@ namespace DarkMode
533533
DMLIB_API void setDarkListView(HWND hWnd);
534534
/// Replaces default list view checkboxes with themed dark-mode versions on Windows 11.
535535
DMLIB_API void setDarkListViewCheckboxes(HWND hWnd);
536+
/// Replaces default tree view checkboxes with themed dark-mode versions on Windows 11.
537+
DMLIB_API void setDarkTreeViewCheckboxes(HWND hWnd);
536538
/// Sets colors and edges for a RichEdit control.
537539
DMLIB_API void setDarkRichEdit(HWND hWnd);
538540

src/DarkModeSubclass.cpp

Lines changed: 152 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <vsstyle.h>
3030

3131
#include <array>
32+
#include <cstdint>
3233
#include <string>
3334

3435
#include "DmlibColor.h"
@@ -142,13 +143,24 @@ int DarkMode::getLibInfo(int libInfoType)
142143
* - `light`: Follow system mode; apply light theme when system is in light mode.
143144
* - `classic`: Follow system mode; apply classic style when system is in light mode.
144145
*/
145-
enum class WinMode : unsigned char
146+
enum class WinMode : std::uint8_t
146147
{
147148
disabled, ///< Manual - system mode is ignored.
148149
light, ///< Use light theme if system is in light mode.
149150
classic ///< Use classic style if system is in light mode.
150151
};
151152

153+
/// Types of list and tree views checkbox styles
154+
enum class ViewCheckbox : std::uint8_t
155+
{
156+
listView, ///< List view with LVS_EX_CHECKBOXES extendend style.
157+
tvSimple, ///< Tree view with TVS_CHECKBOXES style.
158+
159+
/// Tree view with any combination of TVS_EX_PARTIALCHECKBOXES, TVS_EX_EXCLUSIONCHECKBOXES,
160+
/// TVS_EX_DIMMEDCHECKBOXES extendend styles.
161+
tvExtended
162+
};
163+
152164
/**
153165
* @struct DarkModeParams
154166
* @brief Defines theming and subclassing parameters for child controls.
@@ -882,8 +894,9 @@ DWORD DarkMode::getWindowsBuildNumber()
882894
return dmlib_win32api::GetWindowsBuildNumber();
883895
}
884896

885-
/// Check if OS is at leaast Windows 11 version 25H2 build 26200.6899.
886-
static bool isAtLeastWin11Ver25H2() noexcept
897+
/// Check if OS is Windows 11 version 24H2 build 26100 rev 6899+,
898+
/// Windows 11 version 25H2 build 26200 rev 6899+, or later builds.
899+
static bool doesWin11SupportDarkThemeStyle() noexcept
887900
{
888901
static const DWORD win11Revision = []()
889902
{
@@ -899,10 +912,12 @@ static bool isAtLeastWin11Ver25H2() noexcept
899912
return 0UL;
900913
}();
901914

915+
static constexpr DWORD win11Build24H2 = 26100;
902916
static constexpr DWORD win11Build25H2 = 26200;
903-
static constexpr DWORD minWin11Build25H2Revision = 6899;
904-
return (dmlib_win32api::GetWindowsBuildNumber() == win11Build25H2 && win11Revision >= minWin11Build25H2Revision)
905-
|| dmlib_win32api::GetWindowsBuildNumber() > win11Build25H2;
917+
static constexpr DWORD minWin11Revision = 6899;
918+
return dmlib_win32api::GetWindowsBuildNumber() > win11Build25H2
919+
|| (dmlib_win32api::GetWindowsBuildNumber() == win11Build25H2 && win11Revision >= minWin11Revision)
920+
|| (dmlib_win32api::GetWindowsBuildNumber() == win11Build24H2 && win11Revision >= minWin11Revision);
906921
}
907922

908923
/**
@@ -1307,7 +1322,7 @@ static void setTabCtrlSubclassAndTheme(HWND hWnd, DarkModeParams p) noexcept
13071322
if (p.m_theme)
13081323
{
13091324
DarkMode::setDarkTooltips(hWnd, static_cast<int>(DarkMode::ToolTipsType::tabbar));
1310-
if (isAtLeastWin11Ver25H2() && dmlib_subclass::isThemePrefered())
1325+
if (doesWin11SupportDarkThemeStyle() && dmlib_subclass::isThemePrefered())
13111326
{
13121327
DarkMode::setDarkThemeTheme(hWnd);
13131328
}
@@ -1376,7 +1391,7 @@ static void setCustomBorderForListBoxOrEditCtrlSubclassAndTheme(HWND hWnd, DarkM
13761391
&& !isListBox
13771392
&& !hasScrollBar)
13781393
{
1379-
if (isAtLeastWin11Ver25H2())
1394+
if (doesWin11SupportDarkThemeStyle())
13801395
{
13811396
DarkMode::setDarkThemeTheme(hWnd);
13821397
}
@@ -1510,7 +1525,7 @@ static void setComboBoxCtrlSubclassAndTheme(HWND hWnd, DarkModeParams p)
15101525

15111526
if (p.m_theme) // for light dropdown arrow in dark mode
15121527
{
1513-
if (isAtLeastWin11Ver25H2())
1528+
if (doesWin11SupportDarkThemeStyle())
15141529
{
15151530
DarkMode::setDarkThemeTheme(hWnd);
15161531
}
@@ -1640,7 +1655,7 @@ static void setListViewCtrlSubclassAndTheme(HWND hWnd, DarkModeParams p) noexcep
16401655

16411656
if (dmlib_subclass::isThemePrefered())
16421657
{
1643-
if (isAtLeastWin11Ver25H2())
1658+
if (doesWin11SupportDarkThemeStyle())
16441659
{
16451660
DarkMode::setDarkThemeTheme(hHeader);
16461661
}
@@ -1746,7 +1761,7 @@ static void setStatusBarCtrlSubclass(HWND hWnd, DarkModeParams p) noexcept
17461761
{
17471762
if (p.m_theme
17481763
&& dmlib_subclass::isThemePrefered()
1749-
&& isAtLeastWin11Ver25H2())
1764+
&& doesWin11SupportDarkThemeStyle())
17501765
{
17511766
DarkMode::setDarkThemeTheme(hWnd);
17521767
}
@@ -1814,7 +1829,7 @@ static void setProgressBarCtrlSubclass(HWND hWnd, DarkModeParams p) noexcept
18141829
{
18151830
DarkMode::setProgressBarClassicTheme(hWnd);
18161831
}
1817-
else if (isAtLeastWin11Ver25H2())
1832+
else if (doesWin11SupportDarkThemeStyle())
18181833
{
18191834
::SetWindowTheme(hWnd, DarkMode::isExperimentalActive() ? L"DarkMode_CopyEngine" : nullptr, nullptr);
18201835
}
@@ -1993,6 +2008,7 @@ static void setHotKeyCtrlSubclass(HWND hWnd, DarkModeParams p) noexcept
19932008
* @param[in] hWnd Handle to the tree view control.
19942009
* @param[in] p Parameters controlling whether to apply theming.
19952010
*
2011+
* @see DarkMode::setDarkTreeViewCheckboxes(hWnd);
19962012
* @see DarkMode::setTreeViewWindowTheme()
19972013
* @see DarkMode::setDarkTooltips()
19982014
*/
@@ -2004,6 +2020,7 @@ static void setTreeViewCtrlTheme(HWND hWnd, DarkModeParams p) noexcept
20042020
TreeView_SetBkColor(hWnd, DarkMode::getViewBackgroundColor());
20052021

20062022
DarkMode::setTreeViewWindowThemeEx(hWnd, p.m_theme);
2023+
DarkMode::setDarkTreeViewCheckboxes(hWnd);
20072024
DarkMode::setDarkTooltips(hWnd, static_cast<int>(DarkMode::ToolTipsType::treeview));
20082025
}
20092026
}
@@ -2654,10 +2671,12 @@ void DarkMode::setDarkTitleBar(HWND hWnd)
26542671
* @return "DarkMode_DarkTheme" on Windows 11 25H2+
26552672
* @return "DarkMode_Explorer" on Windows 10+
26562673
* @return nullptr if not on supported OS
2674+
*
2675+
* @see doesWin11SupportDarkThemeStyle()
26572676
*/
26582677
const wchar_t* DarkMode::getDarkModeThemeName()
26592678
{
2660-
if (isAtLeastWin11Ver25H2())
2679+
if (doesWin11SupportDarkThemeStyle())
26612680
{
26622681
return L"DarkMode_DarkTheme";
26632682
}
@@ -2816,10 +2835,12 @@ void DarkMode::setDarkTooltips(HWND hWnd, int tooltipType)
28162835
/**
28172836
* @brief Applies "DarkMode_DarkTheme" visual style if supported and experimental mode is active.
28182837
*
2819-
* If OS is at least Windows 11 version 25H2 applies "DarkMode_DarkTheme" visual style,
2838+
* Applies "DarkMode_DarkTheme" visual style if supported,
28202839
* else applies "DarkMode_Explorer" visual style.
28212840
*
28222841
* @param[in] hWnd Handle to the control or window to theme.
2842+
*
2843+
* @see DarkMode::getDarkModeThemeName()
28232844
*/
28242845
void DarkMode::setDarkThemeTheme(HWND hWnd)
28252846
{
@@ -2869,33 +2890,26 @@ void DarkMode::setDarkListView(HWND hWnd)
28692890
DarkMode::setDarkThemeExperimental(hWnd);
28702891
}
28712892

2893+
2894+
28722895
/**
2873-
* @brief Replaces default list view checkboxes with themed dark-mode versions on Windows 11.
2874-
*
2875-
* If the list view uses `LVS_EX_CHECKBOXES` and is running on Windows 11 or later,
2876-
* this function manually draws the unchecked and checked checkbox visuals using
2877-
* themed drawing APIs, then inserts the resulting icons into the state image list.
2896+
* @brief Replaces list view or tree view image list checkbox state images with themed dark mode versions on Windows 11.
28782897
*
28792898
* Uses `"DarkMode_Explorer::Button"` as the theme class if experimental dark mode is active;
28802899
* otherwise falls back to `VSCLASS_BUTTON`.
28812900
*
2882-
* @param[in] hWnd Handle to the list view control with extended checkbox style.
2901+
* @param[in] hWnd Handle to the control to change checkbox style.
2902+
* @param[in] hImgList Handle to the image list of control containing checkbox state images.
28832903
*
2884-
* @note Does nothing on pre-Windows 11 systems or if checkboxes are not enabled.
2904+
* @note Does nothing on pre-Windows 11 systems.
28852905
*/
2886-
void DarkMode::setDarkListViewCheckboxes(HWND hWnd)
2906+
static void setDarkCheckboxes(HWND hWnd, HIMAGELIST hImgList, ViewCheckbox viewCheckbox)
28872907
{
28882908
if (!DarkMode::isAtLeastWindows11())
28892909
{
28902910
return;
28912911
}
28922912

2893-
if (const auto lvExStyle = ListView_GetExtendedListViewStyle(hWnd);
2894-
(lvExStyle & LVS_EX_CHECKBOXES) != LVS_EX_CHECKBOXES)
2895-
{
2896-
return;
2897-
}
2898-
28992913
HDC hdc = ::GetDC(nullptr);
29002914

29012915
const bool useDark = DarkMode::isExperimentalActive() && DarkMode::isThemeDark();
@@ -2906,14 +2920,12 @@ void DarkMode::setDarkListViewCheckboxes(HWND hWnd)
29062920

29072921
const RECT rcBox{ 0, 0, szBox.cx, szBox.cy };
29082922

2909-
auto hImgList = ListView_GetImageList(hWnd, LVSIL_STATE);
29102923
if (hImgList == nullptr)
29112924
{
29122925
::CloseThemeData(hTheme);
29132926
::ReleaseDC(nullptr, hdc);
29142927
return;
29152928
}
2916-
::ImageList_RemoveAll(hImgList);
29172929

29182930
HDC hBoxDC = ::CreateCompatibleDC(hdc);
29192931
HBITMAP hBoxBmp = ::CreateCompatibleBitmap(hdc, szBox.cx, szBox.cy);
@@ -2927,23 +2939,55 @@ void DarkMode::setDarkListViewCheckboxes(HWND hWnd)
29272939
ii.hbmColor = hBoxBmp;
29282940
ii.hbmMask = hMaskBmp;
29292941

2942+
int idx = (viewCheckbox == ViewCheckbox::listView) ? 0 : 1; // tree view state images start from index 1
2943+
29302944
HICON hIcon = ::CreateIconIndirect(&ii);
2931-
if (hIcon != nullptr)
2945+
2946+
auto addIcon = [&hImgList, &idx, &hIcon]()
29322947
{
2933-
::ImageList_AddIcon(hImgList, hIcon);
2934-
::DestroyIcon(hIcon);
2935-
hIcon = nullptr;
2936-
}
2948+
if (hIcon != nullptr)
2949+
{
2950+
::ImageList_ReplaceIcon(hImgList, idx, hIcon);
2951+
::DestroyIcon(hIcon);
2952+
hIcon = nullptr;
2953+
++idx;
2954+
}
2955+
};
29372956

2938-
::DrawThemeBackground(hTheme, hBoxDC, BP_CHECKBOX, CBS_CHECKEDNORMAL, &rcBox, nullptr);
2939-
ii.hbmColor = hBoxBmp;
2957+
addIcon(); // unchecked state
29402958

2941-
hIcon = ::CreateIconIndirect(&ii);
2942-
if (hIcon != nullptr)
2959+
auto addIconState = [&](int iStateId)
29432960
{
2944-
::ImageList_AddIcon(hImgList, hIcon);
2945-
::DestroyIcon(hIcon);
2946-
hIcon = nullptr;
2961+
::DrawThemeBackground(hTheme, hBoxDC, BP_CHECKBOX, iStateId, &rcBox, nullptr);
2962+
ii.hbmColor = hBoxBmp;
2963+
2964+
hIcon = ::CreateIconIndirect(&ii);
2965+
addIcon();
2966+
};
2967+
2968+
addIconState(CBS_CHECKEDNORMAL);
2969+
2970+
if (viewCheckbox == ViewCheckbox::tvExtended)
2971+
{
2972+
const auto tvExStyle = TreeView_GetExtendedStyle(hWnd);
2973+
2974+
// Tree view check boxes: The extended check box states
2975+
// https://devblogs.microsoft.com/oldnewthing/20171205-00/?p=97525
2976+
// The image list states are added in the order: Partial, then dimmed, then exclusion.
2977+
if ((tvExStyle & TVS_EX_PARTIALCHECKBOXES) != 0)
2978+
{
2979+
addIconState(CBS_MIXEDNORMAL);
2980+
}
2981+
2982+
if ((tvExStyle & TVS_EX_DIMMEDCHECKBOXES) != 0)
2983+
{
2984+
addIconState(CBS_IMPLICITPRESSED); // make it different from CBS_CHECKEDNORMAL
2985+
}
2986+
2987+
if ((tvExStyle & TVS_EX_EXCLUSIONCHECKBOXES) != 0)
2988+
{
2989+
addIconState(CBS_EXCLUDEDNORMAL);
2990+
}
29472991
}
29482992

29492993
::SelectObject(hBoxDC, holdBmp);
@@ -2954,6 +2998,71 @@ void DarkMode::setDarkListViewCheckboxes(HWND hWnd)
29542998
::ReleaseDC(nullptr, hdc);
29552999
}
29563000

3001+
/**
3002+
* @brief Replaces default list view checkboxes with themed dark-mode versions on Windows 11.
3003+
*
3004+
* If the list view uses `LVS_EX_CHECKBOXES` and is running on Windows 11 or later,
3005+
* this function then manually draws the unchecked and checked checkbox visuals using
3006+
* themed drawing APIs, then inserts the resulting icons into the state image list.
3007+
*
3008+
* Uses `"DarkMode_Explorer::Button"` as the theme class if experimental dark mode is active;
3009+
* otherwise falls back to `VSCLASS_BUTTON`.
3010+
*
3011+
* @param[in] hWnd Handle to the list view control with extended checkbox style.
3012+
*
3013+
* @see setDarkCheckboxes()
3014+
*
3015+
* @note Does nothing on pre-Windows 11 systems or if checkboxes are not enabled.
3016+
*/
3017+
void DarkMode::setDarkListViewCheckboxes(HWND hWnd)
3018+
{
3019+
if (const auto lvExStyle = ListView_GetExtendedListViewStyle(hWnd);
3020+
(lvExStyle & LVS_EX_CHECKBOXES) != LVS_EX_CHECKBOXES)
3021+
{
3022+
return;
3023+
}
3024+
3025+
setDarkCheckboxes(hWnd, ListView_GetImageList(hWnd, LVSIL_STATE), ViewCheckbox::listView);
3026+
}
3027+
3028+
/**
3029+
* @brief Replaces default tree view checkboxes with themed dark-mode versions on Windows 11.
3030+
*
3031+
* If the tree view uses `TVS_CHECKBOXES` or any combination of `TVS_EX_PARTIALCHECKBOXES`,
3032+
* `TVS_EX_EXCLUSIONCHECKBOXES`, `TVS_EX_DIMMEDCHECKBOXES` extended styles and is running on
3033+
* Windows 11 or later, this function then manually draws the checkbox state visuals using
3034+
* themed drawing APIs, then inserts the resulting icons into the state image list.
3035+
*
3036+
* Uses `"DarkMode_Explorer::Button"` as the theme class if experimental dark mode is active;
3037+
* otherwise falls back to `VSCLASS_BUTTON`.
3038+
*
3039+
* @param[in] hWnd Handle to the tree view control with normal or extended checkbox style.
3040+
*
3041+
* @see setDarkCheckboxes()
3042+
*
3043+
* @note Does nothing on pre-Windows 11 systems or if checkboxes are not enabled.
3044+
*/
3045+
void DarkMode::setDarkTreeViewCheckboxes(HWND hWnd)
3046+
{
3047+
ViewCheckbox tvType = ViewCheckbox::tvSimple;
3048+
if (const auto tvStyle = ::GetWindowLongPtr(hWnd, GWL_STYLE);
3049+
(tvStyle & TVS_CHECKBOXES) == TVS_CHECKBOXES)
3050+
{
3051+
//tvType = ViewCheckbox::tvSimple;
3052+
}
3053+
else if (const auto tvExStyle = TreeView_GetExtendedStyle(hWnd);
3054+
(tvExStyle & (TVS_EX_PARTIALCHECKBOXES | TVS_EX_EXCLUSIONCHECKBOXES | TVS_EX_DIMMEDCHECKBOXES)) != 0)
3055+
{
3056+
tvType = ViewCheckbox::tvExtended;
3057+
}
3058+
else
3059+
{
3060+
return;
3061+
}
3062+
3063+
setDarkCheckboxes(hWnd, TreeView_GetImageList(hWnd, TVSIL_STATE), tvType);
3064+
}
3065+
29573066
/**
29583067
* @brief Sets colors and edges for a RichEdit control.
29593068
*
@@ -3525,7 +3634,7 @@ void DarkMode::setProgressBarClassicTheme(HWND hWnd)
35253634
static constexpr COLORREF greenLight = dmlib_color::HEXRGB(0x06B025);
35263635
static constexpr COLORREF greenDark = dmlib_color::HEXRGB(0x0F7B0F);
35273636
static constexpr COLORREF azureDark = dmlib_color::HEXRGB(0x60CDFF);
3528-
static const auto clrDark = isAtLeastWin11Ver25H2() ? azureDark : greenDark;
3637+
static const auto clrDark = doesWin11SupportDarkThemeStyle() ? azureDark : greenDark;
35293638
::SendMessage(hWnd, PBM_SETBARCOLOR, 0, static_cast<LPARAM>(DarkMode::isExperimentalActive() ? clrDark : greenLight));
35303639
}
35313640
}

0 commit comments

Comments
 (0)