Skip to main content

An accessible data grid for wxPython built on a native virtual wx.ListCtrl, read directly by NVDA, JAWS, and VoiceOver with no WebView: instant population at any size, native row navigation, and native multi-select.

Project description

wx-accessible-grid

An accessible, editable data grid for wxPython that a blind person can actually use. It is a real native wx control: a virtual wx.ListCtrl in report mode. NVDA, JAWS, and VoiceOver read it directly, the way they read any native list, with no WebView and no HTML in the path.

Earlier versions rendered a semantic ARIA grid into a WebView. That worked, but it put a browser document between your data and the screen reader, depended on a WebView2/WKWebView runtime, and paged the DOM to stay fast. The native virtual wx.ListCtrl, proven in Versatile Radio Programmer's channel grid, reads each row correctly as you arrow through it, populates instantly at any size, and needs no browser runtime. That is the library's approach now.

It is built for data entry, not a spreadsheet engine. There are no formulas. What there is, is a native grid that is fully keyboard-operable and announced correctly, with editing that round-trips through your model.

What you get

  • A native virtual list (LC_REPORT | LC_VIRTUAL). Virtual means rows are pulled on demand through a single OnGetItemText callback, so a grid with thousands of rows populates instantly and there is no paging. The screen reader still gets a correct sense of "row N of many" from the native list itself.
  • Up and down move by row; left and right move by cell. Arrow up and down and the screen reader reads the focused row, because it is a genuine native list item, not a styled <div> or an ARIA emulation. Arrow left and right and a cell cursor walks the columns of that row, speaking each cell as "value, column name" so you can read across a row one field at a time. (A native list will not announce per cell on its own, so the grid voices the cell through an announce callback you pass in; see below.)
  • Multi-select by default. Selecting rows for a bulk operation (move, reorder, delete a region) is a native selection the user already knows how to drive. The host reads the selected rows, acts through the model, and refreshes.
  • Selection and focus helpers that keep the screen reader honest. A native list item is only spoken when its control has system focus, so moving to a row takes focus to the grid, makes the row visible, and ensures it is read. Restoring a moved block re-selects the whole block and focuses the first row.
  • Editing round-trips through your model, so the value the screen reader confirms is the validated, normalized one, never the raw keystrokes. If an edit is rejected the user hears why.
  • A pure-Python model with no wx in it. Columns, row data, selection math, and cell formatting are all plain Python, so they are unit-testable headless, without a display.

Install

pip install wx-accessible-grid

That pulls in wxPython.

Use it

Describe your columns and provide the row data through a model, then drop the grid into a sizer. The grid is a virtual wx.ListCtrl, so it asks your model for each cell's text as it paints, rather than holding every row in the control.

import wx
from wx_accessible_grid import AccessibleGrid, GridModel, Column

class ChannelModel(GridModel):
    def __init__(self, rows):
        self._rows = rows
        self._cols = [
            Column("num", "#", is_row_header=True),
            Column("name", "Name"),
            Column("mode", "Mode"),
            Column("active", "Active"),
            Column("volume", "Volume"),
        ]

    def columns(self):
        return self._cols

    def row_count(self):
        return len(self._rows)

    def cell_text(self, row, column):
        # the text the list paints and the screen reader reads for this cell
        if column == "num":
            return str(row + 1)
        val = self._rows[row][column]
        if column == "active":
            return "Yes" if val else "No"
        return str(val)

grid = AccessibleGrid(panel, ChannelModel(rows), label="Memory channels",
                      announce=speak)
sizer.Add(grid.control, 1, wx.EXPAND)

announce is how left/right speaks. A native list does not announce per cell, so when the cell cursor moves the grid calls announce("146.520, Frequency") and your app says it however it speaks elsewhere (VRP routes this to prism, plus the status bar). Leave announce off and left/right still move the cursor, just silently. Up/down need nothing here: the native list reads the row itself.

The grid exposes the native selection and the cell cursor so the host can act on them:

nums = grid.selected_rows()      # selected rows, or the focused row if none
grid.select_rows([3, 4, 5])      # replace the selection
grid.focus_row(3)                # move focus there and make sure it is read
grid.refresh_rows([3])           # repaint just those rows after an edit
row, col = grid.current_cell()   # where the left/right cursor is, for host editing

The dev extra (pip install "wx-accessible-grid[dev]") adds pytest for the model tests, which run without wx.

Keyboard

  • Arrow up and down: move the focused row. The screen reader reads the row as a native list item.
  • Arrow left and right: move the cell cursor across the columns of the focused row. Each move speaks "value, column name". The cursor stops at the first and last columns; it does not wrap.
  • Home / End: first / last row. Page Up / Page Down: a screenful at a time. All standard native wx.ListCtrl navigation works, because it is a native list.
  • Space and Ctrl+Space: extend or toggle the native selection.
  • Editing and row actions are wired by the host through the selection and focus helpers (for example, a native edit dialog or a context menu on Shift+F10), so they use real native controls that read correctly.

How it works

AccessibleGrid wraps a wx.ListCtrl created with LC_REPORT | LC_VIRTUAL. Report mode gives it real column headers; virtual mode means it never stores the rows itself: it calls your model's cell_text(row, column) for each visible cell as it paints. Because it is a native control, the platform accessibility layer (UIA on Windows, NSAccessibility on macOS, AT-SPI on Linux) exposes the rows directly to the screen reader. No HTML, no WebView bridge, no injected JavaScript.

The model carries all the data and the index/number and selection arithmetic, in plain Python with no wx, so it can be tested headless. The grid widget is a thin shell over the native list plus the selection and focus helpers that make sure a moved or edited row is both selected and actually spoken.

Left and right are the one thing the native list cannot do for itself: it has no per-cell cursor. So the grid keeps its own current-column index, handles the left/right keys, and on each move reads the cell from the model and hands the text to your announce callback. Up and down stay entirely native, so the row read you get is the real one from the platform, not something reconstructed.

Status

This is the native build. The library moved off the WebView-hosted ARIA grid to a native virtual wx.ListCtrl after the native approach proved out in Versatile Radio Programmer's channel grid (instant population at full radio size, correct per-row reading, native multi-select). 0.6.0 adds the left/right cell cursor so you can read across a row a field at a time, not just row by row. Tested on macOS and in headless unit tests for the model and navigation. NVDA and JAWS verification on Windows, with a host that wires announce to speech, is the next milestone; reports welcome.

License

MIT. A Community Access open-source project, created by Taylor Arndt.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wx_accessible_grid-0.6.1.tar.gz (44.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wx_accessible_grid-0.6.1-py3-none-any.whl (11.5 kB view details)

Uploaded Python 3

File details

Details for the file wx_accessible_grid-0.6.1.tar.gz.

File metadata

  • Download URL: wx_accessible_grid-0.6.1.tar.gz
  • Upload date:
  • Size: 44.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for wx_accessible_grid-0.6.1.tar.gz
Algorithm Hash digest
SHA256 c8961c3a59e4c3e4e3207e41a344c07caf459dc0acd3dc0066bbb62d4d93449e
MD5 7e95ec2b4b7de5a9711feee1b11ca792
BLAKE2b-256 8ad8c763944905a667f4552b2abd634aae69c571e896a806aa97eafc958f738c

See more details on using hashes here.

File details

Details for the file wx_accessible_grid-0.6.1-py3-none-any.whl.

File metadata

  • Download URL: wx_accessible_grid-0.6.1-py3-none-any.whl
  • Upload date:
  • Size: 11.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.11 {"installer":{"name":"uv","version":"0.11.11","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for wx_accessible_grid-0.6.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f51a7b960de41609cc0eb063ec58972f1054b483eb5160d27dcfece154b70d1b
MD5 b25c0c3d445ed81ebbb81fcf545cf4b3
BLAKE2b-256 0ef282e6c3613ee53899cf071a03ccc3ea8b821985e4a0c1c28603ba2d347c37

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page