Fix wxCheckBox and wxRadioButton accessibility in wxMSW dark mode

Previously these controls acted like generic buttons when they were
owner-drawn which was, in particular, always the case when using dark
mode, because the built-in implementation doesn't do anything useful for
owner-drawn buttons.

Implement IAccessible support for them ourselves in order to make it
work in this case too.

Closes #26184.

Closes #26187.
This commit is contained in:
Quin Gillespie
2026-02-14 22:06:01 +01:00
committed by Vadim Zeitlin
parent e43d57fd40
commit b5ea9cfe86
6 changed files with 192 additions and 0 deletions
+4
View File
@@ -58,6 +58,10 @@ public:
// implementation only from now on
virtual WXDWORD MSWGetStyle(long flags, WXDWORD *exstyle = nullptr) const override;
#if wxUSE_ACCESSIBILITY
virtual wxAccessible* CreateAccessible() override;
#endif
protected:
virtual wxSize DoGetBestClientSize() const override;
+2
View File
@@ -125,6 +125,8 @@ public:
return MSWDrawButton(item) || Base::MSWOnDraw(item);
}
using wxMSWOwnerDrawnButtonBase::MSWIsOwnerDrawn;
protected:
bool IsOwnerDrawn() const { return MSWIsOwnerDrawn(); }
};
+91
View File
@@ -0,0 +1,91 @@
///////////////////////////////////////////////////////////////////////////////
// Name: wx/msw/private/accessible.h
// Purpose: Accessible helpers for wxMSW owner-drawn controls
// Author: Quin Gillespie
// Created: 2026-02-13
// Copyright: (c) 2026 Quin Gillespie
// Licence: wxWindows licence
///////////////////////////////////////////////////////////////////////////////
#ifndef _WX_PRIVATE_ACCESSIBLE_H_
#define _WX_PRIVATE_ACCESSIBLE_H_
#include "wx/access.h"
#if wxUSE_ACCESSIBILITY
// When controls such as wxCheckBox and wxRadioButton are switched to
// owner-drawn mode (e.g. to support custom foreground colours in dark mode),
// Windows replaces the native button style with BS_OWNERDRAW. The standard
// IAccessible implementation that Windows provides via
// CreateStdAccessibleObject() inspects the window style to determine the
// control role, so it reports owner-drawn controls as generic buttons.
//
// This base class provides the correct role (specified as the template
// parameter) and state (via virtual MSWGetCheckedState() function) for
// owner-drawn controls.
//
// Note that it only takes effect when the control is actually owner-drawn;
// otherwise it returns wxACC_NOT_IMPLEMENTED to fall back to the standard
// Windows accessible.
template <class TControl, wxAccRole Role>
class wxOwnerDrawnAccessible : public wxWindowAccessible
{
public:
explicit wxOwnerDrawnAccessible(TControl* win)
: wxWindowAccessible(win)
{
}
virtual wxAccStatus GetRole(int childId, wxAccRole* role) override
{
if ( childId != wxACC_SELF )
return wxACC_NOT_IMPLEMENTED;
TControl* const ctrl = wxStaticCast(GetWindow(), TControl);
wxCHECK(ctrl, wxACC_FAIL);
if ( !ctrl->MSWIsOwnerDrawn() )
return wxACC_NOT_IMPLEMENTED;
*role = Role;
return wxACC_OK;
}
virtual wxAccStatus GetState(int childId, long* state) override
{
if ( childId != wxACC_SELF )
return wxACC_NOT_IMPLEMENTED;
TControl* const ctrl = wxStaticCast(GetWindow(), TControl);
wxCHECK(ctrl, wxACC_FAIL);
if ( !ctrl->MSWIsOwnerDrawn() )
return wxACC_NOT_IMPLEMENTED;
long st = 0;
if ( !ctrl->IsEnabled() )
st |= wxACC_STATE_SYSTEM_UNAVAILABLE;
if ( !ctrl->IsShown() )
st |= wxACC_STATE_SYSTEM_INVISIBLE;
if ( ctrl->IsFocusable() )
st |= wxACC_STATE_SYSTEM_FOCUSABLE;
if ( ctrl->HasFocus() )
st |= wxACC_STATE_SYSTEM_FOCUSED;
st |= MSWGetCheckedState(ctrl);
*state = st;
return wxACC_OK;
}
protected:
// Return checked state flags (e.g. wxACC_STATE_SYSTEM_CHECKED) for the
// current state of the control.
virtual long MSWGetCheckedState(TControl* ctrl) const = 0;
};
#endif // wxUSE_ACCESSIBILITY
#endif // _WX_PRIVATE_ACCESSIBLE_H_
+4
View File
@@ -53,6 +53,10 @@ public:
virtual WXDWORD MSWGetStyle(long style, WXDWORD *exstyle) const override;
#if wxUSE_ACCESSIBILITY
virtual wxAccessible* CreateAccessible() override;
#endif
protected:
virtual wxBorder GetDefaultBorder() const override { return wxBORDER_NONE; }
virtual wxSize DoGetBestSize() const override;
+52
View File
@@ -34,6 +34,10 @@
#include "wx/private/window.h"
#include "wx/msw/missing.h"
#if wxUSE_ACCESSIBILITY
#include "wx/msw/private/accessible.h"
#endif
// ============================================================================
// implementation
// ============================================================================
@@ -277,4 +281,52 @@ void wxCheckBox::MSWDrawButtonBitmap(wxDC& dc, const wxRect& rect, int flags)
wxRendererNative::Get().DrawCheckBox(this, dc, rect, flags);
}
#if wxUSE_ACCESSIBILITY
namespace
{
// When the checkbox is owner-drawn (e.g. in dark mode), the native BS_CHECKBOX
// style is replaced with BS_OWNERDRAW, causing the standard Windows accessible
// object to report the control as a generic button. This custom accessible
// ensures the correct role and checked state are always reported.
class wxCheckBoxAccessible
: public wxOwnerDrawnAccessible<wxCheckBox, wxROLE_SYSTEM_CHECKBUTTON>
{
public:
explicit wxCheckBoxAccessible(wxCheckBox* win)
: wxOwnerDrawnAccessible(win)
{
}
protected:
long MSWGetCheckedState(wxCheckBox* cb) const override
{
switch ( cb->Get3StateValue() )
{
case wxCHK_CHECKED:
return wxACC_STATE_SYSTEM_CHECKED;
case wxCHK_UNDETERMINED:
return wxACC_STATE_SYSTEM_MIXED;
case wxCHK_UNCHECKED:
return 0;
}
wxFAIL_MSG( wxT("unexpected Get3StateValue() return value") );
return 0;
}
};
} // anonymous namespace
wxAccessible* wxCheckBox::CreateAccessible()
{
return new wxCheckBoxAccessible(this);
}
#endif // wxUSE_ACCESSIBILITY
#endif // wxUSE_CHECKBOX
+39
View File
@@ -33,6 +33,10 @@
#include "wx/renderer.h"
#include "wx/msw/uxtheme.h"
#if wxUSE_ACCESSIBILITY
#include "wx/msw/private/accessible.h"
#endif
// ============================================================================
// wxRadioButton implementation
// ============================================================================
@@ -347,4 +351,39 @@ void wxRadioButton::MSWDrawButtonBitmap(wxDC& dc, const wxRect& rect, int flags)
wxRendererNative::Get().DrawRadioBitmap(this, dc, rect, flags);
}
#if wxUSE_ACCESSIBILITY
namespace
{
// When the radio button is owner-drawn (e.g. in dark mode), the native
// BS_RADIOBUTTON style is replaced with BS_OWNERDRAW, causing the standard
// Windows accessible object to report the control as a generic button. This
// custom accessible ensures the correct role and checked state are always
// reported.
class wxRadioButtonAccessible :
public wxOwnerDrawnAccessible<wxRadioButton, wxROLE_SYSTEM_RADIOBUTTON>
{
public:
explicit wxRadioButtonAccessible(wxRadioButton* win)
: wxOwnerDrawnAccessible(win)
{
}
protected:
long MSWGetCheckedState(wxRadioButton* rb) const override
{
return rb->GetValue() ? wxACC_STATE_SYSTEM_CHECKED : 0;
}
};
} // anonymous namespace
wxAccessible* wxRadioButton::CreateAccessible()
{
return new wxRadioButtonAccessible(this);
}
#endif // wxUSE_ACCESSIBILITY
#endif // wxUSE_RADIOBTN