User Guide - CWidgets Library V0.1.1a1:
CWidgets: Your Power in Programming Without Limits
Turn Qt obstacles into creative opportunities
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. From now on, you can design interfaces with multi-line edit boxes with complete freedom and compatibility.
The library provides absolute compatibility for blind developers, as all elements are designed to work seamlessly with the NVDA screen reader.
Continuous creativity: No need to learn new tools, just continue writing the code you're used to while keeping the same functions and features.
Smart engineering: Every element in CWidgets inherits properties from the original Qt elements, ensuring standard performance with radical solutions to compatibility issues.
What will change in how you write your programs using PyQT6 and PySide6 windows?
All you need to do is replace the first letter only (C instead of Q):
CTextEdit instead of QTextEdit.
CButton instead of QPushButton.
Where C stands for Custom or customized.
Complete flexibility in work, as the library supports your preferred working environment with the same efficiency:
With PySide6: from cwidgets.pyside6 import CButton, CLabel, CLineEdit.... With PyQt6: from cwidgets.pyqt6 import CButton, CLabel, CLineEdit .....
CWidgets: Code with confidence, design without limits.
7 fully compatible custom elements provided by this library:
pip install cwidgets
After installation, start exploring the library, its components, properties, and how to use them through these functions:
import cwidgets # List all available components cwidgets.widgets() # List all available sections cwidgets.sections() # Open the full guide in the browser cwidgets.show_help() cwidgets.show_help(lang="en") # Open a specific component directly cwidgets.show_help(lang="en", goto="CButton") cwidgets.show_help(lang="en", goto="CTextEdit") # Open a specific section directly cwidgets.show_help(lang="en", goto="introduction") cwidgets.show_help(lang="en", goto="installation")
#CTextEdit: Multi-line edit box: CTextEdit solves the fundamental problem of QTextEdit, which is incompatible with NVDA.
#CComboBox & CListWidget: Separation of navigation from activation: Separate navigation actions (arrows) from activation actions (Enter/Space) to avoid unintended activations. Disabling available: Maintain NVDA announcement even when the component is disabled.
#CButton: Activation with ENTER, RETURN, SPACE & mouse. Compatibility even when disabled.
#CLabel: Enhanced compatibility for titles and labels
#CLineEdit: Allows retrieving text from within the element by pressing enter without any additional code.
#CMessageBox: Self-closing message dialogs. You can specify a time after which the dialog disappears automatically.
#Except for CTextEdit, all other components inherit from QT and thus retain all their basic properties and functions.
Definition, creation, properties, usage.
CTextEdit is a multi-line edit box fully compatible with NVDA.
This is the barrier that prevents blind developers from designing QT interfaces containing multi-line edit boxes.
Until the emergence of this library, no solutions to this problem were known to be used by blind developers.
The solution provided by this library is to integrate a Win32 RichEdit control directly into the QT window.
Win32 RichEdit is fully compatible with NVDA by default.
The complex phase in designing the solution is integrating it into a Qt window while maintaining this accessibility.
This library also ensured the default use of commands in QT.
QTextEdit element structure:
-QTextEdit
-EditorStyle — Styles, font, color, alignment
-RichEdit Win32 — The original compatible engine
# PySide6
from cwidgets.pyside6 import CTextEdit
# PyQt6
from cwidgets.pyqt6 import CTextEdit
# Creation
self.editor = CTextEdit(self, accessible_name="Box name")
layout.addWidget(self.editor)
# Insert text into the box
self.editor.setText("Hello!")
# Retrieve text from the box:
text = self.editor.text()
text = self.editor.toPlainText()
# Add text to the original text
self.editor.append("New line.")
# Clear text to empty the box
self.editor.clear()
# Make the box read-only
self.editor.setReadOnly(True)
# Disable read-only to make the box writable and readable
self.editor.setReadOnly(False)
# Font — QFont or (str, int, bool, bool)
self.editor.setFont("Arial", 12, True, False) # Name, size, bold, italic
# Colors — name or RGB set
self.editor.setTextColor("red")
self.editor.setTextColor((255, 0, 0))
self.editor.setBackgroundColor("yellow")
# Alignment
self.editor.setAlignment("left")
self.editor.setAlignment("center")
self.editor.setAlignment("right")
# 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 set
(255, 0, 0) # red
(0, 128, 255) # light blue
accessible_name is read by NVDA but is not visually visible.
To add a visual title, add a CLabel to the layout before the editor:
self.editor = CTextEdit(self)
self.lbl = CLabel("Edit box:", self, self.editor)
It is essential to note that adding CLabel to the layout must precede adding the editor so that the box name appears above the box and not below it.
layout.addWidget(self.lbl)
layout.addWidget(self.editor)
CLabel is an NVDA-compatible label that replaces QLabel.
The original QLabel is invisible to NVDA unless linked to a buddy — and only when that buddy has focus.
Two modes:
- Single mode: NVDA-compatible and recognized even with tab navigation.
- Buddy mode: When CLabel is linked to another element.
# PySide6
from cwidgets.pyside6 import CLabel, CLineEdit
# PyQt6
from cwidgets.pyqt6 import CLabel, CLineEdit
# Single mode — NVDA reads the text and can be accessed via tab:
self.lbl = CLabel("File saved", self)
# With prefix — NVDA announces: "status File saved"
self.lbl = CLabel("File saved", self, prefix="status")
# Buddy mode — NVDA announces the label when the field gets focus
# The label must be added to the layout before the edit field so that the label appears above the field visually
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 is a button with additional features that make it usable without needing extra code lines that the original button would require to enable these features.
# 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 — NVDA announces "unavailable", button is unclickable
self.btn.setEnabled(False)
self.btn.setEnabled(True)
# Change title — original Qt
self.btn.setText("New title")
# Check status
if self.btn.isEnabled():
...
CLineEdit is a compatible text input field that replaces QLineEdit.
QLineEdit requires an additional line of code to retrieve text.
CLineEdit makes this activation automatic via the validated signal.
# PySide6
from cwidgets.pyside6 import CLineEdit, CLabel
# PyQt6
from cwidgets.pyqt6 import CLineEdit, CLabel
# Create the field first for buddy
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 text
self.edit = CLineEdit(self, placeholderText="Enter your name...")
# validated signal
self.edit.validated.connect(self.on_validated)
def on_validated(self, text):
print(text)
# Show/hide — original Qt
self.edit.hide()
self.edit.show()
CComboBox is a compatible dropdown list that replaces QComboBox.
The original QComboBox is activated internally by arrows.
This is a problem for blind users who navigate with arrows.
Explicit separation between navigation and activation.
While maintaining compatibility even when the list is disabled.
# PySide6
from cwidgets.pyside6 import CComboBox, CLabel, CMessageBox, CButton
# PyQt6
from cwidgets.pyqt6 import CComboBox, CLabel, CMessageBox, CButton
# Create the list first for buddy
self.combo = CComboBox(self)
self.combo.addItems(["Egypt", "Tunisia", "Morocco"])
self.lbl = CLabel("Country list:", 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)
# Button to clear the list
self.btn_clear = CButton("Clear", self)
self.btn_clear.clicked.connect(self.combo.clear)
def on_selection(self):
text = self.combo.currentText()
index = self.combo.currentIndex()
CMessageBox.information(self, "Selection", f"Country: {text}")
def on_cleared(self):
CMessageBox.warning(self, "Warning", "No items available in the list.")
# Disable / re-enable
self.combo.setEnabled(False)
self.combo.setEnabled(True)
CListWidget is a compatible list that replaces QListWidget.
The original QListWidget is activated immediately when navigating using arrows.
This is an obstacle for blind users, and disabling this activation with arrows requires writing additional code lines.
# PySide6
from cwidgets.pyside6 import CListWidget, CLabel
# PyQt6
from cwidgets.pyqt6 import CListWidget, CLabel
# Create the list
self.list = CListWidget(self)
self.list.addItems(["Iraq", "Saudi Arabia", "Kuwait"])
self.lbl = CLabel("Country list:", self, self.list)
layout.addWidget(self.lbl)
layout.addWidget(self.list)
# Original Qt signal — matching QListWidget
self.list.itemActivated.connect(self.on_item)
def on_item(self, item):
text = item.text()
row = self.list.currentRow()
print(row, text)
# Disable / re-enable
self.list.setEnabled(False)
self.list.setEnabled(True)
# Clear — original Qt
self.list.clear()
CMessageBox is a compatible message dialog.
The original QMessageBox does not provide automatic closing.
It is needed for messages that do not require user intervention.
Adding a mode where the message closes after a specified time.
# PySide6
from cwidgets.pyside6 import CMessageBox
# PyQt6
from cwidgets.pyqt6 import CMessageBox
# Non-timed information
CMessageBox.information(self, "Success", "File saved.")
# Timed information — auto-close after 3 seconds
CMessageBox.information(self, "Success", "File saved.", timeout=3000)
# Non-timed warning
CMessageBox.warning(self, "Warning", "Insufficient disk space.")
# Timed warning
CMessageBox.warning(self, "Warning", "Unstable connection.", timeout=4000)
# Error — non-timed + sound — manual closing mandatory
CMessageBox.critical(self, "Error", "File not found.")
# It is recommended to use timeout only for information and warning
# Timeout is not available for critical
# Not recommended — replaces label
self.combo.setAccessibleName("...")
# Correct
self.lbl = CLabel("Country list:", self, self.combo)
# Correct — element is created before the label
self.combo = CComboBox(self)
self.lbl = CLabel("Country:", self, self.combo)
Provided by the library:
- validate_parent — Regular parent window check
- logger — Error logging (module "cwidgets")
Mohamed Hédi Bettaieb (Tunisia)
Contact: hedidouz@gmail.com
Date: May 2026
This project aims to make Qt applications 100% accessible to blind developers and users, without sacrificing productivity or Qt developers' habits.