Merge branch 'customize-dark-mode'

Allow customizing appearance in dark mode in wxMSW.

See #23275.
This commit is contained in:
Vadim Zeitlin 2023-03-12 13:42:45 +01:00
commit 0fe6f04bfc
13 changed files with 371 additions and 65 deletions

View file

@ -3211,6 +3211,7 @@ COND_TOOLKIT_MSW_GUI_HDR = \
wx/msw/datectrl.h \
wx/msw/calctrl.h \
wx/generic/activityindicator.h \
wx/msw/darkmode.h \
wx/msw/checklst.h \
wx/msw/fdrepdlg.h \
wx/msw/fontdlg.h \

View file

@ -1954,6 +1954,7 @@ IMPORTANT: please read docs/tech/tn0016.txt before modifying this file!
wx/msw/datectrl.h
wx/msw/calctrl.h
wx/generic/activityindicator.h
wx/msw/darkmode.h
</set>
<set var="MSW_RSC" hints="files">
<!-- Resources must be installed together with headers: -->

View file

@ -1844,6 +1844,7 @@ set(MSW_HDR
wx/msw/datetimectrl.h
wx/msw/timectrl.h
wx/generic/activityindicator.h
wx/msw/darkmode.h
)
set(MSW_RSC

View file

@ -1761,6 +1761,7 @@ MSW_HDR =
wx/msw/ctrlsub.h
wx/msw/cursor.h
wx/msw/custombgwin.h
wx/msw/darkmode.h
wx/msw/datectrl.h
wx/msw/datetimectrl.h
wx/msw/dc.h

View file

@ -1518,6 +1518,7 @@
<ClInclude Include="..\..\include\wx\generic\creddlgg.h" />
<ClInclude Include="..\..\include\wx\filedlgcustomize.h" />
<ClInclude Include="..\..\include\wx\compositebookctrl.h" />
<ClInclude Include="..\..\include\wx\msw\darkmode.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">

View file

@ -1741,6 +1741,9 @@
<ClInclude Include="..\..\include\wx\msw\custombgwin.h">
<Filter>MSW Headers</Filter>
</ClInclude>
<ClInclude Include="..\..\include\wx\msw\darkmode.h">
<Filter>MSW Headers</Filter>
</ClInclude>
<ClInclude Include="..\..\include\wx\msw\datectrl.h">
<Filter>MSW Headers</Filter>
</ClInclude>

View file

@ -17,6 +17,7 @@
class WXDLLIMPEXP_FWD_CORE wxFrame;
class WXDLLIMPEXP_FWD_CORE wxWindow;
class WXDLLIMPEXP_FWD_CORE wxApp;
class WXDLLIMPEXP_FWD_CORE wxDarkModeSettings;
class WXDLLIMPEXP_FWD_CORE wxKeyEvent;
class WXDLLIMPEXP_FWD_BASE wxLog;
@ -38,12 +39,16 @@ public:
virtual int GetPrintMode() const { return m_printMode; }
// MSW-specific function to enable experimental dark mode support.
//
// If settings are specified, the function takes ownership of the pointer,
// otherwise the defaults are used.
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);
bool
MSWEnableDarkMode(int flags = 0, wxDarkModeSettings* settings = nullptr);
// implementation only
void OnIdle(wxIdleEvent& event);

52
include/wx/msw/darkmode.h Normal file
View file

@ -0,0 +1,52 @@
///////////////////////////////////////////////////////////////////////////////
// Name: wx/msw/darkmode.h
// Purpose: MSW-specific header with dark mode related declarations.
// Author: Vadim Zeitlin
// Created: 2023-02-19
// Copyright: (c) 2023 Vadim Zeitlin <vadim@wxwidgets.org>
// Licence: wxWindows licence
///////////////////////////////////////////////////////////////////////////////
#ifndef _WX_MSW_DARKMODE_H_
#define _WX_MSW_DARKMODE_H_
#include "wx/settings.h"
// Constants used with wxDarkModeSettings::GetMenuColour().
enum class wxMenuColour
{
StandardFg,
StandardBg,
DisabledFg,
HotBg
};
// ----------------------------------------------------------------------------
// wxDarkModeSettings: allows to customize some of dark mode settings
// ----------------------------------------------------------------------------
class WXDLLIMPEXP_CORE wxDarkModeSettings
{
public:
wxDarkModeSettings() = default;
virtual ~wxDarkModeSettings();
// Get the colour to use for the given system colour when dark mode is on.
virtual wxColour GetColour(wxSystemColour index);
// Menu items don't use any of the standard colours, but are defined by
// this function.
virtual wxColour GetMenuColour(wxMenuColour which);
// Get the pen to use for drawing wxStaticBox border in dark mode.
//
// Returning an invalid pen indicates that the default border drawn by the
// system should be used, which doesn't look very well in dark mode but
// shouldn't result in any problems worse than cosmetic ones.
virtual wxPen GetBorderPen();
private:
wxDECLARE_NO_COPY_CLASS(wxDarkModeSettings);
};
#endif // _WX_MSW_DARKMODE_H_

View file

@ -36,6 +36,9 @@ void AllowForWindow(HWND hwnd,
// colour if it isn't.
wxColour GetColour(wxSystemColour index);
// Get the pen to use for drawing the border, see wxDarkModeSettings.
wxPen GetBorderPen();
// Return the background brush to be used by default in dark mode.
HBRUSH GetBackgroundBrush();

View file

@ -1212,6 +1212,8 @@ public:
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.
@param settings If specified, allows to customize dark mode appearance.
Please see wxDarkModeSettings documentation for more information.
@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.
@ -1220,7 +1222,8 @@ public:
@since 3.3.0
*/
bool MSWEnableDarkMode(int flags = 0);
bool
MSWEnableDarkMode(int flags = 0, wxDarkModeSettings* settings = nullptr);
//@}
};

112
interface/wx/msw/darkmode.h Normal file
View file

@ -0,0 +1,112 @@
/////////////////////////////////////////////////////////////////////////////
// Name: wx/msw/darkmode.h
// Purpose: Documentation for MSW-specific dark mode functionality.
// Author: Vadim Zeitlin
// Created: 2023-02-19
// Copyright: (c) 2023 Vadim Zeitlin
// Licence: wxWindows Licence
/////////////////////////////////////////////////////////////////////////////
/**
Constants used with wxDarkModeSettings::GetMenuColour().
@since 3.3.0
*/
enum class wxMenuColour
{
/// Text colour used for the normal items in dark mode.
StandardFg,
/// Standard menu background colour.
StandardBg,
/// Foreground colour for the disabled items.
DisabledFg,
/// Background colour used for the item over which mouse is hovering.
HotBg
};
/**
Allows to customize some of the settings used in MSW dark mode.
An object of this class may be passed to wxApp::MSWEnableDarkMode() to
customize some aspects of the dark mode when it is used under MSW systems.
For example, to customize the background colour to use a reddish black
instead of normal black used by default, you could do the following:
@code
class MySettings : public wxDarkModeSettings
{
public:
wxColour GetColour(wxSystemColour index) override
{
switch ( index )
{
case wxSYS_COLOUR_ACTIVECAPTION:
case wxSYS_COLOUR_APPWORKSPACE:
case wxSYS_COLOUR_INFOBK:
case wxSYS_COLOUR_LISTBOX:
case wxSYS_COLOUR_WINDOW:
case wxSYS_COLOUR_BTNFACE:
// Default colour used here is 0x202020.
return wxColour(0x402020);
default:
return wxDarkModeSettings::GetColour(index);
}
}
};
wxTheApp->MSWEnableDarkMode(wxApp::DarkMode_Always, new MySettings());
@endcode
@since 3.3.0
*/
class wxDarkModeSettings
{
public:
/**
Default constructor does nothing.
*/
wxDarkModeSettings() = default;
/**
Get the colour to use for the given system colour when dark mode is on.
The base class version of this function returns the colours commonly
used in dark mode. As the rest of dark mode support, their exact values
are not documented and are subject to change in the future Windows or
wxWidgets versions.
@see GetMenuColour()
*/
virtual wxColour GetColour(wxSystemColour index);
/**
Get the colour to use for the menu bar in the given state.
Currently the colours used by the menus in the menu bar in dark mode
don't correspond to any of wxSystemColour values and this separate
function is used for customizing them instead of GetColour().
Note that the colours returned by this function only affect the top
level menus, the colours of the menu items inside them can be
customized in the usual way using wxOwnerDrawn::SetTextColour().
The returned colour must be valid.
*/
virtual wxColour GetMenuColour(wxMenuColour which);
/**
Get the pen to use for drawing wxStaticBox border in dark mode.
Returning an invalid pen indicates that the default border drawn by the
system should be used, which doesn't look very well in dark mode but
shouldn't result in any problems worse than cosmetic ones.
The base class version returns a grey pen, which looks better than the
default white one.
*/
virtual wxPen GetBorderPen();
};

View file

@ -44,11 +44,14 @@
#include "wx/dynlib.h"
#include "wx/module.h"
#include "wx/msw/darkmode.h"
#include "wx/msw/dc.h"
#include "wx/msw/uxtheme.h"
#include "wx/msw/private/darkmode.h"
#include <memory>
static const char* TRACE_DARKMODE = "msw-darkmode";
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
@ -168,10 +171,25 @@ public:
virtual bool OnInit() override { return true; }
virtual void OnExit() override
{
ms_settings.reset();
ms_pfnDwmSetWindowAttribute = (DwmSetWindowAttribute_t)-1;
ms_dllDWM.Unload();
}
// Takes ownership of the provided pointer.
static void SetSettings(wxDarkModeSettings* settings)
{
ms_settings.reset(settings);
}
// Returns the currently used settings: may only be called when the dark
// mode is on.
static wxDarkModeSettings& GetSettings()
{
return *ms_settings;
}
static DwmSetWindowAttribute_t GetDwmSetWindowAttribute()
{
if ( ms_pfnDwmSetWindowAttribute == (DwmSetWindowAttribute_t)-1 )
@ -187,12 +205,15 @@ private:
static wxDynamicLibrary ms_dllDWM;
static DwmSetWindowAttribute_t ms_pfnDwmSetWindowAttribute;
static std::unique_ptr<wxDarkModeSettings> ms_settings;
wxDECLARE_DYNAMIC_CLASS(wxDarkModeModule);
};
wxIMPLEMENT_DYNAMIC_CLASS(wxDarkModeModule, wxModule);
wxDynamicLibrary wxDarkModeModule::ms_dllDWM;
std::unique_ptr<wxDarkModeSettings> wxDarkModeModule::ms_settings;
DwmSetWindowAttribute_t
wxDarkModeModule::ms_pfnDwmSetWindowAttribute = (DwmSetWindowAttribute_t)-1;
@ -201,7 +222,7 @@ wxDarkModeModule::ms_pfnDwmSetWindowAttribute = (DwmSetWindowAttribute_t)-1;
// Public API
// ----------------------------------------------------------------------------
bool wxApp::MSWEnableDarkMode(int flags)
bool wxApp::MSWEnableDarkMode(int flags, wxDarkModeSettings* settings)
{
if ( !wxMSWImpl::InitDarkMode() )
return false;
@ -220,61 +241,23 @@ bool wxApp::MSWEnableDarkMode(int flags)
gs_appMode = mode;
// Set up the settings to use, allocating a default one if none specified.
if ( !settings )
settings = new wxDarkModeSettings();
wxDarkModeModule::SetSettings(settings);
return true;
}
// ----------------------------------------------------------------------------
// Supporting functions for the rest of wxMSW code
// Default wxDarkModeSettings implementation
// ----------------------------------------------------------------------------
namespace wxMSWDarkMode
{
// Implemented here to ensure that it's generated inside the DLL.
wxDarkModeSettings::~wxDarkModeSettings() = default;
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)
wxColour wxDarkModeSettings::GetColour(wxSystemColour index)
{
// This is not great at all, but better than using light mode colours that
// are not appropriate for the dark mode.
@ -347,6 +330,97 @@ wxColour GetColour(wxSystemColour index)
return wxColour();
}
wxColour wxDarkModeSettings::GetMenuColour(wxMenuColour which)
{
switch ( which )
{
case wxMenuColour::StandardFg:
return wxColour(0xffffff);
case wxMenuColour::StandardBg:
return wxColour(0x6d6d6d);
case wxMenuColour::DisabledFg:
return wxColour(0x414141);
case wxMenuColour::HotBg:
return wxColour(0x2b2b2b);
}
wxFAIL_MSG( "unreachable" );
return wxColour();
}
wxPen wxDarkModeSettings::GetBorderPen()
{
// Use a darker pen than the default white one by default. There doesn't
// seem to be any standard colour to use for it, Windows itself uses both
// 0x666666 and 0x797979 for the borders in the "Colours" control panel
// window, so it doesn't seem like anybody cares about consistency here.
return *wxGREY_PEN;
}
// ----------------------------------------------------------------------------
// 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)
{
return wxDarkModeModule::GetSettings().GetColour(index);
}
wxPen GetBorderPen()
{
return wxDarkModeModule::GetSettings().GetBorderPen();
}
HBRUSH GetBackgroundBrush()
{
wxBrush* const brush =
@ -457,14 +531,15 @@ struct MenuBarDrawMenuItem
MenuBarMenuItem mbmi;
};
constexpr COLORREF COL_STANDARD = 0xffffff;
constexpr COLORREF COL_DISABLED = 0x6d6d6d;
constexpr COLORREF COL_MENU_HOT = 0x414141;
wxColour GetMenuColour(wxMenuColour which)
{
return wxDarkModeModule::GetSettings().GetMenuColour(which);
}
HBRUSH GetMenuBrush()
HBRUSH GetMenuBrush(wxMenuColour which = wxMenuColour::StandardBg)
{
wxBrush* const brush =
wxTheBrushList->FindOrCreateBrush(GetColour(wxSYS_COLOUR_MENU));
wxTheBrushList->FindOrCreateBrush(GetMenuColour(which));
return brush ? GetHbrushOf(*brush) : 0;
}
@ -568,9 +643,11 @@ HandleMenuMessage(WXLRESULT* result,
HBRUSH hbr = 0;
int partState = 0;
wxMenuColour colText = wxMenuColour::StandardFg;
if ( itemState & ODS_INACTIVE )
{
partState = MBI_DISABLED;
colText = wxMenuColour::DisabledFg;
}
else if ( (itemState & ODS_GRAYED) && (itemState & ODS_HOTLIGHT) )
{
@ -579,15 +656,13 @@ HandleMenuMessage(WXLRESULT* result,
else if ( itemState & ODS_GRAYED )
{
partState = MBI_DISABLED;
colText = wxMenuColour::DisabledFg;
}
else if ( itemState & (ODS_HOTLIGHT | ODS_SELECTED) )
{
partState = MBI_HOT;
auto* const
brush = wxTheBrushList->FindOrCreateBrush(COL_MENU_HOT);
if ( brush )
hbr = GetHbrushOf(*brush);
hbr = GetMenuBrush(wxMenuColour::HotBg);
}
else
{
@ -607,9 +682,7 @@ HandleMenuMessage(WXLRESULT* result,
DTTOPTS textOpts;
textOpts.dwSize = sizeof(textOpts);
textOpts.dwFlags = DTT_TEXTCOLOR;
textOpts.crText = itemState & (ODS_INACTIVE | ODS_GRAYED)
? COL_DISABLED
: COL_STANDARD;
textOpts.crText = wxColourToRGB(GetMenuColour(colText));
DWORD drawTextFlags = DT_CENTER | DT_SINGLELINE | DT_VCENTER;
if ( itemState & ODS_NOACCEL)
@ -630,7 +703,9 @@ HandleMenuMessage(WXLRESULT* result,
#else // !wxUSE_DARK_MODE
bool wxApp::MSWEnableDarkMode(int WXUNUSED(flags))
bool
wxApp::MSWEnableDarkMode(int WXUNUSED(flags),
wxDarkModeSettings* WXUNUSED(settings))
{
return false;
}
@ -656,6 +731,11 @@ wxColour GetColour(wxSystemColour WXUNUSED(index))
return wxColour();
}
wxPen GetBorderPen()
{
return wxPen{};
}
HBRUSH GetBackgroundBrush()
{
return 0;

View file

@ -42,6 +42,7 @@
#include "wx/msw/private.h"
#include "wx/msw/missing.h"
#include "wx/msw/dc.h"
#include "wx/msw/private/darkmode.h"
#include "wx/msw/private/winstyle.h"
namespace
@ -548,7 +549,49 @@ void wxStaticBox::PaintBackground(wxDC& dc, const RECT& rc)
void wxStaticBox::PaintForeground(wxDC& dc, const RECT&)
{
wxMSWDCImpl *impl = (wxMSWDCImpl*) dc.GetImpl();
MSWDefWindowProc(WM_PAINT, (WPARAM)GetHdcOf(*impl), 0);
// Optionally use this pen to draw a border which has less contrast in dark
// mode than the default white box which is "too shiny"
wxPen penBorder;
if ( wxMSWDarkMode::IsActive() )
{
penBorder = wxMSWDarkMode::GetBorderPen();
}
if ( penBorder.IsOk() )
{
const wxRect clientRect = GetClientRect();
wxRect rect = clientRect;
wxDCBrushChanger brushChanger(dc, *wxTRANSPARENT_BRUSH);
wxDCPenChanger penChanger(dc, penBorder);
if ( !m_labelWin && !GetLabel().empty() )
{
// if the control has a font, use it
wxDCFontChanger fontChanger(dc);
if ( GetFont().IsOk() )
{
dc.SetFont(GetFont());
}
// Make sure that the label is vertically aligned with the border
wxCoord width, height;
// Use "Tp" as our sampling text to get the
// maximum height from the current font
dc.GetTextExtent(L"Tp", &width, &height);
// adjust the border height & Y coordinate
int offsetFromTop = (height / 2) + LABEL_VERT_BORDER;
rect.SetTop(offsetFromTop);
rect.SetHeight(rect.GetHeight() - offsetFromTop);
}
dc.DrawRectangle(rect);
}
else
{
MSWDefWindowProc(WM_PAINT, (WPARAM)GetHdcOf(*impl), 0);
}
#if wxUSE_UXTHEME
// when using XP themes, neither setting the text colour nor transparent