Merge branch 'msw-dark-mode'

Add experimental support for dark mode for wxMSW.

See #23028.
This commit is contained in:
Vadim Zeitlin 2022-12-27 22:20:34 +01:00
commit 208142c14a
62 changed files with 1946 additions and 306 deletions

View file

@ -5291,6 +5291,7 @@ COND_TOOLKIT_MSW___GUI_SRC_OBJECTS = \
monodll_timectrl.o \
monodll_datecontrols.o \
monodll_generic_activityindicator.o \
monodll_darkmode.o \
monodll_msw_checklst.o \
monodll_msw_fdrepdlg.o \
monodll_msw_fontdlg.o
@ -7046,6 +7047,7 @@ COND_TOOLKIT_MSW___GUI_SRC_OBJECTS_1 = \
monolib_timectrl.o \
monolib_datecontrols.o \
monolib_generic_activityindicator.o \
monolib_darkmode.o \
monolib_msw_checklst.o \
monolib_msw_fdrepdlg.o \
monolib_msw_fontdlg.o
@ -8955,6 +8957,7 @@ COND_TOOLKIT_MSW___GUI_SRC_OBJECTS_2 = \
coredll_timectrl.o \
coredll_datecontrols.o \
coredll_generic_activityindicator.o \
coredll_darkmode.o \
coredll_msw_checklst.o \
coredll_msw_fdrepdlg.o \
coredll_msw_fontdlg.o
@ -10440,6 +10443,7 @@ COND_TOOLKIT_MSW___GUI_SRC_OBJECTS_3 = \
corelib_timectrl.o \
corelib_datecontrols.o \
corelib_generic_activityindicator.o \
corelib_darkmode.o \
corelib_msw_checklst.o \
corelib_msw_fdrepdlg.o \
corelib_msw_fontdlg.o
@ -15253,6 +15257,9 @@ monodll_timectrl.o: $(srcdir)/src/msw/timectrl.cpp $(MONODLL_ODEP)
monodll_datecontrols.o: $(srcdir)/src/msw/datecontrols.cpp $(MONODLL_ODEP)
$(CXXC) -c -o $@ $(MONODLL_CXXFLAGS) $(srcdir)/src/msw/datecontrols.cpp
monodll_darkmode.o: $(srcdir)/src/msw/darkmode.cpp $(MONODLL_ODEP)
$(CXXC) -c -o $@ $(MONODLL_CXXFLAGS) $(srcdir)/src/msw/darkmode.cpp
monodll_msw_checklst.o: $(srcdir)/src/msw/checklst.cpp $(MONODLL_ODEP)
$(CXXC) -c -o $@ $(MONODLL_CXXFLAGS) $(srcdir)/src/msw/checklst.cpp
@ -20017,6 +20024,9 @@ monolib_timectrl.o: $(srcdir)/src/msw/timectrl.cpp $(MONOLIB_ODEP)
monolib_datecontrols.o: $(srcdir)/src/msw/datecontrols.cpp $(MONOLIB_ODEP)
$(CXXC) -c -o $@ $(MONOLIB_CXXFLAGS) $(srcdir)/src/msw/datecontrols.cpp
monolib_darkmode.o: $(srcdir)/src/msw/darkmode.cpp $(MONOLIB_ODEP)
$(CXXC) -c -o $@ $(MONOLIB_CXXFLAGS) $(srcdir)/src/msw/darkmode.cpp
monolib_msw_checklst.o: $(srcdir)/src/msw/checklst.cpp $(MONOLIB_ODEP)
$(CXXC) -c -o $@ $(MONOLIB_CXXFLAGS) $(srcdir)/src/msw/checklst.cpp
@ -25465,6 +25475,9 @@ coredll_timectrl.o: $(srcdir)/src/msw/timectrl.cpp $(COREDLL_ODEP)
coredll_datecontrols.o: $(srcdir)/src/msw/datecontrols.cpp $(COREDLL_ODEP)
$(CXXC) -c -o $@ $(COREDLL_CXXFLAGS) $(srcdir)/src/msw/datecontrols.cpp
coredll_darkmode.o: $(srcdir)/src/msw/darkmode.cpp $(COREDLL_ODEP)
$(CXXC) -c -o $@ $(COREDLL_CXXFLAGS) $(srcdir)/src/msw/darkmode.cpp
coredll_msw_checklst.o: $(srcdir)/src/msw/checklst.cpp $(COREDLL_ODEP)
$(CXXC) -c -o $@ $(COREDLL_CXXFLAGS) $(srcdir)/src/msw/checklst.cpp
@ -29197,6 +29210,9 @@ corelib_timectrl.o: $(srcdir)/src/msw/timectrl.cpp $(CORELIB_ODEP)
corelib_datecontrols.o: $(srcdir)/src/msw/datecontrols.cpp $(CORELIB_ODEP)
$(CXXC) -c -o $@ $(CORELIB_CXXFLAGS) $(srcdir)/src/msw/datecontrols.cpp
corelib_darkmode.o: $(srcdir)/src/msw/darkmode.cpp $(CORELIB_ODEP)
$(CXXC) -c -o $@ $(CORELIB_CXXFLAGS) $(srcdir)/src/msw/darkmode.cpp
corelib_msw_checklst.o: $(srcdir)/src/msw/checklst.cpp $(CORELIB_ODEP)
$(CXXC) -c -o $@ $(CORELIB_CXXFLAGS) $(srcdir)/src/msw/checklst.cpp

View file

@ -1847,6 +1847,7 @@ IMPORTANT: please read docs/tech/tn0016.txt before modifying this file!
src/msw/timectrl.cpp
src/msw/datecontrols.cpp
src/generic/activityindicator.cpp
src/msw/darkmode.cpp
</set>
<set var="MSW_HDR" hints="files">
wx/generic/clrpickerg.h

View file

@ -1736,6 +1736,7 @@ set(MSW_SRC
src/msw/datetimectrl.cpp
src/msw/hyperlink.cpp
src/generic/activityindicator.cpp
src/msw/darkmode.cpp
)
set(MSW_HDR

View file

@ -1685,6 +1685,7 @@ MSW_SRC =
src/msw/commandlinkbutton.cpp
src/msw/control.cpp
src/msw/customdraw.cpp
src/msw/darkmode.cpp
src/msw/datecontrols.cpp
src/msw/datectrl.cpp
src/msw/datetimectrl.cpp

View file

@ -1997,6 +1997,7 @@ ____CORE_SRC_FILENAMES_OBJECTS = \
$(OBJS)\monodll_timectrl.o \
$(OBJS)\monodll_datecontrols.o \
$(OBJS)\monodll_activityindicator.o \
$(OBJS)\monodll_darkmode.o \
$(OBJS)\monodll_msw_checklst.o \
$(OBJS)\monodll_msw_fdrepdlg.o \
$(OBJS)\monodll_fontdlg.o \
@ -2845,6 +2846,7 @@ ____CORE_SRC_FILENAMES_1_OBJECTS = \
$(OBJS)\monolib_timectrl.o \
$(OBJS)\monolib_datecontrols.o \
$(OBJS)\monolib_activityindicator.o \
$(OBJS)\monolib_darkmode.o \
$(OBJS)\monolib_msw_checklst.o \
$(OBJS)\monolib_msw_fdrepdlg.o \
$(OBJS)\monolib_fontdlg.o \
@ -3575,6 +3577,7 @@ ____CORE_SRC_FILENAMES_2_OBJECTS = \
$(OBJS)\coredll_timectrl.o \
$(OBJS)\coredll_datecontrols.o \
$(OBJS)\coredll_activityindicator.o \
$(OBJS)\coredll_darkmode.o \
$(OBJS)\coredll_msw_checklst.o \
$(OBJS)\coredll_msw_fdrepdlg.o \
$(OBJS)\coredll_fontdlg.o \
@ -4262,6 +4265,7 @@ ____CORE_SRC_FILENAMES_3_OBJECTS = \
$(OBJS)\corelib_timectrl.o \
$(OBJS)\corelib_datecontrols.o \
$(OBJS)\corelib_activityindicator.o \
$(OBJS)\corelib_darkmode.o \
$(OBJS)\corelib_msw_checklst.o \
$(OBJS)\corelib_msw_fdrepdlg.o \
$(OBJS)\corelib_fontdlg.o \
@ -7507,6 +7511,9 @@ $(OBJS)\monodll_timectrl.o: ../../src/msw/timectrl.cpp
$(OBJS)\monodll_datecontrols.o: ../../src/msw/datecontrols.cpp
$(CXX) -c -o $@ $(MONODLL_CXXFLAGS) $(CPPDEPS) $<
$(OBJS)\monodll_darkmode.o: ../../src/msw/darkmode.cpp
$(CXX) -c -o $@ $(MONODLL_CXXFLAGS) $(CPPDEPS) $<
$(OBJS)\monodll_msw_checklst.o: ../../src/msw/checklst.cpp
$(CXX) -c -o $@ $(MONODLL_CXXFLAGS) $(CPPDEPS) $<
@ -10094,6 +10101,9 @@ $(OBJS)\monolib_timectrl.o: ../../src/msw/timectrl.cpp
$(OBJS)\monolib_datecontrols.o: ../../src/msw/datecontrols.cpp
$(CXX) -c -o $@ $(MONOLIB_CXXFLAGS) $(CPPDEPS) $<
$(OBJS)\monolib_darkmode.o: ../../src/msw/darkmode.cpp
$(CXX) -c -o $@ $(MONOLIB_CXXFLAGS) $(CPPDEPS) $<
$(OBJS)\monolib_msw_checklst.o: ../../src/msw/checklst.cpp
$(CXX) -c -o $@ $(MONOLIB_CXXFLAGS) $(CPPDEPS) $<
@ -13107,6 +13117,9 @@ $(OBJS)\coredll_timectrl.o: ../../src/msw/timectrl.cpp
$(OBJS)\coredll_datecontrols.o: ../../src/msw/datecontrols.cpp
$(CXX) -c -o $@ $(COREDLL_CXXFLAGS) $(CPPDEPS) $<
$(OBJS)\coredll_darkmode.o: ../../src/msw/darkmode.cpp
$(CXX) -c -o $@ $(COREDLL_CXXFLAGS) $(CPPDEPS) $<
$(OBJS)\coredll_msw_checklst.o: ../../src/msw/checklst.cpp
$(CXX) -c -o $@ $(COREDLL_CXXFLAGS) $(CPPDEPS) $<
@ -14857,6 +14870,9 @@ $(OBJS)\corelib_timectrl.o: ../../src/msw/timectrl.cpp
$(OBJS)\corelib_datecontrols.o: ../../src/msw/datecontrols.cpp
$(CXX) -c -o $@ $(CORELIB_CXXFLAGS) $(CPPDEPS) $<
$(OBJS)\corelib_darkmode.o: ../../src/msw/darkmode.cpp
$(CXX) -c -o $@ $(CORELIB_CXXFLAGS) $(CPPDEPS) $<
$(OBJS)\corelib_msw_checklst.o: ../../src/msw/checklst.cpp
$(CXX) -c -o $@ $(CORELIB_CXXFLAGS) $(CPPDEPS) $<

View file

@ -2314,6 +2314,7 @@ ____CORE_SRC_FILENAMES_OBJECTS = \
$(OBJS)\monodll_timectrl.obj \
$(OBJS)\monodll_datecontrols.obj \
$(OBJS)\monodll_activityindicator.obj \
$(OBJS)\monodll_darkmode.obj \
$(OBJS)\monodll_msw_checklst.obj \
$(OBJS)\monodll_msw_fdrepdlg.obj \
$(OBJS)\monodll_fontdlg.obj \
@ -3162,6 +3163,7 @@ ____CORE_SRC_FILENAMES_1_OBJECTS = \
$(OBJS)\monolib_timectrl.obj \
$(OBJS)\monolib_datecontrols.obj \
$(OBJS)\monolib_activityindicator.obj \
$(OBJS)\monolib_darkmode.obj \
$(OBJS)\monolib_msw_checklst.obj \
$(OBJS)\monolib_msw_fdrepdlg.obj \
$(OBJS)\monolib_fontdlg.obj \
@ -3942,6 +3944,7 @@ ____CORE_SRC_FILENAMES_2_OBJECTS = \
$(OBJS)\coredll_timectrl.obj \
$(OBJS)\coredll_datecontrols.obj \
$(OBJS)\coredll_activityindicator.obj \
$(OBJS)\coredll_darkmode.obj \
$(OBJS)\coredll_msw_checklst.obj \
$(OBJS)\coredll_msw_fdrepdlg.obj \
$(OBJS)\coredll_fontdlg.obj \
@ -4627,6 +4630,7 @@ ____CORE_SRC_FILENAMES_3_OBJECTS = \
$(OBJS)\corelib_timectrl.obj \
$(OBJS)\corelib_datecontrols.obj \
$(OBJS)\corelib_activityindicator.obj \
$(OBJS)\corelib_darkmode.obj \
$(OBJS)\corelib_msw_checklst.obj \
$(OBJS)\corelib_msw_fdrepdlg.obj \
$(OBJS)\corelib_fontdlg.obj \
@ -7952,6 +7956,9 @@ $(OBJS)\monodll_timectrl.obj: ..\..\src\msw\timectrl.cpp
$(OBJS)\monodll_datecontrols.obj: ..\..\src\msw\datecontrols.cpp
$(CXX) /c /nologo /TP /Fo$@ $(MONODLL_CXXFLAGS) ..\..\src\msw\datecontrols.cpp
$(OBJS)\monodll_darkmode.obj: ..\..\src\msw\darkmode.cpp
$(CXX) /c /nologo /TP /Fo$@ $(MONODLL_CXXFLAGS) ..\..\src\msw\darkmode.cpp
$(OBJS)\monodll_msw_checklst.obj: ..\..\src\msw\checklst.cpp
$(CXX) /c /nologo /TP /Fo$@ $(MONODLL_CXXFLAGS) ..\..\src\msw\checklst.cpp
@ -10539,6 +10546,9 @@ $(OBJS)\monolib_timectrl.obj: ..\..\src\msw\timectrl.cpp
$(OBJS)\monolib_datecontrols.obj: ..\..\src\msw\datecontrols.cpp
$(CXX) /c /nologo /TP /Fo$@ $(MONOLIB_CXXFLAGS) ..\..\src\msw\datecontrols.cpp
$(OBJS)\monolib_darkmode.obj: ..\..\src\msw\darkmode.cpp
$(CXX) /c /nologo /TP /Fo$@ $(MONOLIB_CXXFLAGS) ..\..\src\msw\darkmode.cpp
$(OBJS)\monolib_msw_checklst.obj: ..\..\src\msw\checklst.cpp
$(CXX) /c /nologo /TP /Fo$@ $(MONOLIB_CXXFLAGS) ..\..\src\msw\checklst.cpp
@ -13552,6 +13562,9 @@ $(OBJS)\coredll_timectrl.obj: ..\..\src\msw\timectrl.cpp
$(OBJS)\coredll_datecontrols.obj: ..\..\src\msw\datecontrols.cpp
$(CXX) /c /nologo /TP /Fo$@ $(COREDLL_CXXFLAGS) ..\..\src\msw\datecontrols.cpp
$(OBJS)\coredll_darkmode.obj: ..\..\src\msw\darkmode.cpp
$(CXX) /c /nologo /TP /Fo$@ $(COREDLL_CXXFLAGS) ..\..\src\msw\darkmode.cpp
$(OBJS)\coredll_msw_checklst.obj: ..\..\src\msw\checklst.cpp
$(CXX) /c /nologo /TP /Fo$@ $(COREDLL_CXXFLAGS) ..\..\src\msw\checklst.cpp
@ -15302,6 +15315,9 @@ $(OBJS)\corelib_timectrl.obj: ..\..\src\msw\timectrl.cpp
$(OBJS)\corelib_datecontrols.obj: ..\..\src\msw\datecontrols.cpp
$(CXX) /c /nologo /TP /Fo$@ $(CORELIB_CXXFLAGS) ..\..\src\msw\datecontrols.cpp
$(OBJS)\corelib_darkmode.obj: ..\..\src\msw\darkmode.cpp
$(CXX) /c /nologo /TP /Fo$@ $(CORELIB_CXXFLAGS) ..\..\src\msw\darkmode.cpp
$(OBJS)\corelib_msw_checklst.obj: ..\..\src\msw\checklst.cpp
$(CXX) /c /nologo /TP /Fo$@ $(CORELIB_CXXFLAGS) ..\..\src\msw\checklst.cpp

View file

@ -613,6 +613,7 @@
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|x64'">$(IntDir)msw_%(Filename).obj</ObjectFileName>
<ObjectFileName Condition="'$(Configuration)|$(Platform)'=='DLL Release|x64'">$(IntDir)msw_%(Filename).obj</ObjectFileName>
</ClCompile>
<ClCompile Include="..\..\src\msw\darkmode.cpp" />
<ClCompile Include="..\..\src\msw\graphicsd2d.cpp" />
<ClCompile Include="..\..\src\msw\ole\access.cpp" />
<ClCompile Include="..\..\src\msw\ole\activex.cpp" />

View file

@ -1074,6 +1074,9 @@
<ClCompile Include="..\..\src\xrc\xmlreshandler.cpp">
<Filter>Common Sources</Filter>
</ClCompile>
<ClCompile Include="..\..\src\msw\darkmode.cpp">
<Filter>MSW Sources</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\..\src\msw\version.rc">

View file

@ -59,6 +59,10 @@ Changes in behaviour not resulting in compilation errors
- Calling wxListCtrl::EditLabel() now asserts if the control doesn't have
wxLC_EDIT_LABELS style: previously this silently didn't work in wxMSW.
- wxSystemAppearance::IsDark() now returns whether this application uses dark
mode under MSW, use the new AreAppsDark() or IsSystemDark() to check if the
other applications or the system are using dark mode.
Changes in behaviour which may result in build errors
-----------------------------------------------------

View file

@ -37,6 +37,14 @@ public:
virtual void SetPrintMode(int mode) override { m_printMode = mode; }
virtual int GetPrintMode() const { return m_printMode; }
// MSW-specific function to enable experimental dark mode support.
enum
{
DarkMode_Auto = 0, // Use dark mode if the system is using it.
DarkMode_Always = 1 // Force using dark mode.
};
bool MSWEnableDarkMode(int flags = 0);
// implementation only
void OnIdle(wxIdleEvent& event);
void OnEndSession(wxCloseEvent& event);

View file

@ -62,6 +62,8 @@ public:
protected:
virtual wxSize DoGetBestClientSize() const override;
virtual bool MSWGetDarkModeSupport(MSWDarkModeSupport& support) const override;
virtual void DoSet3StateValue(wxCheckBoxState value) override;
virtual wxCheckBoxState DoGet3StateValue() const override;

View file

@ -129,6 +129,8 @@ protected:
int sizeFlags = wxSIZE_AUTO) override;
virtual wxSize DoGetSizeFromTextSize(int xlen, int ylen = -1) const override;
virtual bool MSWGetDarkModeSupport(MSWDarkModeSupport& support) const override;
// Show or hide the popup part of the control.
void MSWDoPopupOrDismiss(bool show);

View file

@ -125,6 +125,34 @@ protected:
virtual wxWindow *MSWFindItem(long id, WXHWND hWnd) const override;
// Struct used for MSWGetDarkModeSupport() below.
struct MSWDarkModeSupport
{
// The name of the theme to use (also called "app name").
const wchar_t* themeName = nullptr;
// The theme IDs to use. If neither this field nor the theme name is
// set, no theme is applied to the window.
const wchar_t* themeId = nullptr;
// For some controls we need to set the foreground explicitly, even if
// they have some support for the dark theme.
bool setForeground = false;
};
// Called after creating the control to enable dark mode support if needed.
//
// If this function returns true, wxControl allows using dark mode for the
// window and set its theme to the one specified by MSWDarkModeSupport
// fields.
virtual bool MSWGetDarkModeSupport(MSWDarkModeSupport& support) const;
// Return the message that can be used to retrieve the tooltip window used
// by a native control. If this message is non-zero and sending it returns
// a valid HWND, the dark theme is also applied to it, if appropriate.
virtual int MSWGetToolTipMessage() const { return 0; }
// for controls like radiobuttons which are really composite this array
// holds the ids (not HWNDs!) of the sub controls
wxArrayLong m_subControls;

View file

@ -16,7 +16,7 @@
#include "wx/vector.h"
class wxMSWListItemData;
class wxMSWListHeaderCustomDraw;
class wxMSWHeaderCtrlCustomDraw;
// define this symbol to indicate the availability of SetColumnsOrder() and
// related functions
@ -366,10 +366,7 @@ public:
virtual bool ShouldInheritColours() const override { return false; }
virtual wxVisualAttributes GetDefaultAttributes() const override
{
return GetClassDefaultAttributes(GetWindowVariant());
}
virtual wxVisualAttributes GetDefaultAttributes() const override;
static wxVisualAttributes
GetClassDefaultAttributes(wxWindowVariant variant = wxWINDOW_VARIANT_NORMAL);
@ -401,6 +398,10 @@ protected:
virtual void MSWAfterReparent() override;
virtual bool MSWGetDarkModeSupport(MSWDarkModeSupport& support) const override;
virtual int MSWGetToolTipMessage() const override;
void OnDPIChanged(wxDPIChangedEvent& event);
wxSize MSWGetBestViewRect(int x, int y) const;
@ -443,6 +444,9 @@ private:
// UpdateStyle()), only should be called if InReportView()
void MSWSetExListStyles();
// Initialize the header control if it exists.
void MSWInitHeader();
// initialize the (already created) m_textCtrl with the associated HWND
void InitEditControl(WXHWND hWnd);
@ -460,7 +464,7 @@ private:
void DrawSortArrow();
// Object using for header custom drawing if necessary, may be null.
wxMSWListHeaderCustomDraw* m_headerCustomDraw;
wxMSWHeaderCtrlCustomDraw* m_headerCustomDraw;
wxDECLARE_DYNAMIC_CLASS(wxListCtrl);

View file

@ -141,6 +141,8 @@ protected:
// common part of all ctors
void Init();
virtual int MSWGetToolTipMessage() const override;
// hides the currently shown page and shows the given one (if not -1) and
// updates m_selection accordingly
void UpdateSelection(int selNew);
@ -181,6 +183,10 @@ protected:
void OnEraseBackground(wxEraseEvent& event);
void OnPaint(wxPaintEvent& event);
// Paint the notebook ourselves on the provided DC.
void MSWNotebookPaint(wxDC& dc);
// true if we have already subclassed our updown control
bool m_hasSubclassedUpdown;

View file

@ -386,6 +386,21 @@ inline RECT wxGetClientRect(HWND hwnd)
return rect;
}
// Call MapWindowPoints() on a RECT: because a RECT is (intentionally) laid out
// as 2 consecutive POINTs, the cast below is valid but we still prefer to hide
// it in this function instead of writing it out in the rest of the code.
inline void wxMapWindowPoints(HWND hwndFrom, HWND hwndTo, RECT* rc)
{
::MapWindowPoints(hwndFrom, hwndTo, reinterpret_cast<POINT *>(rc), 2);
}
// For consistency also provide an overload taking a POINT, even if this one is
// even more trivial.
inline void wxMapWindowPoints(HWND hwndFrom, HWND hwndTo, POINT* pt)
{
::MapWindowPoints(hwndFrom, hwndTo, pt, 1);
}
// ---------------------------------------------------------------------------
// small helper classes
// ---------------------------------------------------------------------------
@ -421,38 +436,46 @@ private:
#endif // __WXMSW__
// create an instance of this class and use it as the HDC for screen, will
// automatically release the DC going out of scope
class ScreenHDC
// RAII helper for releasing an HDC in its dtor.
class AutoHDC
{
public:
ScreenHDC() { m_hdc = ::GetDC(nullptr); }
~ScreenHDC() { ::ReleaseDC(nullptr, m_hdc); }
~AutoHDC() { if ( m_hdc ) { ::ReleaseDC(m_hwnd, m_hdc); } }
operator HDC() const { return m_hdc; }
protected:
AutoHDC(HWND hwnd, HDC hdc) : m_hwnd(hwnd), m_hdc(hdc) { }
private:
HWND m_hwnd;
HDC m_hdc;
wxDECLARE_NO_COPY_CLASS(ScreenHDC);
wxDECLARE_NO_COPY_CLASS(AutoHDC);
};
// the same as ScreenHDC but for window DCs (and if HWND is null, then exactly
// the same as it)
class WindowHDC
// create an instance of this class and use it as the HDC for screen, will
// automatically release the DC going out of scope
class ScreenHDC : public AutoHDC
{
public:
WindowHDC() : m_hwnd(nullptr), m_hdc(nullptr) { }
WindowHDC(HWND hwnd) { m_hdc = ::GetDC(m_hwnd = hwnd); }
~WindowHDC() { if ( m_hdc ) { ::ReleaseDC(m_hwnd, m_hdc); } }
ScreenHDC() : AutoHDC(nullptr, ::GetDC(nullptr)) { }
};
operator HDC() const { return m_hdc; }
// the same as ScreenHDC but for client part of the window (if HWND is null,
// then it's exactly the same as ScreenHDC)
class ClientHDC : public AutoHDC
{
public:
ClientHDC() : AutoHDC(nullptr, nullptr) { }
explicit ClientHDC(HWND hwnd) : AutoHDC(hwnd, ::GetDC(hwnd)) { }
};
private:
HWND m_hwnd;
HDC m_hdc;
wxDECLARE_NO_COPY_CLASS(WindowHDC);
// same as ClientHDC but includes the non-client part of the window
class WindowHDC : public AutoHDC
{
public:
explicit WindowHDC(HWND hwnd) : AutoHDC(hwnd, ::GetWindowDC(hwnd)) { }
};
// the same as ScreenHDC but for memory DCs: creates the HDC compatible with

View file

@ -12,6 +12,7 @@
#include "wx/itemattr.h"
#include "wx/msw/uxtheme.h"
#include "wx/msw/wrapcctl.h"
namespace wxMSWImpl
@ -56,4 +57,53 @@ private:
} // namespace wxMSWImpl
// ----------------------------------------------------------------------------
// wxMSWHeaderCtrlCustomDraw: custom draw helper for header control
// ----------------------------------------------------------------------------
class wxMSWHeaderCtrlCustomDraw : public wxMSWImpl::CustomDraw
{
public:
wxMSWHeaderCtrlCustomDraw()
{
}
// Set the colours to the ones used by "Header" theme.
//
// This is required, for unknown reasons, when using dark mode, because
// enabling it for the header control changes the background, but not the
// foreground, making the text completely unreadable.
//
// If this is ever fixed in later Windows versions, this function wouldn't
// need to be called any more.
void UseHeaderThemeColors(HWND hwndHdr)
{
wxUxThemeHandle theme{hwndHdr, L"Header"};
m_attr.SetTextColour(theme.GetColour(HP_HEADERITEM, TMT_TEXTCOLOR));
// Note that TMT_FILLCOLOR doesn't seem to exist in this theme but the
// correct background colour is already used in "ItemsView" theme by
// default anyhow.
}
// Make this field public to let the control update it directly when its
// attributes change.
wxItemAttr m_attr;
private:
virtual bool HasCustomDrawnItems() const override
{
// We only exist if the header does need to be custom drawn.
return true;
}
virtual const wxItemAttr*
GetItemAttr(DWORD_PTR WXUNUSED(dwItemSpec)) const override
{
// We use the same attribute for all items for now.
return &m_attr;
}
};
#endif // _WX_MSW_CUSTOMDRAW_H_

View file

@ -0,0 +1,61 @@
///////////////////////////////////////////////////////////////////////////////
// Name: wx/msw/private/darkmode.h
// Purpose: Dark mode support in wxMSW
// Author: Vadim Zeitlin
// Created: 2022-06-25
// Copyright: (c) 2022 Vadim Zeitlin <vadim@wxwidgets.org>
// Licence: wxWindows licence
///////////////////////////////////////////////////////////////////////////////
#ifndef _WX_MSW_PRIVATE_DARKMODE_H_
#define _WX_MSW_PRIVATE_DARKMODE_H_
#include "wx/settings.h"
namespace wxMSWDarkMode
{
// Return true if the application is using dark mode: note that this will only
// be the case if wxApp::MSWEnableDarkMode() was called.
bool IsActive();
// Enable dark mode for the given TLW if appropriate.
void EnableForTLW(HWND hwnd);
// Set dark theme for the given (child) window if appropriate.
//
// Optional theme name and ID can be specified if something other than the
// default "Explorer" should be used. If both theme name and theme ID are null,
// no theme is set.
void AllowForWindow(HWND hwnd,
const wchar_t* themeName = L"Explorer",
const wchar_t* themeId = nullptr);
// Return the colour value appropriate for dark mode if it's used or an invalid
// colour if it isn't.
wxColour GetColour(wxSystemColour index);
// Return the background brush to be used by default in dark mode.
HBRUSH GetBackgroundBrush();
// If dark mode is active, paint the given window using inverted colours.
// Otherwise just return false without doing anything.
//
// This can only be called from WM_PAINT handler for a native control and
// assumes that this control handles WPARAM argument of WM_PAINT as HDC to
// paint on.
bool PaintIfNecessary(HWND hwnd, WXWNDPROC defWndProc);
// If dark mode is active and if the message is one of those used for menu
// drawing, process it and return true, otherwise just return false without
// doing anything.
bool
HandleMenuMessage(WXLRESULT* result,
wxWindow* w,
WXUINT nMsg,
WXWPARAM wParam,
WXLPARAM lParam);
} // namespace wxMSWDarkMode
#endif // _WX_MSW_PRIVATE_DARKMODE_H_

View file

@ -58,6 +58,8 @@ protected:
virtual wxBorder GetDefaultBorder() const override { return wxBORDER_NONE; }
virtual wxSize DoGetBestSize() const override;
virtual bool MSWGetDarkModeSupport(MSWDarkModeSupport& support) const override;
// Implement wxMSWOwnerDrawnButtonBase methods.
virtual int MSWGetButtonStyle() const override;
virtual void MSWOnButtonResetOwnerDrawn() override;

View file

@ -61,6 +61,8 @@ public:
virtual void SetIncrement(int value) override;
virtual int GetIncrement() const override;
virtual bool MSWShouldUseAutoDarkMode() const override;
protected:
virtual wxSize DoGetBestSize() const override;

View file

@ -79,6 +79,8 @@ public:
protected:
virtual wxWindowList GetCompositeWindowParts() const override;
virtual bool MSWGetDarkModeSupport(MSWDarkModeSupport& support) const override;
// return the region with all the windows inside this static box excluded
WXHRGN MSWGetRegionWithoutChildren();

View file

@ -69,6 +69,8 @@ protected:
virtual bool MSWOnNotify(int idCtrl, WXLPARAM lParam, WXLPARAM* result) override;
#endif
virtual bool MSWGetDarkModeSupport(MSWDarkModeSupport& support) const override;
// implementation of the public SetStatusWidths()
void MSWUpdateFieldsWidths();

View file

@ -101,6 +101,9 @@ protected:
// common part of all ctors
void Init();
virtual bool MSWGetDarkModeSupport(MSWDarkModeSupport& support) const override;
virtual int MSWGetToolTipMessage() const override;
// create the native toolbar control
bool MSWCreateToolbar(const wxPoint& pos, const wxSize& size);

View file

@ -212,6 +212,8 @@ protected:
virtual bool MSWShouldSetDefaultFont() const override { return false; }
virtual int MSWGetToolTipMessage() const override;
virtual void OnImagesChanged() override;
// SetImageList helper

View file

@ -169,9 +169,14 @@ WXDLLIMPEXP_CORE bool wxUxThemeIsActive();
class wxUxThemeHandle
{
public:
wxUxThemeHandle(const wxWindow *win, const wchar_t *classes)
wxUxThemeHandle(HWND hwnd, const wchar_t *classes) :
m_hTheme{::OpenThemeData(hwnd, classes)}
{
}
wxUxThemeHandle(const wxWindow *win, const wchar_t *classes) :
wxUxThemeHandle(GetHwndOf(win), classes)
{
m_hTheme = (HTHEME)::OpenThemeData(GetHwndOf(win), classes);
}
operator HTHEME() const { return m_hTheme; }
@ -184,8 +189,14 @@ public:
}
}
// Return the colour for the given part, property and state.
//
// Note that the order of arguments here is _not_ the same as for
// GetThemeColor() because we want to default the state.
wxColour GetColour(int part, int prop, int state = 0) const;
private:
HTHEME m_hTheme;
const HTHEME m_hTheme;
wxDECLARE_NO_COPY_CLASS(wxUxThemeHandle);
};

View file

@ -211,6 +211,13 @@ public:
void OnPaint(wxPaintEvent& event);
// Override this to return true to automatically invert the window colours
// in dark mode.
//
// This doesn't result in visually great results, but may still be better
// than using light background.
virtual bool MSWShouldUseAutoDarkMode() const { return false; }
public:
// Windows subclassing
void SubclassWin(WXHWND hWnd);
@ -475,21 +482,6 @@ public:
// querying the parent windows via MSWGetBgBrushForChild() recursively
WXHBRUSH MSWGetBgBrush(WXHDC hDC);
enum MSWThemeColour
{
ThemeColourText = 0,
ThemeColourBackground,
ThemeColourBorder
};
// returns a specific theme colour, or if that is not possible then
// wxSystemSettings::GetColour(fallback)
wxColour MSWGetThemeColour(const wchar_t *themeName,
int themePart,
int themeState,
MSWThemeColour themeColour,
wxSystemColour fallback) const;
// gives the parent the possibility to draw its children background, e.g.
// this is used by wxNotebook to do it using DrawThemeBackground()
//

View file

@ -172,8 +172,15 @@ public:
// Return the name if available or empty string otherwise.
wxString GetName() const;
// Return true if the current system there is explicitly recognized as
// being a dark theme or if the default window background is dark.
// Return true if the applications on this system use dark theme by default.
bool AreAppsDark() const;
// Return true if the system elements use dark theme: this can only differ
// from AreAppsDark() under MSW where it's possible to configure the system
// (taskbar etc) to use a different theme.
bool IsSystemDark() const;
// Return true if this application itself uses a dark theme.
bool IsDark() const;
// Return true if the background is darker than foreground. This is used by
@ -221,6 +228,12 @@ public:
// get the object describing the current system appearance
static wxSystemAppearance GetAppearance();
// get the first colour for light appearance and the second one for the dark
static wxColour SelectLightDark(wxColour colForLight, wxColour colForDark)
{
return GetAppearance().IsDark() ? colForDark : colForLight;
}
// return true if the port has certain feature
static bool HasFeature(wxSystemFeature index);
};

View file

@ -1181,6 +1181,48 @@ public:
///@}
/**
@name MSW-specific functions
*/
//@{
/**
Enable experimental dark mode support for MSW applications.
This function uses @e undocumented, and unsupported by Microsoft,
functions to enable dark mode support for the desktop applications
under Windows 10 20H1 or later (including all Windows 11 versions).
Note that dark mode can also be enabled by setting the "msw.dark-mode"
@ref wxSystemOptions "system option" via an environment variable from
outside the application.
Known limitations of dark mode support include:
- wxMessageBox() contents doesn't use dark mode. Consider using
wxGenericMessageDialog if dark mode support is more important
than using the native dialog.
- wxDatePickerCtrl and wxTimePickerCtrl 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
dark mode by default. Otherwise dark mode is only used if it is the
default mode for the applications on the current system.
@return @true if dark mode support was enabled, @false if it couldn't
be done, most likely because the system doesn't support dark mode.
@onlyfor{wxmsw}
@since 3.3.0
*/
bool MSWEnableDarkMode(int flags = 0);
//@}
};

View file

@ -271,6 +271,22 @@ enum wxSystemScreenType
class wxSystemAppearance
{
public:
/**
Return true if the applications on this system use dark theme by
default.
This function returns @true if dark mode is enabled for the
applications system-wide, even if it's not enabled for this particular
application.
Note that for non-MSW platforms this is currently the same as IsDark(),
but under MSW these two functions can return different values as dark
mode requires to opt-in into it specifically.
@since 3.3.0
*/
bool AreAppsDark() const;
/**
Return the name if available or empty string otherwise.
@ -286,9 +302,28 @@ public:
This method should be used to check whether custom colours more
appropriate for the default (light) or dark appearance should be used.
Note that this checks the appearance of the current application and not
the other applications on the system, so under MSW, for example, it
will return @false even if dark mode is used system-wide unless the
application opted in using dark mode using wxApp::MSWEnableDarkMode().
You can use IsSystemDark() or AreAppsDark() to check if the system is
using dark mode by default.
*/
bool IsDark() const;
/**
Return true if the system UI uses dark theme.
This is the same as AreAppsDark() on the non-MSW platforms, but can be
different from the other function under MSW as it is possible to
configure default "Windows mode" and "app mode" to use different colour
schemes under Windows.
@since 3.3.0
*/
bool IsSystemDark() const;
/**
Return true if the default window background is significantly darker
than foreground.
@ -388,5 +423,20 @@ public:
See the ::wxSystemFeature enum values.
*/
static bool HasFeature(wxSystemFeature index);
/**
Select one of the two colours depending on whether light or dark mode
is used.
This is just a convenient helper using wxSystemAppearance::IsDark() to
select between the two colours.
@param colForLight Colour returned when using light appearance.
@param colForDark Colour returned when using dark appearance, as
detected by wxSystemAppearance::IsDark().
@since 3.3.0
*/
static wxColour SelectLightDark(wxColour colForLight, wxColour colForDark);
};

View file

@ -76,6 +76,11 @@
If set to 1, disables the use of composited, i.e. double-buffered,
windows by default in wxMSW. This is not recommended, but can be useful
for debugging or working around redraw problems in the existing code.
@flag{msw.dark-mode}
If set to 1, enable experimental support of dark mode if the system is
using it, i.e. this has the same effect as calling
wxApp::MSWEnableDarkMode(). If set to 2, use dark mode unconditionally,
as if this function were called with wxApp::DarkMode_Always argument.
@endFlagTable

View file

@ -524,6 +524,13 @@ MyCanvas::MyCanvas(MyFrame *parent)
m_useBuffer = false;
m_showBBox = false;
m_sizeDIP = wxSize(0, 0);
Bind(wxEVT_SYS_COLOUR_CHANGED, [this](wxSysColourChangedEvent& event) {
event.Skip();
if ( m_show == File_ShowSystemColours )
Refresh();
});
}
void MyCanvas::DrawTestBrushes(wxDC& dc)
@ -1676,21 +1683,31 @@ void MyCanvas::DrawSystemColours(wxDC& dc)
wxCoord x(FromDIP(10));
wxRect r(textSize.GetWidth() + x, x, dc.FromDIP(100), lineHeight);
wxString title = "System colours";
dc.DrawText("System colours", x, r.y);
r.y += 2*lineHeight;
const wxSystemAppearance appearance = wxSystemSettings::GetAppearance();
const wxString appearanceName = appearance.GetName();
if ( !appearanceName.empty() )
title += wxString::Format(" for \"%s\"", appearanceName);
if ( appearance.IsDark() )
title += " (using dark system theme)";
dc.DrawText(title, x, r.y);
r.y += 2*lineHeight;
dc.DrawText(wxString::Format("Window background is %s",
appearance.IsUsingDarkBackground() ? "dark"
: "light"),
x, r.y);
r.y += 3*lineHeight;
{
dc.DrawText(wxString::Format("System appearance: %s", appearanceName),
x, r.y);
r.y += lineHeight;
}
auto const showDarkOrLight = [&](const char* what, bool dark)
{
dc.DrawText(wxString::Format("%s: %s", what, dark ? "dark" : "light"),
x, r.y);
r.y += 1.5*lineHeight;
};
showDarkOrLight("System", appearance.IsSystemDark());
showDarkOrLight("App default", appearance.AreAppsDark());
showDarkOrLight("Current app", appearance.IsDark());
showDarkOrLight("Background", appearance.IsUsingDarkBackground());
r.y += lineHeight;
dc.SetPen(*wxTRANSPARENT_PEN);

View file

@ -68,6 +68,21 @@ void wxSystemSettings::SetScreenType( wxSystemScreenType screen )
// Trivial wxSystemAppearance implementation
// ----------------------------------------------------------------------------
// wxMSW has its own implementation of these functions.
#if !defined(__WXMSW__)
bool wxSystemAppearance::AreAppsDark() const
{
return IsDark();
}
bool wxSystemAppearance::IsSystemDark() const
{
return IsDark();
}
#endif // !__WXMSW__
#if !defined(__WXOSX__)
wxString wxSystemAppearance::GetName() const

View file

@ -25,6 +25,7 @@
#ifndef WX_PRECOMP
#include "wx/dcclient.h"
#include "wx/settings.h"
#include "wx/timer.h"
#endif // WX_PRECOMP
@ -147,6 +148,9 @@ private:
// the next position every time.
gc->Rotate(m_frame*angle);
// Choose a contrasting background colour.
wxColour colBg = wxSystemSettings::SelectLightDark(*wxBLACK, *wxWHITE);
const bool isEnabled = m_win->IsThisEnabled();
for ( int n = 0; n < NUM_DOTS; n++ )
{
@ -159,7 +163,8 @@ private:
// it in 0..wxALPHA_OPAQUE range.
const int opacity = opacityIndex*(wxALPHA_OPAQUE + 1)/NUM_DOTS - 1;
gc->SetBrush(wxBrush(wxColour(0, 0, 0, opacity)));
colBg.Set(colBg.Red(), colBg.Green(), colBg.Blue(), opacity);
gc->SetBrush(colBg);
gc->FillPath(path);
gc->Rotate(angle);

View file

@ -22,6 +22,10 @@
#if wxUSE_FONTPICKERCTRL
#ifndef WX_PRECOMP
#include "wx/settings.h"
#endif // WX_PRECOMP
#include "wx/fontpicker.h"
#include "wx/fontdlg.h"
@ -68,7 +72,7 @@ bool wxGenericFontButton::Create( wxWindow *parent, wxWindowID id,
void wxGenericFontButton::InitFontData()
{
m_data.SetAllowSymbols(true);
m_data.SetColour(*wxBLACK);
m_data.SetColour(wxSystemSettings::GetColour(wxSYS_COLOUR_BTNTEXT));
m_data.EnableEffects(true);
}

View file

@ -541,7 +541,7 @@ protected:
#endif
#ifdef __WXMSW__
cairo_surface_t* m_mswSurface;
WindowHDC m_mswWindowHDC;
ClientHDC m_mswWindowHDC;
int m_mswStateSavedDC;
#endif
#ifdef __WXGTK__

View file

@ -3031,11 +3031,7 @@ void wxGrid::Init()
m_cellHighlightColour = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
m_cellHighlightPenWidth = 2;
m_cellHighlightROPenWidth = 1;
if ( wxSystemSettings::GetAppearance().IsDark() )
m_gridFrozenBorderColour = *wxWHITE;
else
m_gridFrozenBorderColour = *wxBLACK;
m_gridFrozenBorderColour = wxSystemSettings::SelectLightDark(*wxBLACK, *wxWHITE);
m_gridFrozenBorderPenWidth = 2;
m_canDragRowMove = false;

View file

@ -96,21 +96,15 @@ public:
{
titleFont.MakeLarger();
COLORREF c;
if ( FAILED(::GetThemeColor
(
wxUxThemeHandle(parent, L"TOOLTIP"),
TTP_BALLOONTITLE,
0,
TMT_TEXTCOLOR,
&c
)) )
wxUxThemeHandle theme(parent, L"TOOLTIP");
wxColour c = theme.GetColour(TTP_BALLOONTITLE, TMT_TEXTCOLOR);
if ( !c.IsOk() )
{
// Use the standard value of this colour as fallback.
c = 0x993300;
c.Set(0x00, 0x33, 0x99);
}
labelTitle->SetForegroundColour(wxRGBToColour(c));
labelTitle->SetForegroundColour(c);
}
else
#endif // HAVE_MSW_THEME
@ -175,30 +169,14 @@ public:
{
wxUxThemeHandle hTheme(GetParent(), L"TOOLTIP");
COLORREF c1, c2;
if ( FAILED(::GetThemeColor
(
hTheme,
TTP_BALLOONTITLE,
0,
TMT_GRADIENTCOLOR1,
&c1
)) ||
FAILED(::GetThemeColor
(
hTheme,
TTP_BALLOONTITLE,
0,
TMT_GRADIENTCOLOR2,
&c2
)) )
{
c1 = 0xffffff;
c2 = 0xf0e5e4;
}
colStart = hTheme.GetColour(TTP_BALLOONTITLE, TMT_GRADIENTCOLOR1);
if ( !colStart.IsOk() )
colStart = wxSystemSettings::SelectLightDark(*wxWHITE, *wxBLACK);
colStart = wxRGBToColour(c1);
colEnd = wxRGBToColour(c2);
colEnd = hTheme.GetColour(TTP_BALLOONTITLE, TMT_GRADIENTCOLOR2);
if ( !colEnd.IsOk() )
colEnd = wxSystemSettings::SelectLightDark({0xe4, 0xe5, 0xf0},
{0x40, 0x40, 0x20});
}
else
#endif // HAVE_MSW_THEME

View file

@ -56,6 +56,7 @@
#include "wx/msw/private.h"
#include "wx/msw/dc.h"
#include "wx/msw/ole/oleutils.h"
#include "wx/msw/private/darkmode.h"
#include "wx/msw/private/timer.h"
#if wxUSE_TOOLTIPS
@ -633,6 +634,14 @@ bool wxApp::Initialize(int& argc_, wxChar **argv_)
wxSetKeyboardHook(true);
// this is useful to allow users to enable dark mode for the applications
// not enabling it themselves by setting the corresponding environment
// variable
if ( const int darkMode = wxSystemOptions::GetOptionInt("msw.dark-mode") )
{
MSWEnableDarkMode(darkMode > 1 ? DarkMode_Always : DarkMode_Auto);
}
callBaseCleanup.Dismiss();
return true;
@ -656,6 +665,15 @@ const wxChar *wxApp::GetRegisteredClassName(const wxChar *name,
return gs_regClassesInfo[n].GetRequestedName(flags);
}
// In dark mode, use the dark background brush instead of specified colour
// which would result in light background.
HBRUSH hbrBackground;
if ( wxMSWDarkMode::IsActive() )
hbrBackground = wxMSWDarkMode::GetBackgroundBrush();
else
hbrBackground = (HBRUSH)wxUIntToPtr(bgBrushCol + 1);
// we need to register this class
WNDCLASS wndclass;
wxZeroMemory(wndclass);
@ -663,7 +681,7 @@ const wxChar *wxApp::GetRegisteredClassName(const wxChar *name,
wndclass.lpfnWndProc = (WNDPROC)wxWndProc;
wndclass.hInstance = wxGetInstance();
wndclass.hCursor = ::LoadCursor(nullptr, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)wxUIntToPtr(bgBrushCol + 1);
wndclass.hbrBackground = hbrBackground;
wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS | extraStyles;

View file

@ -88,6 +88,18 @@ WXDWORD wxCheckBox::MSWGetStyle(long style, WXDWORD *exstyle) const
return msStyle;
}
bool wxCheckBox::MSWGetDarkModeSupport(MSWDarkModeSupport& support) const
{
// Just as radio buttons, check boxes have some dark theme support, but we
// still need to change their foreground manually to make it readable in
// dark mode.
wxCheckBoxBase::MSWGetDarkModeSupport(support);
support.setForeground = true;
return true;
}
// ----------------------------------------------------------------------------
// wxCheckBox geometry
// ----------------------------------------------------------------------------

View file

@ -35,6 +35,9 @@
#include "wx/dynlib.h"
#include "wx/msw/private.h"
#include "wx/msw/uxtheme.h"
#include "wx/msw/private/darkmode.h"
// ============================================================================
// implementation
@ -193,15 +196,31 @@ wxChoice::GetClassDefaultAttributes(wxWindowVariant WXUNUSED(variant))
attrs.colFg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
// NB: use EDIT, not COMBOBOX (the latter works in XP but not Vista)
attrs.colBg = wnd->MSWGetThemeColour(L"EDIT",
EP_EDITTEXT,
ETS_NORMAL,
ThemeColourBackground,
wxSYS_COLOUR_WINDOW);
wxUxThemeHandle hTheme(wnd, L"EDIT");
attrs.colBg = hTheme.GetColour(EP_EDITTEXT, TMT_FILLCOLOR, ETS_NORMAL);
if ( !attrs.colBg.IsOk() )
attrs.colBg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
return attrs;
}
bool wxChoice::MSWGetDarkModeSupport(MSWDarkModeSupport& support) const
{
support.themeName = L"CFD";
// It is slightly improper to do this in a const function, but as we know
// that this will only be called when we're using the dark mode, we also
// use it to enable it for the drop down list, if any, to ensure that it
// uses dark scrollbars.
WinStruct<COMBOBOXINFO> info;
if ( ::GetComboBoxInfo(GetHwnd(), &info) && info.hwndList )
{
wxMSWDarkMode::AllowForWindow(info.hwndList);
}
return true;
}
wxChoice::~wxChoice()
{
Clear();

View file

@ -40,6 +40,7 @@
#include "wx/msw/uxtheme.h"
#include "wx/msw/dc.h" // for wxDCTemp
#include "wx/msw/ownerdrawnbutton.h"
#include "wx/msw/private/darkmode.h"
#include "wx/msw/private/winstyle.h"
// ----------------------------------------------------------------------------
@ -137,6 +138,22 @@ bool wxControl::MSWCreateControl(const wxChar *classname,
return false;
}
MSWDarkModeSupport support;
if ( wxMSWDarkMode::IsActive() && MSWGetDarkModeSupport(support) )
{
wxMSWDarkMode::AllowForWindow(m_hWnd, support.themeName, support.themeId);
if ( support.setForeground )
SetForegroundColour(wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOXTEXT));
if ( const int msgTT = MSWGetToolTipMessage() )
{
const HWND hwndTT = (HWND)::SendMessage(GetHwnd(), msgTT, 0, 0);
if ( ::IsWindow(hwndTT) )
wxMSWDarkMode::AllowForWindow(hwndTT);
}
}
// saving the label in m_labelOrig to return it verbatim
// later in GetLabel()
m_labelOrig = label;
@ -183,6 +200,16 @@ bool wxControl::MSWCreateControl(const wxChar *classname,
return true;
}
bool wxControl::MSWGetDarkModeSupport(MSWDarkModeSupport& support) const
{
// This theme works for a few controls (buttons, texts, comboboxes) and
// doesn't seem to do any harm for those that don't support it, so use it
// by default.
support.themeName = L"Explorer";
return true;
}
// ----------------------------------------------------------------------------
// various accessors
// ----------------------------------------------------------------------------
@ -291,6 +318,8 @@ WXHBRUSH wxControl::DoMSWControlColor(WXHDC pDC, wxColour colBg, WXHWND hWnd)
{
HDC hdc = (HDC)pDC;
wxColour colFg;
WXHBRUSH hbr = 0;
if ( !colBg.IsOk() )
{
@ -325,11 +354,22 @@ WXHBRUSH wxControl::DoMSWControlColor(WXHDC pDC, wxColour colBg, WXHWND hWnd)
if ( win )
hbr = win->MSWGetBgBrush(pDC);
// if the control doesn't have any bg colour, foreground colour will be
// ignored as the return value would be 0 -- so forcefully give it a
// non default background brush in this case
if ( !hbr && m_hasFgCol )
colBg = GetBackgroundColour();
if ( !hbr )
{
if ( wxMSWDarkMode::IsActive() )
{
colBg = wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOX);
if ( !m_hasFgCol )
colFg = wxSystemSettings::GetColour(wxSYS_COLOUR_LISTBOXTEXT);
}
// if the control doesn't have any bg colour, foreground colour will be
// ignored as the return value would be 0 -- so forcefully give it a
// non default background brush in this case
else if ( m_hasFgCol )
{
colBg = GetBackgroundColour();
}
}
}
// use the background colour override if a valid colour is given: this is
@ -347,7 +387,9 @@ WXHBRUSH wxControl::DoMSWControlColor(WXHDC pDC, wxColour colBg, WXHWND hWnd)
// default just the simple black is used
if ( hbr )
{
::SetTextColor(hdc, wxColourToRGB(GetForegroundColour()));
if ( !colFg.IsOk() )
colFg = GetForegroundColour();
::SetTextColor(hdc, wxColourToRGB(colFg));
}
// finally also set the background colour for text drawing: without this,

663
src/msw/darkmode.cpp Normal file
View file

@ -0,0 +1,663 @@
///////////////////////////////////////////////////////////////////////////////
// Name: src/msw/darkmode.cpp
// Purpose: Support for dark mode in wxMSW
// Author: Vadim Zeitlin
// Created: 2022-06-24
// Copyright: (c) 2022 Vadim Zeitlin <vadim@wxwidgets.org>
// Licence: wxWindows licence
///////////////////////////////////////////////////////////////////////////////
/*
The code in this file is based on the following sources:
- win32-darkmode by Richard Yu (https://github.com/ysc3839/win32-darkmode)
- UAH menu by adzm (https://github.com/adzm/win32-custom-menubar-aero-theme)
*/
// ============================================================================
// declarations
// ============================================================================
// ----------------------------------------------------------------------------
// headers
// ----------------------------------------------------------------------------
// for compilers that support precompilation, includes "wx.h".
#include "wx/wxprec.h"
// Allow predefining this as 0 to disable dark mode support completely.
#ifndef wxUSE_DARK_MODE
// Otherwise enable it by default.
#define wxUSE_DARK_MODE 1
#endif
#if wxUSE_DARK_MODE
#ifndef WX_PRECOMP
#include "wx/app.h"
#include "wx/bitmap.h"
#include "wx/dcmemory.h"
#include "wx/image.h"
#include "wx/log.h"
#endif // WX_PRECOMP
#include "wx/dynlib.h"
#include "wx/module.h"
#include "wx/msw/dc.h"
#include "wx/msw/uxtheme.h"
static const char* TRACE_DARKMODE = "msw-darkmode";
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
namespace
{
// Constants for use with SetPreferredAppMode().
enum PreferredAppMode
{
AppMode_Default,
AppMode_AllowDark,
AppMode_ForceDark,
AppMode_ForceLight
};
PreferredAppMode gs_appMode = AppMode_Default;
template <typename T>
bool TryLoadByOrd(T& func, const wxDynamicLibrary& lib, int ordinal)
{
func = (T)::GetProcAddress(lib.GetLibHandle(), MAKEINTRESOURCEA(ordinal));
if ( !func )
{
wxLogTrace(TRACE_DARKMODE,
"Required function with ordinal %d not found", ordinal);
return false;
}
return true;
}
} // anonymous namespace
// ============================================================================
// implementation
// ============================================================================
namespace wxMSWImpl
{
// Global pointers of the functions we use: they're not only undocumented, but
// don't appear in the SDK headers at all.
BOOL (WINAPI *ShouldAppsUseDarkMode)() = nullptr;
BOOL (WINAPI *AllowDarkModeForWindow)(HWND hwnd, BOOL allow) = nullptr;
DWORD (WINAPI *SetPreferredAppMode)(DWORD) = nullptr;
bool InitDarkMode()
{
// Note: for simplicity, we support dark mode only in Windows 10 v2004
// ("20H1", build number 19041) and later, even if, in principle, it could
// be supported as far back as v1809 (build 17763) -- but very few people
// must still use it by now and so it just doesn't seem to be worth it.
if ( !wxCheckOsVersion(10, 0, 19041) )
{
wxLogTrace(TRACE_DARKMODE, "Unsupported due to OS version");
return false;
}
wxLoadedDLL dllUxTheme(wxS("uxtheme.dll"));
// These functions are not only undocumented but are not even exported by
// name, and have to be resolved using their ordinals.
return TryLoadByOrd(ShouldAppsUseDarkMode, dllUxTheme, 132) &&
TryLoadByOrd(AllowDarkModeForWindow, dllUxTheme, 133) &&
TryLoadByOrd(SetPreferredAppMode, dllUxTheme, 135);
}
// This function is only used in this file as it's more clear than using
// IsActive() without the namespace name -- but in the rest of our code, it's
// IsActive() which is more clear.
bool ShouldUseDarkMode()
{
switch ( gs_appMode )
{
case AppMode_Default:
// Dark mode support not enabled, don't try using dark mode.
return false;
case AppMode_AllowDark:
// Follow the global setting.
return wxMSWImpl::ShouldAppsUseDarkMode();
case AppMode_ForceDark:
return true;
case AppMode_ForceLight:
return false;
}
wxFAIL_MSG( "unreachable" );
return false;
}
} // namespace wxMSWImpl
// ----------------------------------------------------------------------------
// Module keeping dark mode-related data
// ----------------------------------------------------------------------------
namespace
{
// This function is documented, but we still load it dynamically to avoid
// having to link with dwmapi.lib.
typedef HRESULT
(WINAPI *DwmSetWindowAttribute_t)(HWND, DWORD, const void*, DWORD);
} // anonymous namespace
class wxDarkModeModule : public wxModule
{
public:
virtual bool OnInit() override { return true; }
virtual void OnExit() override
{
ms_pfnDwmSetWindowAttribute = (DwmSetWindowAttribute_t)-1;
ms_dllDWM.Unload();
}
static DwmSetWindowAttribute_t GetDwmSetWindowAttribute()
{
if ( ms_pfnDwmSetWindowAttribute == (DwmSetWindowAttribute_t)-1 )
{
ms_dllDWM.Load(wxS("dwmapi.dll"), wxDL_VERBATIM | wxDL_QUIET);
wxDL_INIT_FUNC(ms_pfn, DwmSetWindowAttribute, ms_dllDWM);
}
return ms_pfnDwmSetWindowAttribute;
}
private:
static wxDynamicLibrary ms_dllDWM;
static DwmSetWindowAttribute_t ms_pfnDwmSetWindowAttribute;
wxDECLARE_DYNAMIC_CLASS(wxDarkModeModule);
};
wxIMPLEMENT_DYNAMIC_CLASS(wxDarkModeModule, wxModule);
wxDynamicLibrary wxDarkModeModule::ms_dllDWM;
DwmSetWindowAttribute_t
wxDarkModeModule::ms_pfnDwmSetWindowAttribute = (DwmSetWindowAttribute_t)-1;
// ----------------------------------------------------------------------------
// Public API
// ----------------------------------------------------------------------------
bool wxApp::MSWEnableDarkMode(int flags)
{
if ( !wxMSWImpl::InitDarkMode() )
return false;
const PreferredAppMode mode = flags & DarkMode_Always ? AppMode_ForceDark
: AppMode_AllowDark;
const DWORD rc = wxMSWImpl::SetPreferredAppMode(mode);
// It's supposed to return the old mode normally.
if ( rc != static_cast<DWORD>(gs_appMode) )
{
wxLogTrace(TRACE_DARKMODE,
"SetPreferredAppMode(%d) unexpectedly returned %d",
mode, rc);
}
gs_appMode = mode;
return true;
}
// ----------------------------------------------------------------------------
// Supporting functions for the rest of wxMSW code
// ----------------------------------------------------------------------------
namespace wxMSWDarkMode
{
bool IsActive()
{
return wxMSWImpl::ShouldUseDarkMode();
}
void EnableForTLW(HWND hwnd)
{
// Nothing to do, dark mode support not enabled or dark mode is not used.
if ( !wxMSWImpl::ShouldUseDarkMode() )
return;
BOOL useDarkMode = TRUE;
HRESULT hr = wxDarkModeModule::GetDwmSetWindowAttribute()
(
hwnd,
DWMWA_USE_IMMERSIVE_DARK_MODE,
&useDarkMode,
sizeof(useDarkMode)
);
if ( FAILED(hr) )
wxLogApiError("DwmSetWindowAttribute(USE_IMMERSIVE_DARK_MODE)", hr);
wxMSWImpl::AllowDarkModeForWindow(hwnd, TRUE);
}
void AllowForWindow(HWND hwnd, const wchar_t* themeName, const wchar_t* themeId)
{
if ( !wxMSWImpl::ShouldUseDarkMode() )
return;
if ( wxMSWImpl::AllowDarkModeForWindow(hwnd, TRUE) )
wxLogTrace(TRACE_DARKMODE, "Allow dark mode for %p failed", hwnd);
if ( themeName || themeId )
{
HRESULT hr = ::SetWindowTheme(hwnd, themeName, themeId);
if ( FAILED(hr) )
{
wxLogApiError(wxString::Format("SetWindowTheme(%p, %s, %s)",
hwnd, themeName, themeId), hr);
}
}
}
wxColour GetColour(wxSystemColour index)
{
// This is not great at all, but better than using light mode colours that
// are not appropriate for the dark mode.
switch ( index )
{
case wxSYS_COLOUR_BTNSHADOW:
return *wxBLACK;
case wxSYS_COLOUR_ACTIVECAPTION:
case wxSYS_COLOUR_APPWORKSPACE:
case wxSYS_COLOUR_INFOBK:
case wxSYS_COLOUR_LISTBOX:
case wxSYS_COLOUR_WINDOW:
case wxSYS_COLOUR_BTNFACE:
return wxColour(0x202020);
case wxSYS_COLOUR_BTNTEXT:
case wxSYS_COLOUR_CAPTIONTEXT:
case wxSYS_COLOUR_HIGHLIGHTTEXT:
case wxSYS_COLOUR_INFOTEXT:
case wxSYS_COLOUR_LISTBOXHIGHLIGHTTEXT:
case wxSYS_COLOUR_LISTBOXTEXT:
case wxSYS_COLOUR_MENUTEXT:
case wxSYS_COLOUR_WINDOWTEXT:
return wxColour(0xe0e0e0);
case wxSYS_COLOUR_HOTLIGHT:
return wxColour(0x474747);
case wxSYS_COLOUR_SCROLLBAR:
return wxColour(0x4d4d4d);
case wxSYS_COLOUR_INACTIVECAPTION:
case wxSYS_COLOUR_MENU:
return wxColour(0x2b2b2b);
case wxSYS_COLOUR_MENUBAR:
return wxColour(0x626262);
case wxSYS_COLOUR_MENUHILIGHT:
return wxColour(0x353535);
case wxSYS_COLOUR_HIGHLIGHT:
return wxColour(0x777777);
case wxSYS_COLOUR_INACTIVECAPTIONTEXT:
return wxColour(0xaaaaaa);
case wxSYS_COLOUR_3DDKSHADOW:
case wxSYS_COLOUR_3DLIGHT:
case wxSYS_COLOUR_ACTIVEBORDER:
case wxSYS_COLOUR_BTNHIGHLIGHT:
case wxSYS_COLOUR_DESKTOP:
case wxSYS_COLOUR_GRADIENTACTIVECAPTION:
case wxSYS_COLOUR_GRADIENTINACTIVECAPTION:
case wxSYS_COLOUR_GRAYTEXT:
case wxSYS_COLOUR_INACTIVEBORDER:
case wxSYS_COLOUR_WINDOWFRAME:
return wxColour();
case wxSYS_COLOUR_MAX:
break;
}
wxFAIL_MSG( "unreachable" );
return wxColour();
}
HBRUSH GetBackgroundBrush()
{
wxBrush* const brush =
wxTheBrushList->FindOrCreateBrush(GetColour(wxSYS_COLOUR_WINDOW));
return brush ? GetHbrushOf(*brush) : 0;
}
bool PaintIfNecessary(HWND hwnd, WXWNDPROC defWndProc)
{
#if wxUSE_IMAGE
if ( !wxMSWImpl::ShouldUseDarkMode() )
return false;
const RECT rc = wxGetClientRect(hwnd);
const wxSize size{rc.right - rc.left, rc.bottom - rc.top};
// Don't bother doing anything with the empty windows.
if ( size == wxSize() )
return false;
// Ask the control to paint itself on the given bitmap.
wxBitmap bmp(size);
{
wxMemoryDC mdc(bmp);
WPARAM wparam = (WPARAM)GetHdcOf(mdc);
if ( defWndProc )
::CallWindowProc(defWndProc, hwnd, WM_PAINT, wparam, 0);
else
::DefWindowProc(hwnd, WM_PAINT, wparam, 0);
}
wxImage image = bmp.ConvertToImage();
unsigned char *data = image.GetData();
for ( int i = 0; i < size.x*size.y; ++i, data += 3 )
{
wxImage::RGBValue rgb(data[0], data[1], data[2]);
wxImage::HSVValue hsv = wxImage::RGBtoHSV(rgb);
// There is no really good way to convert normal colours to dark mode,
// but try to do a bit better than just inverting the value because
// this results in colours which are much too dark.
hsv.value = sqrt(1.0 - hsv.value*hsv.value);
rgb = wxImage::HSVtoRGB(hsv);
data[0] = rgb.red;
data[1] = rgb.green;
data[2] = rgb.blue;
}
PAINTSTRUCT ps;
wxDCTemp dc(::BeginPaint(hwnd, &ps), size);
dc.DrawBitmap(wxBitmap(image), 0, 0);
::EndPaint(hwnd, &ps);
return true;
#else // !wxUSE_IMAGE
wxUnusedVar(hwnd);
wxUnusedVar(defWndProc);
return false;
#endif
}
// ----------------------------------------------------------------------------
// Menu bar custom drawing
// ----------------------------------------------------------------------------
namespace wxMSWMenuImpl
{
// Definitions for undocumented messages and structs used in this code.
constexpr int WM_MENUBAR_DRAWMENU = 0x91;
constexpr int WM_MENUBAR_DRAWMENUITEM = 0x92;
// This is passed via LPARAM of WM_MENUBAR_DRAWMENU.
struct MenuBarDrawMenu
{
HMENU hmenu;
HDC hdc;
DWORD dwReserved;
};
struct MenuBarMenuItem
{
int iPosition;
// There are more fields in this (undocumented) struct but we don't
// currently need them, so don't bother with declaring them.
};
struct MenuBarDrawMenuItem
{
DRAWITEMSTRUCT dis;
MenuBarDrawMenu mbdm;
MenuBarMenuItem mbmi;
};
constexpr COLORREF COL_STANDARD = 0xffffff;
constexpr COLORREF COL_DISABLED = 0x6d6d6d;
constexpr COLORREF COL_MENU_HOT = 0x414141;
HBRUSH GetMenuBrush()
{
wxBrush* const brush =
wxTheBrushList->FindOrCreateBrush(GetColour(wxSYS_COLOUR_MENU));
return brush ? GetHbrushOf(*brush) : 0;
}
} // namespace wxMSWMenuImpl
bool
HandleMenuMessage(WXLRESULT* result,
wxWindow* w,
WXUINT nMsg,
WXWPARAM wParam,
WXLPARAM lParam)
{
if ( !wxMSWImpl::ShouldUseDarkMode() )
return false;
using namespace wxMSWMenuImpl;
switch ( nMsg )
{
case WM_MENUBAR_DRAWMENU:
// Erase the menu bar background using custom brush.
if ( auto* const drawMenu = (MenuBarDrawMenu*)lParam )
{
HWND hwnd = GetHwndOf(w);
WinStruct<MENUBARINFO> mbi;
if ( !::GetMenuBarInfo(hwnd, OBJID_MENU, 0, &mbi) )
{
wxLogLastError("GetMenuBarInfo");
break;
}
const RECT rcWindow = wxGetWindowRect(hwnd);
// rcBar is expressed in screen coordinates.
::OffsetRect(&mbi.rcBar, -rcWindow.left, -rcWindow.top);
::FillRect(drawMenu->hdc, &mbi.rcBar, GetMenuBrush());
}
*result = 0;
return true;
case WM_NCPAINT:
case WM_NCACTIVATE:
// Drawing the menu bar background in WM_MENUBAR_DRAWMENU somehow
// leaves a single pixel line unpainted (and increasing the size of
// the rectangle doesn't help, i.e. drawing is clipped to an area
// which is one pixel too small), so we have to draw over it here
// to get rid of it.
{
*result = w->MSWDefWindowProc(nMsg, wParam, lParam);
HWND hwnd = GetHwndOf(w);
WindowHDC hdc(hwnd);
// Create a RECT one pixel above the client area: note that we
// have to use window (and not client) coordinates for this as
// this is outside of the client area of the window.
const RECT rcWindow = wxGetWindowRect(hwnd);
RECT rc = wxGetClientRect(hwnd);
// Convert client coordinates to window ones.
wxMapWindowPoints(hwnd, HWND_DESKTOP, &rc);
::OffsetRect(&rc, -rcWindow.left, -rcWindow.top);
rc.bottom = rc.top;
rc.top--;
::FillRect(hdc, &rc, GetMenuBrush());
}
return true;
case WM_MENUBAR_DRAWMENUITEM:
if ( auto* const drawMenuItem = (MenuBarDrawMenuItem*)lParam )
{
const DRAWITEMSTRUCT& dis = drawMenuItem->dis;
// Just a sanity check.
if ( dis.CtlType != ODT_MENU )
break;
wchar_t buf[256];
WinStruct<MENUITEMINFO> mii;
mii.fMask = MIIM_STRING;
mii.dwTypeData = buf;
mii.cch = sizeof(buf) - 1;
// Note that we need to use the iPosition field of the
// undocumented struct here because DRAWITEMSTRUCT::itemID is
// not initialized in the struct passed to us here, so this is
// the only way to identify the item we're dealing with.
if ( !::GetMenuItemInfo((HMENU)dis.hwndItem,
drawMenuItem->mbmi.iPosition,
TRUE,
&mii) )
break;
const UINT itemState = dis.itemState;
HBRUSH hbr = 0;
int partState = 0;
if ( itemState & ODS_INACTIVE )
{
partState = MBI_DISABLED;
}
else if ( (itemState & ODS_GRAYED) && (itemState & ODS_HOTLIGHT) )
{
partState = MBI_DISABLEDHOT;
}
else if ( itemState & ODS_GRAYED )
{
partState = MBI_DISABLED;
}
else if ( itemState & (ODS_HOTLIGHT | ODS_SELECTED) )
{
partState = MBI_HOT;
auto* const
brush = wxTheBrushList->FindOrCreateBrush(COL_MENU_HOT);
if ( brush )
hbr = GetHbrushOf(*brush);
}
else
{
partState = MBI_NORMAL;
}
RECT* const rcItem = const_cast<RECT*>(&dis.rcItem);
// Don't use DrawThemeBackground() here, as it doesn't use the
// correct colours in the dark mode, at least not when using
// the "Menu" theme.
::FillRect(dis.hDC, &dis.rcItem, hbr ? hbr : GetMenuBrush());
// 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);
textOpts.dwFlags = DTT_TEXTCOLOR;
textOpts.crText = itemState & (ODS_INACTIVE | ODS_GRAYED)
? COL_DISABLED
: COL_STANDARD;
DWORD drawTextFlags = DT_CENTER | DT_SINGLELINE | DT_VCENTER;
if ( itemState & ODS_NOACCEL)
drawTextFlags |= DT_HIDEPREFIX;
wxUxThemeHandle menuTheme(GetHwndOf(w), L"Menu");
::DrawThemeTextEx(menuTheme, dis.hDC, MENU_BARITEM, partState,
buf, mii.cch, drawTextFlags, rcItem,
&textOpts);
}
return true;
}
return false;
}
} // namespace wxMSWDarkMode
#else // !wxUSE_DARK_MODE
bool wxApp::MSWEnableDarkMode(int WXUNUSED(flags))
{
return false;
}
namespace wxMSWDarkMode
{
bool IsActive()
{
return false;
}
void EnableForTLW(HWND WXUNUSED(hwnd))
{
}
void AllowForWindow(HWND WXUNUSED(hwnd), const wchar_t* WXUNUSED(themeClass))
{
}
wxColour GetColour(wxSystemColour WXUNUSED(index))
{
return wxColour();
}
HBRUSH GetBackgroundBrush()
{
return 0;
}
bool PaintIfNecessary(HWND WXUNUSED(hwnd), WXWNDPROC WXUNUSED(defWndProc))
{
return false;
}
bool
HandleMenuMessage(WXLRESULT* WXUNUSED(result),
wxWindow* WXUNUSED(w),
WXUINT WXUNUSED(nMsg),
WXWPARAM WXUNUSED(wParam),
WXLPARAM WXUNUSED(lParam))
{
return false;
}
} // namespace wxMSWDarkMode
#endif // wxUSE_DARK_MODE/!wxUSE_DARK_MODE

View file

@ -36,6 +36,8 @@
#endif
#include "wx/msw/private.h"
#include "wx/msw/private/darkmode.h"
#include "wx/evtloop.h"
#include "wx/scopedptr.h"
@ -209,6 +211,10 @@ void wxDialog::SetWindowStyleFlag(long style)
{
wxDialogBase::SetWindowStyleFlag(style);
// Don't do anything if we're setting the style before creating the dialog.
if ( !GetHwnd() )
return;
if ( HasFlag(wxRESIZE_BORDER) )
CreateGripper();
else
@ -235,6 +241,8 @@ void wxDialog::CreateGripper()
wxGetInstance(),
nullptr
);
wxMSWDarkMode::AllowForWindow((HWND)m_hGripper);
}
}
@ -348,6 +356,13 @@ WXLRESULT wxDialog::MSWWindowProc(WXUINT message, WXWPARAM wParam, WXLPARAM lPar
::InvalidateRect(GetHwnd(), nullptr, false /* erase bg */);
}
break;
case WM_CTLCOLORDLG:
// We need to explicitly set the dark background colour when using
// dark mode, otherwise we'd be using the default light background.
if ( wxMSWDarkMode::IsActive() )
return (WXLRESULT)wxMSWDarkMode::GetBackgroundBrush();
break;
}
if ( !processed )

View file

@ -39,6 +39,7 @@
#endif // WX_PRECOMP
#include "wx/msw/private.h"
#include "wx/msw/private/darkmode.h"
#include "wx/generic/statusbr.h"
@ -837,6 +838,12 @@ WXLRESULT wxFrame::MSWWindowProc(WXUINT message, WXWPARAM wParam, WXLPARAM lPara
WXLRESULT rc = 0;
bool processed = false;
if ( GetMenuBar() &&
wxMSWDarkMode::HandleMenuMessage(&rc, this, message, wParam, lParam) )
{
return rc;
}
switch ( message )
{
case WM_CLOSE:

View file

@ -35,6 +35,7 @@
#include "wx/msw/wrapcctl.h"
#include "wx/msw/private.h"
#include "wx/msw/private/customdraw.h"
#include "wx/msw/private/darkmode.h"
#include "wx/msw/private/winstyle.h"
#ifndef HDM_SETBITMAPMARGIN
@ -49,36 +50,6 @@
// from src/msw/listctrl.cpp
extern int WXDLLIMPEXP_CORE wxMSWGetColumnClicked(NMHDR *nmhdr, POINT *ptClick);
// ----------------------------------------------------------------------------
// wxMSWHeaderCtrlCustomDraw: our custom draw helper
// ----------------------------------------------------------------------------
class wxMSWHeaderCtrlCustomDraw : public wxMSWImpl::CustomDraw
{
public:
wxMSWHeaderCtrlCustomDraw()
{
}
// Make this field public to let wxHeaderCtrl update it directly when its
// attributes change.
wxItemAttr m_attr;
private:
virtual bool HasCustomDrawnItems() const override
{
// We only exist if the header does need to be custom drawn.
return true;
}
virtual const wxItemAttr*
GetItemAttr(DWORD_PTR WXUNUSED(dwItemSpec)) const override
{
// We use the same attribute for all items for now.
return &m_attr;
}
};
// ----------------------------------------------------------------------------
// wxMSWHeaderCtrl: the native header control
// ----------------------------------------------------------------------------
@ -123,6 +94,8 @@ protected:
int sizeFlags = wxSIZE_AUTO) override;
virtual void MSWUpdateFontOnDPIChange(const wxSize& newDPI) override;
virtual bool MSWGetDarkModeSupport(MSWDarkModeSupport& support) const override;
// This function can be used as event handle for wxEVT_DPI_CHANGED event.
void WXHandleDPIChanged(wxDPIChangedEvent& event);
@ -246,6 +219,12 @@ bool wxMSWHeaderCtrl::Create(wxWindow *parent,
if ( !MSWCreateControl(WC_HEADER, wxT(""), pos, size) )
return false;
if ( wxMSWDarkMode::IsActive() )
{
m_customDraw = new wxMSWHeaderCtrlCustomDraw();
m_customDraw->UseHeaderThemeColors(GetHwnd());
}
// special hack for margins when using comctl32.dll v6 or later: the
// default margin is too big and results in label truncation when the
// column width is just about right to show it together with the sort
@ -276,6 +255,13 @@ WXDWORD wxMSWHeaderCtrl::MSWGetStyle(long style, WXDWORD *exstyle) const
return msStyle;
}
bool wxMSWHeaderCtrl::MSWGetDarkModeSupport(MSWDarkModeSupport& support) const
{
support.themeName = L"ItemsView";
return true;
}
wxMSWHeaderCtrl::~wxMSWHeaderCtrl()
{
delete m_imageList;

View file

@ -569,7 +569,7 @@ void wxListBox::SetHorizontalExtent(const wxString& s)
return;
WindowHDC dc(GetHwnd());
ClientHDC dc(GetHwnd());
SelectInHDC selFont(dc, GetHfontOf(GetFont()));
TEXTMETRIC lpTextMetric;

View file

@ -39,7 +39,9 @@
#include "wx/vector.h"
#include "wx/msw/private.h"
#include "wx/msw/uxtheme.h"
#include "wx/msw/private/customdraw.h"
#include "wx/msw/private/darkmode.h"
#include "wx/msw/private/keyboard.h"
// Currently gcc doesn't define NMLVFINDITEM, and DMC only defines
@ -214,34 +216,6 @@ public:
wxDECLARE_NO_COPY_CLASS(wxMSWListItemData);
};
// wxMSWListHeaderCustomDraw: custom draw helper for the header
class wxMSWListHeaderCustomDraw : public wxMSWImpl::CustomDraw
{
public:
wxMSWListHeaderCustomDraw()
{
}
// Make this field public to let wxListCtrl update it directly when its
// header attributes change.
wxItemAttr m_attr;
private:
virtual bool HasCustomDrawnItems() const override
{
// We only exist if the header does need to be custom drawn.
return true;
}
virtual const wxItemAttr*
GetItemAttr(DWORD_PTR WXUNUSED(dwItemSpec)) const override
{
// We use the same attribute for all items for now.
return &m_attr;
}
};
wxBEGIN_EVENT_TABLE(wxListCtrl, wxListCtrlBase)
EVT_PAINT(wxListCtrl::OnPaint)
EVT_CHAR_HOOK(wxListCtrl::OnCharHook)
@ -289,15 +263,30 @@ bool wxListCtrl::Create(wxWindow *parent,
// this style for them.
MSWDisableComposited();
EnableSystemThemeByDefault();
const wxVisualAttributes& defAttrs = GetDefaultAttributes();
if ( wxMSWDarkMode::IsActive() )
{
MSWInitHeader();
// We also need to explicitly set the background colour as the value
// returned by GetBackgroundColour() by default doesn't match the
// actually used colour neither when using dark mode.
SetBackgroundColour(defAttrs.colBg);
}
else
{
EnableSystemThemeByDefault();
}
// explicitly say that we want to use Unicode because otherwise we get ANSI
// versions of _some_ messages (notably LVN_GETDISPINFOA)
wxSetCCUnicodeFormat(GetHwnd());
// We must set the default text colour to the system/theme color, otherwise
// GetTextColour will always return black
SetTextColour(GetDefaultAttributes().colFg);
// GetTextColour will always return black even if this is not what is used
// by default.
SetTextColour(defAttrs.colFg);
if ( InReportView() )
MSWSetExListStyles();
@ -360,6 +349,28 @@ void wxListCtrl::MSWSetExListStyles()
::SendMessage(GetHwnd(), LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle);
}
void wxListCtrl::MSWInitHeader()
{
// Currently we only need to do something here in dark mode.
if ( !wxMSWDarkMode::IsActive() )
return;
// It's not an error if the header doesn't exist.
HWND hwndHdr = ListView_GetHeader(GetHwnd());
if ( !hwndHdr )
return;
// But if it does, configure it to use dark mode.
wxMSWDarkMode::AllowForWindow(hwndHdr, L"ItemsView");
// It's not clear why do we have to do it, but without using custom drawing
// the header text is drawn in black, making it unreadable, so do use it.
if ( !m_headerCustomDraw )
m_headerCustomDraw = new wxMSWHeaderCtrlCustomDraw();
m_headerCustomDraw->UseHeaderThemeColors(hwndHdr);
}
void wxListCtrl::MSWAfterReparent()
{
// We did it for the original parent in our Create(), but we need to do it
@ -593,10 +604,14 @@ void wxListCtrl::SetWindowStyleFlag(long flag)
m_windowStyle &= ~(wxHSCROLL | wxVSCROLL);
// if we switched to the report view, set the extended styles for
// it too
// it too and configure the header which hadn't existed before
if ( !wasInReportView && InReportView() )
{
MSWSetExListStyles();
MSWInitHeader();
}
Refresh();
}
}
@ -605,6 +620,48 @@ void wxListCtrl::SetWindowStyleFlag(long flag)
// accessors
// ----------------------------------------------------------------------------
bool wxListCtrl::MSWGetDarkModeSupport(MSWDarkModeSupport& support) const
{
// There doesn't seem to be any theme that works well here:
// - "Explorer" draws bluish hover highlight rectangle which is not at
// all like the greyish one used by the actual Explorer in dark mode.
// - "DarkMode_Explorer" uses the same selection colours as the light mode
// and doesn't draw hover rectangle at all.
// - "ItemsView", which we use currently, draws the selection and hover as
// expected, but uses light mode scrollbars.
support.themeName = L"ItemsView";
return true;
}
int wxListCtrl::MSWGetToolTipMessage() const
{
return LVM_GETTOOLTIPS;
}
wxVisualAttributes wxListCtrl::GetDefaultAttributes() const
{
wxVisualAttributes attrs = GetClassDefaultAttributes(GetWindowVariant());
if ( wxMSWDarkMode::IsActive() )
{
// Note that we intentionally do not use this window HWND for the
// theme, as it doesn't have dark values for it -- but does have them
// when we use null window.
wxUxThemeHandle theme{HWND(0), L"ItemsView"};
wxColour col = theme.GetColour(0, TMT_TEXTCOLOR);
if ( col.IsOk() )
attrs.colFg = col;
col = theme.GetColour(0, TMT_FILLCOLOR);
if ( col.IsOk() )
attrs.colBg = col;
}
return attrs;
}
/* static */ wxVisualAttributes
wxListCtrl::GetClassDefaultAttributes(wxWindowVariant variant)
{
@ -667,7 +724,7 @@ bool wxListCtrl::SetHeaderAttr(const wxItemAttr& attr)
else // We do have custom attributes.
{
if ( !m_headerCustomDraw )
m_headerCustomDraw = new wxMSWListHeaderCustomDraw();
m_headerCustomDraw = new wxMSWHeaderCtrlCustomDraw();
if ( m_headerCustomDraw->m_attr == attr )
{
@ -3082,7 +3139,7 @@ static void HandleItemPostpaint(NMCUSTOMDRAW nmcd)
RECT rc = GetCustomDrawnItemRect(nmcd);
// don't use the provided HDC, it's in some strange state by now
::DrawFocusRect(WindowHDC(nmcd.hdr.hwndFrom), &rc);
::DrawFocusRect(ClientHDC(nmcd.hdr.hwndFrom), &rc);
}
}

View file

@ -26,6 +26,7 @@
#include "wx/ptr_scpd.h"
#include "wx/dynlib.h"
#include "wx/msw/private/button.h"
#include "wx/msw/private/darkmode.h"
#include "wx/msw/private/metrics.h"
#include "wx/msw/private/msgdlg.h"
#include "wx/modalhook.h"
@ -86,10 +87,7 @@ wxMessageDialogMap& HookMap()
void ScreenRectToClient(HWND hwnd, RECT& rc)
{
// map from desktop (i.e. screen) coordinates to ones of this window
//
// notice that a RECT is laid out as 2 consecutive POINTs so the cast is
// valid
::MapWindowPoints(HWND_DESKTOP, hwnd, reinterpret_cast<POINT *>(&rc), 2);
wxMapWindowPoints(HWND_DESKTOP, hwnd, &rc);
}
// set window position to the given rect
@ -599,6 +597,24 @@ void wxMessageDialog::DoCentre(int dir)
// Helpers of the wxMSWMessageDialog namespace
// ----------------------------------------------------------------------------
namespace
{
HRESULT CALLBACK
wxTaskDialogCallback(HWND hwnd, UINT msg, WPARAM, LPARAM, LONG_PTR)
{
switch ( msg )
{
case TDN_DIALOG_CONSTRUCTED:
wxMSWDarkMode::EnableForTLW(hwnd);
break;
}
return S_OK;
}
} // anonymous namespace
wxMSWTaskDialogConfig::wxMSWTaskDialogConfig(const wxMessageDialogBase& dlg)
: buttons(new TASKDIALOG_BUTTON[MAX_BUTTONS])
{
@ -747,6 +763,8 @@ void wxMSWTaskDialogConfig::MSWCommonTaskDialogInit(TASKDIALOGCONFIG &tdc)
AddTaskDialogButton(tdc, IDHELP, 0 /* not used */, btnHelpLabel);
}
tdc.pfCallback = wxTaskDialogCallback;
}
void wxMSWTaskDialogConfig::AddTaskDialogButton(TASKDIALOGCONFIG &tdc,

View file

@ -25,11 +25,11 @@
#include "wx/app.h"
#include "wx/dcclient.h"
#include "wx/dcmemory.h"
#include "wx/control.h"
#include "wx/panel.h"
#include "wx/settings.h"
#endif // WX_PRECOMP
#include "wx/imaglist.h"
#include "wx/renderer.h"
#include "wx/sysopt.h"
#include "wx/msw/private.h"
@ -38,6 +38,8 @@
#include <windowsx.h>
#include "wx/msw/winundef.h"
#include "wx/msw/private/darkmode.h"
#if wxUSE_UXTHEME
#include "wx/msw/uxtheme.h"
#endif
@ -221,12 +223,34 @@ bool wxNotebook::Create(wxWindow *parent,
if ( !MSWCreateControl(className, wxEmptyString, pos, size) )
return false;
Bind(wxEVT_PAINT, &wxNotebook::OnPaint, this);
// Inherit parent attributes and, unlike the default, also inherit the
// parent background colour in order to blend in with its background if
// it's set to a non-default value.
// it's set to a non-default value -- or if we're using dark mode, in which
// the default colour always needs to be changed.
InheritAttributes();
if ( parent->InheritsBackgroundColour() && !UseBgCol() )
SetBackgroundColour(parent->GetBackgroundColour());
if ( !UseBgCol() )
{
wxColour colBg;
if ( parent->InheritsBackgroundColour() )
{
colBg = parent->GetBackgroundColour();
}
else if ( wxMSWDarkMode::IsActive() )
{
colBg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
// We also need to change the foreground in this case to ensure a
// good contrast with the dark background.
SetForegroundColour(
wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT)
);
}
if ( colBg.IsOk() )
SetBackgroundColour(colBg);
}
#if wxUSE_UXTHEME
if ( HasFlag(wxNB_NOPAGETHEME) ||
@ -281,6 +305,11 @@ WXDWORD wxNotebook::MSWGetStyle(long style, WXDWORD *exstyle) const
return tabStyle;
}
int wxNotebook::MSWGetToolTipMessage() const
{
return TCM_GETTOOLTIPS;
}
wxNotebook::~wxNotebook()
{
#if wxUSE_UXTHEME
@ -976,8 +1005,16 @@ int wxNotebook::HitTest(const wxPoint& pt, long *flags) const
LRESULT APIENTRY
wxNotebookSpinBtnWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
if ( message == WM_ERASEBKGND )
return 0;
switch ( message )
{
case WM_ERASEBKGND:
return 0;
case WM_PAINT:
if ( wxMSWDarkMode::PaintIfNecessary(hwnd, gs_wndprocNotebookSpinBtn) )
return 0;
break;
}
return ::CallWindowProc(CASTWNDPROC gs_wndprocNotebookSpinBtn,
hwnd, message, wParam, lParam);
@ -995,9 +1032,288 @@ void wxNotebook::OnEraseBackground(wxEraseEvent& WXUNUSED(event))
// do nothing here
}
void wxNotebook::OnPaint(wxPaintEvent& WXUNUSED(event))
namespace
{
// Flags may include:
// - wxCONTROL_SELECTED for the currently selected tab
// - wxCONTROL_CURRENT for the "hot" tab, i.e. the one under mouse pointer
// - wxCONTROL_SPECIAL for the first tab.
void
DrawNotebookTab(wxWindow* win,
wxDC& dc,
const wxRect& rectOrig,
const wxString& text,
const wxBitmap& image,
wxDirection tabOrient,
int flags = wxCONTROL_NONE)
{
// This colour is just an approximation which seems to look acceptable.
dc.SetPen(wxSystemSettings::GetColour(wxSYS_COLOUR_MENUBAR));
const int selectedOffset = win->FromDIP(2);
const int labelOffset = 3*selectedOffset;
wxRect rectTab = rectOrig;
wxColour colTab;
if ( flags & wxCONTROL_SELECTED )
{
// Selected tab literally stands out, so make it bigger -- but clip
// drawing to ensure we don't draw the inner border of the inflated
// selected tab rectangle, it shouldn't overflow into the notebook
// page area.
rectTab.Inflate(selectedOffset);
wxRect rectClip = rectTab;
switch ( tabOrient )
{
case wxTOP:
rectClip.height -= selectedOffset;
break;
case wxBOTTOM:
rectClip.y += selectedOffset;
rectClip.height -= selectedOffset;
break;
case wxLEFT:
rectClip.width -= selectedOffset;
break;
case wxRIGHT:
rectClip.x += selectedOffset;
rectClip.width -= selectedOffset;
break;
default:
wxFAIL_MSG("unreachable");
}
dc.SetClippingRegion(rectClip);
colTab = win->GetBackgroundColour();
}
else // not the selected tab
{
// All tab rectangles overlap the previous one to avoid double pixel
// borders between them in Windows 10 flat look, except for the first
// one which has nothing to overlap.
if ( !(flags & wxCONTROL_SPECIAL) )
{
switch ( tabOrient )
{
case wxTOP:
case wxBOTTOM:
rectTab.x--;
rectTab.width++;
break;
case wxLEFT:
case wxRIGHT:
rectTab.y--;
rectTab.height++;
break;
default:
wxFAIL_MSG("unreachable");
}
}
if ( flags & wxCONTROL_CURRENT )
colTab = wxSystemSettings::GetColour(wxSYS_COLOUR_HOTLIGHT);
else
colTab = wxSystemSettings::GetColour(wxSYS_COLOUR_BTNSHADOW);
}
dc.SetBrush(colTab);
dc.DrawRectangle(rectTab);
wxRect rectLabel = rectOrig;
if ( flags & wxCONTROL_SELECTED )
{
dc.DestroyClippingRegion();
// Also shift the label to mimic the native control which makes it "pop
// up" for the selected tab (with "up" being "in the tab direction").
switch ( tabOrient )
{
case wxTOP:
rectLabel.y -= selectedOffset;
break;
case wxBOTTOM:
rectLabel.y += selectedOffset;
break;
case wxLEFT:
rectLabel.x -= selectedOffset;
break;
case wxRIGHT:
rectLabel.x += selectedOffset;
break;
default:
wxFAIL_MSG("unreachable");
}
}
rectLabel.Deflate(labelOffset);
// Draw the label and the image, if any.
switch ( tabOrient )
{
case wxTOP:
case wxBOTTOM:
// We can use an existing helper that will do everything for us.
dc.DrawLabel(text, image, rectLabel,
wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
break;
case wxLEFT:
case wxRIGHT:
{
const wxSize textSize = dc.GetTextExtent(text);
// Exchange width and height because we're drawing text
// vertically.
wxSize totalSize{textSize.y, textSize.x};
int textOfs = 0;
wxSize imageSize;
if ( image.IsOk() )
{
imageSize = image.GetSize();
// Use label offset for the gap between image and the label
// too because why not.
totalSize.y += imageSize.y + labelOffset;
if ( imageSize.x > totalSize.x )
{
textOfs = (imageSize.x - totalSize.x) / 2;
totalSize.x = imageSize.x;
}
}
// Native control actually draws text bottom/top aligned in the
// first/only row but centers them if there is more than one
// row of tabs. Don't bother with this, especially because it's
// really not obvious that it looks any better and just center
// them always.
const wxRect rect = wxRect(totalSize).CentreIn(rectLabel);
if ( tabOrient == wxLEFT )
{
int y = rect.y + textSize.x;
dc.DrawRotatedText(text, rect.x + textOfs, y, 90.0);
if ( image.IsOk() )
dc.DrawBitmap(image, rect.x, y + labelOffset, true);
}
else // tabOrient == wxRIGHT
{
int y = rect.y;
if ( image.IsOk() )
{
dc.DrawBitmap(image, rect.x, y, true);
y += imageSize.y + labelOffset;
}
dc.DrawRotatedText(text, rect.GetRight() - textOfs, y, -90.0);
}
}
break;
default:
wxFAIL_MSG("unreachable");
}
}
} // anonymous namespace
void wxNotebook::MSWNotebookPaint(wxDC& dc)
{
dc.Clear();
const wxDirection tabOrient = GetTabOrientation();
const wxSize sizeWindow = GetClientSize();
const int selected = GetSelection();
const wxPoint posCursor = ScreenToClient(wxGetMousePosition());
const auto drawTab = [this, &dc, tabOrient](wxRect rect, size_t n, int flags)
{
if ( n == 0 )
flags |= wxCONTROL_SPECIAL;
DrawNotebookTab(this, dc, rect,
GetPageText(n),
GetImageBitmapFor(this, GetPageImage(n)),
tabOrient,
flags);
};
const size_t pages = GetPageCount();
for ( size_t n = 0; n < pages; ++n )
{
if ( static_cast<int>(n) == selected )
{
// We're going to draw this one after all the other ones as it
// overlaps them.
continue;
}
const wxRect rect = GetTabRect(n);
// For horizontal tabs, some of them can be scrolled out of view, skip
// drawing them just in case we have zillions of tabs to avoid drawing
// the off-screen ones unnecessarily.
if ( rect.x > sizeWindow.x )
{
// This tab, and all the remaining ones, can't be seen anyhow, so
// don't bother drawing them.
break;
}
// We can't track the "hot" tab when using non-top tabs as the native
// control doesn't refresh them on mouse move (it seems to switch to
// comctl32.dll v5-like implementation in this case), so don't paint
// them specially.
int flags = wxCONTROL_NONE;
if ( tabOrient == wxTOP && rect.Contains(posCursor) )
flags |= wxCONTROL_CURRENT;
drawTab(rect, n, flags);
}
if ( selected != wxNOT_FOUND )
drawTab(GetTabRect(selected), selected, wxCONTROL_SELECTED);
}
void wxNotebook::OnPaint(wxPaintEvent& event)
{
// We can rely on the default implementation if we don't have a custom
// background colour (note that it is always set when using dark mode).
if ( !m_hasBgCol )
{
event.Skip();
return;
}
wxPaintDC dc(this);
if ( wxMSWDarkMode::IsActive() )
{
// We can't use default painting in dark mode, it just doesn't work
// there, whichever theme we use, so draw everything ourselves.
MSWNotebookPaint(dc);
return;
}
RECT rc;
::GetClientRect(GetHwnd(), &rc);
if ( !rc.right || !rc.bottom )
@ -1306,11 +1622,9 @@ bool wxNotebook::SetBackgroundColour(const wxColour& colour)
#if USE_NOTEBOOK_ANTIFLICKER
Unbind(wxEVT_ERASE_BACKGROUND, &wxNotebook::OnEraseBackground, this);
Unbind(wxEVT_PAINT, &wxNotebook::OnPaint, this);
if ( m_hasBgCol || !wxUxThemeIsActive() )
{
Bind(wxEVT_ERASE_BACKGROUND, &wxNotebook::OnEraseBackground, this);
Bind(wxEVT_PAINT, &wxNotebook::OnPaint, this);
}
#endif // USE_NOTEBOOK_ANTIFLICKER
@ -1330,7 +1644,7 @@ WXHBRUSH wxNotebook::QueryBgBitmap()
if ( !theme )
return 0;
WindowHDC hDC(GetHwnd());
ClientHDC hDC(GetHwnd());
RECT rcBg;
::GetThemeBackgroundContentRect(theme,
@ -1398,7 +1712,7 @@ bool wxNotebook::MSWPrintChild(WXHDC hDC, wxWindow *child)
// map rect to the coords of the window we're drawing in
if ( child )
::MapWindowPoints(GetHwnd(), GetHwndOf(child), (POINT *)&rc, 2);
wxMapWindowPoints(GetHwnd(), GetHwndOf(child), &rc);
// If we're using a solid colour (for example if we've switched off
// theming for this notebook), paint it
@ -1457,14 +1771,8 @@ wxColour wxNotebook::GetThemeBackgroundColour() const
// This is total guesswork.
// See PlatformSDK\Include\Tmschema.h for values.
// JACS: can also use 9 (TABP_PANE)
COLORREF themeColor;
bool success = (S_OK == ::GetThemeColor(
hTheme,
10 /* TABP_BODY */,
1 /* NORMAL */,
3821 /* FILLCOLORHINT */,
&themeColor));
if (!success)
wxColour colour = hTheme.GetColour(TABP_BODY, TMT_FILLCOLORHINT, TIS_NORMAL);
if ( !colour.IsOk() )
return GetBackgroundColour();
/*
@ -1476,17 +1784,8 @@ wxColour wxNotebook::GetThemeBackgroundColour() const
This workaround potentially breaks appearance of some themes,
but in practice it already fixes some themes.
*/
if (themeColor == 1)
{
::GetThemeColor(
hTheme,
10 /* TABP_BODY */,
1 /* NORMAL */,
3802 /* FILLCOLOR */,
&themeColor);
}
wxColour colour = wxRGBToColour(themeColor);
if ( colour.GetRGB() == 1 )
colour = hTheme.GetColour(TABP_BODY, TMT_FILLCOLOR, TIS_NORMAL);
// Under Vista, the tab background colour is reported incorrectly.
// So for the default theme at least, hard-code the colour to something
@ -1582,8 +1881,15 @@ bool wxNotebook::MSWOnNotify(int idCtrl, WXLPARAM lParam, WXLPARAM* result)
// Change the selection before generating the event as its handler should
// already see the new page selected.
if ( hdr->code == TCN_SELCHANGE )
{
UpdateSelection(event.GetSelection());
// We need to update the tabs after the selection change when drawing
// them ourselves, otherwise the previously selected tab is not redrawn.
if ( wxMSWDarkMode::IsActive() )
Refresh();
}
bool processed = HandleWindowEvent(event);
*result = !event.IsAllowed();
return processed;

View file

@ -75,6 +75,19 @@ bool wxRadioButton::Create(wxWindow *parent,
return true;
}
bool wxRadioButton::MSWGetDarkModeSupport(MSWDarkModeSupport& support) const
{
// Weirdly enough, even though radio buttons support dark theme (they
// notably change the way they draw the focus rectangle if we set it), they
// still use the default black foreground colour in it, making their text
// unreadable, so we need to change it manually.
wxRadioButtonBase::MSWGetDarkModeSupport(support);
support.setForeground = true;
return true;
}
// ----------------------------------------------------------------------------
// wxRadioButton functions
// ----------------------------------------------------------------------------

View file

@ -1223,23 +1223,11 @@ void wxRendererXP::DrawTextCtrl(wxWindow* win,
return;
}
wxColour fill;
wxColour bdr;
COLORREF cref;
::GetThemeColor(hTheme, EP_EDITTEXT,
ETS_NORMAL, TMT_FILLCOLOR, &cref);
fill = wxRGBToColour(cref);
int etsState;
if ( flags & wxCONTROL_DISABLED )
etsState = ETS_DISABLED;
else
etsState = ETS_NORMAL;
::GetThemeColor(hTheme, EP_EDITTEXT,
etsState, TMT_BORDERCOLOR, &cref);
bdr = wxRGBToColour(cref);
wxColour fill = hTheme.GetColour(EP_EDITTEXT, TMT_FILLCOLOR, ETS_NORMAL);
wxColour bdr = hTheme.GetColour(EP_EDITTEXT, TMT_BORDERCOLOR,
flags & wxCONTROL_DISABLED
? ETS_DISABLED
: ETS_NORMAL);
wxDCPenChanger setPen(dc, bdr);
wxDCBrushChanger setBrush(dc, fill);

View file

@ -31,6 +31,7 @@
#include "wx/msw/private.h"
#include "wx/msw/missing.h" // for SM_CXCURSOR, SM_CYCURSOR, SM_TABLETPC
#include "wx/msw/private/darkmode.h"
#include "wx/msw/private/metrics.h"
#include "wx/msw/registry.h"
@ -98,6 +99,14 @@ void wxSystemSettingsModule::OnExit()
wxColour wxSystemSettingsNative::GetColour(wxSystemColour index)
{
// As GetSysColor() doesn't support dark mode, check for it before using it.
if ( wxMSWDarkMode::IsActive() )
{
const wxColour colDark = wxMSWDarkMode::GetColour(index);
if ( colDark.IsOk() )
return colDark;
}
if ( index == wxSYS_COLOUR_LISTBOXTEXT)
{
// there is no standard colour with this index, map to another one
@ -362,22 +371,50 @@ extern wxFont wxGetCCDefaultFont()
#endif // wxUSE_LISTCTRL || wxUSE_TREECTRL
// There is no official API for determining whether dark mode is being used,
// but // HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize has
// a value AppsUseLightTheme = 0 for dark mode and 1 for normal mode, so use it
// and fall back to the generic algorithm in IsUsingDarkBackground() if it's
// absent.
// but HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize
// contains AppsUseLightTheme and SystemUsesLightTheme values determining
// whether the applications/system use light or dark mode, so use them.
//
// Adapted from https://stackoverflow.com/a/51336913/15275 ("How to detect
// Windows 10 light/dark mode in Win32 application?").
bool wxSystemAppearance::IsDark() const
namespace
{
// Return false unless we are sure we're using the dark mode.
bool IsUsingDarkTheme(const wxString& forWhat)
{
wxRegKey rk(wxRegKey::HKCU, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
if ( rk.Exists() && rk.HasValue("AppsUseLightTheme") )
if ( rk.Exists() && rk.HasValue(forWhat) )
{
long value = -1;
if ( rk.QueryValue("AppsUseLightTheme", &value) )
if ( rk.QueryValue(forWhat, &value) )
return value <= 0;
}
return false;
}
} // anonymous namespace
bool wxSystemAppearance::IsDark() const
{
// If the application opted in using dark mode, use the undocumented API
// which we use for dark mode support directly.
if ( wxMSWDarkMode::IsActive() )
return true;
// Note that we should _not_ check if the system is configured to use the
// dark mode for the other applications here, what matters is whether this
// application itself uses dark colour schema or not.
return IsUsingDarkBackground();
}
bool wxSystemAppearance::AreAppsDark() const
{
return IsUsingDarkTheme("AppsUseLightTheme");
}
bool wxSystemAppearance::IsSystemDark() const
{
return IsUsingDarkTheme("SystemUsesLightTheme");
}

View file

@ -139,6 +139,13 @@ wxSpinButton::~wxSpinButton()
{
}
bool wxSpinButton::MSWShouldUseAutoDarkMode() const
{
// Native control doesn't seem to have any support for dark theme, so
// invert it in dark mode -- this is not great, but better than nothing.
return true;
}
// ----------------------------------------------------------------------------
// size calculation
// ----------------------------------------------------------------------------

View file

@ -95,6 +95,15 @@ bool wxStaticBox::Create(wxWindow *parent,
return true;
}
bool wxStaticBox::MSWGetDarkModeSupport(MSWDarkModeSupport& support) const
{
// Static boxes don't seem to have any dark mode support, so just set the
// foreground colour contrasting with the dark background for them.
support.setForeground = true;
return true;
}
bool wxStaticBox::ShouldUseCustomPaint() const
{
// When not using double buffering, we paint the box ourselves by default

View file

@ -679,4 +679,15 @@ bool wxStatusBar::MSWOnNotify(int WXUNUSED(idCtrl), WXLPARAM lParam, WXLPARAM* W
}
#endif // wxUSE_TOOLTIPS
bool wxStatusBar::MSWGetDarkModeSupport(MSWDarkModeSupport& support) const
{
// This is not documented anywhere but seems to work.
//
// Note that we should _not_ set the theme name to "Explorer", this ID only
// works if we do _not_ do it.
support.themeId = L"ExplorerStatusBar";
return true;
}
#endif // wxUSE_STATUSBAR && wxUSE_NATIVE_STATUSBAR

View file

@ -60,6 +60,8 @@
#include "wx/msw/uxtheme.h"
#endif
#include "wx/msw/private/darkmode.h"
// ----------------------------------------------------------------------------
// constants
// ----------------------------------------------------------------------------
@ -334,6 +336,11 @@ static bool MSWShouldBeChecked(const wxToolBarToolBase *tool)
return tool->IsToggled();
}
static COLORREF wxSysColourToRGB(wxSystemColour syscol)
{
return wxColourToRGB(wxSystemSettings::GetColour(syscol));
}
// ============================================================================
// implementation
// ============================================================================
@ -463,6 +470,18 @@ bool wxToolBar::MSWCreateToolbar(const wxPoint& pos, const wxSize& size)
}
#endif // wxUSE_TOOLTIPS
// Change the color scheme when using the dark mode even though MSDN says
// that it's not used with comctl32 v6, it actually still is for "3D"
// separator above the toolbar, which is drawn partially in white by
// default and so looks very ugly in dark mode.
if ( wxMSWDarkMode::IsActive() )
{
COLORSCHEME colScheme{sizeof(COLORSCHEME)};
colScheme.clrBtnHighlight =
colScheme.clrBtnShadow = wxSysColourToRGB(wxSYS_COLOUR_WINDOW);
::SendMessage(GetHwnd(), TB_SETCOLORSCHEME, 0, (LPARAM)&colScheme);
}
return true;
}
@ -714,6 +733,22 @@ WXDWORD wxToolBar::MSWGetStyle(long style, WXDWORD *exstyle) const
return msStyle;
}
bool wxToolBar::MSWGetDarkModeSupport(MSWDarkModeSupport& support) const
{
wxToolBarBase::MSWGetDarkModeSupport(support);
// This ensures GetForegroundColour(), used in our custom draw code,
// returns the correct colour.
support.setForeground = true;
return true;
}
int wxToolBar::MSWGetToolTipMessage() const
{
return TB_GETTOOLTIPS;
}
// ----------------------------------------------------------------------------
// adding/removing tools
// ----------------------------------------------------------------------------
@ -1611,7 +1646,7 @@ bool wxToolBar::MSWCommand(WXUINT WXUNUSED(cmd), WXWORD id_)
bool wxToolBar::MSWOnNotify(int WXUNUSED(idCtrl),
WXLPARAM lParam,
WXLPARAM *WXUNUSED(result))
WXLPARAM *result)
{
LPNMHDR hdr = (LPNMHDR)lParam;
if ( hdr->code == TBN_DROPDOWN )
@ -1640,6 +1675,32 @@ bool wxToolBar::MSWOnNotify(int WXUNUSED(idCtrl),
return true;
}
if ( hdr->code == NM_CUSTOMDRAW )
{
NMTBCUSTOMDRAW* const nmtbcd = (NMTBCUSTOMDRAW*)lParam;
switch ( nmtbcd->nmcd.dwDrawStage )
{
case CDDS_PREPAINT:
if ( !wxMSWDarkMode::IsActive() )
break;
*result = CDRF_NOTIFYITEMDRAW;
return true;
case CDDS_ITEMPREPAINT:
// If we get here, we must have returned CDRF_NOTIFYITEMDRAW
// from above, so we're using the dark mode and need to
// customize the colours for it.
nmtbcd->clrText =
nmtbcd->clrTextHighlight = wxColourToRGB(GetForegroundColour());
nmtbcd->clrHighlightHotTrack = wxSysColourToRGB(wxSYS_COLOUR_HOTLIGHT);
*result = CDRF_DODEFAULT | TBCDRF_USECDCOLORS | TBCDRF_HILITEHOTTRACK;
return true;
}
return false;
}
if( !HasFlag(wxTB_NO_TOOLTIPS) )
{
@ -2103,7 +2164,7 @@ bool wxToolBar::HandlePaint(WXWPARAM wParam, WXLPARAM lParam)
{
// erase the dummy separators region ourselves now as nobody painted
// over them
WindowHDC hdc(GetHwnd());
ClientHDC hdc(GetHwnd());
::SelectClipRgn(hdc, GetHrgnOf(rgnDummySeps));
MSWDoEraseBackground(hdc);
}

View file

@ -34,6 +34,7 @@
#include "wx/tokenzr.h"
#include "wx/vector.h"
#include "wx/msw/private.h"
#include "wx/msw/private/darkmode.h"
#ifndef TTTOOLINFO_V1_SIZE
#define TTTOOLINFO_V1_SIZE 0x28
@ -314,6 +315,7 @@ WXHWND wxToolTip::GetToolTipCtrl()
if ( ms_hwndTT )
{
HWND hwnd = (HWND)ms_hwndTT;
wxMSWDarkMode::AllowForWindow(hwnd);
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);

View file

@ -39,6 +39,7 @@
#include "wx/tooltip.h"
#include "wx/msw/private.h"
#include "wx/msw/private/darkmode.h"
#include "wx/msw/private/winstyle.h"
#include "wx/msw/winundef.h"
@ -510,6 +511,8 @@ bool wxTopLevelWindowMSW::Create(wxWindow *parent,
// focus rectangles) work under Win2k+
MSWUpdateUIState(UIS_INITIALIZE);
wxMSWDarkMode::EnableForTLW(GetHwnd());
return true;
}

View file

@ -2220,6 +2220,11 @@ void wxTreeCtrl::SortChildren(const wxTreeItemId& item)
// implementation
// ----------------------------------------------------------------------------
int wxTreeCtrl::MSWGetToolTipMessage() const
{
return TVM_GETTOOLTIPS;
}
bool wxTreeCtrl::MSWShouldPreProcessMessage(WXMSG* msg)
{
if ( msg->message == WM_KEYDOWN )
@ -3667,7 +3672,7 @@ bool wxTreeCtrl::MSWOnNotify(int idCtrl, WXLPARAM lParam, WXLPARAM *result)
POINT point;
point.x = GET_X_LPARAM(pos);
point.y = GET_Y_LPARAM(pos);
::MapWindowPoints(HWND_DESKTOP, GetHwnd(), &point, 1);
wxMapWindowPoints(HWND_DESKTOP, GetHwnd(), &point);
int htFlags = 0;
wxTreeItemId item = HitTest(wxPoint(point.x, point.y), htFlags);

View file

@ -36,6 +36,23 @@ bool wxUxThemeIsActive()
{
return ::IsAppThemed() && ::IsThemeActive();
}
wxColour wxUxThemeHandle::GetColour(int part, int prop, int state) const
{
COLORREF col;
HRESULT hr = ::GetThemeColor(m_hTheme, part, state, prop, &col);
if ( FAILED(hr) )
{
wxLogApiError(
wxString::Format("GetThemeColor(%i, %i, %i)", part, state, prop),
hr
);
return wxColour{};
}
return wxRGBToColour(col);
}
#else
bool wxUxThemeIsActive()
{

View file

@ -76,6 +76,7 @@
#endif
#include "wx/msw/private.h"
#include "wx/msw/private/darkmode.h"
#include "wx/msw/private/dpiaware.h"
#include "wx/msw/private/keyboard.h"
#include "wx/msw/private/paint.h"
@ -1962,8 +1963,9 @@ void wxWindowMSW::DoGetPosition(int *x, int *y) const
{
// In RTL mode, we want the logical left x-coordinate,
// which would be the physical right x-coordinate.
::MapWindowPoints(nullptr, parent ? GetHwndOf(parent) : HWND_DESKTOP,
(LPPOINT)&rect, 2);
wxMapWindowPoints(HWND_DESKTOP,
parent ? GetHwndOf(parent) : HWND_DESKTOP,
&rect);
}
pos.x = rect.left;
@ -3145,7 +3147,11 @@ wxWindowMSW::MSWHandleMessage(WXLRESULT *result,
}
else // no DC given
{
processed = HandlePaint();
if ( MSWShouldUseAutoDarkMode() &&
wxMSWDarkMode::PaintIfNecessary(GetHwnd(), m_oldWndProc) )
processed = true;
else
processed = HandlePaint();
}
break;
@ -3824,7 +3830,7 @@ wxWindowMSW::MSWHandleMessage(WXLRESULT *result,
// it below if it fails.
RECT rcClient;
WindowHDC hdc(GetHwnd());
ClientHDC hdc(GetHwnd());
if ( ::GetThemeBackgroundContentRect
(
@ -4082,6 +4088,16 @@ bool wxWindowMSW::MSWCreate(const wxChar *wclass,
return false;
}
if ( wxMSWDarkMode::IsActive() )
{
// We currently allow customizing the theme at wxControl level as some
// native controls require using a different theme, but for plain
// windows it looks like the default ("Explorer") should always be used
// and its only (but important) effect is to make their scrollbars
// dark, if they're used.
wxMSWDarkMode::AllowForWindow(m_hWnd);
}
SubclassWin(m_hWnd);
return true;
@ -4886,7 +4902,7 @@ wxSize wxWindowMSW::GetDPI() const
if ( !dpi.x || !dpi.y )
{
dpi = wxGetDPIofHDC(WindowHDC(hwnd));
dpi = wxGetDPIofHDC(ClientHDC(hwnd));
}
return dpi;
@ -5103,6 +5119,13 @@ bool wxWindowMSW::HandleCaptureChanged(WXHWND hWndGainedCapture)
bool wxWindowMSW::HandleSettingChange(WXWPARAM wParam, WXLPARAM lParam)
{
// Check for the special case of changing the system light/dark mode.
if ( lParam && wxStrcmp((TCHAR*)lParam, wxT("ImmersiveColorSet")) == 0 )
{
// Forward to the existing function generating an event for this.
HandleSysColorChange();
}
// despite MSDN saying "(This message cannot be sent directly to a window.)"
// we need to send this to child windows (it is only sent to top-level
// windows) so {list,tree}ctrls can adjust their font size if necessary
@ -5252,68 +5275,6 @@ extern wxCOLORMAP *wxGetStdColourMap()
return s_cmap;
}
#if wxUSE_UXTHEME && !defined(TMT_FILLCOLOR)
#define TMT_FILLCOLOR 3802
#define TMT_TEXTCOLOR 3803
#define TMT_BORDERCOLOR 3801
#endif
wxColour wxWindowMSW::MSWGetThemeColour(const wchar_t *themeName,
int themePart,
int themeState,
MSWThemeColour themeColour,
wxSystemColour fallback) const
{
#if wxUSE_UXTHEME
if ( wxUxThemeIsActive() )
{
int themeProperty = 0;
// TODO: Convert this into a table? Sure would be faster.
switch ( themeColour )
{
case ThemeColourBackground:
themeProperty = TMT_FILLCOLOR;
break;
case ThemeColourText:
themeProperty = TMT_TEXTCOLOR;
break;
case ThemeColourBorder:
themeProperty = TMT_BORDERCOLOR;
break;
default:
wxFAIL_MSG(wxT("unsupported theme colour"));
}
wxUxThemeHandle hTheme((const wxWindow *)this, themeName);
COLORREF col;
HRESULT hr = ::GetThemeColor
(
hTheme,
themePart,
themeState,
themeProperty,
&col
);
if ( SUCCEEDED(hr) )
return wxRGBToColour(col);
wxLogApiError(
wxString::Format(
"GetThemeColor(%s, %i, %i, %i)",
themeName, themePart, themeState, themeProperty),
hr);
}
#else
wxUnusedVar(themeName);
wxUnusedVar(themePart);
wxUnusedVar(themeState);
wxUnusedVar(themeColour);
#endif
return wxSystemSettings::GetColour(fallback);
}
// ---------------------------------------------------------------------------
// painting
// ---------------------------------------------------------------------------
@ -5526,7 +5487,7 @@ wxWindowMSW::MSWGetBgBrushForChild(WXHDC hDC, wxWindowMSW *child)
// uses RTL layout, which is exactly what we need here as the child
// window origin is its _right_ top corner in this case and not the
// left one.
::MapWindowPoints(nullptr, GetHwnd(), (POINT *)&rc, 2);
wxMapWindowPoints(HWND_DESKTOP, GetHwnd(), &rc);
int x = rc.left,
y = rc.top;

View file

@ -208,8 +208,7 @@ void FontPickerCtrlTestCase::ColourSelection()
{
wxColour selectedColour(0xFF4269UL);
CPPUNIT_ASSERT_EQUAL_MESSAGE("Default font picker color must be black",
m_font->GetSelectedColour(), wxColour(*wxBLACK));
CHECK( m_font->GetSelectedColour() != selectedColour );
m_font->SetSelectedColour(selectedColour);