Introduction

CWidgets is a specialized Python library that gives blind developers full control over PyQt6 and PySide6 interfaces.

The era of limitations with QTextEdit is over — you can now design interfaces containing multi-line edit areas with complete freedom and full NVDA compatibility.

All you have to do is replace the first letter (C instead of Q):
CTextEdit instead of QTextEdit.
CButton instead of QPushButton.
Where C stands for Custom.

The library supports both Qt environments:

# PySide6
from cwidgets.pyside6 import CButton, CLabel, CLineEdit

# PyQt6
from cwidgets.pyqt6 import CButton, CLabel, CLineEdit
    

CWidgets: Code with confidence, design without limits.

Available Components

7 fully NVDA-compatible custom components:

  1. CTextEdit
  2. CButton
  3. CLabel
  4. CLineEdit
  5. CComboBox
  6. CListWidget
  7. CMessageBox

Installation

pip install cwidgets

Accessing Help

After installation, explore the library using these functions:

import cwidgets

# List of all available components
cwidgets.widgets()

# List of all available sections
cwidgets.sections()

# Open the full guide in the browser
cwidgets.show_help()
cwidgets.show_help(lang="fr")
cwidgets.show_help(lang="ar")

# Open a specific component directly
cwidgets.show_help(lang="fr", goto="CButton")
cwidgets.show_help(lang="fr", goto="CTextEdit")

# Open a specific section directly
cwidgets.show_help(lang="fr", goto="introduction")
cwidgets.show_help(lang="fr", goto="installation")

# CTextEdit API from the terminal
import cwidgets
cwidgets.ctextedit.show()          # names only
cwidgets.ctextedit.show_details()  # names + descriptions
    

Common Issues Resolved

CTextEdit — Multi-line edit area: resolves QTextEdit's incompatibility with NVDA.

CComboBox & CListWidget — Separation of navigation/activation: prevents unintended activations during arrow key navigation. NVDA accessibility maintained even when disabled.

CButton — Activation via Enter, Return, Space, and click. NVDA compatibility maintained even when disabled.

CLabel — Improved compatibility for titles and labels.

CLineEdit — Text retrieval via Enter without additional code.

CMessageBox — Auto-closing dialog boxes with configurable delay.

With the exception of CTextEdit, all components inherit from Qt and retain all their original properties and functions.

Component Details

Definition, Creation, Properties, Usage.

CTextEdit

Definition

CTextEdit is a fully NVDA-accessible multi-line edit area, based on the native Win32 RichEdit engine.

Why — The Fundamental Problem

Qt's native QTextEdit is incompatible with NVDA.
Blind developers cannot read or write in this element.
Until this library appeared, no known solution existed.

Solution — Win32 RichEdit Integrated into Qt

The solution directly integrates a Win32 RichEdit control into the Qt window. Win32 RichEdit is natively compatible with NVDA.

Internal structure:

Features

Import

# PySide6
from cwidgets.pyside6 import CTextEdit
# PyQt6
from cwidgets.pyqt6 import CTextEdit
    

Creation

self.editor = CTextEdit(self, accessible_name="Edit area name")
layout.addWidget(self.editor)
    

Available API

# Display all methods from the terminal
import cwidgets
cwidgets.ctextedit.show()          # names only
cwidgets.ctextedit.show_details()  # names + descriptions

# From code
CTextEdit.api()
    

Content

# Set content
self.editor.setText("Hello!")

# 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 at cursor position
# Tags are removed, <br> and <p> become line breaks
self.editor.insertHtml("<p>Hello <b>world</b></p>")  # inserts: "Hello world"
self.editor.insertHtml("<p>Line 1</p><br/>Line 2")          # inserts: "Line 1\nLine 2"

# Clear content
self.editor.clear()
    

Selection

# Select all
self.editor.selectAll()

# Get selected text (returns "" if no selection)
text = self.editor.selectedText()

# Combined example — get all text
self.editor.selectAll()
text = self.editor.selectedText()
    

Properties

# Line count
count = self.editor.lineCount()

# Read-only
self.editor.setReadOnly(True)    # enable
self.editor.setReadOnly(False)   # disable
state = self.editor.isReadOnly() # check
    

Formatting

# Font — QFont or (name, size, bold, italic)
self.editor.setFont("Arial", 12, True, False)

# Text color — name or (R, G, B)
self.editor.setTextColor("red")
self.editor.setTextColor((255, 0, 0))

# Background color
self.editor.setBackgroundColor("yellow")

# Alignment
self.editor.setAlignment("left")
self.editor.setAlignment("center")
self.editor.setAlignment("right")
    

Available Colors

# Supported names
"black", "white", "red", "green", "blue", "yellow",
"cyan", "magenta", "gray", "darkgray", "lightgray",
"orange", "purple", "violet", "pink", "brown",
"navy", "teal", "lime", "olive", "maroon",
"coral", "salmon", "gold", "silver"

# Or RGB
(255, 0, 0)    # red
(0, 128, 255)  # light blue
    

Clipboard

# Shortcuts Ctrl+C/X/V/Z/Y work natively via keyboard.
# These methods allow programmatic use (e.g., via a button).

self.editor.copy()   # copy selection
self.editor.cut()    # cut selection
self.editor.paste()  # paste
self.editor.undo()   # undo
self.editor.redo()   # redo
    

Signals

# React to real-time changes
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())
    

Behavior in Layouts

CTextEdit has a minimum size of 50x50 pixels to ensure visibility.

Case 1 — QVBoxLayout (editor alone on its line)

layout.addWidget(self.editor)

Case 2 — QHBoxLayout shared with QListWidget or QComboBox
Without stretch=1, other widgets take all the space.

layout = QHBoxLayout()
layout.addWidget(self.list_widget, 1)
layout.addWidget(self.editor, 1)
    

Case 3 — Fixed dimensions

self.editor = CTextEdit(self, width=400, height=200)

Case 4 — Hide / Show

self.editor.hide()  # hides without destroying content
self.editor.show()  # redisplays with content intact
    

Optional Visual Title

accessible_name is read by NVDA but not visually visible. To add a visual title, use CLabel before the editor in the layout:

self.editor = CTextEdit(self)
self.lbl    = CLabel("Edit area:", self, self.editor)
layout.addWidget(self.lbl)
layout.addWidget(self.editor)
    

CLabel

Definition

CLabel is an NVDA-compatible label that replaces QLabel.

Why?

QLabel is invisible to NVDA unless linked to a buddy — and only when that buddy has focus.

Solution

Simple mode: NVDA-compatible, recognized via tab navigation.
Buddy mode: linked to another component, announced when that component has focus.

Features

Usage

# PySide6
from cwidgets.pyside6 import CLabel, CLineEdit
# PyQt6
from cwidgets.pyqt6 import CLabel, CLineEdit

# Simple mode
self.lbl = CLabel("File saved", self)

# With prefix — NVDA announces: "status file saved"
self.lbl = CLabel("File saved", self, prefix="status")

# Buddy mode
self.edit = CLineEdit(self)
self.lbl  = CLabel("Name:", self, self.edit)
layout.addWidget(self.lbl)
layout.addWidget(self.edit)

# Dynamic update
self.lbl.setText("Processing")
self.lbl.setPrefix("error")

CButton

Definition

CButton is an accessible button that replaces QPushButton.

Why?

QPushButton only accepts the space bar — Enter and Return are ignored.
setEnabled(False) makes the button invisible to NVDA.

Solution

Activation via Enter, Return, Space, and mouse click.
Disabled mode: not clickable but visible to NVDA ("unavailable").

Features

Usage

# PySide6
from cwidgets.pyside6 import CButton
# PyQt6
from cwidgets.pyqt6 import CButton

self.btn = CButton("Save", self)
self.btn.clicked.connect(self.on_click)

# Disable / re-enable
self.btn.setEnabled(False)
self.btn.setEnabled(True)

# Check state
if self.btn.isEnabled():
    ...
    

CLineEdit

Definition

CLineEdit is an accessible input field that replaces QLineEdit.

Why?

QLineEdit requires additional code to retrieve text upon validation. CLineEdit automates this via the validated signal.

Features

Usage

# PySide6
from cwidgets.pyside6 import CLineEdit, CLabel
# PyQt6
from cwidgets.pyqt6 import CLineEdit, CLabel

self.edit = CLineEdit(self)
self.lbl  = CLabel("Name:", self, self.edit)
layout.addWidget(self.lbl)
layout.addWidget(self.edit)

# With initial text
self.edit = CLineEdit(self, "Cairo")

# With placeholder
self.edit = CLineEdit(self, placeholderText="Enter your name...")

# validated signal
self.edit.validated.connect(self.on_validated)

def on_validated(self, text: str) -> None:
    print(text)
    

CComboBox

Definition

CComboBox is an accessible dropdown list that replaces QComboBox.

Why?

QComboBox activates the item during arrow key navigation — problematic for blind users.

Solution

Explicit separation between navigation (arrows) and activation (Enter/Space). NVDA accessibility maintained when disabled.

Features

Usage

# PySide6
from cwidgets.pyside6 import CComboBox, CLabel, CMessageBox
# PyQt6
from cwidgets.pyqt6 import CComboBox, CLabel, CMessageBox

self.combo = CComboBox(self)
self.combo.addItems(["Egypt", "Tunisia", "Morocco"])
self.lbl = CLabel("Country:", self, self.combo)
layout.addWidget(self.lbl)
layout.addWidget(self.combo)

self.combo.validated.connect(self.on_selection)
self.combo.cleared.connect(self.on_cleared)

def on_selection(self) -> None:
    text  = self.combo.currentText()
    index = self.combo.currentIndex()
    CMessageBox.information(self, "Selection", f"Country: {text}")

def on_cleared(self) -> None:
    CMessageBox.warning(self, "Warning", "Empty list.")

# Disable / re-enable
self.combo.setEnabled(False)
self.combo.setEnabled(True)
    

CListWidget

Definition

CListWidget is an accessible list that replaces QListWidget.

Why?

QListWidget activates the item immediately during navigation — an obstacle for blind users.

Solution

Separation between navigation and activation. NVDA accessibility maintained when disabled.

Features

Usage

# PySide6
from cwidgets.pyside6 import CListWidget, CLabel
# PyQt6
from cwidgets.pyqt6 import CListWidget, CLabel

self.liste = CListWidget(self)
self.liste.addItems(["Iraq", "Saudi Arabia", "Kuwait"])
self.lbl = CLabel("Country:", self, self.liste)
layout.addWidget(self.lbl)
layout.addWidget(self.liste)

self.liste.itemActivated.connect(self.on_item)

def on_item(self, item) -> None:
    text = item.text()
    row  = self.liste.currentRow()
    print(row, text)

# Disable / re-enable
self.liste.setEnabled(False)
self.liste.setEnabled(True)

# Clear
self.liste.clear()
    

CMessageBox

Definition

CMessageBox is an accessible dialog box that replaces QMessageBox.

Why?

QMessageBox does not offer automatic closing.

Solution

Added timed mode with automatic closing after a delay.

Features

Usage

# PySide6
from cwidgets.pyside6 import CMessageBox
# PyQt6
from cwidgets.pyqt6 import CMessageBox

# Information
CMessageBox.information(self, "Success", "File saved.")
CMessageBox.information(self, "Success", "File saved.", timeout=3000)

# Warning
CMessageBox.warning(self, "Warning", "Insufficient disk space.")
CMessageBox.warning(self, "Warning", "Unstable connection.", timeout=4000)

# Error — manual closing + sound
CMessageBox.critical(self, "Error", "File not found.")
    

Best Practices

1 — Titles: always use CLabel with buddy

# Not recommended
self.combo.setAccessibleName("...")

# Correct
self.lbl = CLabel("Country:", self, self.combo)
    

2 — Disabling: setEnabled(False) available on all C* components.

3 — Activation
CLineEdit and CComboBox → validated signal
CListWidget → itemActivated signal

4 — Creation order with buddy: always create the element before its CLabel.

self.combo = CComboBox(self)
self.lbl   = CLabel("Country:", self, self.combo)
    

System Requirements

Limitations

Developer

Mohamed Hédi Bettaieb (Tunisia)
Email: hedidouz@gmail.com
Design date: May 2026

Conclusion

CWidgets aims to make Qt applications 100% accessible to blind developers and users, without sacrificing productivity or Qt habits.