diff --git a/include/wx/msw/checkbox.h b/include/wx/msw/checkbox.h index feb20c97de..bc4c53c2e8 100644 --- a/include/wx/msw/checkbox.h +++ b/include/wx/msw/checkbox.h @@ -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; diff --git a/include/wx/msw/ownerdrawnbutton.h b/include/wx/msw/ownerdrawnbutton.h index e243a987ad..302143d8d1 100644 --- a/include/wx/msw/ownerdrawnbutton.h +++ b/include/wx/msw/ownerdrawnbutton.h @@ -125,6 +125,8 @@ public: return MSWDrawButton(item) || Base::MSWOnDraw(item); } + using wxMSWOwnerDrawnButtonBase::MSWIsOwnerDrawn; + protected: bool IsOwnerDrawn() const { return MSWIsOwnerDrawn(); } }; diff --git a/include/wx/msw/private/accessible.h b/include/wx/msw/private/accessible.h new file mode 100644 index 0000000000..13c9977a62 --- /dev/null +++ b/include/wx/msw/private/accessible.h @@ -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 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_ diff --git a/include/wx/msw/radiobut.h b/include/wx/msw/radiobut.h index 6fee8acb74..f66c5b2f00 100644 --- a/include/wx/msw/radiobut.h +++ b/include/wx/msw/radiobut.h @@ -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; diff --git a/src/msw/checkbox.cpp b/src/msw/checkbox.cpp index a38645b18f..25e8abcd71 100644 --- a/src/msw/checkbox.cpp +++ b/src/msw/checkbox.cpp @@ -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 +{ +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 diff --git a/src/msw/radiobut.cpp b/src/msw/radiobut.cpp index d9b2c9cc23..2af2263be4 100644 --- a/src/msw/radiobut.cpp +++ b/src/msw/radiobut.cpp @@ -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 +{ +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