Blind Python Developers: QTextEdit is Finally Accessible in Qt (PyQt6 & PySide6)
Introduction
cwidgets is a specialized Python library that enhances NVDA accessibility
for PyQt6 and PySide6 interfaces.
It provides a complete alternative to QTextEdit — natively inaccessible with NVDA —
allowing blind Python developers to build interfaces containing multi-line text editors
with full NVDA compatibility.
The library supports both Qt environments:
python
# PySide6
from cwidgets.pyside6 import CTextEdit
# PyQt6
from cwidgets.pyqt6 import CTextEdit
The Problem
QTextEdit, Qt's native multi-line text editor, has serious limitations with NVDA:
Content is announced twice during navigation
Impossible to distinguish the current line from a new line
Unpredictable behavior during editing
No reliable feedback on cursor position
These limitations make QTextEdit unusable in practice for a blind developer. QLineEdit is accessible but single-line — it cannot replace QTextEdit.
Result: blind developers using PyQt6 or PySide6 had no reliable solution for integrating a multi-line text editor into their applications.
The Solution — CTextEdit
cwidgets is a Python library that solves this problem by directly integrating the native Win32 RICHEDIT20W control inside a Qt window.
RichEdit is the engine used by WordPad and many Windows applications. It is natively and fully compatible with NVDA — correct reading, precise navigation, reliable line-by-line announcement.
The complex part was integrating it into Qt while maintaining:
Full NVDA accessibility
Standard Qt API (setText, toPlainText, append, clear...)
PyQt6 and PySide6 compatibility
Tab navigation between Qt widgets
Focus restoration after Alt+Tab
All of this is handled internally by cwidgets. You use it exactly like QTextEdit.
Installation
pip install cwidgets
Requirements:
Python 3.10+
Windows (Win32 RichEdit dependency)
PyQt6 or PySide6
pywin32 (installed automatically)
Help and Documentation
After installation, explore the library directly from your code:
import cwidgets
# List all available components
cwidgets.widgets()
# List all available documentation sections
cwidgets.sections()
# Open the full guide in the browser
cwidgets.show_help()
cwidgets.show_help(lang="fr") # French
cwidgets.show_help(lang="ar") # Arabic
# Open CTextEdit documentation directly
cwidgets.show_help(lang="en", goto="CTextEdit")
# Explore CTextEdit API from code
cwidgets.ctextedit.show() # method names only
cwidgets.ctextedit.show_details() # names + descriptions
Using CTextEdit
# PySide6
from cwidgets.pyside6 import CTextEdit, CLabel
# PyQt6
from cwidgets.pyqt6 import CTextEdit, CLabel
Creation
self.editor = CTextEdit(self, accessible_name="Notes")
layout.addWidget(self.editor)
Content
# Set content
self.editor.setText("Hello from cwidgets!")
# Get content
text = self.editor.toPlainText()
text = self.editor.text() # alias for toPlainText()
# Append text at the end
self.editor.append("New line.")
# Insert at cursor position
self.editor.insertPlainText("Inserted text\n")
# Insert HTML — tags are stripped
self.editor.insertHtml("<p>Hello <b>world</b></p>")
# Clear content
self.editor.clear()
Selection
self.editor.selectAll()
text = self.editor.selectedText() # returns "" if no selection
count = self.editor.lineCount()
Properties
self.editor.setReadOnly(True)
self.editor.setReadOnly(False)
state = self.editor.isReadOnly()
Formatting
# Font
self.editor.setFont("Arial", 12, True, False) # name, size, bold, italic
# Text color — name or (R, G, B)
self.editor.setTextColor("blue")
self.editor.setTextColor((0, 0, 255))
# Background color
self.editor.setBackgroundColor("yellow")
# Alignment
self.editor.setAlignment("left")
self.editor.setAlignment("center")
self.editor.setAlignment("right")
Clipboard
# Ctrl+C/X/V/Z/Y shortcuts work natively via keyboard.
# These methods allow programmatic use, e.g. via a button.
self.editor.copy()
self.editor.cut()
self.editor.paste()
self.editor.undo()
self.editor.redo()
Signals
self.editor.textChanged.connect(self.on_text_changed)
self.editor.cursorPositionChanged.connect(self.on_cursor_changed)
self.editor.selectionChanged.connect(self.on_selection_changed)
def on_text_changed(self):
print(self.editor.toPlainText())
def on_cursor_changed(self):
print("Cursor moved")
def on_selection_changed(self):
print(self.editor.selectedText())
Visual Title with CLabel
self.editor = CTextEdit(self, accessible_name="Description")
self.lbl = CLabel("Text area:", self, self.editor)
layout.addWidget(self.lbl)
layout.addWidget(self.editor)
Behavior in Layouts
CTextEdit has a minimum size of 50x50 pixels. When sharing a QHBoxLayout with other widgets, use stretch=1:
layout = QHBoxLayout()
layout.addWidget(self.list_widget, 1)
layout.addWidget(self.editor, 1)
Other Widgets
cwidgets also enhances NVDA accessibility for six additional Qt widgets: CButton, CLabel, CLineEdit, CComboBox, CListWidget and CMessageBox. These widgets preserve the complete native Qt API while fixing known NVDA accessibility issues.
Links
PyPI: https://pypi.org/project/cwidgets/
GitHub: https://github.com/hedi-bettaieb/cwidgets
Install: pip install cwidgets
About the Author
Mohamed Hédi Bettaieb — Tunisia
A blind developer who built this library because he needed it himself.
All widgets and all APIs have been personally tested by the developer and by a group of blind Python developers.
Top comments (0)