[Git][wxwidgets/wxwidgets][master] 52 commits: Factor out wxIsSystemColourChange() helper function

2 views
Skip to first unread message

Vadim Zeitlin (@_VZ_)

unread,
Jun 13, 2026, 3:01:06 PM (12 days ago) Jun 13
to wx-commi...@googlegroups.com


Vadim Zeitlin pushed to branch master at wxWidgets / wxWidgets


Commits:
92254ce2 by Vadim Zeitlin at 2026-06-07T18:14:58+02:00
Factor out wxIsSystemColourChange() helper function

Check whether the LPARAM of WM_SETTINGCHANGE is "ImmersiveColorSet".

No real changes yet, just make it possible to reuse this in the upcoming
commits.

- - - - -
8e2ac384 by Mohmed abdel-fattah at 2026-06-07T18:14:58+02:00
Add dark mode support for task dialog-based dialogs

Show wxMessageDialog and wxProgressDialog using appropriate colours when
dark mode is active.

Closes #26240.

Closes #26311.

- - - - -
2ebde404 by Vadim Zeitlin at 2026-06-07T18:14:58+02:00
Remove WM_SETTINGCHANGE handling from task dialog proc

Task dialog callback never gets this message, so this code is useless.

- - - - -
091ba7a8 by Vadim Zeitlin at 2026-06-07T18:14:58+02:00
Remove unnecessary member initialization in TDPageState ctor

Members are already initialized in their declarations.

- - - - -
0dce46f5 by Vadim Zeitlin at 2026-06-07T18:14:58+02:00
Use AutoHBRUSH instead of manually allocating/deleting HBRUSHes

This allows to remove DestroyBrushes() and Destroy() itself as it was
only called before destroying TDPageState object and the brushes are now
deleted by the dtor.

Also allocate the brushes immediately when creating this object, they
will be always used and so it doesn't seem worth it to postpone doing
this.

- - - - -
12eed00e by Vadim Zeitlin at 2026-06-07T18:14:58+02:00
Make IUIAutomation pointer a local variable

There is no need to make it a global.

Also reformat the code a bit.

- - - - -
80485247 by Vadim Zeitlin at 2026-06-07T18:14:59+02:00
Return TRUE, not 1, when handling WM_ERASEBKGND

No real changes, just use the usual convention.

- - - - -
3ed49f01 by Vadim Zeitlin at 2026-06-07T18:14:59+02:00
Use safer wxIID_PPV_ARGS() instead of cast to "void**"

The macro ensures that the pointer is of the correct type.

- - - - -
739cefeb by Vadim Zeitlin at 2026-06-07T18:14:59+02:00
Localize GET_[BGR] macros

Ideal would be to replace them with wxColour functions but for now at
least make their scope as small as possible.

- - - - -
e1ca1dd7 by Vadim Zeitlin at 2026-06-07T18:14:59+02:00
Use wxColour and wxUxThemeHandle instead of raw Windows API

Also don't try to get TDLG_SECONDARYPANEL and TDLG_FOOTNOTESEPARATOR
from the "TaskDialog" theme under Windows 10 as it doesn't define them.

No real changes.

- - - - -
8b2b9069 by Vadim Zeitlin at 2026-06-07T18:14:59+02:00
Don't define CLSID_CUIAutomation and IID_IUIAutomation ourselves

They should be already defined in the SDK headers.

- - - - -
fac69ca3 by Vadim Zeitlin at 2026-06-07T18:14:59+02:00
Add default ctor and move assignment to wxUxThemeHandle

Simplify the code in task dialog dark mode support by using the new
special member functions instead of ugly workarounds that were used
before.

- - - - -
1c6d7e96 by Vadim Zeitlin at 2026-06-07T18:30:53+02:00
Don't use thread-local in task dialog dark mode support code

The global variables are only ever accessed from the GUI thread and
don't need to be thread-local.

Also add a wxModule to clean them up on exit.

- - - - -
a5979f40 by Vadim Zeitlin at 2026-06-07T21:46:43+02:00
Add helper GetHWNDFromElement(IUIAutomationElement) function

Hide ugly cast from UIA_HWND to HWND in a function and also add error
handling after IUIAutomationElement::get_CurrentNativeWindowHandle()
call.

- - - - -
db39a68c by Vadim Zeitlin at 2026-06-07T21:47:49+02:00
Stop using wxBasicString unnecessarily

Just use wcscmp() to check BSTR contents directly.

- - - - -
e0a95d55 by Vadim Zeitlin at 2026-06-07T21:49:21+02:00
Rename LPARAM parameter to standard "lParam" name

No real changes, just don't deviate from the usual convention
unnecessarily.

- - - - -
dede73a5 by Vadim Zeitlin at 2026-06-07T21:49:56+02:00
Don't store IUIAutomation in TDEnumData

This is unnecessary, as this pointer is stored in a global variable
already accessed directly from the other function, so just access it
from here as well.

- - - - -
bd03fbb6 by Vadim Zeitlin at 2026-06-07T21:51:44+02:00
Don't pass IUIAutomation as parameter to TDApplyToChildren()

Similar to the last comment, this IUIAutomation pointer is stored in a
global variable, so just access it directly for consistency.

- - - - -
88b1cafa by Vadim Zeitlin at 2026-06-07T21:58:06+02:00
Reuse GetWindowDPI() from task dialog dark mode code

Don't duplicate this function definition, make it internally public,
i.e. declare it, with "wx" prefix, in a private header and reuse it.

- - - - -
026e93d2 by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Check for error after calling ::SetWindowTheme()

Reuse the already existing code in darkmode.cpp by extracting it into
SetTheme() function and call it from task dialog dark mode code.

Also add a helper wxHasRealDarkTheme() function to make the code more
readable.

- - - - -
6d45067a by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Remove TDPageState::CloseThemes()

Calling this function to reset "themesOk" member variable was
inconsistent with resetting similar "elemsOk" directly, just remove the
function to make things simpler and more consistent.

- - - - -
683d7352 by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Remove TDPageState::hTDS which was same as hTD

The 2 themes handles were always identical, so remove one of them.

- - - - -
39691558 by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Remove TDPageState::isDark

This wasn't used consistently, in many places TDHasNativeDarkTheme()
(which is used to initialize this member variable) was called directly,
so just stop storing its value in this variable and always call this
function, which is cheap as it caches the return value.

- - - - -
23409e05 by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Don't do anything in TDPaintGlyphs() when using native dark theme

This function effectively didn't do anything anyhow in this case, just
make it more clear and save some useless work by returning immediately.

- - - - -
847561bd by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Extend wxUxThemeHandle and use it more in dark task dialog code

Add HDC parameter to ::GetThemePartSize() wrappers and also add wrappers
for ::GetThemeMargins() and ::GetThemeFont().

The net effect is a small simplification of the task dialog code and
checking and logging of errors when calling these functions.

- - - - -
3de0aec9 by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Add WinStructWordSize<> helper and use it

This is similar to WinStruct<> but initializes dwSize and not cbSize
struct member.

In the future we should auto-detect the xxSize existence in WinStruct
using either SFINAE or, preferably, concepts, but for now keep things
simple.

- - - - -
26bf503a by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Use recently added wxUxThemeHandle::GetFont()

No real changes, just simplify the code by using wx wrapper.

- - - - -
5acd403f by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Reformat task dialog dark mode support code

No real changes, a lot of formatting and indentation fixes and some
minor changes restructuring the code to be simpler and more readable.

This commit is best viewed ignoring whitespace-only changes.

- - - - -
8ab0173e by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Add GetCurrentAutomationId() helper

Wrap calls to IUIAutomationElement::get_CurrentAutomationId().

- - - - -
b5baa3e6 by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Remove unnecessary "static" from functions in anonymous namespace

Use of "static" is unnecessary, these functions already have internal
linkage.

- - - - -
361dbd96 by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Use wx-specific subclass IDs

Use the same prefix for all of our subclass IDs, this should hopefully
minimize the probability of a clash.

- - - - -
272875a9 by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Add wrappers around window subclassing functions

Simplify the code a bit more by avoiding calls to GetWindowSubclass()
and then SetWindowSubclass() or RemoveWindowSubclass() with redundant
parameters.

- - - - -
a2d7a75a by Vadim Zeitlin at 2026-06-08T02:44:16+02:00
Fix HBRUSH leaks in task dialog dark mode code

For not totally clear reasons, the brushes allocated here were not
freed, resulting in the number of allocated brushes growing every time a
dialog was shown.

Use brushes from wxBrushList to prevent this from happening.

This doesn't fix all GDI leaks yet, there is still a USER handle and 3
GDI handlers leaked for every dialog shown, but they don't seem to be
brushes any more.

- - - - -
1ec6163e by Vadim Zeitlin at 2026-06-08T03:06:00+02:00
Include wx/brush.h to fix the build when not using PCH

- - - - -
37ff69e8 by Vadim Zeitlin at 2026-06-08T03:06:00+02:00
Disable support for dark mode in task dialog for wxUniv

Native dialogs shouldn't be used anyhow when using wxUniv.

- - - - -
97c1f3bb by Vadim Zeitlin at 2026-06-08T03:10:24+02:00
Ignore unused parameters when dark mode support is disabled

Suppress harmless warnings.

- - - - -
53d16f01 by Vadim Zeitlin at 2026-06-08T03:18:33+02:00
Fix HICON leak in task dialog dark mode support code

Destroy the icon returned by SHGetStockIconInfo().

- - - - -
af620105 by Vadim Zeitlin at 2026-06-10T16:12:39+02:00
Add GitHub user name of Mohmed Abdel-Fattah to "Author" lines

See https://github.com/wxWidgets/wxWidgets/pull/26570#discussion_r3385217953

- - - - -
bbb3fc78 by Vadim Zeitlin at 2026-06-10T16:34:31+02:00
Keep a single real implementation of SetWindowSubclassIfNeeded()

Don't duplicate it for the special case of passing 0 as data, the
compiler should be able to optimize out the trivial lambda and doing it
like this reduces code duplication.

No real changes.

- - - - -
ec85de78 by Vadim Zeitlin at 2026-06-10T16:35:25+02:00
Fix problem with wrongly deleting the background brush

We shouldn't install HBRUSH owned by us as class background brush
because this brush is deleted by Windows itself, invalidating the HBRUSH
and resulting in visible problems when the background is painted using
this HBRUSH after showing a task dialog in dark mode.

Fix this by uninstalling the brush before deleting the window.

This also avoids the awkward code deleting the old class brush only if
it's not one of the standard brushes: now we don't need to do it any
longer, as we restore this brush before deleting the window.

- - - - -
6132c170 by Vadim Zeitlin at 2026-06-11T04:09:12+02:00
Add AutoBSTR helper to fix BSTR leaks in task dialog code

Also check that the returned BSTR is non-NULL before using it, as it can
be NULL even if the accessor returned S_OK.

- - - - -
aeef4594 by Vadim Zeitlin at 2026-06-11T04:14:50+02:00
Fix progress dialog background

Pass the dark brush to use in the dialog subclass proc and do not set
the class background brush to avoid it being freed.

- - - - -
afe8b984 by Vadim Zeitlin at 2026-06-11T23:23:34+02:00
Simplify check for null element class

Return early in this case to ensure that the pointer can be used without
any checks in the code below.

No real changes.

- - - - -
357265c2 by Mohmed abdel-fattah at 2026-06-11T23:30:04+02:00
Apply dark theme to the task dialog scrollbars

They may be shown after expanding the "Details" section if the contents
is big enough and didn't adapt to the dark theme without this change.

- - - - -
458549bd by Mohmed abdel-fattah at 2026-06-11T23:33:35+02:00
Draw background of expanded footer area under Windows 11

Fix wrong background appearance under some (newest?) Windows 11 systems.

- - - - -
c3c3b70a by Vadim Zeitlin at 2026-06-12T18:42:17+02:00
Don't say that task dialogs don't support dark mode any more

This limitation is lifted by the changes in this branch.

- - - - -
29e64673 by Steve Cornett at 2026-06-12T18:44:00+02:00
Preserve wxToolBar custom background colour upon dark mode switch

Avoid overwriting wxToolBar custom colours upon switching into or out of
dark mode.

Closes #26581.

- - - - -
5bedad68 by Vadim Zeitlin at 2026-06-13T20:21:58+02:00
Remove note about wxToolBar::SetDropdownMenu() in dark mode

This remark is obsolete, the problem described by it was fixed by
c301af42f9 (Fix wxMSW toolbar drop down arrow appearance in dark mode,
2024-10-22).

- - - - -
0e60afa6 by Steve Cornett at 2026-06-13T20:25:33+02:00
Preserve wxListCtrl custom colours upon dark mode switch in wxMSW

Avoid overwriting custom colours upon switching into or out of dark
mode.

The text colour is now the same thing as the foreground colour, as with
wxGenericListCtrl.

Closes #26584.

- - - - -
fd35f189 by Vadim Zeitlin at 2026-06-13T20:28:01+02:00
Merge branch 'msw-dark-taskdlg'

Add dark mode support to wxMSW task dialog-based dialogs.

See #26570.

- - - - -
7ee01fa8 by dxbjavid at 2026-06-13T20:42:07+02:00
Validate background colour index in wxGIFDecoder::LoadGIF()

Don't set background colour to uninitialized memory contents, just
ignore the invalid index.

Closes #26582.

- - - - -
47945b13 by Vadim Zeitlin at 2026-06-13T20:52:51+02:00
Use "windows-2025" image for wxQt builds using MSVS 2022

This MSVS version is not available in "windows-2025-vs2026" image to
which "windows-latest" maps now.

- - - - -


30 changed files:

- .github/workflows/ci_cmake.yml
- Makefile.in
- build/bakefiles/files.bkl
- build/cmake/files.cmake
- build/files
- build/msw/makefile.gcc
- build/msw/makefile.vc
- build/msw/wx_core.vcxproj
- build/msw/wx_core.vcxproj.filters
- include/wx/msw/listctrl.h
- include/wx/msw/private.h
- include/wx/msw/private/darkmode.h
- + include/wx/msw/private/taskdlg.h
- include/wx/msw/uxtheme.h
- interface/wx/app.h
- src/common/gifdecod.cpp
- src/msw/darkmode.cpp
- src/msw/datecontrols.cpp
- src/msw/joystick.cpp
- src/msw/listctrl.cpp
- src/msw/msgdlg.cpp
- src/msw/progdlg.cpp
- src/msw/renderer.cpp
- src/msw/statbox.cpp
- + src/msw/taskdlg.cpp
- src/msw/toolbar.cpp
- src/msw/utils.cpp
- src/msw/uxtheme.cpp
- src/msw/window.cpp
- tests/image/image.cpp


Changes:

=====================================
.github/workflows/ci_cmake.yml
=====================================
@@ -101,7 +101,7 @@ jobs:
cmake_tests: CONSOLE_ONLY
cmake_build_toolkit: msw
- name: MSW/MSVC wxQt 5.15
- runner: windows-latest
+ runner: windows-2025
no_sudo: 1
cmake_generator: 'Visual Studio 17 2022'
cmake_defines: -DCMAKE_BUILD_TYPE=Debug -DwxBUILD_SHARED=ON


=====================================
Makefile.in
=====================================
@@ -5067,7 +5067,8 @@ COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS = \
monodll_darkmode.o \
monodll_msw_appprogress.o \
monodll_taskbarbutton.o \
- monodll_msw_power.o
+ monodll_msw_power.o \
+ monodll_taskdlg.o
@COND_TOOLKIT_MSW@__LOWLEVEL_SRC_OBJECTS = $(COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS)
@COND_TOOLKIT_OSX_COCOA@__LOWLEVEL_SRC_OBJECTS = \
@COND_TOOLKIT_OSX_COCOA@ $(__OSX_LOWLEVEL_SRC_OBJECTS)
@@ -5811,7 +5812,8 @@ COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_1 = \
monodll_darkmode.o \
monodll_msw_appprogress.o \
monodll_taskbarbutton.o \
- monodll_msw_power.o
+ monodll_msw_power.o \
+ monodll_taskdlg.o
@COND_TOOLKIT_MSW@__LOWLEVEL_SRC_OBJECTS_1 = $(COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_1)
@COND_TOOLKIT_OSX_COCOA@__LOWLEVEL_SRC_OBJECTS_1 = \
@COND_TOOLKIT_OSX_COCOA@ $(__OSX_LOWLEVEL_SRC_OBJECTS)
@@ -6855,7 +6857,8 @@ COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_2 = \
monolib_darkmode.o \
monolib_msw_appprogress.o \
monolib_taskbarbutton.o \
- monolib_msw_power.o
+ monolib_msw_power.o \
+ monolib_taskdlg.o
@COND_TOOLKIT_MSW@__LOWLEVEL_SRC_OBJECTS_2 = $(COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_2)
@COND_TOOLKIT_OSX_COCOA@__LOWLEVEL_SRC_OBJECTS_2 = \
@COND_TOOLKIT_OSX_COCOA@ $(__OSX_LOWLEVEL_SRC_OBJECTS_17)
@@ -7599,7 +7602,8 @@ COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_3 = \
monolib_darkmode.o \
monolib_msw_appprogress.o \
monolib_taskbarbutton.o \
- monolib_msw_power.o
+ monolib_msw_power.o \
+ monolib_taskdlg.o
@COND_TOOLKIT_MSW@__LOWLEVEL_SRC_OBJECTS_3 = $(COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_3)
@COND_TOOLKIT_OSX_COCOA@__LOWLEVEL_SRC_OBJECTS_3 = \
@COND_TOOLKIT_OSX_COCOA@ $(__OSX_LOWLEVEL_SRC_OBJECTS_17)
@@ -8789,7 +8793,8 @@ COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_4 = \
coredll_darkmode.o \
coredll_msw_appprogress.o \
coredll_taskbarbutton.o \
- coredll_msw_power.o
+ coredll_msw_power.o \
+ coredll_taskdlg.o
@COND_TOOLKIT_MSW@__LOWLEVEL_SRC_OBJECTS_4 = $(COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_4)
@COND_TOOLKIT_OSX_COCOA@__LOWLEVEL_SRC_OBJECTS_4 = \
@COND_TOOLKIT_OSX_COCOA@ $(__OSX_LOWLEVEL_SRC_OBJECTS_1_1)
@@ -9533,7 +9538,8 @@ COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_5 = \
coredll_darkmode.o \
coredll_msw_appprogress.o \
coredll_taskbarbutton.o \
- coredll_msw_power.o
+ coredll_msw_power.o \
+ coredll_taskdlg.o
@COND_TOOLKIT_MSW@__LOWLEVEL_SRC_OBJECTS_5 = $(COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_5)
@COND_TOOLKIT_OSX_COCOA@__LOWLEVEL_SRC_OBJECTS_5 = \
@COND_TOOLKIT_OSX_COCOA@ $(__OSX_LOWLEVEL_SRC_OBJECTS_1_1)
@@ -10302,7 +10308,8 @@ COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_6 = \
corelib_darkmode.o \
corelib_msw_appprogress.o \
corelib_taskbarbutton.o \
- corelib_msw_power.o
+ corelib_msw_power.o \
+ corelib_taskdlg.o
@COND_TOOLKIT_MSW@__LOWLEVEL_SRC_OBJECTS_6 = $(COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_6)
@COND_TOOLKIT_OSX_COCOA@__LOWLEVEL_SRC_OBJECTS_6 = \
@COND_TOOLKIT_OSX_COCOA@ $(__OSX_LOWLEVEL_SRC_OBJECTS_1_4)
@@ -11046,7 +11053,8 @@ COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_7 = \
corelib_darkmode.o \
corelib_msw_appprogress.o \
corelib_taskbarbutton.o \
- corelib_msw_power.o
+ corelib_msw_power.o \
+ corelib_taskdlg.o
@COND_TOOLKIT_MSW@__LOWLEVEL_SRC_OBJECTS_7 = $(COND_TOOLKIT_MSW___LOWLEVEL_SRC_OBJECTS_7)
@COND_TOOLKIT_OSX_COCOA@__LOWLEVEL_SRC_OBJECTS_7 = \
@COND_TOOLKIT_OSX_COCOA@ $(__OSX_LOWLEVEL_SRC_OBJECTS_1_4)
@@ -18082,6 +18090,9 @@ monodll_sound_sdl.o: $(srcdir)/src/unix/sound_sdl.cpp $(MONODLL_ODEP)
@COND_TOOLKIT_MSW_USE_GUI_1@monodll_msw_power.o: $(srcdir)/src/msw/power.cpp $(MONODLL_ODEP)
@COND_TOOLKIT_MSW_USE_GUI_1@ $(CXXC) -c -o $@ $(MONODLL_CXXFLAGS) $(srcdir)/src/msw/power.cpp

+@COND_TOOLKIT_MSW_USE_GUI_1@monodll_taskdlg.o: $(srcdir)/src/msw/taskdlg.cpp $(MONODLL_ODEP)
+@COND_TOOLKIT_MSW_USE_GUI_1@ $(CXXC) -c -o $@ $(MONODLL_CXXFLAGS) $(srcdir)/src/msw/taskdlg.cpp
+
@COND_PLATFORM_MACOSX_1_TOOLKIT_OSX_COCOA_USE_GUI_1@monodll_artmac.o: $(srcdir)/src/osx/artmac.cpp $(MONODLL_ODEP)
@COND_PLATFORM_MACOSX_1_TOOLKIT_OSX_COCOA_USE_GUI_1@ $(CXXC) -c -o $@ $(MONODLL_CXXFLAGS) $(srcdir)/src/osx/artmac.cpp

@@ -22876,6 +22887,9 @@ monolib_sound_sdl.o: $(srcdir)/src/unix/sound_sdl.cpp $(MONOLIB_ODEP)
@COND_TOOLKIT_MSW_USE_GUI_1@monolib_msw_power.o: $(srcdir)/src/msw/power.cpp $(MONOLIB_ODEP)
@COND_TOOLKIT_MSW_USE_GUI_1@ $(CXXC) -c -o $@ $(MONOLIB_CXXFLAGS) $(srcdir)/src/msw/power.cpp

+@COND_TOOLKIT_MSW_USE_GUI_1@monolib_taskdlg.o: $(srcdir)/src/msw/taskdlg.cpp $(MONOLIB_ODEP)
+@COND_TOOLKIT_MSW_USE_GUI_1@ $(CXXC) -c -o $@ $(MONOLIB_CXXFLAGS) $(srcdir)/src/msw/taskdlg.cpp
+
@COND_PLATFORM_MACOSX_1_TOOLKIT_OSX_COCOA_USE_GUI_1@monolib_artmac.o: $(srcdir)/src/osx/artmac.cpp $(MONOLIB_ODEP)
@COND_PLATFORM_MACOSX_1_TOOLKIT_OSX_COCOA_USE_GUI_1@ $(CXXC) -c -o $@ $(MONOLIB_CXXFLAGS) $(srcdir)/src/osx/artmac.cpp

@@ -27733,6 +27747,9 @@ coredll_sound_sdl.o: $(srcdir)/src/unix/sound_sdl.cpp $(COREDLL_ODEP)
@COND_TOOLKIT_MSW_USE_GUI_1@coredll_msw_power.o: $(srcdir)/src/msw/power.cpp $(COREDLL_ODEP)
@COND_TOOLKIT_MSW_USE_GUI_1@ $(CXXC) -c -o $@ $(COREDLL_CXXFLAGS) $(srcdir)/src/msw/power.cpp

+@COND_TOOLKIT_MSW_USE_GUI_1@coredll_taskdlg.o: $(srcdir)/src/msw/taskdlg.cpp $(COREDLL_ODEP)
+@COND_TOOLKIT_MSW_USE_GUI_1@ $(CXXC) -c -o $@ $(COREDLL_CXXFLAGS) $(srcdir)/src/msw/taskdlg.cpp
+
@COND_PLATFORM_MACOSX_1_TOOLKIT_OSX_COCOA_USE_GUI_1@coredll_artmac.o: $(srcdir)/src/osx/artmac.cpp $(COREDLL_ODEP)
@COND_PLATFORM_MACOSX_1_TOOLKIT_OSX_COCOA_USE_GUI_1@ $(CXXC) -c -o $@ $(COREDLL_CXXFLAGS) $(srcdir)/src/osx/artmac.cpp

@@ -31489,6 +31506,9 @@ corelib_sound_sdl.o: $(srcdir)/src/unix/sound_sdl.cpp $(CORELIB_ODEP)
@COND_TOOLKIT_MSW_USE_GUI_1@corelib_msw_power.o: $(srcdir)/src/msw/power.cpp $(CORELIB_ODEP)
@COND_TOOLKIT_MSW_USE_GUI_1@ $(CXXC) -c -o $@ $(CORELIB_CXXFLAGS) $(srcdir)/src/msw/power.cpp

+@COND_TOOLKIT_MSW_USE_GUI_1@corelib_taskdlg.o: $(srcdir)/src/msw/taskdlg.cpp $(CORELIB_ODEP)
+@COND_TOOLKIT_MSW_USE_GUI_1@ $(CXXC) -c -o $@ $(CORELIB_CXXFLAGS) $(srcdir)/src/msw/taskdlg.cpp
+
@COND_PLATFORM_MACOSX_1_TOOLKIT_OSX_COCOA_USE_GUI_1@corelib_artmac.o: $(srcdir)/src/osx/artmac.cpp $(CORELIB_ODEP)
@COND_PLATFORM_MACOSX_1_TOOLKIT_OSX_COCOA_USE_GUI_1@ $(CXXC) -c -o $@ $(CORELIB_CXXFLAGS) $(srcdir)/src/osx/artmac.cpp



=====================================
build/bakefiles/files.bkl
=====================================
@@ -1765,6 +1765,7 @@ IMPORTANT: please read docs/tech/tn0016.txt before modifying this file!
src/msw/appprogress.cpp
src/msw/taskbarbutton.cpp
src/msw/power.cpp
+ src/msw/taskdlg.cpp
</set>
<set var="MSW_LOWLEVEL_HDR" hints="files">
wx/msw/nonownedwnd.h


=====================================
build/cmake/files.cmake
=====================================
@@ -1641,6 +1641,7 @@ set(MSW_LOWLEVEL_SRC
src/msw/appprogress.cpp
src/msw/taskbarbutton.cpp
src/msw/power.cpp
+ src/msw/taskdlg.cpp
)

set(MSW_LOWLEVEL_HDR


=====================================
build/files
=====================================
@@ -1641,6 +1641,7 @@ MSW_LOWLEVEL_SRC =
src/msw/sound.cpp
src/msw/taskbar.cpp
src/msw/taskbarbutton.cpp
+ src/msw/taskdlg.cpp
src/msw/textmeasure.cpp
src/msw/tooltip.cpp
src/msw/toplevel.cpp


=====================================
build/msw/makefile.gcc
=====================================
@@ -2145,6 +2145,7 @@ ____CORE_SRC_FILENAMES_OBJECTS = \
$(OBJS)\monodll_appprogress.o \
$(OBJS)\monodll_taskbarbutton.o \
$(OBJS)\monodll_power.o \
+ $(OBJS)\monodll_taskdlg.o \
$(OBJS)\monodll_clrpickerg.o \
$(OBJS)\monodll_collpaneg.o \
$(OBJS)\monodll_filepickerg.o \
@@ -2498,6 +2499,7 @@ ____CORE_SRC_FILENAMES_OBJECTS = \
$(OBJS)\monodll_appprogress.o \
$(OBJS)\monodll_taskbarbutton.o \
$(OBJS)\monodll_power.o \
+ $(OBJS)\monodll_taskdlg.o \
$(OBJS)\monodll_generic_accel.o \
$(OBJS)\monodll_clrpickerg.o \
$(OBJS)\monodll_collpaneg.o \
@@ -3015,6 +3017,7 @@ ____CORE_SRC_FILENAMES_1_OBJECTS = \
$(OBJS)\monolib_appprogress.o \
$(OBJS)\monolib_taskbarbutton.o \
$(OBJS)\monolib_power.o \
+ $(OBJS)\monolib_taskdlg.o \
$(OBJS)\monolib_clrpickerg.o \
$(OBJS)\monolib_collpaneg.o \
$(OBJS)\monolib_filepickerg.o \
@@ -3368,6 +3371,7 @@ ____CORE_SRC_FILENAMES_1_OBJECTS = \
$(OBJS)\monolib_appprogress.o \
$(OBJS)\monolib_taskbarbutton.o \
$(OBJS)\monolib_power.o \
+ $(OBJS)\monolib_taskdlg.o \
$(OBJS)\monolib_generic_accel.o \
$(OBJS)\monolib_clrpickerg.o \
$(OBJS)\monolib_collpaneg.o \
@@ -3760,6 +3764,7 @@ ____CORE_SRC_FILENAMES_2_OBJECTS = \
$(OBJS)\coredll_appprogress.o \
$(OBJS)\coredll_taskbarbutton.o \
$(OBJS)\coredll_power.o \
+ $(OBJS)\coredll_taskdlg.o \
$(OBJS)\coredll_clrpickerg.o \
$(OBJS)\coredll_collpaneg.o \
$(OBJS)\coredll_filepickerg.o \
@@ -4113,6 +4118,7 @@ ____CORE_SRC_FILENAMES_2_OBJECTS = \
$(OBJS)\coredll_appprogress.o \
$(OBJS)\coredll_taskbarbutton.o \
$(OBJS)\coredll_power.o \
+ $(OBJS)\coredll_taskdlg.o \
$(OBJS)\coredll_generic_accel.o \
$(OBJS)\coredll_clrpickerg.o \
$(OBJS)\coredll_collpaneg.o \
@@ -4461,6 +4467,7 @@ ____CORE_SRC_FILENAMES_3_OBJECTS = \
$(OBJS)\corelib_appprogress.o \
$(OBJS)\corelib_taskbarbutton.o \
$(OBJS)\corelib_power.o \
+ $(OBJS)\corelib_taskdlg.o \
$(OBJS)\corelib_clrpickerg.o \
$(OBJS)\corelib_collpaneg.o \
$(OBJS)\corelib_filepickerg.o \
@@ -4814,6 +4821,7 @@ ____CORE_SRC_FILENAMES_3_OBJECTS = \
$(OBJS)\corelib_appprogress.o \
$(OBJS)\corelib_taskbarbutton.o \
$(OBJS)\corelib_power.o \
+ $(OBJS)\corelib_taskdlg.o \
$(OBJS)\corelib_generic_accel.o \
$(OBJS)\corelib_clrpickerg.o \
$(OBJS)\corelib_collpaneg.o \
@@ -9297,6 +9305,11 @@ $(OBJS)\monodll_power.o: ../../src/msw/power.cpp
$(CXX) -c -o $@ $(MONODLL_CXXFLAGS) $(CPPDEPS) $<
endif

+ifeq ($(USE_GUI),1)
+$(OBJS)\monodll_taskdlg.o: ../../src/msw/taskdlg.cpp
+ $(CXX) -c -o $@ $(MONODLL_CXXFLAGS) $(CPPDEPS) $<
+endif
+
ifeq ($(USE_GUI),1)
$(OBJS)\monodll_clrpickerg.o: ../../src/generic/clrpickerg.cpp
$(CXX) -c -o $@ $(MONODLL_CXXFLAGS) $(CPPDEPS) $<
@@ -11925,6 +11938,11 @@ $(OBJS)\monolib_power.o: ../../src/msw/power.cpp
$(CXX) -c -o $@ $(MONOLIB_CXXFLAGS) $(CPPDEPS) $<
endif

+ifeq ($(USE_GUI),1)
+$(OBJS)\monolib_taskdlg.o: ../../src/msw/taskdlg.cpp
+ $(CXX) -c -o $@ $(MONOLIB_CXXFLAGS) $(CPPDEPS) $<
+endif
+
ifeq ($(USE_GUI),1)
$(OBJS)\monolib_clrpickerg.o: ../../src/generic/clrpickerg.cpp
$(CXX) -c -o $@ $(MONOLIB_CXXFLAGS) $(CPPDEPS) $<
@@ -14493,6 +14511,11 @@ $(OBJS)\coredll_power.o: ../../src/msw/power.cpp
$(CXX) -c -o $@ $(COREDLL_CXXFLAGS) $(CPPDEPS) $<
endif

+ifeq ($(USE_GUI),1)
+$(OBJS)\coredll_taskdlg.o: ../../src/msw/taskdlg.cpp
+ $(CXX) -c -o $@ $(COREDLL_CXXFLAGS) $(CPPDEPS) $<
+endif
+
ifeq ($(USE_GUI),1)
$(OBJS)\coredll_clrpickerg.o: ../../src/generic/clrpickerg.cpp
$(CXX) -c -o $@ $(COREDLL_CXXFLAGS) $(CPPDEPS) $<
@@ -16278,6 +16301,11 @@ $(OBJS)\corelib_power.o: ../../src/msw/power.cpp
$(CXX) -c -o $@ $(CORELIB_CXXFLAGS) $(CPPDEPS) $<
endif

+ifeq ($(USE_GUI),1)
+$(OBJS)\corelib_taskdlg.o: ../../src/msw/taskdlg.cpp
+ $(CXX) -c -o $@ $(CORELIB_CXXFLAGS) $(CPPDEPS) $<
+endif
+
ifeq ($(USE_GUI),1)
$(OBJS)\corelib_clrpickerg.o: ../../src/generic/clrpickerg.cpp
$(CXX) -c -o $@ $(CORELIB_CXXFLAGS) $(CPPDEPS) $<


=====================================
build/msw/makefile.vc
=====================================
@@ -2531,6 +2531,7 @@ ____CORE_SRC_FILENAMES_OBJECTS = \
$(OBJS)\monodll_appprogress.obj \
$(OBJS)\monodll_taskbarbutton.obj \
$(OBJS)\monodll_power.obj \
+ $(OBJS)\monodll_taskdlg.obj \
$(OBJS)\monodll_clrpickerg.obj \
$(OBJS)\monodll_collpaneg.obj \
$(OBJS)\monodll_filepickerg.obj \
@@ -2882,6 +2883,7 @@ ____CORE_SRC_FILENAMES_OBJECTS = \
$(OBJS)\monodll_appprogress.obj \
$(OBJS)\monodll_taskbarbutton.obj \
$(OBJS)\monodll_power.obj \
+ $(OBJS)\monodll_taskdlg.obj \
$(OBJS)\monodll_generic_accel.obj \
$(OBJS)\monodll_clrpickerg.obj \
$(OBJS)\monodll_collpaneg.obj \
@@ -3401,6 +3403,7 @@ ____CORE_SRC_FILENAMES_1_OBJECTS = \
$(OBJS)\monolib_appprogress.obj \
$(OBJS)\monolib_taskbarbutton.obj \
$(OBJS)\monolib_power.obj \
+ $(OBJS)\monolib_taskdlg.obj \
$(OBJS)\monolib_clrpickerg.obj \
$(OBJS)\monolib_collpaneg.obj \
$(OBJS)\monolib_filepickerg.obj \
@@ -3752,6 +3755,7 @@ ____CORE_SRC_FILENAMES_1_OBJECTS = \
$(OBJS)\monolib_appprogress.obj \
$(OBJS)\monolib_taskbarbutton.obj \
$(OBJS)\monolib_power.obj \
+ $(OBJS)\monolib_taskdlg.obj \
$(OBJS)\monolib_generic_accel.obj \
$(OBJS)\monolib_clrpickerg.obj \
$(OBJS)\monolib_collpaneg.obj \
@@ -4196,6 +4200,7 @@ ____CORE_SRC_FILENAMES_2_OBJECTS = \
$(OBJS)\coredll_appprogress.obj \
$(OBJS)\coredll_taskbarbutton.obj \
$(OBJS)\coredll_power.obj \
+ $(OBJS)\coredll_taskdlg.obj \
$(OBJS)\coredll_clrpickerg.obj \
$(OBJS)\coredll_collpaneg.obj \
$(OBJS)\coredll_filepickerg.obj \
@@ -4547,6 +4552,7 @@ ____CORE_SRC_FILENAMES_2_OBJECTS = \
$(OBJS)\coredll_appprogress.obj \
$(OBJS)\coredll_taskbarbutton.obj \
$(OBJS)\coredll_power.obj \
+ $(OBJS)\coredll_taskdlg.obj \
$(OBJS)\coredll_generic_accel.obj \
$(OBJS)\coredll_clrpickerg.obj \
$(OBJS)\coredll_collpaneg.obj \
@@ -4895,6 +4901,7 @@ ____CORE_SRC_FILENAMES_3_OBJECTS = \
$(OBJS)\corelib_appprogress.obj \
$(OBJS)\corelib_taskbarbutton.obj \
$(OBJS)\corelib_power.obj \
+ $(OBJS)\corelib_taskdlg.obj \
$(OBJS)\corelib_clrpickerg.obj \
$(OBJS)\corelib_collpaneg.obj \
$(OBJS)\corelib_filepickerg.obj \
@@ -5246,6 +5253,7 @@ ____CORE_SRC_FILENAMES_3_OBJECTS = \
$(OBJS)\corelib_appprogress.obj \
$(OBJS)\corelib_taskbarbutton.obj \
$(OBJS)\corelib_power.obj \
+ $(OBJS)\corelib_taskdlg.obj \
$(OBJS)\corelib_generic_accel.obj \
$(OBJS)\corelib_clrpickerg.obj \
$(OBJS)\corelib_collpaneg.obj \
@@ -9799,6 +9807,11 @@ $(OBJS)\monodll_power.obj: ..\..\src\msw\power.cpp
$(CXX) /c /nologo /TP /Fo$@ $(MONODLL_CXXFLAGS) ..\..\src\msw\power.cpp
!endif

+!if "$(USE_GUI)" == "1"
+$(OBJS)\monodll_taskdlg.obj: ..\..\src\msw\taskdlg.cpp
+ $(CXX) /c /nologo /TP /Fo$@ $(MONODLL_CXXFLAGS) ..\..\src\msw\taskdlg.cpp
+!endif
+
!if "$(USE_GUI)" == "1"
$(OBJS)\monodll_clrpickerg.obj: ..\..\src\generic\clrpickerg.cpp
$(CXX) /c /nologo /TP /Fo$@ $(MONODLL_CXXFLAGS) ..\..\src\generic\clrpickerg.cpp
@@ -12427,6 +12440,11 @@ $(OBJS)\monolib_power.obj: ..\..\src\msw\power.cpp
$(CXX) /c /nologo /TP /Fo$@ $(MONOLIB_CXXFLAGS) ..\..\src\msw\power.cpp
!endif

+!if "$(USE_GUI)" == "1"
+$(OBJS)\monolib_taskdlg.obj: ..\..\src\msw\taskdlg.cpp
+ $(CXX) /c /nologo /TP /Fo$@ $(MONOLIB_CXXFLAGS) ..\..\src\msw\taskdlg.cpp
+!endif
+
!if "$(USE_GUI)" == "1"
$(OBJS)\monolib_clrpickerg.obj: ..\..\src\generic\clrpickerg.cpp
$(CXX) /c /nologo /TP /Fo$@ $(MONOLIB_CXXFLAGS) ..\..\src\generic\clrpickerg.cpp
@@ -14995,6 +15013,11 @@ $(OBJS)\coredll_power.obj: ..\..\src\msw\power.cpp
$(CXX) /c /nologo /TP /Fo$@ $(COREDLL_CXXFLAGS) ..\..\src\msw\power.cpp
!endif

+!if "$(USE_GUI)" == "1"
+$(OBJS)\coredll_taskdlg.obj: ..\..\src\msw\taskdlg.cpp
+ $(CXX) /c /nologo /TP /Fo$@ $(COREDLL_CXXFLAGS) ..\..\src\msw\taskdlg.cpp
+!endif
+
!if "$(USE_GUI)" == "1"
$(OBJS)\coredll_clrpickerg.obj: ..\..\src\generic\clrpickerg.cpp
$(CXX) /c /nologo /TP /Fo$@ $(COREDLL_CXXFLAGS) ..\..\src\generic\clrpickerg.cpp
@@ -16780,6 +16803,11 @@ $(OBJS)\corelib_power.obj: ..\..\src\msw\power.cpp
$(CXX) /c /nologo /TP /Fo$@ $(CORELIB_CXXFLAGS) ..\..\src\msw\power.cpp
!endif

+!if "$(USE_GUI)" == "1"
+$(OBJS)\corelib_taskdlg.obj: ..\..\src\msw\taskdlg.cpp
+ $(CXX) /c /nologo /TP /Fo$@ $(CORELIB_CXXFLAGS) ..\..\src\msw\taskdlg.cpp
+!endif
+
!if "$(USE_GUI)" == "1"
$(OBJS)\corelib_clrpickerg.obj: ..\..\src\generic\clrpickerg.cpp
$(CXX) /c /nologo /TP /Fo$@ $(CORELIB_CXXFLAGS) ..\..\src\generic\clrpickerg.cpp


=====================================
build/msw/wx_core.vcxproj
=====================================
@@ -1706,6 +1706,7 @@
<ClCompile Include="..\..\src\common\curbndl.cpp" />
<ClCompile Include="..\..\src\common\imagwebp.cpp" />
<ClCompile Include="..\..\src\common\webpdecoder.cpp" />
+ <ClCompile Include="..\..\src\msw\taskdlg.cpp" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\..\src\msw\version.rc">


=====================================
build/msw/wx_core.vcxproj.filters
=====================================
@@ -1044,6 +1044,9 @@
<ClCompile Include="..\..\src\msw\taskbarbutton.cpp">
<Filter>MSW Sources</Filter>
</ClCompile>
+ <ClCompile Include="..\..\src\msw\taskdlg.cpp">
+ <Filter>MSW Sources</Filter>
+ </ClCompile>
<ClCompile Include="..\..\src\msw\textctrl.cpp">
<Filter>MSW Sources</Filter>
</ClCompile>


=====================================
include/wx/msw/listctrl.h
=====================================
@@ -466,6 +466,12 @@ private:
// Draw the sort arrow in the header.
void DrawSortArrow();

+ void OnSysColourChanged(wxSysColourChangedEvent& event);
+
+ // Set the native control's text and background colours to the
+ // current foreground and background colours.
+ void UpdateNativeColours();
+
// Object using for header custom drawing if necessary, may be null.
wxMSWHeaderCtrlCustomDraw* m_headerCustomDraw;



=====================================
include/wx/msw/private.h
=====================================
@@ -195,6 +195,23 @@ struct WinStruct : public T
}
};

+// Life wouldn't be fun if Windows didn't call the size member differently in
+// different structs, so define the equivalent of the above for the ones where
+// it's called dwSize.
+//
+// When we can require C++17 or preferably C++20 we could merge this with
+// WinStruct by detecting the presence of cbSize/dwSize member using SFINAE,
+// but for now just define it separately to keep things simple.
+template <class T>
+struct WinStructWordSize : public T
+{
+ WinStructWordSize()
+ {
+ wxZeroMemory(*this);
+
+ this->dwSize = sizeof(T);
+ }
+};

// Macros for converting wxString to the type expected by API functions.
//
@@ -313,6 +330,12 @@ extern HICON wxBitmapToHICON(const wxBitmap& bmp);
extern
HCURSOR wxBitmapToHCURSOR(const wxBitmap& bmp, int hotSpotX, int hotSpotY);

+// Return DPI for the given window.
+//
+// Implemented in src/msw/window.cpp.
+wxSize wxGetWindowDPI(HWND hwnd);
+
+// Also implemented in src/msw/window.cpp.
extern int wxGetSystemMetrics(int nIndex, const wxWindow* win);

extern bool wxSystemParametersInfo(UINT uiAction, UINT uiParam,
@@ -1165,6 +1188,21 @@ inline wxLayoutDirection wxGetEditLayoutDirection(WXHWND hWnd)
: wxLayout_LeftToRight;
}

+// Check if WM_SETTINGCHANGE notifies about the system colours change.
+inline bool wxIsSystemColourChange(LPARAM lParam)
+{
+ // Note that "ImmersiveColorSet" is set both when switching between
+ // light and dark themes and also when changing high contrast mode,
+ // for which an additional message with "WindowsThemeElement" is
+ // also sent, but we don't need to check for it as handling this
+ // one is enough
+ if ( !lParam )
+ return false;
+
+ auto* const what = reinterpret_cast<const TCHAR*>(lParam);
+ return wxStrcmp(what, wxT("ImmersiveColorSet")) == 0;
+}
+
// ----------------------------------------------------------------------------
// functions mapping HWND to wxWindow
// ----------------------------------------------------------------------------


=====================================
include/wx/msw/private/darkmode.h
=====================================
@@ -31,6 +31,11 @@ bool HasChanged();
// Enable or disable dark mode for the given TLW if appropriate.
void ConfigureTLW(HWND hwnd);

+// Helper function: call SetWindowTheme() and log a debug error if it fails.
+void SetTheme(HWND hwnd,
+ const wchar_t* themeName,
+ const wchar_t* themeId = nullptr);
+
// Set dark theme for the given (child) window if appropriate.
//
// Optional theme name and ID can be specified if something other than the


=====================================
include/wx/msw/private/taskdlg.h
=====================================
@@ -0,0 +1,56 @@
+///////////////////////////////////////////////////////////////////////////////
+// Name: wx/msw/private/taskdlg.h
+// Purpose: Dark mode support for native TaskDialog.
+// Author: Mohmed Abdel-Fattah (memoarfaa)
+// Created: 2026-06-07
+// Copyright: (c) 2026 wxWidgets development team
+// Licence: wxWindows licence
+///////////////////////////////////////////////////////////////////////////////
+
+#ifndef _WX_MSW_PRIVATE_TASKDLG_H_
+#define _WX_MSW_PRIVATE_TASKDLG_H_
+
+#include "wx/msw/wrapcctl.h"
+
+namespace wxMSWDarkMode
+{
+
+// ----------------------------------------------------------------------------
+// TaskDialog dark mode helpers
+// ----------------------------------------------------------------------------
+//
+// These two functions bracket the lifetime of dark-mode theming for any
+// TaskDialog-based dialog (wxMessageDialog, wxRichMessageDialog,
+// wxProgressDialog, wxAboutBox).
+//
+// Typical usage inside a PFTASKDIALOGCALLBACK:
+//
+// case TDN_CREATED:
+// wxMSWDarkMode::ApplyToTaskDialog(hwnd, pCfg);
+// break;
+// case TDN_DESTROYED:
+// wxMSWDarkMode::RemoveFromTaskDialog(hwnd);
+// break;
+//
+// Both functions are no-ops when IsActive() returns false, so no guard is
+// needed at the call site.
+
+// Apply dark mode theming to an existing TaskDialog window.
+//
+// Must be called from TDN_CREATED (not TDN_DIALOG_CONSTRUCTED) so that the
+// complete child-window hierarchy exists and UI Automation can walk it.
+//
+// pCfg may be nullptr; when provided it enables correct icon overdraw on
+// Windows 10 where there is no native dark TaskDialog theme.
+void AllowForTaskDialog(HWND hwnd, const TASKDIALOGCONFIG* pCfg = nullptr);
+
+// Remove all subclasses and free all GDI/theme resources installed by
+// ApplyToTaskDialog().
+//
+// Must be called from TDN_DESTROYED or WM_DESTROY.
+// Safe to call even if ApplyToTaskDialog() was never called for this HWND.
+void RemoveFromTaskDialog(HWND hwnd);
+
+} // namespace wxMSWDarkMode
+
+#endif // _WX_MSW_PRIVATE_TASKDLG_H_


=====================================
include/wx/msw/uxtheme.h
=====================================
@@ -204,9 +204,14 @@ public:
return NewAtStdDPI(0, classes, classesDark);
}

+ // Default ctor, use move assignment to really initialize this object later.
+ wxUxThemeHandle() = default;
+
// wxWindow pointer here must be valid and its DPI is always used.
// If classesDark is non-nullptr and the dark mode is active, it's used
// instead of classes.
+ //
+ // Prefer using factory functions above instead of this ctor for clarity.
wxUxThemeHandle(const wxWindowMSW* win,
const wchar_t* classes,
const wchar_t* classesDark = nullptr);
@@ -217,14 +222,20 @@ public:
other.m_hTheme = 0;
}

+ wxUxThemeHandle& operator=(wxUxThemeHandle&& other)
+ {
+ Free();
+ m_hTheme = other.m_hTheme;
+ other.m_hTheme = 0;
+
+ return *this;
+ }
+
operator HTHEME() const { return m_hTheme; }

~wxUxThemeHandle()
{
- if ( m_hTheme )
- {
- ::CloseThemeData(m_hTheme);
- }
+ Free();
}

// Return the colour for the given part, property and state.
@@ -236,21 +247,34 @@ public:
// Return the size of a theme element, either "as is" (TS_TRUE size) or as
// it would be used for drawing (TS_DRAW size).
//
- // For now we don't allow specifying the HDC or rectangle as they don't
- // seem to be useful.
- wxSize GetTrueSize(int part, int state = 0) const
+ // For now we don't allow specifying the rectangle as it doesn't seem to be
+ // useful.
+ wxSize GetTrueSize(int part, int state = 0, HDC hdc = 0) const
{
- return DoGetSize(part, state, TS_TRUE);
+ return DoGetSize(hdc, part, state, TS_TRUE);
}

- wxSize GetDrawSize(int part, int state = 0) const
+ wxSize GetDrawSize(int part, int state = 0, HDC hdc = 0) const
{
- return DoGetSize(part, state, TS_DRAW);
+ return DoGetSize(hdc, part, state, TS_DRAW);
}

+ // Get the margins of a theme element in the output parameter.
+ //
+ // The output parameter is not modified if the function fails, so it can be
+ // initialized with some default values before calling this function.
+ bool
+ GetMargins(MARGINS& margins,
+ int part, int prop, int state = 0, HDC hdc = 0) const;
+
+ // Get the font of a theme element.
+ bool GetFont(LOGFONTW& lf, HDC hdc, int part, int state = 0) const;
+
+
// Draw theme background: if the caller already has a RECT, it can be
// provided directly, otherwise wxRect is converted to it.
- void DrawBackground(HDC hdc, const RECT& rc, int part, int state = 0);
+ void DrawBackground(HDC hdc, const RECT& rc, int part, int state = 0,
+ const RECT* rcClip = nullptr);
void DrawBackground(HDC hdc, const wxRect& rect, int part, int state = 0);

private:
@@ -270,11 +294,18 @@ private:
{
}

- wxSize DoGetSize(int part, int state, THEMESIZE ts) const;
+ void Free()
+ {
+ if ( m_hTheme )
+ {
+ ::CloseThemeData(m_hTheme);
+ }
+ }
+
+ wxSize DoGetSize(HDC hdc, int part, int state, THEMESIZE ts) const;


- // This is almost, but not quite, const: it's only reset in move ctor.
- HTHEME m_hTheme;
+ HTHEME m_hTheme = 0;

wxDECLARE_NO_COPY_CLASS(wxUxThemeHandle);
};


=====================================
interface/wx/app.h
=====================================
@@ -1433,19 +1433,11 @@ public:

Known limitations of dark mode support include:

- - Anything based on `TaskDialog()` Win32 API doesn't support dark mode:
- wxMessageBox(), wxMessageDialog, wxRichMessageDialog, wxProgressDialog
- and simple (i.e., without hyperlink or licence) wxAboutBox(). Consider
- using generic versions (e.g. wxGenericMessageDialog or wxGenericProgressDialog)
- if dark mode support is more important than using the native dialog.
- The following dialogs wrapping common windows dialogs don't support
dark mode: wxColourDialog, wxFindReplaceDialog, wxFontDialog,
wxPageSetupDialog, wxPrintDialog.
- wxTimePickerCtrl, wxDatePickerCtrl and wxCalendarCtrl don't support dark mode
and use the same (light) background as by default in it.
- - Toolbar items for which wxToolBar::SetDropdownMenu() was called
- don't draw the menu drop-down correctly, making it almost
- invisible.

@param flags Can include @c wxApp::DarkMode_Always to force enabling
dark mode for the application, even if the system doesn't use the


=====================================
src/common/gifdecod.cpp
=====================================
@@ -689,7 +689,7 @@ wxGIFErrorCode wxGIFDecoder::LoadGIF(wxInputStream& stream)
// load global color map if available
if ((buf[4] & 0x80) == 0x80)
{
- int backgroundColIndex = buf[5];
+ unsigned int backgroundColIndex = buf[5];

global_ncolors = 2 << (buf[4] & 0x07);
unsigned int numBytes = 3 * global_ncolors;
@@ -699,9 +699,18 @@ wxGIFErrorCode wxGIFDecoder::LoadGIF(wxInputStream& stream)
return wxGIF_INVFORMAT;
}

- m_background.Set(pal[backgroundColIndex*3 + 0],
- pal[backgroundColIndex*3 + 1],
- pal[backgroundColIndex*3 + 2]);
+ // The background colour index must reference an entry in the global
+ // colour table: only the first 3*global_ncolors bytes of pal were read
+ // from the file above, so an index past that would take the background
+ // colour from the uninitialised tail of the buffer (and leak it out
+ // through GetBackgroundColour()). Leave the background unset in that
+ // case, as we do for a GIF that doesn't specify one at all.
+ if (backgroundColIndex < global_ncolors)
+ {
+ m_background.Set(pal[backgroundColIndex*3 + 0],
+ pal[backgroundColIndex*3 + 1],
+ pal[backgroundColIndex*3 + 2]);
+ }
}

// transparent colour, disposal method and delay default to unused


=====================================
src/msw/darkmode.cpp
=====================================
@@ -492,6 +492,16 @@ void ConfigureTLW(HWND hwnd)
wxMSWImpl::AllowDarkModeForWindow(hwnd, true);
}

+void SetTheme(HWND hwnd, const wchar_t* themeName, const wchar_t* themeId)
+{
+ HRESULT hr = ::SetWindowTheme(hwnd, themeName, themeId);
+ if ( FAILED(hr) )
+ {
+ wxLogApiError(wxString::Format("SetWindowTheme(%p, %s, %s)",
+ hwnd, themeName, themeId), hr);
+ }
+}
+
void AllowForWindow(HWND hwnd, const wchar_t* themeName, const wchar_t* themeId)
{
if ( wxMSWImpl::AllowDarkModeForWindow(hwnd, true) )
@@ -499,12 +509,7 @@ void AllowForWindow(HWND hwnd, const wchar_t* themeName, const wchar_t* themeId)

if ( themeName || themeId )
{
- HRESULT hr = ::SetWindowTheme(hwnd, themeName, themeId);
- if ( FAILED(hr) )
- {
- wxLogApiError(wxString::Format("SetWindowTheme(%p, %s, %s)",
- hwnd, themeName, themeId), hr);
- }
+ SetTheme(hwnd, themeName, themeId);
}

// Some native controls need this message to switch themes, such as
@@ -769,8 +774,7 @@ HandleMenuMessage(WXLRESULT* result,
// We have to specify the text colour explicitly as by default
// black would be used, making the menu label unreadable on the
// (almost) black background.
- DTTOPTS textOpts;
- textOpts.dwSize = sizeof(textOpts);
+ WinStructWordSize<DTTOPTS> textOpts;
textOpts.dwFlags = DTT_TEXTCOLOR;
textOpts.crText = wxColourToRGB(GetMenuColour(colText));



=====================================
src/msw/datecontrols.cpp
=====================================
@@ -51,8 +51,7 @@ bool wxMSWDateControls::CheckInitialization()
// it's enough to give the error only once
s_initResult = false;

- INITCOMMONCONTROLSEX icex;
- icex.dwSize = sizeof(icex);
+ WinStructWordSize<INITCOMMONCONTROLSEX> icex;
icex.dwICC = ICC_DATE_CLASSES;

// see comment in wxApp::GetComCtl32Version() explaining the


=====================================
src/msw/joystick.cpp
=====================================
@@ -266,9 +266,8 @@ bool wxJoystick::GetButtonState(unsigned id) const
int wxJoystick::GetPOVPosition() const
{
#ifndef NO_JOYGETPOSEX
- JOYINFOEX joyInfo;
+ WinStructWordSize<JOYINFOEX> joyInfo;
joyInfo.dwFlags = JOY_RETURNPOV;
- joyInfo.dwSize = sizeof(joyInfo);
MMRESULT res = joyGetPosEx(m_joystick, & joyInfo);
if (res == JOYERR_NOERROR )
{
@@ -288,9 +287,8 @@ int wxJoystick::GetPOVPosition() const
int wxJoystick::GetPOVCTSPosition() const
{
#ifndef NO_JOYGETPOSEX
- JOYINFOEX joyInfo;
+ WinStructWordSize<JOYINFOEX> joyInfo;
joyInfo.dwFlags = JOY_RETURNPOVCTS;
- joyInfo.dwSize = sizeof(joyInfo);
MMRESULT res = joyGetPosEx(m_joystick, & joyInfo);
if (res == JOYERR_NOERROR )
{
@@ -306,9 +304,8 @@ int wxJoystick::GetPOVCTSPosition() const
int wxJoystick::GetRudderPosition() const
{
#ifndef NO_JOYGETPOSEX
- JOYINFOEX joyInfo;
+ WinStructWordSize<JOYINFOEX> joyInfo;
joyInfo.dwFlags = JOY_RETURNR;
- joyInfo.dwSize = sizeof(joyInfo);
MMRESULT res = joyGetPosEx(m_joystick, & joyInfo);
if (res == JOYERR_NOERROR )
{
@@ -324,9 +321,8 @@ int wxJoystick::GetRudderPosition() const
int wxJoystick::GetUPosition() const
{
#ifndef NO_JOYGETPOSEX
- JOYINFOEX joyInfo;
+ WinStructWordSize<JOYINFOEX> joyInfo;
joyInfo.dwFlags = JOY_RETURNU;
- joyInfo.dwSize = sizeof(joyInfo);
MMRESULT res = joyGetPosEx(m_joystick, & joyInfo);
if (res == JOYERR_NOERROR )
{
@@ -342,9 +338,8 @@ int wxJoystick::GetUPosition() const
int wxJoystick::GetVPosition() const
{
#ifndef NO_JOYGETPOSEX
- JOYINFOEX joyInfo;
+ WinStructWordSize<JOYINFOEX> joyInfo;
joyInfo.dwFlags = JOY_RETURNV;
- joyInfo.dwSize = sizeof(joyInfo);
MMRESULT res = joyGetPosEx(m_joystick, & joyInfo);
if (res == JOYERR_NOERROR )
{


=====================================
src/msw/listctrl.cpp
=====================================
@@ -220,6 +220,7 @@ wxBEGIN_EVENT_TABLE(wxListCtrl, wxListCtrlBase)
EVT_PAINT(wxListCtrl::OnPaint)
EVT_CHAR_HOOK(wxListCtrl::OnCharHook)
EVT_DPI_CHANGED(wxListCtrl::OnDPIChanged)
+ EVT_SYS_COLOUR_CHANGED(wxListCtrl::OnSysColourChanged)
wxEND_EVENT_TABLE()

// ============================================================================
@@ -267,7 +268,7 @@ bool wxListCtrl::Create(wxWindow *parent,
// We must set the default text colour to the system/theme color, otherwise
// GetTextColour will always return black even if this is not what is used
// by default.
- SetTextColour(GetDefaultAttributes().colFg);
+ UpdateNativeColours();

if ( InReportView() )
MSWSetExListStyles();
@@ -595,16 +596,6 @@ void wxListCtrl::MSWSetDarkOrLightMode(SetMode setmode)

// Update header.
MSWInitHeader();
-
- // Background color must always be set.
- // Verified with the widgets sample, selecting the wxFileCtrl page.
- const auto attrs = GetDefaultAttributes();
- SetBackgroundColour(attrs.colBg);
-
- // When switching modes, the text color remains the same. We must
- // explicitly update the color.
- if ( setmode == SetMode::Change )
- SetTextColour(attrs.colFg);
}

int wxListCtrl::MSWGetToolTipMessage() const
@@ -652,7 +643,7 @@ bool wxListCtrl::SetForegroundColour(const wxColour& col)
if ( !wxWindow::SetForegroundColour(col) )
return false;

- SetTextColour(col);
+ UpdateNativeColours();

return true;
}
@@ -663,17 +654,21 @@ bool wxListCtrl::SetBackgroundColour(const wxColour& col)
if ( !wxWindow::SetBackgroundColour(col) )
return false;

- // we set the same colour for both the "empty" background and the items
- // background
- COLORREF color = wxColourToRGB(col);
- if ( !ListView_SetBkColor(GetHwnd(), color) )
- wxLogLastError(wxS("ListView_SetBkColor()"));
- if ( !ListView_SetTextBkColor(GetHwnd(), color) )
- wxLogLastError(wxS("ListView_SetTextBkColor()"));
+ UpdateNativeColours();

return true;
}

+void wxListCtrl::UpdateNativeColours()
+{
+ // we set the same colour for both the "empty" background and the items
+ // background
+ const auto colBg = wxColourToRGB(GetBackgroundColour());
+ ListView_SetBkColor(m_hWnd, colBg);
+ ListView_SetTextBkColor(m_hWnd, colBg);
+ ListView_SetTextColor(m_hWnd, wxColourToRGB(GetForegroundColour()));
+}
+
bool wxListCtrl::SetHeaderAttr(const wxItemAttr& attr)
{
// We need to propagate the change of the font to the native header window
@@ -1578,16 +1573,13 @@ int wxListCtrl::GetSelectedItemCount() const
// Gets the text colour of the listview
wxColour wxListCtrl::GetTextColour() const
{
- COLORREF ref = ListView_GetTextColor(GetHwnd());
- wxColour col(GetRValue(ref), GetGValue(ref), GetBValue(ref));
- return col;
+ return GetForegroundColour();
}

// Sets the text colour of the listview
void wxListCtrl::SetTextColour(const wxColour& col)
{
- if ( !ListView_SetTextColor(GetHwnd(), wxColourToPalRGB(col)) )
- wxLogLastError(wxS("ListView_SetTextColor()"));
+ SetForegroundColour(col);
}

// Gets the index of the topmost visible item when in
@@ -3701,6 +3693,12 @@ void wxListCtrl::OnCharHook(wxKeyEvent& event)
event.Skip();
}

+void wxListCtrl::OnSysColourChanged(wxSysColourChangedEvent& event)
+{
+ UpdateNativeColours();
+ event.Skip();
+}
+
void wxListCtrl::DrawSortArrow()
{
if ( HasFlag(wxLC_SORT_ASCENDING) || HasFlag(wxLC_SORT_DESCENDING) )


=====================================
src/msw/msgdlg.cpp
=====================================
@@ -27,6 +27,7 @@
#include "wx/msw/private/darkmode.h"
#include "wx/msw/private/metrics.h"
#include "wx/msw/private/msgdlg.h"
+#include "wx/msw/private/taskdlg.h"
#include "wx/modalhook.h"
#include "wx/fontutil.h"
#include "wx/textbuf.h"
@@ -588,12 +589,34 @@ namespace
{

HRESULT CALLBACK
-wxTaskDialogCallback(HWND hwnd, UINT msg, WPARAM, LPARAM, LONG_PTR)
+wxTaskDialogCallback(HWND hwnd,
+ UINT msg,
+ WPARAM wParam,
+ LPARAM WXUNUSED(lParam),
+ LONG_PTR refData)
{
+ const auto* const
+ config = reinterpret_cast<const TASKDIALOGCONFIG*>(refData);
+
switch ( msg )
{
- case TDN_DIALOG_CONSTRUCTED:
- wxMSWDarkMode::ConfigureTLW(hwnd);
+ case TDN_CREATED:
+ // The dialog is fully constructed now, so we can enable dark mode
+ // for it if necessary (doing it on TDN_DIALOG_CONSTRUCTED wouldn't
+ // work because all dialog elements wouldn't be constructed yet).
+ wxMSWDarkMode::AllowForTaskDialog(hwnd, config);
+ break;
+
+ case TDN_EXPANDO_BUTTON_CLICKED:
+ // Store the expanded state as a window property so the subclassed
+ // TaskPage panel can read it during WM_PAINT without a UIA call.
+ SetProp(hwnd, L"IsExpanded",
+ reinterpret_cast<HANDLE>(static_cast<ULONG_PTR>(wParam)));
+ break;
+
+ case TDN_DESTROYED:
+ // Clean up all resources allocated by AllowForTaskDialog().
+ wxMSWDarkMode::RemoveFromTaskDialog(hwnd);
break;
}

@@ -757,6 +780,7 @@ void wxMSWTaskDialogConfig::MSWCommonTaskDialogInit(TASKDIALOGCONFIG &tdc)
}

tdc.pfCallback = wxTaskDialogCallback;
+ tdc.lpCallbackData = (LONG_PTR) &tdc;
}

void wxMSWTaskDialogConfig::AddTaskDialogButton(TASKDIALOGCONFIG &tdc,


=====================================
src/msw/progdlg.cpp
=====================================
@@ -30,8 +30,10 @@
#include "wx/msw/private.h"
#endif

-#include "wx/msw/private/msgdlg.h"
#include "wx/evtloop.h"
+#include "wx/msw/private/darkmode.h"
+#include "wx/msw/private/msgdlg.h"
+#include "wx/msw/private/taskdlg.h"

using namespace wxMSWMessageDialog;

@@ -1144,6 +1146,11 @@ wxProgressDialogTaskRunner::TaskDialogCallbackProc
switch ( uNotification )
{
case TDN_CREATED:
+ // The dialog is fully constructed now, so we can enable dark mode
+ // for it if necessary (doing it on TDN_DIALOG_CONSTRUCTED wouldn't
+ // work because all dialog elements wouldn't be constructed yet).
+ wxMSWDarkMode::AllowForTaskDialog(hwnd);
+
// Let the main thread know that the dialog can be used.
sharedData->m_status = wxProgressDialogStatus::Initialized;

@@ -1166,6 +1173,18 @@ wxProgressDialogTaskRunner::TaskDialogCallbackProc
EnableCloseButtons(hwnd, false);
break;

+ case TDN_EXPANDO_BUTTON_CLICKED:
+ // Store the expanded state as a window property so the subclassed
+ // TaskPage panel can read it during WM_PAINT without a UIA call.
+ SetProp(hwnd, L"IsExpanded",
+ reinterpret_cast<HANDLE>(static_cast<ULONG_PTR>(wParam)));
+ break;
+
+ case TDN_DESTROYED:
+ // Clean up all resources allocated by AllowForTaskDialog().
+ wxMSWDarkMode::RemoveFromTaskDialog(hwnd);
+ break;
+
case TDN_BUTTON_CLICKED:
switch ( wParam )
{


=====================================
src/msw/renderer.cpp
=====================================
@@ -956,8 +956,7 @@ void wxRendererXP::DrawItemText(wxWindow* win,
{
RECT rc = ConvertToRECT(dc, rect);

- DTTOPTS textOpts;
- textOpts.dwSize = sizeof(textOpts);
+ WinStructWordSize<DTTOPTS> textOpts;
textOpts.dwFlags = DTT_STATEID;
textOpts.iStateId = itemState;



=====================================
src/msw/statbox.cpp
=====================================
@@ -599,15 +599,7 @@ void wxStaticBox::PaintForeground(wxDC& dc, const RECT&)
if ( hTheme )
{
LOGFONTW themeFont;
- if ( ::GetThemeFont
- (
- hTheme,
- hdc,
- BP_GROUPBOX,
- GBS_NORMAL,
- TMT_FONT,
- &themeFont
- ) == S_OK )
+ if ( hTheme.GetFont(themeFont, hdc, BP_GROUPBOX, GBS_NORMAL) )
{
font.Init(themeFont);
if ( font )


=====================================
src/msw/taskdlg.cpp
=====================================
@@ -0,0 +1,1344 @@
+///////////////////////////////////////////////////////////////////////////////
+// Name: src/msw/taskdlg.cpp
+// Purpose: Dark mode support for native TaskDialog.
+// Author: Mohmed Abdel-Fattah (memoarfaa)
+// Created: 2026-06-07
+// Copyright: (c) 2026 wxWidgets development team
+// Licence: wxWindows licence
+///////////////////////////////////////////////////////////////////////////////
+
+// ============================================================================
+// declarations
+// ============================================================================
+
+// ----------------------------------------------------------------------------
+// headers
+// ----------------------------------------------------------------------------
+
+// for compilers that support precompilation, includes "wx.h".
+#include "wx/wxprec.h"
+
+#ifndef wxUSE_DARK_MODE
+ #ifdef __WXUNIVERSAL__
+ #define wxUSE_DARK_MODE 0
+ #else
+ #define wxUSE_DARK_MODE 1
+ #endif
+#endif
+
+#ifndef WX_PRECOMP
+ #include "wx/brush.h"
+#endif // WX_PRECOMP
+
+#include "wx/msw/private/taskdlg.h"
+
+#if wxUSE_DARK_MODE
+
+#include "wx/dynlib.h"
+#include "wx/log.h"
+#include "wx/module.h"
+
+#include "wx/msw/uxtheme.h"
+
+#include "wx/msw/private/darkmode.h"
+
+#include "wx/msw/private/comptr.h"
+#include <uiautomation.h>
+#include <windowsx.h>
+
+// ============================================================================
+// TaskDialog dark mode — implementation detail types
+// ============================================================================
+
+// Colour palette used for the dark TaskDialog panels.
+// Values match wxDarkModeSettings::GetColour() so dialogs blend with the app.
+namespace TDDarkCol
+{
+ static constexpr COLORREF kPrimary = RGB(0x20, 0x20, 0x20);
+ static constexpr COLORREF kSecondary = RGB(0x2c, 0x2c, 0x2c);
+ static constexpr COLORREF kFootnote = RGB(0x2c, 0x2c, 0x2c);
+ static constexpr COLORREF kSeparator = RGB(0x3c, 0x3c, 0x3c);
+
+ static constexpr COLORREF kTextNormal = RGB(0xe0, 0xe0, 0xe0);
+ static constexpr COLORREF kTextInstruct = RGB(0x00, 0x99, 0xff);
+ static constexpr COLORREF kTextContent = RGB(0xe0, 0xe0, 0xe0);
+ static constexpr COLORREF kTextExpando = RGB(0xe0, 0xe0, 0xe0);
+ static constexpr COLORREF kTextVerify = RGB(0xe0, 0xe0, 0xe0);
+ static constexpr COLORREF kTextFootnote = RGB(0xb0, 0xb0, 0xb0);
+ static constexpr COLORREF kTextFtrExp = RGB(0xb0, 0xb0, 0xb0);
+ static constexpr COLORREF kTextRadio = RGB(0xe0, 0xe0, 0xe0);
+}
+
+namespace
+{
+
+// Small helper class freeing BSTR automatically if necessary.
+class AutoBSTR
+{
+public:
+ AutoBSTR() = default;
+
+ AutoBSTR(const AutoBSTR&) = delete;
+ AutoBSTR& operator=(const AutoBSTR&) = delete;
+
+ ~AutoBSTR()
+ {
+ if ( m_bstr )
+ ::SysFreeString(m_bstr);
+ }
+
+ operator const wchar_t*() const
+ {
+ return m_bstr;
+ }
+
+ // May be called once to fill in the BSTR, which will be freed in dtor.
+ BSTR* Out()
+ {
+ wxASSERT_MSG( !m_bstr, "Can't reuse same object" );
+
+ return &m_bstr;
+ }
+
+private:
+ BSTR m_bstr = nullptr;
+};
+
+// Helper function create an HBRUSH from a brush stored in wxTheBrushList.
+// This allows not to recreate the brushes for the same colour and also ensures
+// that the brushes are eventually deleted.
+HBRUSH GetSolidBrush(COLORREF rgb)
+{
+ const wxColour col = wxRGBToColour(rgb);
+ wxBrush* const brush = wxTheBrushList->FindOrCreateBrush(col);
+ if ( !brush )
+ return 0;
+
+ return GetHbrushOf(*brush);
+}
+
+// Helper function to get an HWND from IUIAutomationElement.
+HWND GetHWNDFromElement(IUIAutomationElement* element)
+{
+ UIA_HWND hwnd = 0;
+ HRESULT hr = element->get_CurrentNativeWindowHandle(&hwnd);
+ if ( FAILED(hr) )
+ {
+ wxLogApiError("get_CurrentNativeWindowHandle", hr);
+ return 0;
+ }
+
+ return reinterpret_cast<HWND>(hwnd);
+}
+
+// Another helper to get the automation ID of an element.
+std::wstring GetCurrentAutomationId(IUIAutomationElement* element)
+{
+ std::wstring result;
+
+ AutoBSTR bstr;
+ if ( element->get_CurrentAutomationId(bstr.Out()) == S_OK && bstr )
+ result = bstr;
+
+ return result;
+}
+
+// Cached bounding rect + metadata for a single TaskDialog UI element.
+struct TDLayoutElement
+{
+ RECT rect = {};
+ std::wstring automationId;
+ std::wstring name; // visible text (used for overdraw)
+ LONG legacyState = 0; // STATE_SYSTEM_* flags
+};
+
+// All per-dialog rendering state, stored in a thread_local map.
+struct TDPageState
+{
+ wxUxThemeHandle hTD ; // TaskDialog panel + glyph parts
+ wxUxThemeHandle hButton ; // Button (checkbox glyph)
+ bool themesOk = false;
+
+ AutoHBRUSH brPrimary{TDDarkCol::kPrimary};
+ AutoHBRUSH brSecondary{TDDarkCol::kSecondary};
+ AutoHBRUSH brFootnote{TDDarkCol::kFootnote};
+
+ std::vector<TDLayoutElement> elements;
+ bool elemsOk = false;
+
+ // Mouse interaction (message-driven, no polling)
+ bool tracking = false;
+ bool pressing = false;
+ int hotIdx = -1;
+
+ // Logical dialog state
+ bool isExpanded = false;
+ bool isChecked = false;
+ bool defExpanded = false;
+ bool defChecked = false;
+
+ const TASKDIALOGCONFIG* pCfg = nullptr;
+
+ using TDStateMap = std::unordered_map<HWND, TDPageState>;
+
+ static TDStateMap& GetAll()
+ {
+ static TDStateMap s_states;
+ return s_states;
+ }
+
+ static TDPageState& Get(HWND h)
+ {
+ return GetAll()[h];
+ }
+
+ static void Destroy(HWND h)
+ {
+ auto& states = GetAll();
+ auto const it = states.find(h);
+ if ( it != states.end() )
+ {
+ states.erase(it);
+ }
+ }
+};
+
+// ----------------------------------------------------------------------------
+// Module for cleaning up resources used by the code in this file
+// ----------------------------------------------------------------------------
+
+// Function to get IUIAutomation instance
+class wxTaskDialogDarkModule : public wxModule
+{
+public:
+ virtual bool OnInit() override { return true; }
+ virtual void OnExit() override
+ {
+ ms_uiAutomation.reset();
+ TDPageState::GetAll().clear();
+ }
+
+ static IUIAutomation* GetUIAutomation()
+ {
+ if ( !ms_uiAutomation )
+ {
+ HRESULT hr = CoCreateInstance
+ (
+ CLSID_CUIAutomation,
+ nullptr,
+ CLSCTX_INPROC_SERVER,
+ wxIID_PPV_ARGS(IUIAutomation, &ms_uiAutomation)
+ );
+ if ( FAILED(hr) )
+ {
+ wxLogLastError("CoCreateInstance(IUIAutomation)");
+ }
+ }
+
+ return ms_uiAutomation.get();
+ }
+
+private:
+ static wxCOMPtr<IUIAutomation> ms_uiAutomation;
+
+ wxDECLARE_DYNAMIC_CLASS(wxTaskDialogDarkModule);
+};
+
+wxIMPLEMENT_DYNAMIC_CLASS(wxTaskDialogDarkModule, wxModule);
+
+wxCOMPtr<IUIAutomation> wxTaskDialogDarkModule::ms_uiAutomation;
+
+
+// Subclass IDs: 0x7778 == "wx"
+static constexpr UINT_PTR kTDMainSubclassId = 0x77780010ul;
+static constexpr UINT_PTR kTDPageSubclassId = 0x77780011ul;
+static constexpr UINT_PTR kTDCtrlSubclassId = 0x77780012ul;
+
+// ============================================================================
+// TaskDialog theme helpers
+// ============================================================================
+
+// Check if the theme with the "dark" name exists and is different from the
+// standard one.
+bool
+wxHasRealDarkTheme(const wchar_t* stdClass, const wchar_t* darkClass)
+{
+ const auto darkTheme = wxUxThemeHandle::NewAtStdDPI(darkClass);
+ if ( darkTheme )
+ {
+ const auto stdTheme = wxUxThemeHandle::NewAtStdDPI(stdClass);
+ if ( stdTheme && stdTheme != darkTheme )
+ return true;
+ }
+
+ return false;
+}
+
+// Cached probe: does the OS have a native dark TaskDialog theme (Win11+)?
+bool TDHasNativeDarkTheme()
+{
+ static int s_hasDarkTheme = -1;
+ if ( s_hasDarkTheme == -1 )
+ {
+ s_hasDarkTheme = wxHasRealDarkTheme(L"TaskDialog",
+ L"DarkMode_DarkTheme::TaskDialog");
+ }
+
+ return s_hasDarkTheme == 1;
+}
+
+void TDRefreshThemes(HWND hwnd, TDPageState& s)
+{
+ const int dpi = wxGetWindowDPI(hwnd).x;
+
+ if ( TDHasNativeDarkTheme() )
+ {
+ const wchar_t* mainClass = L"DarkMode_Explorer::TaskDialog";
+ const wchar_t* btnClass = L"DarkMode_Explorer::Button";
+
+ s.hTD = wxUxThemeHandle::NewAtDPI(hwnd, mainClass, mainClass, dpi);
+ s.hButton = wxUxThemeHandle::NewAtDPI(hwnd, btnClass, btnClass, dpi);
+ }
+ else // Try the best we can with the themes available on older OS versions.
+ {
+ s.hTD = wxUxThemeHandle::NewAtDPI(hwnd, L"TaskDialog", dpi);
+ s.hButton = wxUxThemeHandle::NewAtDPI(hwnd, L"Button", dpi);
+ }
+
+ s.themesOk = true;
+}
+
+COLORREF TDGetTextColour(const TDPageState& s, int uiPart)
+{
+ if ( TDHasNativeDarkTheme() )
+ {
+ const wxColour col = s.hTD.GetColour(uiPart, TMT_TEXTCOLOR);
+ if ( col.IsOk() )
+ return wxColourToRGB(col);
+ }
+
+ switch ( uiPart )
+ {
+ case TDLG_MAININSTRUCTIONPANE:
+ return TDDarkCol::kTextInstruct;
+ case TDLG_CONTENTPANE:
+ return TDDarkCol::kTextContent;
+ case TDLG_EXPANDOTEXT:
+ return TDDarkCol::kTextExpando;
+ case TDLG_VERIFICATIONTEXT:
+ return TDDarkCol::kTextVerify;
+ case TDLG_FOOTNOTEPANE:
+ return TDDarkCol::kTextFootnote;
+ case TDLG_EXPANDEDFOOTERAREA:
+ return TDDarkCol::kTextFtrExp;
+ case TDLG_RADIOBUTTONPANE:
+ return TDDarkCol::kTextRadio;
+ default:
+ return TDDarkCol::kTextNormal;
+ }
+}
+
+// ============================================================================
+// Icon loading
+// ============================================================================
+
+HICON TDLoadStockIcon(const TASKDIALOGCONFIG* cfg, bool isMain)
+{
+ if ( !cfg )
+ return nullptr;
+
+ if ( isMain )
+ {
+ if ( cfg->dwFlags & TDF_USE_HICON_MAIN )
+ return cfg->hMainIcon;
+ }
+ else
+ {
+ if ( cfg->dwFlags & TDF_USE_HICON_FOOTER )
+ return cfg->hFooterIcon;
+ }
+
+ SHSTOCKICONID id;
+
+ const LPCWSTR res = isMain ? cfg->pszMainIcon : cfg->pszFooterIcon;
+ if ( res == TD_WARNING_ICON )
+ id = SIID_WARNING;
+ else if ( res == TD_ERROR_ICON )
+ id = SIID_ERROR;
+ else if ( res == TD_INFORMATION_ICON )
+ id = SIID_INFO;
+ else if ( res == TD_SHIELD_ICON )
+ id = SIID_SHIELD;
+ else
+ return nullptr;
+
+ WinStruct<SHSTOCKICONINFO> sii;
+ if ( ::SHGetStockIconInfo(id, SHGSI_ICON | SHGSI_LARGEICON, &sii) != S_OK )
+ return nullptr;
+
+ return sii.hIcon;
+}
+
+// ============================================================================
+// UIA layout cache
+// ============================================================================
+
+void TDBuildLayoutCache(HWND hwnd, std::vector<TDLayoutElement>& out)
+{
+ out.clear();
+ IUIAutomation* const pAuto = wxTaskDialogDarkModule::GetUIAutomation();
+ if ( !pAuto )
+ return;
+
+ wxCOMPtr <IUIAutomationElement> pRoot;
+ if ( FAILED(pAuto->ElementFromHandle(hwnd, &pRoot)) )
+ return;
+
+ wxCOMPtr<IUIAutomationTreeWalker> pWalker;
+ pAuto->get_ContentViewWalker(&pWalker);
+ if ( !pWalker )
+ return;
+
+ wxCOMPtr<IUIAutomationElement> pChild;
+ pWalker->GetFirstChildElement(pRoot, &pChild);
+
+ while ( pChild )
+ {
+ TDLayoutElement info;
+ pChild->get_CurrentBoundingRectangle(&info.rect);
+ ::ScreenToClient(hwnd, reinterpret_cast<POINT*>(&info.rect.left));
+ ::ScreenToClient(hwnd, reinterpret_cast<POINT*>(&info.rect.right));
+
+ info.automationId = GetCurrentAutomationId(pChild);
+
+ AutoBSTR b;
+ if ( pChild->get_CurrentName(b.Out()) == S_OK && b )
+ info.name = b;
+
+ if ( info.automationId == L"VerificationCheckBox" )
+ {
+ VARIANT v;
+ ::VariantInit(&v);
+
+ if ( SUCCEEDED(pChild->GetCurrentPropertyValue
+ (
+ UIA_LegacyIAccessibleStatePropertyId,
+ &v
+ )) && v.vt == VT_I4 )
+ {
+ info.legacyState = v.lVal;
+ }
+
+ ::VariantClear(&v);
+ }
+
+ if ( !info.automationId.empty() && !::IsRectEmpty(&info.rect) )
+ {
+ const std::wstring& id = info.automationId;
+ if ( id == L"MainIcon" ||
+ id == L"FootnoteIcon" ||
+ id == L"MainInstruction" ||
+ id == L"ContentText" ||
+ id == L"ExpandedFooterText" ||
+ id == L"FootnoteText" ||
+ id == L"ExpandoButton" ||
+ id == L"VerificationCheckBox" ||
+ id.find(L"RadioButton_") == 0 ||
+ id.find(L"CommandLink_") == 0 ||
+ id.find(L"CommandButton_") == 0 )
+ {
+ out.push_back(std::move(info));
+ }
+ }
+
+ wxCOMPtr<IUIAutomationElement> pNext;
+ pWalker->GetNextSiblingElement(pChild, &pNext);
+ pChild = pNext;
+ }
+}
+
+void TDUpdateLayoutCache(HWND hwnd, TDPageState& s)
+{
+ if ( !s.elemsOk )
+ {
+ TDBuildLayoutCache(hwnd, s.elements);
+ s.elemsOk = true;
+ }
+
+ for ( const auto& el : s.elements )
+ {
+ if ( el.automationId == L"VerificationCheckBox" )
+ {
+ s.isChecked = (el.legacyState & STATE_SYSTEM_CHECKED) != 0;
+ break;
+ }
+ }
+
+ if ( s.defChecked )
+ s.isChecked = true;
+}
+
+int TDHitTest(const std::vector<TDLayoutElement>& els, POINT pt)
+{
+ for ( int i = 0; i < wxSsize(els); ++i )
+ {
+ if ( ::PtInRect(&els[i].rect, pt) )
+ return i;
+ }
+
+ return -1;
+}
+
+// ============================================================================
+// Paint routines
+// ============================================================================
+
+void TDPaintPixelSwap(HPAINTBUFFER hbp, int w, int h)
+{
+ RGBQUAD* pPx = nullptr;
+ int rw = 0;
+ if ( FAILED(::GetBufferedPaintBits(hbp, &pPx, &rw)) )
+ return;
+
+ wxColour srcPri(0xff, 0xff, 0xff),
+ srcSec(0xf0, 0xf0, 0xf0),
+ srcSep(0x80, 0x80, 0x80),
+ srcSp2(0xdf, 0xdf, 0xdf);
+
+ // Get the primary colour from the theme in case it's different.
+ //
+ // Note that under Windows 10 the theme doesn't define the other colours
+ // and under Windows 11 this code is not executed at all because it has
+ // native task dialog dark theme.
+ wxUxThemeHandle theme(wxUxThemeHandle::NewAtStdDPI(L"TaskDialog"));
+ if ( theme )
+ srcPri = theme.GetColour(TDLG_PRIMARYPANEL, TMT_FILLCOLOR);
+
+ // Define our own macros doing explicit cast because the standard
+ // Get[RGB]Value() result in warnings about "truncating constant value".
+#define GET_B(c) static_cast<BYTE>((c) & 0xFF)
+#define GET_G(c) static_cast<BYTE>(((c) >> 8) & 0xFF)
+#define GET_R(c) static_cast<BYTE>(((c) >> 16) & 0xFF)
+
+ struct Rule { BYTE sR, sG, sB, dR, dG, dB; };
+ const Rule rules[] =
+ {
+ { srcPri.Red(), srcPri.Green(), srcPri.Blue(),
+ GET_R(TDDarkCol::kPrimary), GET_G(TDDarkCol::kPrimary), GET_B(TDDarkCol::kPrimary) },
+ { srcSec.Red(), srcSec.Green(), srcSec.Blue(),
+ GET_R(TDDarkCol::kSecondary), GET_G(TDDarkCol::kSecondary), GET_B(TDDarkCol::kSecondary) },
+ { srcSep.Red(), srcSep.Green(), srcSep.Blue(),
+ GET_R(TDDarkCol::kSeparator), GET_G(TDDarkCol::kSeparator), GET_B(TDDarkCol::kSeparator) },
+ { srcSp2.Red(), srcSp2.Green(), srcSp2.Blue(),
+ GET_R(TDDarkCol::kSeparator), GET_G(TDDarkCol::kSeparator), GET_B(TDDarkCol::kSeparator) },
+ };
+
+#undef GET_B
+#undef GET_G
+#undef GET_R
+
+ for ( int y = 0; y < h; ++y )
+ {
+ RGBQUAD* row = pPx + static_cast<ptrdiff_t>(y) * rw;
+ for ( int x = 0; x < w; ++x )
+ {
+ RGBQUAD& p = row[x];
+ for ( const Rule& r : rules )
+ {
+ if ( p.rgbRed == r.sR && p.rgbGreen == r.sG && p.rgbBlue == r.sB )
+ {
+ p.rgbRed = r.dR;
+ p.rgbGreen = r.dG;
+ p.rgbBlue = r.dB;
+ p.rgbReserved = 55;
+ break;
+ }
+ }
+ }
+ }
+}
+
+void TDPaintIcons(HDC hdc, const TDPageState& s)
+{
+ if ( !s.pCfg || TDHasNativeDarkTheme() )
+ return;
+
+ for ( const auto& el : s.elements )
+ {
+ if ( ::IsRectEmpty(&el.rect) )
+ continue;
+
+ HICON hIcon = nullptr;
+ HBRUSH brBg = nullptr;
+ if ( el.automationId == L"MainIcon" )
+ {
+ hIcon = TDLoadStockIcon(s.pCfg, true);
+ brBg = s.brPrimary;
+ }
+ else if ( el.automationId == L"FootnoteIcon" )
+ {
+ hIcon = TDLoadStockIcon(s.pCfg, false);
+ brBg = s.brFootnote;
+ }
+
+ if ( !hIcon )
+ continue;
+
+ ::FillRect(hdc, &el.rect, brBg);
+ ::DrawIconEx
+ (
+ hdc,
+ el.rect.left,
+ el.rect.top,
+ hIcon,
+ el.rect.right - el.rect.left,
+ el.rect.bottom - el.rect.top,
+ 0,
+ nullptr,
+ DI_NORMAL
+ );
+
+ if ( ::DestroyIcon(hIcon) == 0 )
+ {
+ wxLogLastError("DestroyIcon");
+ }
+ }
+}
+
+void TDPaintGlyphs(HDC hdc, TDPageState& s)
+{
+ if ( !s.hTD && !s.hButton )
+ return;
+
+ if ( TDHasNativeDarkTheme() )
+ return;
+
+ for ( int i = 0; i < wxSsize(s.elements); ++i )
+ {
+ const TDLayoutElement& el = s.elements[i];
+ if ( ::IsRectEmpty(&el.rect) )
+ continue;
+
+ const bool hot = i == s.hotIdx;
+ const bool press = hot && s.pressing;
+
+ if ( el.automationId == L"ExpandoButton" && s.hTD )
+ {
+ const int width =
+ s.hTD.GetTrueSize(TDLG_EXPANDOBUTTON, TDLGEBS_NORMAL, hdc).x;
+
+ RECT rc = el.rect;
+ rc.right = el.rect.left + width + 3;
+
+ int state;
+ if ( press )
+ state = s.isExpanded ? TDLGEBS_EXPANDEDPRESSED : TDLGEBS_PRESSED;
+ else if ( hot )
+ state = s.isExpanded ? TDLGEBS_EXPANDEDHOVER : TDLGEBS_HOVER;
+ else
+ state = s.isExpanded ? TDLGEBS_EXPANDEDNORMAL : TDLGEBS_NORMAL;
+
+ ::FillRect(hdc, &rc, s.brSecondary);
+ s.hTD.DrawBackground(hdc, rc, TDLG_EXPANDOBUTTON, state, &el.rect);
+ }
+ else if ( el.automationId == L"VerificationCheckBox" && s.hButton )
+ {
+ const wxSize size =
+ s.hButton.GetDrawSize(BP_CHECKBOX, CBS_UNCHECKEDNORMAL, hdc);
+
+ const int mg = (el.rect.bottom - el.rect.top - size.y) / 3;
+ RECT rc = { el.rect.left + mg + 1,el.rect.top + mg + 1,el.rect.left + mg + 1 + size.x,el.rect.bottom };
+
+ int state;
+ if ( press )
+ state = s.isChecked ? CBS_CHECKEDPRESSED : CBS_UNCHECKEDPRESSED;
+ else if ( hot )
+ state = s.isChecked ? CBS_CHECKEDHOT : CBS_UNCHECKEDHOT;
+ else
+ state = s.isChecked ? CBS_CHECKEDNORMAL : CBS_UNCHECKEDNORMAL;
+
+ ::FillRect(hdc, &rc, s.brSecondary);
+ s.hButton.DrawBackground(hdc, rc, BP_CHECKBOX, state);
+ }
+ }
+}
+
+void TDPaintText(HDC hdc, const TDPageState& s)
+{
+ if ( !s.hTD )
+ return;
+
+ const bool native = TDHasNativeDarkTheme();
+
+ for ( const auto& el : s.elements )
+ {
+ if ( ::IsRectEmpty(&el.rect) )
+ continue;
+
+ RECT rcText = el.rect;
+ int part = 0;
+ HBRUSH brBg = 0;
+ DWORD dtF = DT_LEFT | DT_VCENTER | DT_WORDBREAK | DT_NOPREFIX;
+
+ if ( el.automationId == L"MainInstruction" )
+ {
+ part = TDLG_MAININSTRUCTIONPANE;
+ brBg = s.brPrimary;
+ }
+ else if ( el.automationId == L"ContentText" )
+ {
+ part = TDLG_CONTENTPANE;
+ brBg = s.brPrimary;
+ }
+ else if ( el.automationId == L"ExpandedFooterText" )
+ {
+ part = TDLG_EXPANDEDFOOTERAREA;
+ brBg = s.brFootnote;
+ }
+ else if ( el.automationId == L"FootnoteText" )
+ {
+ part = TDLG_FOOTNOTEPANE;
+ brBg = s.brFootnote;
+ }
+ else if ( el.automationId == L"ExpandoButton" && s.hTD )
+ {
+ const int width =
+ s.hTD.GetTrueSize(TDLG_EXPANDOBUTTON, TDLGEBS_NORMAL, hdc).x;
+
+ MARGINS vm = {};
+ s.hTD.GetMargins(vm, TDLG_VERIFICATIONTEXT, TMT_CONTENTMARGINS, 0, hdc);
+ rcText.left += width + vm.cxLeftWidth - 2;
+ rcText.top += 1;
+
+ part = TDLG_EXPANDOTEXT;
+ brBg = s.brSecondary;
+
+ dtF = DT_LEFT | DT_VCENTER | DT_NOPREFIX;
+ }
+ else if ( el.automationId == L"VerificationCheckBox" && s.hButton && s.hTD )
+ {
+ const int width =
+ s.hButton.GetDrawSize(BP_CHECKBOX, CBS_UNCHECKEDNORMAL, hdc).x;
+
+ MARGINS tm = {};
+ s.hTD.GetMargins(tm, TDLG_VERIFICATIONTEXT, TMT_CONTENTMARGINS, 0, hdc);
+ rcText.left = el.rect.left + width + tm.cxLeftWidth + 3;
+ rcText.top += 5;
+
+ part = TDLG_VERIFICATIONTEXT;
+ brBg = s.brSecondary;
+
+ dtF = DT_LEFT | DT_VCENTER | DT_NOPREFIX;
+ }
+
+ if ( !part )
+ continue;
+
+ WinStructWordSize<DTTOPTS> opts;
+ if ( native )
+ {
+ if (part == TDLG_EXPANDEDFOOTERAREA)
+ {
+ WinStructWordSize<DTBGOPTS> optsBg;
+ optsBg.dwFlags = DTBG_OMITBORDER;
+
+ ::DrawThemeBackgroundEx(s.hTD, hdc, TDLG_SECONDARYPANEL, 0, &el.rect, &optsBg);
+ }
+ }
+ else
+ {
+ opts.dwFlags = DTT_COMPOSITED | DTT_TEXTCOLOR;
+ opts.crText = TDGetTextColour(s, part);
+
+ ::FillRect(hdc, &rcText, brBg);
+ }
+
+ ::DrawThemeTextEx(s.hTD, hdc, part, 0, el.name.c_str(), -1, dtF, &rcText, &opts);
+ }
+}
+
+void TDPaintPage(HWND hwnd, HDC hdcWin, TDPageState& s)
+{
+ if ( !s.themesOk )
+ TDRefreshThemes(hwnd, s);
+
+ const RECT rc = wxGetClientRect(hwnd);
+ HDC hdcBuf = 0;
+ HPAINTBUFFER hbp = ::BeginBufferedPaint(hdcWin, &rc, BPBF_TOPDOWNDIB, nullptr, &hdcBuf);
+ if ( !hbp )
+ {
+ ::DefSubclassProc(hwnd, WM_PRINTCLIENT, reinterpret_cast<WPARAM>(hdcWin), PRF_CLIENT);
+ return;
+ }
+
+ ::DefSubclassProc(hwnd, WM_PRINTCLIENT, reinterpret_cast<WPARAM>(hdcBuf), PRF_CLIENT);
+
+ if ( !TDHasNativeDarkTheme() )
+ {
+ RECT rcBuf;
+ ::GetBufferedPaintTargetRect(hbp, &rcBuf);
+ TDPaintPixelSwap(hbp, rcBuf.right - rcBuf.left, rcBuf.bottom - rcBuf.top);
+ }
+
+ TDPaintIcons(hdcBuf, s);
+ TDPaintGlyphs(hdcBuf, s);
+ TDPaintText(hdcBuf, s);
+
+ ::EndBufferedPaint(hbp, TRUE);
+}
+
+// ============================================================================
+// Subclass procedures
+// ============================================================================
+
+LRESULT CALLBACK
+TDPageSubclassProc(HWND hwnd,
+ UINT msg,
+ WPARAM wParam,
+ LPARAM lParam,
+ UINT_PTR uId,
+ DWORD_PTR dwRef)
+{
+ switch ( msg )
+ {
+ case WM_ERASEBKGND:
+ return TRUE;
+
+ case WM_PAINT:
+ {
+ PAINTSTRUCT ps;
+ HDC hdc = ::BeginPaint(hwnd, &ps);
+ TDPageState& s = TDPageState::Get(hwnd);
+ s.isExpanded = ::GetPropW(GetParent(hwnd), L"IsExpanded");
+ s.elemsOk = false;
+ TDUpdateLayoutCache(hwnd, s);
+ TDPaintPage(hwnd, hdc, s);
+ ::EndPaint(hwnd, &ps);
+ }
+ return 0;
+
+ case WM_MOUSEMOVE:
+ {
+ TDPageState& s = TDPageState::Get(hwnd);
+ if ( !s.tracking )
+ {
+ WinStruct<TRACKMOUSEEVENT> tme;
+ tme.dwFlags = TME_LEAVE;
+ tme.hwndTrack = hwnd;
+ ::TrackMouseEvent(&tme);
+
+ s.tracking = true;
+ }
+ POINT pt{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
+
+ const int nh = TDHitTest(s.elements, pt);
+ if ( nh != s.hotIdx )
+ {
+ s.hotIdx = nh;
+ ::InvalidateRect(hwnd, nullptr, FALSE);
+ }
+ }
+ break;
+
+ case WM_MOUSELEAVE:
+ {
+ TDPageState& s = TDPageState::Get(hwnd);
+ s.tracking = false;
+ s.pressing = false;
+
+ if ( s.hotIdx != -1 )
+ {
+ s.hotIdx = -1;
+ ::InvalidateRect(hwnd, nullptr, FALSE);
+ }
+ }
+ break;
+
+ case WM_LBUTTONDOWN:
+ TDPageState::Get(hwnd).pressing = true;
+ TDUpdateLayoutCache(hwnd, TDPageState::Get(hwnd));
+ ::InvalidateRect(hwnd, nullptr, FALSE);
+ break;
+
+ case WM_LBUTTONUP:
+ TDPageState::Get(hwnd).pressing = false;
+ TDUpdateLayoutCache(hwnd, TDPageState::Get(hwnd));
+ ::InvalidateRect(hwnd, nullptr, FALSE);
+ break;
+
+ case WM_THEMECHANGED:
+ {
+ TDPageState& s = TDPageState::Get(hwnd);
+ s.themesOk = false;
+ s.elemsOk = false;
+ ::InvalidateRect(hwnd, nullptr, FALSE);
+ }
+ break;
+
+ case WM_DESTROY:
+ // Restore the original class brush we had changed.
+ ::SetClassLongPtr(hwnd, GCLP_HBRBACKGROUND, dwRef);
+ TDPageState::Destroy(hwnd);
+ ::RemoveWindowSubclass(hwnd, TDPageSubclassProc, uId);
+ break;
+ }
+
+ return ::DefSubclassProc(hwnd, msg, wParam, lParam);
+}
+
+LRESULT CALLBACK
+TDCtrlContainerSubclassProc(HWND hwnd,
+ UINT msg,
+ WPARAM wParam,
+ LPARAM lParam,
+ UINT_PTR uId,
+ DWORD_PTR dwRef)
+{
+ HBRUSH hbr = reinterpret_cast<HBRUSH>(dwRef);
+
+ switch ( msg )
+ {
+ case WM_ERASEBKGND:
+ {
+ HDC hdc = reinterpret_cast<HDC>(wParam);
+ wxFillRect(hwnd, hdc, hbr);
+ return 1;
+ }
+
+ case WM_CTLCOLORMSGBOX:
+ case WM_CTLCOLOREDIT:
+ case WM_CTLCOLORLISTBOX:
+ case WM_CTLCOLORBTN:
+ case WM_CTLCOLORDLG:
+ case WM_CTLCOLORSCROLLBAR:
+ case WM_CTLCOLORSTATIC:
+ {
+ HDC hdc = reinterpret_cast<HDC>(wParam);
+ COLORREF bg = TDDarkCol::kSecondary;
+ if ( hbr )
+ {
+ LOGBRUSH lb = {};
+ ::GetObject(hbr, sizeof(lb), &lb);
+ if ( lb.lbStyle == BS_SOLID )
+ bg = lb.lbColor;
+ }
+
+ ::SetBkColor(hdc, bg);
+ ::SetTextColor(hdc, TDDarkCol::kTextNormal);
+
+ if ( !hbr )
+ hbr = GetSolidBrush(TDDarkCol::kSecondary);
+
+ return reinterpret_cast<LRESULT>(hbr);
+ }
+
+ case WM_DESTROY:
+ ::RemoveWindowSubclass(hwnd, TDCtrlContainerSubclassProc, uId);
+ break;
+ }
+
+ return ::DefSubclassProc(hwnd, msg, wParam, lParam);
+}
+
+LRESULT CALLBACK
+TDRadioButtonSubclassProc(HWND hwnd,
+ UINT msg,
+ WPARAM wParam,
+ LPARAM lParam,
+ UINT_PTR uId,
+ DWORD_PTR WXUNUSED(dwRef))
+{
+ switch ( msg )
+ {
+ case WM_PAINT:
+ {
+ PAINTSTRUCT ps;
+ HDC hdc = ::BeginPaint(hwnd, &ps);
+
+ const int dpi = wxGetWindowDPI(hwnd).x;
+ auto hStyle = wxUxThemeHandle::NewAtStdDPI(L"TaskDialogStyle");
+ auto hBtn = wxUxThemeHandle::NewAtDPI(nullptr, L"Button", dpi);
+
+ const RECT rcClient = wxGetClientRect(hwnd);
+ HDC hdcBuf = 0;
+ HPAINTBUFFER hbp = ::BeginBufferedPaint(hdc, &rcClient, BPBF_TOPDOWNDIB, nullptr, &hdcBuf);
+
+ ::DefSubclassProc(hwnd, WM_PRINTCLIENT, reinterpret_cast<WPARAM>(hdcBuf), PRF_CLIENT);
+
+ wchar_t text[512] = {};
+ GetWindowTextW(hwnd, text, static_cast<int>(std::size(text)));
+
+ auto gs = hBtn.GetTrueSize(BP_RADIOBUTTON, RBS_UNCHECKEDNORMAL);
+ RECT rcText = { gs.x + 2, 0, rcClient.right, rcClient.bottom };
+
+ WinStructWordSize<DTTOPTS> opts;
+ opts.dwFlags = DTT_COMPOSITED | DTT_TEXTCOLOR;
+ opts.crText = TDDarkCol::kTextNormal;
+
+ LOGFONT lf = {};
+ if ( hStyle.GetFont(lf, hdcBuf, TDLG_RADIOBUTTONPANE) )
+ {
+ AutoHFONT hFont(lf);
+ SelectInHDC sel(hdcBuf, hFont);
+
+ ::DrawThemeTextEx(hStyle, hdcBuf, TDLG_RADIOBUTTONPANE, 0,
+ text, -1,
+ DT_LEFT | DT_VCENTER | DT_END_ELLIPSIS,
+ &rcText, &opts);
+ }
+
+ if ( hbp )
+ ::EndBufferedPaint(hbp, TRUE);
+ ::EndPaint(hwnd, &ps);
+ }
+ return 0;
+
+ case WM_DESTROY:
+ ::RemoveWindowSubclass(hwnd, TDRadioButtonSubclassProc, uId);
+ break;
+ }
+
+ return ::DefSubclassProc(hwnd, msg, wParam, lParam);
+}
+
+// ============================================================================
+// Attachment helpers
+// ============================================================================
+
+// Helper which calls SetWindowSubclass() only if the subclass is not already
+// set.
+//
+// This overload takes a lambda as the last parameter which is called to create
+// the reference data if the subclass needs to be set and to avoid creating any
+// resources passed to the subclass procedure unnecessarily.
+template <typename InitFunc>
+void
+SetWindowSubclassIfNeeded(HWND hwnd,
+ SUBCLASSPROC proc,
+ UINT_PTR uId,
+ InitFunc initFunc)
+{
+ DWORD_PTR dwRef = 0;
+ if ( ::GetWindowSubclass(hwnd, proc, uId, &dwRef) )
+ return;
+
+ dwRef = static_cast<DWORD_PTR>(initFunc());
+ if ( !::SetWindowSubclass(hwnd, proc, uId, dwRef) )
+ {
+ wxLogLastError("SetWindowSubclass");
+ }
+}
+
+// This is a simple overload when we don't need to pass any reference data to
+// the subclass procedure.
+void
+SetWindowSubclassIfNeeded(HWND hwnd,
+ SUBCLASSPROC proc,
+ UINT_PTR uId)
+{
+ return SetWindowSubclassIfNeeded(hwnd, proc, uId, []() { return 0; });
+}
+
+// Remove the subclass if it was installed.
+//
+// Return false if it wasn't.
+bool RemoveWindowSubclassIfNeeded(HWND hwnd, SUBCLASSPROC proc, UINT_PTR uId)
+{
+ DWORD_PTR dwRef = 0;
+ if ( !::GetWindowSubclass(hwnd, proc, uId, &dwRef) )
+ return false;
+
+ if ( !::RemoveWindowSubclass(hwnd, proc, uId) )
+ {
+ wxLogLastError("RemoveWindowSubclass");
+ }
+
+ return true;
+}
+
+void TDSubclassContainer(HWND hwndParent, COLORREF bg)
+{
+ if ( !hwndParent )
+ return;
+
+ SetWindowSubclassIfNeeded
+ (
+ hwndParent,
+ TDCtrlContainerSubclassProc,
+ kTDCtrlSubclassId,
+ [bg]() { return reinterpret_cast<DWORD_PTR>(GetSolidBrush(bg)); }
+ );
+}
+
+void TDApplyToChildren(IUIAutomationElement* pEl)
+{
+ IUIAutomation* const uiAuto = wxTaskDialogDarkModule::GetUIAutomation();
+ if ( !uiAuto )
+ return;
+
+ const bool native = TDHasNativeDarkTheme();
+
+ wxCOMPtr<IUIAutomationTreeWalker> pWalker;
+ uiAuto->get_ContentViewWalker(&pWalker);
+ if ( !pWalker )
+ return;
+
+ wxCOMPtr<IUIAutomationElement> pChild;
+ pWalker->GetFirstChildElement(pEl, &pChild);
+
+ while ( pChild )
+ {
+ CONTROLTYPEID ct = 0;
+ pChild->get_CurrentControlType(&ct);
+
+ if ( ct == UIA_ButtonControlTypeId ||
+ ct == UIA_RadioButtonControlTypeId ||
+ ct == UIA_ProgressBarControlTypeId ||
+ ct == UIA_HyperlinkControlTypeId ||
+ ct == UIA_ScrollBarControlTypeId ||
+ ct == UIA_PaneControlTypeId)
+ {
+ if ( const HWND hBtn = GetHWNDFromElement(pChild) )
+ {
+ const std::wstring id = GetCurrentAutomationId(pChild);
+
+ HWND hwndParent = ::GetParent(hBtn);
+
+ if ( ct == UIA_ProgressBarControlTypeId )
+ {
+ wxMSWDarkMode::SetTheme
+ (
+ hBtn,
+ wxHasRealDarkTheme(L"Progress", L"DarkMode_CopyEngine::Progress")
+ ? L"DarkMode_CopyEngine"
+ : L"DarkMode_Explorer"
+ );
+ }
+ else if ( ct == UIA_RadioButtonControlTypeId ||
+ id.find(L"RadioButton_") == 0 ||
+ ct == UIA_HyperlinkControlTypeId )
+ {
+ if ( native )
+ {
+ wxMSWDarkMode::SetTheme(hBtn, L"DarkMode_DarkTheme");
+ }
+ else
+ {
+ SetWindowSubclassIfNeeded(hBtn, TDRadioButtonSubclassProc, kTDCtrlSubclassId);
+ }
+
+ TDSubclassContainer(hwndParent, native ? TDDarkCol::kSecondary : TDDarkCol::kPrimary);
+ }
+ else if ( id.find(L"CommandLink_") == 0 || id.find(L"CommandButton_") == 0 )
+ {
+ wxMSWDarkMode::SetTheme(hBtn, L"DarkMode_Explorer");
+ TDSubclassContainer(hwndParent, TDDarkCol::kSecondary);
+ }
+ else
+ {
+ wxMSWDarkMode::SetTheme(hBtn, L"DarkMode_Explorer");
+ }
+ }
+ }
+
+ wxCOMPtr<IUIAutomationElement> pNext;
+ pWalker->GetNextSiblingElement(pChild, &pNext);
+ pChild = pNext;
+ }
+}
+
+// EnumData is used by TDEnumAttachProc below to get the input parameters and
+// return the "found" result.
+struct TDEnumData
+{
+ HWND hwndTD = 0;
+ const TASKDIALOGCONFIG* pCfg = nullptr;
+ bool found = false;
+};
+
+BOOL CALLBACK TDEnumAttachProc(HWND hwndChild, LPARAM lparam)
+{
+ IUIAutomation* const uiAuto = wxTaskDialogDarkModule::GetUIAutomation();
+ if ( !uiAuto )
+ return FALSE;
+
+ wxCOMPtr<IUIAutomationElement> pEl;
+ if ( FAILED(uiAuto->ElementFromHandle(hwndChild, &pEl)) )
+ return TRUE;
+
+ AutoBSTR cls;
+ pEl->get_CurrentClassName(cls.Out());
+
+ if ( !cls )
+ return TRUE;
+
+ // SysLink controls (footnote / content hyperlinks)
+ if ( wcscmp(cls, L"CCSysLink") == 0 )
+ {
+ if ( const HWND hLink = GetHWNDFromElement(pEl) )
+ {
+ const std::wstring id = GetCurrentAutomationId(pEl);
+ bool isFn = id.find(L"Footnote") != std::wstring::npos ||
+ id.find(L"ExpandedFooter") != std::wstring::npos;
+
+ TDSubclassContainer
+ (
+ GetParent(hLink),
+ isFn && !TDHasNativeDarkTheme()
+ ? TDDarkCol::kFootnote
+ : TDDarkCol::kPrimary
+ );
+ }
+
+ return TRUE;
+ }
+
+ if ( wcscmp(cls, L"CCVScrollBar") == 0 || wcscmp(cls, L"CCHScrollBar") == 0 )
+ {
+ if ( const HWND hScroll = GetHWNDFromElement(pEl) )
+ {
+ wxMSWDarkMode::SetTheme(hScroll, L"DarkMode_Explorer");
+ }
+
+ return TRUE;
+ }
+
+ // Main TaskPage (DirectUI "TaskDialog" class)
+ if ( wcscmp(cls, L"TaskDialog") != 0 )
+ return TRUE;
+
+ const HWND hDUI = GetHWNDFromElement(pEl);
+ if ( !hDUI )
+ return TRUE;
+
+ TDApplyToChildren(pEl);
+ wxMSWDarkMode::SetTheme(hDUI, L"DarkMode_Explorer");
+
+ // Initialise per-page state
+ TDEnumData* const d = reinterpret_cast<TDEnumData*>(lparam);
+ TDPageState& s = TDPageState::Get(hDUI);
+ s.pCfg = d->pCfg;
+ s.defExpanded = d->pCfg && (d->pCfg->dwFlags & TDF_EXPANDED_BY_DEFAULT);
+ s.defChecked = d->pCfg && (d->pCfg->dwFlags & TDF_VERIFICATION_FLAG_CHECKED);
+ s.isExpanded = ::GetPropW(d->hwndTD, L"IsExpanded");
+ s.isChecked = s.defChecked || ::GetPropW(d->hwndTD, L"IsChecked");
+ s.elemsOk = false;
+ TDUpdateLayoutCache(hDUI, s);
+
+ SetWindowSubclassIfNeeded
+ (
+ hDUI,
+ TDPageSubclassProc,
+ kTDPageSubclassId,
+ [hDUI]()
+ {
+ // Change class background brush when subclassing.
+ const auto newBrush = reinterpret_cast<LONG_PTR>(
+ GetSolidBrush(TDDarkCol::kSecondary)
+ );
+ return ::SetClassLongPtr(hDUI, GCLP_HBRBACKGROUND, newBrush);
+ }
+ );
+
+ d->found = true;
+ return TRUE;
+}
+
+BOOL CALLBACK TDEnumSysColorProc(HWND h, LPARAM)
+{
+ ::SendMessage(h, WM_SYSCOLORCHANGE, 0, 0);
+ return TRUE;
+}
+
+BOOL CALLBACK TDEnumDetachProc(HWND hChild, LPARAM)
+{
+ if ( RemoveWindowSubclassIfNeeded(hChild, TDPageSubclassProc, kTDPageSubclassId) )
+ TDPageState::Destroy(hChild);
+
+ RemoveWindowSubclassIfNeeded(hChild, TDCtrlContainerSubclassProc, kTDCtrlSubclassId);
+
+ RemoveWindowSubclassIfNeeded(hChild, TDRadioButtonSubclassProc, kTDCtrlSubclassId);
+
+ ::SetWindowTheme(hChild, nullptr, nullptr);
+
+ return TRUE;
+}
+
+void TDAttach(HWND hwndTD, const TASKDIALOGCONFIG* pCfg)
+{
+ const bool native = TDHasNativeDarkTheme();
+
+ TDEnumData data;
+ data.hwndTD = hwndTD;
+ data.pCfg = pCfg;
+
+ ::EnumChildWindows(hwndTD, TDEnumAttachProc, reinterpret_cast<LPARAM>(&data));
+
+ if ( !data.found )
+ return;
+
+ if ( native )
+ wxMSWDarkMode::AllowForWindow(hwndTD, L"DarkMode_Explorer", nullptr);
+
+ HBRUSH nb = GetSolidBrush(TDDarkCol::kPrimary);
+
+ SetWindowSubclassIfNeeded
+ (
+ hwndTD,
+ TDCtrlContainerSubclassProc,
+ kTDMainSubclassId,
+ [nb]() { return reinterpret_cast<DWORD_PTR>(nb); }
+ );
+
+ // Dark title bar
+ wxMSWDarkMode::ConfigureTLW(hwndTD);
+ ::EnumChildWindows(hwndTD, TDEnumSysColorProc, 0);
+ ::SendMessage(hwndTD, WM_THEMECHANGED, 0, 0);
+}
+
+void TDDetach(HWND hwndTD)
+{
+ ::EnumChildWindows(hwndTD, TDEnumDetachProc, 0);
+}
+
+} // anonymous namespace
+
+#endif // wxUSE_DARK_MODE
+
+// ----------------------------------------------------------------------------
+// TaskDialog dark mode — public entry points
+// ----------------------------------------------------------------------------
+
+void wxMSWDarkMode::AllowForTaskDialog(HWND hwnd, const TASKDIALOGCONFIG* pCfg)
+{
+#if wxUSE_DARK_MODE
+ if ( !wxMSWDarkMode::IsActive() )
+ return;
+
+ // Ensure COM is initialised for UIA on this thread.
+ // S_FALSE means already initialised by the caller — that is fine.
+ const HRESULT hr = CoInitializeEx
+ (
+ nullptr,
+ COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE
+ );
+ if ( FAILED(hr) && hr != S_FALSE )
+ return;
+
+ TDAttach(hwnd, pCfg);
+
+ CoUninitialize();
+#else // !wxUSE_DARK_MODE
+ wxUnusedVar(hwnd);
+ wxUnusedVar(pCfg);
+#endif // wxUSE_DARK_MODE
+}
+
+void wxMSWDarkMode::RemoveFromTaskDialog(HWND hwnd)
+{
+#if wxUSE_DARK_MODE
+ TDDetach(hwnd);
+#else // !wxUSE_DARK_MODE
+ wxUnusedVar(hwnd);
+#endif // wxUSE_DARK_MODE
+}


=====================================
src/msw/toolbar.cpp
=====================================
@@ -723,19 +723,9 @@ void wxToolBar::MSWSetDarkOrLightMode(SetMode setmode)
{
wxToolBarBase::MSWSetDarkOrLightMode(setmode);

- // Background color does not respond when switching to dark mode.
- const auto attrs = GetDefaultAttributes();
- if ( setmode == SetMode::Change )
- SetBackgroundColour(attrs.colBg);
-
- // This ensures GetForegroundColour(), used in our custom draw code,
- // returns the correct colour.
- SetForegroundColour(attrs.colFg);
-
// Update the separator above the toolbar which is drawn partially in
// white by default and so looks very ugly in dark mode.
- COLORSCHEME colScheme;
- colScheme.dwSize = sizeof(COLORSCHEME);
+ WinStructWordSize<COLORSCHEME> colScheme;
colScheme.clrBtnHighlight =
colScheme.clrBtnShadow = wxSysColourToRGB(wxSYS_COLOUR_WINDOW);
::SendMessage(GetHwnd(), TB_SETCOLORSCHEME, 0, (LPARAM)&colScheme);
@@ -2052,9 +2042,6 @@ void wxToolBar::OnSysColourChanged(wxSysColourChangedEvent& event)
// let the event propagate further in any case
event.Skip();

- if ( !UseBgCol() )
- wxRGBToColour(m_backgroundColour, ::GetSysColor(COLOR_BTNFACE));
-
// Remap the buttons
Realize();



=====================================
src/msw/utils.cpp
=====================================
@@ -818,9 +818,7 @@ int wxKillAllChildren(long pid, wxSignal sig, wxKillError *krc, int flags)
}

//Fill in the size of the structure before using it.
- PROCESSENTRY32 pe;
- wxZeroMemory(pe);
- pe.dwSize = sizeof(PROCESSENTRY32);
+ WinStructWordSize<PROCESSENTRY32> pe;

// Walk the snapshot of the processes, and for each process,
// kill it if its parent is pid.


=====================================
src/msw/uxtheme.cpp
=====================================
@@ -110,10 +110,11 @@ wxColour wxUxThemeHandle::GetColour(int part, int prop, int state) const
return wxRGBToColour(col);
}

-wxSize wxUxThemeHandle::DoGetSize(int part, int state, THEMESIZE ts) const
+wxSize
+wxUxThemeHandle::DoGetSize(HDC hdc, int part, int state, THEMESIZE ts) const
{
SIZE size;
- HRESULT hr = ::GetThemePartSize(m_hTheme, nullptr, part, state, nullptr, ts,
+ HRESULT hr = ::GetThemePartSize(m_hTheme, hdc, part, state, nullptr, ts,
&size);
if ( FAILED(hr) )
{
@@ -127,10 +128,51 @@ wxSize wxUxThemeHandle::DoGetSize(int part, int state, THEMESIZE ts) const
return wxSize{size.cx, size.cy};
}

+bool
+wxUxThemeHandle::GetMargins(MARGINS& margins,
+ int part,
+ int prop,
+ int state,
+ HDC hdc) const
+{
+ HRESULT hr = ::GetThemeMargins(m_hTheme, hdc, part, state, prop, nullptr,
+ &margins);
+ if ( FAILED(hr) )
+ {
+ wxLogApiError(
+ wxString::Format("GetThemeMargins(%i, %i, %i)", part, state, prop),
+ hr
+ );
+ return false;
+ }
+
+ return true;
+}
+
+bool
+wxUxThemeHandle::GetFont(LOGFONTW& lf, HDC hdc, int part, int state) const
+{
+ HRESULT hr = ::GetThemeFont(m_hTheme, hdc, part, state, TMT_FONT, &lf);
+ if ( FAILED(hr) )
+ {
+ wxLogApiError(
+ wxString::Format("GetThemeFont(%i, %i)", part, state),
+ hr
+ );
+ return false;
+ }
+
+ return true;
+}
+
void
-wxUxThemeHandle::DrawBackground(HDC hdc, const RECT& rc, int part, int state)
+wxUxThemeHandle::DrawBackground(HDC hdc,
+ const RECT& rc,
+ int part,
+ int state,
+ const RECT* rcClip)
{
- HRESULT hr = ::DrawThemeBackground(m_hTheme, hdc, part, state, &rc, nullptr);
+ HRESULT hr = ::DrawThemeBackground(m_hTheme, hdc, part, state, &rc, rcClip);
if ( FAILED(hr) )
{
wxLogApiError(


=====================================
src/msw/window.cpp
=====================================
@@ -3555,12 +3555,7 @@ wxWindowMSW::MSWHandleMessage(WXLRESULT *result,
case WM_SETTINGCHANGE:
// Check for the special case of the message which notifies about
// the colours change.
- // Note that "ImmersiveColorSet" is set both when switching between
- // light and dark themes and also when changing high contrast mode,
- // for which an additional message with "WindowsThemeElement" is
- // also sent, but we don't need to check for it as handling this
- // one is enough
- if ( lParam && wxStrcmp((TCHAR*)lParam, wxT("ImmersiveColorSet")) == 0 )
+ if ( wxIsSystemColourChange(lParam) )
processed = HandleSysColorChange();
else
processed = HandleSettingChange(wParam, lParam);
@@ -4839,10 +4834,7 @@ wxWindowMSW::MSWOnMeasureItem(int id, WXMEASUREITEMSTRUCT *itemStruct)
// DPI
// ---------------------------------------------------------------------------

-namespace
-{
-
-static wxSize GetWindowDPI(HWND hwnd)
+wxSize wxGetWindowDPI(HWND hwnd)
{
typedef UINT (WINAPI *GetDpiForWindow_t)(HWND hwnd);
static GetDpiForWindow_t s_pfnGetDpiForWindow = nullptr;
@@ -4864,8 +4856,6 @@ static wxSize GetWindowDPI(HWND hwnd)
return wxSize();
}

-}
-
/*extern*/
int wxGetSystemMetrics(int nIndex, const wxWindow* window)
{
@@ -4956,7 +4946,7 @@ wxSize wxWindowMSW::GetDPI() const
}
}

- wxSize dpi = GetWindowDPI(hwnd);
+ wxSize dpi = wxGetWindowDPI(hwnd);

if ( !dpi.x || !dpi.y )
{


=====================================
tests/image/image.cpp
=====================================
@@ -20,6 +20,7 @@
#endif // WX_PRECOMP

#include "wx/anidecod.h" // wxImageArray
+#include "wx/gifdecod.h"
#include "wx/bitmap.h"
#include "wx/cursor.h"
#include "wx/icon.h"
@@ -1472,6 +1473,36 @@ TEST_CASE_METHOD(ImageHandlersInit, "wxImage::BadGIFPaletteIndex",
REQUIRE( !img.LoadFile(mis, wxBITMAP_TYPE_GIF) );
}

+TEST_CASE_METHOD(ImageHandlersInit, "wxImage::GIFBadBackgroundIndex",
+ "[image][gif][error]")
+{
+ // The logical screen descriptor's background colour index is read straight
+ // from the file and used to index the global colour table, but only the
+ // first 3*global_ncolors bytes of the 768-byte palette buffer are populated
+ // from the stream. This GIF declares a 2-entry global colour table and a
+ // background index of 255, so wxGIFDecoder::LoadGIF() used to take the
+ // background colour from the uninitialised tail of the buffer and expose it
+ // via GetBackgroundColour(). The frame itself is valid so loading still
+ // succeeds; the background should simply be left unset.
+ static const unsigned char data[] =
+ {
+ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, // "GIF89a"
+ 0x01, 0x00, 0x01, 0x00, 0x80, 0xff, 0x00, // LSD 1x1, GCT 2col, bg=255
+ 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, // GCT entries
+ 0x2c, // image separator
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, // 1x1 frame at 0,0
+ 0x00, // no LCT
+ 0x02, // LZW min code size=2
+ 0x02, 0x44, 0x01, // sub-block CLEAR,0,EOI
+ 0x00, // sub-block terminator
+ 0x3b, // trailer
+ };
+ wxMemoryInputStream mis(data, WXSIZEOF(data));
+ wxGIFDecoder decoder;
+ REQUIRE( decoder.LoadGIF(mis) == wxGIF_OK );
+ CHECK( !decoder.GetBackgroundColour().IsOk() );
+}
+
TEST_CASE_METHOD(ImageHandlersInit, "wxImage::BadGIFZeroFrameSize",
"[image][gif][error]")
{



View it on GitLab: https://gitlab.com/wxwidgets/wxwidgets/-/compare/aef89c0e875d67738b44f5557a55737f74df434f...47945b13b07d4d45b6c7c9d08866ae264981b5d8

--
View it on GitLab: https://gitlab.com/wxwidgets/wxwidgets/-/compare/aef89c0e875d67738b44f5557a55737f74df434f...47945b13b07d4d45b6c7c9d08866ae264981b5d8
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help


Reply all
Reply to author
Forward
0 new messages