Skip to content

Commit efdf739

Browse files
authored
Update README.md
1 parent 84c3749 commit efdf739

1 file changed

Lines changed: 323 additions & 1 deletion

File tree

README.md

Lines changed: 323 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,324 @@
11
# DarkTaskDialog-Native
2-
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).
2+
3+
> Win32 `TaskDialogIndirect` with complete dark mode support —
4+
> **zero dependencies · zero hooks · documented APIs only · MIT licensed.**
5+
> Windows 10 (UIA + subclassing + owner-draw) and Windows 11 (native `DarkMode_TaskDialog` panels + owner-drawn text).
6+
7+
[![Platform](https://img.shields.io/badge/platform-Windows%2010%2B-blue?logo=windows&logoColor=white)](https://docs.microsoft.com/windows)
8+
[![Language](https://img.shields.io/badge/language-C%2B%2B17-blue?logo=cplusplus)](https://en.cppreference.com)
9+
[![License](https://img.shields.io/badge/license-MIT-brightgreen)](LICENSE)
10+
[![Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)]()
11+
[![API hooks](https://img.shields.io/badge/API%20hooks-none-brightgreen)]()
12+
[![Undocumented APIs](https://img.shields.io/badge/undocumented%20APIs-none-brightgreen)]()
13+
14+
---
15+
16+
## Screenshots
17+
18+
| Progress dialog | Expando | Expando + footer | Command links | Rtl + Nave |
19+
|:---:|:---:| :---:| :---:| :---:|
20+
| ![Progress dark](docs/screenshot-progress.png) | ![Expando dark](docs/screenshot-expando.png) | ![Expando dark](docs/screenshot-expando_1.png) | ![Command links](docs/Screenshot-Command_links.png) | ![Command links](docs/Screenshot-Nave.png) |
21+
22+
---
23+
The only other public solution —
24+
[SFTRS/DarkTaskDialog](https://github.com/SFTRS/DarkTaskDialog) — hooks
25+
`DrawTheme*` APIs via **Microsoft Detours**. That requires a third-party build
26+
dependency and is GPL-3.0 licensed.
27+
28+
This library uses **UI Automation**, **window subclassing**, **UxTheme**, and
29+
**`DrawThemeTextEx`** — all fully documented Win32 APIs — and adds features the
30+
Detours approach cannot support:
31+
32+
| | SFTRS/DarkTaskDialog | **DarkTaskDialog-Native** |
33+
|---|:---:|:---:|
34+
| Dependency | Microsoft Detours | **None** |
35+
| Approach | `DrawTheme*` API hooking | UIA + subclassing + UxTheme |
36+
| Documented APIs only |||
37+
| License | GPL-3.0 | **MIT** |
38+
---
39+
40+
## Quick start
41+
42+
### Requirements
43+
44+
| | Version |
45+
|---|---|
46+
| Windows SDK | 10.0.19041+ |
47+
| Visual Studio | 2022 (v143) |
48+
| C++ standard | C++17 |
49+
| Target OS | Windows 10 1809+ (build 17763) |
50+
51+
### Integration
52+
53+
**1.** Copy `DarkMode.h` and `DarkMode.cpp` into your project.
54+
55+
**2.** Call once at startup, before any windows are created:
56+
57+
```cpp
58+
#include "DarkMode.h"
59+
60+
int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
61+
{
62+
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
63+
DarkMode::Init(); // reads OS dark-mode state; safe no-op on pre-Win10
64+
// ...
65+
}
66+
```
67+
68+
**3.** Add two lines to your `TaskDialogIndirect` callback:
69+
70+
```cpp
71+
static HRESULT CALLBACK MyCallback(
72+
HWND hwnd, UINT note, WPARAM wParam, LPARAM lParam, LONG_PTR dwRef)
73+
{
74+
auto* pCfg = reinterpret_cast<TASKDIALOGCONFIG*>(dwRef);
75+
switch (note)
76+
{
77+
case TDN_CREATED:
78+
DarkMode::AllowForTaskDialog(hwnd, pCfg); // ← applies dark mode
79+
break;
80+
case TDN_NAVIGATED:
81+
// Required when using TDM_NAVIGATE_PAGE
82+
DarkMode::AllowForTaskDialog(hwnd, reinterpret_cast<TASKDIALOGCONFIG*>(lParam));
83+
break;
84+
case TDN_DESTROYED:
85+
DarkMode::RemoveFromTaskDialog(hwnd); // ← frees per-dialog state
86+
break;
87+
}
88+
return S_OK;
89+
}
90+
91+
TASKDIALOGCONFIG cfg = { sizeof(cfg) };
92+
cfg.pfCallback = MyCallback;
93+
cfg.lpCallbackData = (LONG_PTR)&cfg;
94+
TaskDialogIndirect(&cfg, &nButton, nullptr, nullptr);
95+
```
96+
97+
If the system is in light mode every call is a no-op.
98+
If the user switches themes while the dialog is open it adapts automatically.
99+
100+
---
101+
102+
## API reference
103+
104+
```cpp
105+
namespace DarkMode
106+
{
107+
// Call once at startup. Reads OS theme state.
108+
bool Init();
109+
110+
// True when the OS is currently in dark mode.
111+
bool IsActive();
112+
113+
// True on Windows 11 where DarkMode_TaskDialog UxTheme class exists.
114+
bool HasNativeTaskDialogTheme();
115+
116+
// Sets DWMWA_USE_IMMERSIVE_DARK_MODE on a top-level window (dark title bar).
117+
void EnableForTLW(HWND hwnd);
118+
119+
// Applies SetWindowTheme to any child control.
120+
void AllowForWindow(HWND hwnd, const wchar_t* themeClass = nullptr);
121+
122+
// Main entry point. Call from TDN_CREATED (and TDN_NAVIGATED for page nav).
123+
void AllowForTaskDialog(HWND hwndTaskDialog, TASKDIALOGCONFIG* pConfig);
124+
125+
// Call from TDN_DESTROYED to free per-dialog state.
126+
void RemoveFromTaskDialog(HWND hwndTaskDialog);
127+
}
128+
```
129+
130+
---
131+
132+
## How it works
133+
134+
### The comctl32 UIFILE bug — why text needs owner-draw on every Windows version
135+
136+
The TaskDialog layout engine reads its colours from a stylesheet embedded in
137+
`comctl32.dll` as resource 4255 (`UIFILE`). Every text element in that
138+
stylesheet queries colour via:
139+
140+
```
141+
foreground="gtc(TaskDialogStyle, <part>, 0, 3803)"
142+
```
143+
144+
The key is the class name: **`TaskDialogStyle`** — not `DarkMode_TaskDialogStyle`.
145+
`DarkMode_TaskDialogStyle` does not exist in any shipping version of Windows.
146+
Because `TaskDialogStyle` always returns light-mode colours, every text element
147+
paints black-on-dark even when the panel backgrounds are correctly dark.
148+
149+
150+
### Windows 11 (build 22000+)
151+
152+
`DarkMode_TaskDialog` **does** exist — it covers all panel backgrounds and the
153+
expando glyph. `AllowForTaskDialog` walks the `DirectUIHWND` child tree, calls
154+
`SetWindowTheme(pane, L"DarkMode_TaskDialog", nullptr)` on each structural
155+
pane, then broadcasts `WM_THEMECHANGED`. comctl32 repaints the backgrounds via
156+
`dtb(DarkMode_TaskDialog, part, state)` natively. Text is then owner-drawn to
157+
fix the `TaskDialogStyle` colour bug described above.
158+
159+
### Windows 10 (build 17763–19045)
160+
161+
`DarkMode_TaskDialog` does not exist. The library:
162+
163+
1. Uses **UI Automation** to walk the `DirectUIHWND` child tree, identifying
164+
each element by its `AutomationId` (the UIFILE atom names).
165+
2. Subclasses each pane with `WM_ERASEBKGND` / `WM_CTLCOLORDLG` /
166+
`WM_CTLCOLORSTATIC` handlers that return a dark brush with the correct
167+
`SetTextColor` per element type.
168+
3. Owner-draws all text elements with `DrawThemeTextEx` + `DTT_TEXTCOLOR`
169+
(background filled first since there is no native dark panel paint).
170+
4. Owner-draws the expando glyph via `DrawThemeBackground(TaskDialog, 13, state)`
171+
reading the current expand/collapse state from window properties
172+
(`"IsExpanded"`, `"IsChecked"`) seeded by `TDN_EXPANDO_BUTTON_CLICKED` /
173+
`TDN_VERIFICATION_CLICKED`.
174+
175+
| Element | Windows 10 | Windows 11 |
176+
|---|---|---|
177+
| `PrimaryPanel` background | `WM_ERASEBKGND` dark brush | `dtb(DarkMode_TaskDialog, 1, 0)`|
178+
| `SecondaryPanel` background | `WM_ERASEBKGND` dark brush | `dtb(DarkMode_TaskDialog, 8, 0)`|
179+
| `FootnoteArea` background | `WM_ERASEBKGND` dark brush | `dtb(DarkMode_TaskDialog, 15, 0)`|
180+
| `SeparatorLine` background | dark brush | `dtb(DarkMode_TaskDialog, 17, 0)`|
181+
| Expando glyph | `dtb(TaskDialog, 13, state)` | `dtb(DarkMode_TaskDialog, 13, state)`|
182+
| **All text** (`gtc(TaskDialogStyle,*,*)`) | `FillRect` + `DrawThemeTextEx` + `DTT_TEXTCOLOR` | `DrawThemeText` + `DTT_TEXTCOLOR` |
183+
184+
---
185+
186+
## UIA element reference
187+
188+
These `AutomationId` strings come directly from the comctl32 UIFILE (resource
189+
4255, Windows 11 build 26100.7965). They are the **atom names** the DirectUI
190+
engine uses internally and are exactly what `IUIAutomationElement::get_CurrentAutomationId`
191+
returns when walking the `DirectUIHWND` tree.
192+
193+
> **This is the only public cross-reference of these identifiers against their
194+
> UxTheme part IDs and resolved dark-mode colour values.**
195+
196+
### Root
197+
198+
| AutomationId | Description |
199+
|---|---|
200+
| `"TaskDialog"` | The `DirectUIHWND` TaskPage window — UIA walk entry point |
201+
202+
### Primary panel
203+
204+
| AutomationId | UIFILE atom | Control type | UxTheme query | Dark colour |
205+
|---|---|---|---|---|
206+
| `MainIcon` | `atom(MainIcon)` | Image || No recolour |
207+
| `MainInstruction` | `atom(MainInstruction)` | Text | `gtc(TaskDialogStyle, 2, 0, 3803)` | `RGB(153, 235, 255)` |
208+
| `ContentText` | `atom(ContentText)` | Text | `gtc(TaskDialogStyle, 4, 0, 3803)` | `RGB(255, 255, 255)` |
209+
| `ContentLink` | `atom(ContentLink)` | Hyperlink | `gtc(TaskDialogStyle, 4, 0, 3803)` + `dtb(TaskDialog,1,0)` bg | `RGB(255, 255, 255)` |
210+
| `ExpandedInformationText` | `atom(ExpandedInformationText)` | Text | `gtc(TaskDialogStyle, 6, 0, 3803)` | `RGB(255, 255, 255)` |
211+
| `ExpandedInformationLink` | `atom(ExpandedInformationLink)` | Hyperlink | `gtc(TaskDialogStyle, 6, 0, 3803)` + `dtb(TaskDialog,1,0)` bg | `RGB(255, 255, 255)` |
212+
| `ExpandoButton` | `atom(ExpandoButton)` | Button | `dtb(TaskDialog, 13, state)` | Owner-drawn glyph |
213+
| `ExpandoTextExpanded` | `atom(ExpandoTextExpanded)` | Text | `gtc(TaskDialogStyle, 12, 0, 3803)` | `RGB(255, 255, 255)` |
214+
| `ExpandoTextCollapsed` | `atom(ExpandoTextCollapsed)` | Text | `gtc(TaskDialogStyle, 12, 0, 3803)` | `RGB(255, 255, 255)` |
215+
| `VerificationCheckBox` | `atom(VerificationCheckBox)` | CheckBox || System-themed |
216+
| `VerificationText` | `atom(VerificationText)` | Text | `gtc(TaskDialogStyle, 14, 0, 3803)` | `RGB(255, 255, 255)` |
217+
| `RadioButton_0``_N` | `class RadioButton` | RadioButton | `dtb(TaskDialog, 1, 0)` bg | System-themed |
218+
| `CommandLink_0``_N` | `class CommandLink` | Button | `dtb(TaskDialog, 1, 0)` bg | `DarkMode_Explorer` theme |
219+
| `CommandButton_0``_N` | `class CommandButton` | Button || `DarkMode_Explorer` theme |
220+
| `ProgressBar` | `atom(ProgressBar)` | ProgressBar || System-themed |
221+
222+
### Secondary panel (button row)
223+
224+
| AutomationId | UIFILE atom | UxTheme query | Dark colour |
225+
|---|---|---|---|
226+
| *(push buttons)* | `class CommandButton` | `dtb(TaskDialog, 8, 0)` bg | `DarkMode_Explorer` / `DarkMode_CFD` |
227+
| `ButtonArea` | `atom(ButtonArea)` | `gtc(TaskDialogStyle, **15**, 0, 3803)` ⚠️ | Uses footnote part — UIFILE quirk |
228+
229+
### Separator
230+
231+
| AutomationId | UIFILE atom | UxTheme query | Dark colour |
232+
|---|---|---|---|
233+
| `Separator` | `atom(Separator)` | `dtb(TaskDialog, 15, 0)` bg | `RGB(44, 44, 44)` |
234+
| `SeparatorLine` | `atom(SeparatorLine)` | `dtb(TaskDialog, 17, 0)` bg | `RGB(77, 77, 77)` |
235+
236+
### Footnote / expanded footer panel
237+
238+
| AutomationId | UIFILE atom | UxTheme query | Dark colour |
239+
|---|---|---|---|
240+
| `FootnoteIcon` | `atom(FootnoteIcon)` || No recolour |
241+
| `FootnoteText` | `atom(FootnoteText)` | `gtc(TaskDialogStyle, 15, 0, 3803)` | `RGB(224, 224, 224)` |
242+
| `FootnoteTextLink` | `atom(FootnoteTextLink)` | `gtc(TaskDialogStyle, 15, 0, 3803)` | `RGB(224, 224, 224)` |
243+
| `ExpandedFooterText` | `atom(ExpandedFooterText)` | `gtc(TaskDialogStyle, 18, 0, 3803)` | `RGB(224, 224, 224)` |
244+
| `ExpandedFooterTextLink` | `atom(ExpandedFooterTextLink)` | `gtc(TaskDialogStyle, **4**, 0, 3803)` ⚠️ | `RGB(224, 224, 224)` |
245+
246+
> ⚠️ **`ExpandedFooterTextLink` uses part 4** (the content text part) instead
247+
> of part 18 in the UIFILE. This is a Microsoft bug — the wrong part ID is
248+
> hardcoded. The library handles this by checking the `AutomationId` and
249+
> applying part-18 colours regardless of what `gtc()` returns.
250+
251+
---
252+
253+
## FAQ
254+
255+
**Does this work with the simple `TaskDialog()` overload?**
256+
`TaskDialog()` has no callback, so there is no `TDN_CREATED` hook point.
257+
Use `TaskDialogIndirect()` with a `TASKDIALOGCONFIG`.
258+
259+
**Does `TDM_NAVIGATE_PAGE` work?**
260+
Yes — call `DarkMode::AllowForTaskDialog(hwnd, pNewConfig)` from `TDN_NAVIGATED`.
261+
The included `main.cpp` demonstrates page navigation to an Arabic RTL page.
262+
263+
**Why does my text still appear black on Windows 11?**
264+
Because `DarkMode_TaskDialogStyle` does not exist — see the UIFILE bug section
265+
above. All text must be owner-drawn with `DTT_TEXTCOLOR`. If you call only
266+
`SetWindowTheme` without the text owner-draw path you will get dark panel
267+
backgrounds but black text.
268+
269+
**I see a white flash when the dialog first opens.**
270+
Ensure `DarkMode::AllowForTaskDialog` is called from `TDN_CREATED`, not
271+
`TDN_DIALOG_CONSTRUCTED`. `TDN_CREATED` fires after the window is fully
272+
initialised.
273+
274+
**Can I use this from MFC?**
275+
Yes — no MFC dependency. Override `DoMessageBox` and call
276+
`TaskDialogIndirect` directly. `DarkMode::Init()` can go in `InitInstance`.
277+
278+
**What about pre-Windows 10?**
279+
`DarkMode::Init()` reads OS state at startup. All calls are silent no-ops
280+
on pre-Win10 systems.
281+
282+
---
283+
284+
## Building
285+
286+
```cmd
287+
git clone https://github.com/YourUsername/DarkTaskDialog-NoDetours.git
288+
cd DarkTaskDialog-NoDetours
289+
msbuild DarkTaskDialog-NoDetours.sln /p:Configuration=Release /p:Platform=x64
290+
```
291+
292+
Links only against: `uxtheme.lib` `dwmapi.lib` `comctl32.lib` `shell32.lib`
293+
`uiautomationcore.lib` `msimg32.lib` — all part of the Windows SDK.
294+
295+
---
296+
297+
## Related
298+
299+
### Tools
300+
301+
- [memoarfaa/TaskDialog-Stylesheet-Dumper](https://github.com/memoarfaa/TaskDialog-Stylesheet-Dumper)
302+
Win32 tool that extracts and parses `comctl32.dll` resource 4255 (`UIFILE`) at
303+
runtime, then evaluates every `gtf()`, `gtc()`, `gtmar()`, `gtmet()`, and
304+
`dtb()` call live against `OpenThemeData(L"TaskDialog")` and
305+
`OpenThemeData(L"TaskDialogStyle")`. The resolved colour table and UIFILE
306+
bug findings documented in this README were produced with this tool.
307+
Uses only `IXmlReader`, `FindResourceW`, and `GetThemeColor` — no third-party
308+
dependencies. Output can be saved as XML.
309+
310+
### Other implementations
311+
312+
- [SFTRS/DarkTaskDialog](https://github.com/SFTRS/DarkTaskDialog)
313+
alternative approach using Microsoft Detours to hook `DrawTheme*` APIs (GPL-3.0)
314+
315+
### References
316+
317+
- [Stack Overflow #79403975: Dark Mode Task Dialog](https://stackoverflow.com/questions/79403975/)
318+
- [TaskDialogIndirect — Win32 docs](https://docs.microsoft.com/windows/win32/api/commctrl/nf-commctrl-taskdialogindirect)
319+
320+
---
321+
322+
## License
323+
324+
MIT — see [LICENSE](LICENSE).

0 commit comments

Comments
 (0)