Skip to content

Repository files navigation

DarkTaskDialog-Native

Win32 TaskDialogIndirect with complete dark mode support — zero dependencies · zero hooks · documented APIs only · MIT licensed. Windows 10 (UIA + subclassing + owner-draw) and Windows 11 (native DarkMode_TaskDialog panels + owner-drawn text).

PlatformLanguageLicenseDependenciesAPI hooksUndocumented APIs


Screenshots

Progress dialogExpandoExpando + footerCommand linksRtl + Nave
Progress darkExpando darkExpando darkCommand linksCommand links

The only other public solution — SFTRS/DarkTaskDialog — hooks DrawTheme* APIs via Microsoft Detours. That requires a third-party build dependency and is GPL-3.0 licensed.

This library uses UI Automation, window subclassing, UxTheme, and DrawThemeTextEx — all fully documented Win32 APIs — and adds features the Detours approach cannot support:

SFTRS/DarkTaskDialogDarkTaskDialog-Native
DependencyMicrosoft DetoursNone
ApproachDrawTheme* API hookingUIA + subclassing + UxTheme
Documented APIs only
LicenseGPL-3.0MIT

Quick start

Requirements

Version
Windows SDK10.0.19041+
Visual Studio2022 (v143)
C++ standardC++17
Target OSWindows 10 1809+ (build 17763)

Integration

1. Copy DarkMode.h and DarkMode.cpp into your project.

2. Call once at startup, before any windows are created:

#include"DarkMode.h"intWINAPIwWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
{
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
DarkMode::Init(); // reads OS dark-mode state; safe no-op on pre-Win10// ...
}

3. Add two lines to your TaskDialogIndirect callback:

staticHRESULTCALLBACKMyCallback(
HWND hwnd, UINT note, WPARAM wParam, LPARAM lParam, LONG_PTR dwRef)
{
auto* pCfg = reinterpret_cast<TASKDIALOGCONFIG*>(dwRef);
switch (note)
{
caseTDN_CREATED:
DarkMode::AllowForTaskDialog(hwnd, pCfg); // ← applies dark modebreak;
caseTDN_NAVIGATED:
// Required when using TDM_NAVIGATE_PAGEDarkMode::AllowForTaskDialog(hwnd, reinterpret_cast<TASKDIALOGCONFIG*>(lParam));
break;
caseTDN_DESTROYED:
DarkMode::RemoveFromTaskDialog(hwnd); // ← frees per-dialog statebreak;
}
returnS_OK;
}
TASKDIALOGCONFIG cfg = { sizeof(cfg) };
cfg.pfCallback = MyCallback;
cfg.lpCallbackData = (LONG_PTR)&cfg;
TaskDialogIndirect(&cfg, &nButton, nullptr, nullptr);

If the system is in light mode every call is a no-op. If the user switches themes while the dialog is open it adapts automatically.


API reference

namespaceDarkMode
{
// Call once at startup. Reads OS theme state.boolInit();
// True when the OS is currently in dark mode.boolIsActive();
// True on Windows 11 where DarkMode_TaskDialog UxTheme class exists.boolHasNativeTaskDialogTheme();
// Sets DWMWA_USE_IMMERSIVE_DARK_MODE on a top-level window (dark title bar).voidEnableForTLW(HWND hwnd);
// Applies SetWindowTheme to any child control.voidAllowForWindow(HWND hwnd, constwchar_t* themeClass = nullptr);
// Main entry point. Call from TDN_CREATED (and TDN_NAVIGATED for page nav).voidAllowForTaskDialog(HWND hwndTaskDialog, TASKDIALOGCONFIG* pConfig);
// Call from TDN_DESTROYED to free per-dialog state.voidRemoveFromTaskDialog(HWND hwndTaskDialog);
}

How it works

The comctl32 UIFILE bug — why text needs owner-draw on every Windows version

The TaskDialog layout engine reads its colours from a stylesheet embedded in comctl32.dll as resource 4255 (UIFILE). Every text element in that stylesheet queries colour via:

foreground="gtc(TaskDialogStyle, <part>, 0, 3803)"

The key is the class name: TaskDialogStyle — not DarkMode_TaskDialogStyle. DarkMode_TaskDialogStyle does not exist in any shipping version of Windows. Because TaskDialogStyle always returns light-mode colours, every text element paints black-on-dark even when the panel backgrounds are correctly dark.

Windows 11

1- DarkMode_TaskDialogdoes exist — it covers all panel backgrounds and the expando glyph. AllowForTaskDialog walks the DirectUIHWND child tree by its AutomationId, calls SetWindowTheme(pane, L"DarkMode_TaskDialog", nullptr) 2- Text is then owner-drawn to fix the TaskDialogStyle colour bug described above.

Windows 10 (build 17763–19045)

DarkMode_TaskDialog does not exist. The library:

  1. Uses UI Automation to walk the DirectUIHWND child tree, identifying each element by its AutomationId (the UIFILE atom names).
  2. Owner-draws all elements.
ElementWindows 10Windows 11
PrimaryPanel backgroundWM_ERASEBKGND dark brushdtb(DarkMode_TaskDialog, 1, 0)
SecondaryPanel backgroundWM_ERASEBKGND dark brushdtb(DarkMode_TaskDialog, 8, 0)
FootnoteArea backgroundWM_ERASEBKGND dark brushdtb(DarkMode_TaskDialog, 15, 0)
SeparatorLine backgrounddark brushdtb(DarkMode_TaskDialog, 17, 0)
Expando glyphdtb(TaskDialog, 13, state)dtb(DarkMode_TaskDialog, 13, state)
All text (gtc(TaskDialogStyle,*,*))FillRect + DrawThemeTextEx + DTT_TEXTCOLORDrawThemeText + DTT_TEXTCOLOR

UIA element reference

These AutomationId strings come directly from the comctl32 UIFILE (resource 4255, Windows 11 build 26100.7965). They are the atom names the DirectUI engine uses internally and are exactly what IUIAutomationElement::get_CurrentAutomationId returns when walking the DirectUIHWND tree.

This is the only public cross-reference of these identifiers against their UxTheme part IDs and resolved dark-mode colour values.

Root

AutomationIdDescription
"TaskDialog"The DirectUIHWND TaskPage window — UIA walk entry point

Primary panel

AutomationIdUIFILE atomControl typeUxTheme queryDark colour
MainIconatom(MainIcon)ImageNo recolour
MainInstructionatom(MainInstruction)Textgtc(TaskDialogStyle, 2, 0, 3803)RGB(153, 235, 255)
ContentTextatom(ContentText)Textgtc(TaskDialogStyle, 4, 0, 3803)RGB(255, 255, 255)
ContentLinkatom(ContentLink)Hyperlinkgtc(TaskDialogStyle, 4, 0, 3803) + dtb(TaskDialog,1,0) bgRGB(255, 255, 255)
ExpandedInformationTextatom(ExpandedInformationText)Textgtc(TaskDialogStyle, 6, 0, 3803)RGB(255, 255, 255)
ExpandedInformationLinkatom(ExpandedInformationLink)Hyperlinkgtc(TaskDialogStyle, 6, 0, 3803) + dtb(TaskDialog,1,0) bgRGB(255, 255, 255)
ExpandoButtonatom(ExpandoButton)Buttondtb(TaskDialog, 13, state)Owner-drawn glyph
ExpandoTextExpandedatom(ExpandoTextExpanded)Textgtc(TaskDialogStyle, 12, 0, 3803)RGB(255, 255, 255)
ExpandoTextCollapsedatom(ExpandoTextCollapsed)Textgtc(TaskDialogStyle, 12, 0, 3803)RGB(255, 255, 255)
VerificationCheckBoxatom(VerificationCheckBox)CheckBoxSystem-themed
VerificationTextatom(VerificationText)Textgtc(TaskDialogStyle, 14, 0, 3803)RGB(255, 255, 255)
RadioButton_0_Nclass RadioButtonRadioButtondtb(TaskDialog, 1, 0) bgSystem-themed
CommandLink_0_Nclass CommandLinkButtondtb(TaskDialog, 1, 0) bgDarkMode_Explorer theme
CommandButton_0_Nclass CommandButtonButtonDarkMode_Explorer theme
ProgressBaratom(ProgressBar)ProgressBarSystem-themed

Secondary panel (button row)

AutomationIdUIFILE atomUxTheme queryDark colour
(push buttons)class CommandButtondtb(TaskDialog, 8, 0) bgDarkMode_Explorer / DarkMode_CFD
ButtonAreaatom(ButtonArea)gtc(TaskDialogStyle, **15**, 0, 3803)⚠️Uses footnote part — UIFILE quirk

Separator

AutomationIdUIFILE atomUxTheme queryDark colour
Separatoratom(Separator)dtb(TaskDialog, 15, 0) bgRGB(44, 44, 44)
SeparatorLineatom(SeparatorLine)dtb(TaskDialog, 17, 0) bgRGB(77, 77, 77)

Footnote / expanded footer panel

AutomationIdUIFILE atomUxTheme queryDark colour
FootnoteIconatom(FootnoteIcon)No recolour
FootnoteTextatom(FootnoteText)gtc(TaskDialogStyle, 15, 0, 3803)RGB(224, 224, 224)
FootnoteTextLinkatom(FootnoteTextLink)gtc(TaskDialogStyle, 15, 0, 3803)RGB(224, 224, 224)
ExpandedFooterTextatom(ExpandedFooterText)gtc(TaskDialogStyle, 18, 0, 3803)RGB(224, 224, 224)
ExpandedFooterTextLinkatom(ExpandedFooterTextLink)gtc(TaskDialogStyle, **4**, 0, 3803)⚠️RGB(224, 224, 224)

⚠️ExpandedFooterTextLink uses part 4 (the content text part) instead of part 18 in the UIFILE. This is a Microsoft bug — the wrong part ID is hardcoded. The library handles this by checking the AutomationId and applying part-18 colours regardless of what gtc() returns.


FAQ

Does this work with the simple TaskDialog() overload?TaskDialog() has no callback, so there is no TDN_CREATED hook point. Use TaskDialogIndirect() with a TASKDIALOGCONFIG.

Does TDM_NAVIGATE_PAGE work? Yes — call DarkMode::AllowForTaskDialog(hwnd, pNewConfig) from TDN_NAVIGATED. The included main.cpp demonstrates page navigation to an Arabic RTL page.

I see a white flash when the dialog first opens. Ensure DarkMode::AllowForTaskDialog is called from TDN_CREATED, not TDN_DIALOG_CONSTRUCTED. TDN_CREATED fires after the window is fully initialised.

Can I use this from MFC? Yes — no MFC dependency. Override DoMessageBox and call TaskDialogIndirect directly. DarkMode::Init() can go in InitInstance.

Related

Tools

  • memoarfaa/TaskDialog-Stylesheet-Dumper — Win32 tool that extracts and parses comctl32.dll resource 4255 (UIFILE) at runtime, then evaluates every gtf(), gtc(), gtmar(), gtmet(), and dtb() call live against OpenThemeData(L"TaskDialog") and OpenThemeData(L"TaskDialogStyle"). The resolved colour table and UIFILE bug findings documented in this README were produced with this tool. Uses only IXmlReader, FindResourceW, and GetThemeColor — no third-party dependencies. Output can be saved as XML.

Other implementations

  • SFTRS/DarkTaskDialog — alternative approach using Microsoft Detours to hook DrawTheme* APIs (GPL-3.0)

References


License

MIT — see LICENSE.

About

Win32 TaskDialogIndirect with complete dark mode support — documented APIs, zero dependencies, no hooking. Works on Windows 10 (UIA + subclassing path) and Windows 11 / 25H2 (native DarkMode_* UxTheme path).

Topics

Resources

Stars

11 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages