Keyboard-accessible native menu bars for wxPython. Plain Alt activates the bar,
Alt plus a mnemonic opens a menu, F10 works, Left and Right walk across the
top-level menus, and focus returns to your content afterward. It works in plain
native apps and, crucially, in apps where a focused wx.html2.WebView
(Edge WebView2) would otherwise eat every keystroke before the menu bar sees it.
It drives the real native menu bar. It does not draw its own menu, because a native Win32 menu is the thing NVDA and JAWS already read perfectly.
A native wx.MenuBar is the most screen-reader-friendly menu you can ship: the
OS handles Alt, mnemonics, arrow navigation, and announces everything to NVDA
and JAWS for free. The trouble is reaching it from the keyboard when something
swallows Alt first.
The worst offender is wx.html2.WebView. On Windows it is Edge WebView2, which
runs out of process and captures the keyboard at the OS level. wx never sees Alt
at all (wxWidgets issue #24786), so the menu bar simply never opens. Even a
frame-level EVT_CHAR_HOOK fires zero times, because the keys never reach wx.
Catch the keys where they actually are, then hand them to the real menu bar.
- In a native-control app the frame still sees the keys, so a frame-level key hook catches Alt, Alt plus a mnemonic, and F10.
- In a webview app the one place that still sees the keys is the page itself, so a tiny key listener is injected into the webview and bridged back over the webview's own script-message channel.
Either way, on Windows the menu bar is opened by posting the exact message Alt
normally sends (WM_SYSCOMMAND with SC_KEYMENU). That puts the genuine menu
bar into its keyboard loop, so Left and Right move across the top-level menus and
Up, Down, and Enter work natively, read correctly by the screen reader. On other
platforms it falls back to native handling and a popup, so it is safe to attach
everywhere.
pip install wx-accessible-menubarIt depends only on wxPython.
Build your menu bar as usual, with & mnemonics in the top-level labels, then
wrap it. For a plain native app:
from wx_accessible_menubar import AccessibleMenuBar
bar = wx.MenuBar()
file_menu = wx.Menu()
file_menu.Append(wx.ID_OPEN, "&Open\tCtrl+O")
bar.Append(file_menu, "&File")
# ... more menus ...
AccessibleMenuBar(self, bar, focus_target=self.text_ctrl)For an app whose content is a webview, pass the underlying
wx.html2.WebView so the page-side listener gets installed too. If you use
wx-accessible-webview, that control is AccessibleWebView.view:
AccessibleMenuBar(
self,
bar,
webview=self.web.view, # the wx.html2.WebView
focus_target=self.web.view, # return focus here after a menu closes
)That is it. Tap Alt and use the arrow keys.
AccessibleMenuBar(frame, menubar, *, focus_target=None, webview=None, restore_focus=None, bridge_name=..., bare_alt_ms=200)
frame— thewx.Framethat owns the menu bar.menubar— yourwx.MenuBar. It is set on the frame if it is not already.focus_target— awx.Windowto focus after a menu closes, after the window is maximized, and after it is reactivated. For a webview app this is the webview, so the in-page key handling keeps working.webview— an optionalwx.html2.WebView. When given, the menu keys are also caught inside the page and bridged back automatically.restore_focus— an optional callable used instead offocus_target.SetFocus.bridge_name— the script-message handler name for the webview bridge. Change it only if it collides with one you already use.bare_alt_ms— how long the native path waits before treating a lone Alt as a tap. Any other key cancels it.capture_native_keys— off by default. On a normal Windows app the OS already routes Alt to the menu bar, so we don't hook it. Turn this on only if you have a native control that eats Alt itself and you want the library to drive the menu bar for it.
- Windows is where the work happens: the synthetic Alt signal (
SC_KEYMENU) and the webview key bridge. - macOS uses the global Apple menu bar, which is already accessible: VoiceOver
reaches it with VO+M, Full Keyboard Access with Ctrl+F2. Option is a typing
key on Mac, so the library does not hijack it.
activate()andopen()are no-ops there by design; focus restoration still runs. - Linux desktops route Alt to the menu bar themselves for native-focus apps.
Wherever the synthetic driving doesn't apply, the library stays out of the way and the native, screen-reader-aware mechanism takes over.
Methods you can call yourself:
activate()— highlight the menu bar (what plain Alt and F10 do).open(index)— open a top-level menu by position.handle_bridge_message(raw)— if your app already owns the webview's script-message channel, route messages here instead of passingwebview=.
Be honest with yourself about your app. In a plain native app, Windows already routes Alt to the menu bar on its own, so here the value is consistency and the focus-restoration guarantees (the parts wx sometimes flubs after a menu closes or the window is maximized). The big, can't-live-without-it value is the webview and hybrid case, where without this the menu bar is unreachable by keyboard.
wx-accessible-webview
makes the content accessible. This makes the menu bar accessible when that content
holds focus. Together they cover the whole window.
This is a generalized extraction of the menu-bar keyboard handling built for VRP, the accessible CHIRP radio programmer, which hosts its UI in a webview and needed a menu bar a blind user could actually drive.
Taylor Arndt, a Community Access project.
See CONTRIBUTING.md. Bug reports, screen-reader testing, and pull requests are all welcome.
MIT. See LICENSE.