Understanding NVDAโs object system for representing GUI controls
NVDA Objects are the fundamental building blocks that represent controls and GUI elements in NVDA. Every widget, regardless of its underlying API, is represented as an NVDAObject instance.
The NVDAObject class is defined in source/NVDAObjects/__init__.py:
NVDAObject base class
class NVDAObject( documentBase.TextContainerObject, baseObject.ScriptableObject, metaclass=DynamicNVDAObjectType,): """NVDA's representation of a single control/widget. Every widget, regardless of how it is exposed by an application or the operating system, is represented as a single NVDAObject instance. """ cachePropertiesByDefault = True TextInfo = NVDAObjectTextInfo # Core properties implemented via auto-property pattern def _get_name(self) -> str: """The label or name of this object""" return "" def _get_role(self) -> controlTypes.Role: """The role or type of control""" return controlTypes.Role.UNKNOWN def _get_value(self) -> str: """The value of the control""" return "" def _get_states(self) -> Set[controlTypes.State]: """Set of state constants""" return set()
from NVDAObjects.IAccessible import IAccessibleclass CustomButton(IAccessible): def _get_name(self): # Override name from IAccessible return self.IAccessibleObject.accName(self.IAccessibleChildID) or "Unnamed" def _get_role(self): return controlTypes.Role.BUTTON
One of NVDAโs most powerful features is dynamic class creation. When an object is instantiated:
Dynamic class creation flow
class DynamicNVDAObjectType(baseObject.ScriptableObject.__class__): _dynamicClassCache = {} def __call__(self, chooseBestAPI=True, **kwargs): # 1. Find the best API class if chooseBestAPI: APIClass = self.findBestAPIClass(kwargs) if not APIClass: return None # 2. Instantiate the API class obj = APIClass.__new__(APIClass, **kwargs) obj.__init__(**kwargs) # 3. Build overlay class list clsList = [] obj.findOverlayClasses(clsList) # 4. Let app module choose classes if obj.appModule: obj.appModule.chooseNVDAObjectOverlayClasses(obj, clsList) # 5. Let global plugins choose classes for plugin in globalPluginHandler.runningPlugins: plugin.chooseNVDAObjectOverlayClasses(obj, clsList) # 6. Construct dynamic class bases = tuple(optimized_bases) name = "Dynamic_" + "_".join([x.__name__ for x in clsList]) newCls = type(name, bases, {"__module__": __name__}) # 7. Mutate object to new class obj.__class__ = newCls # 8. Initialize overlay classes for cls in reversed(newCls.__mro__): if hasattr(cls, 'initOverlayClass'): cls.initOverlayClass(obj) return obj
This allows NVDA to combine generic API support with application-specific behaviors without modifying core code.
from NVDAObjects.IAccessible import IAccessiblefrom scriptHandler import scriptimport uiimport controlTypesclass EnhancedEditField(IAccessible): """Custom edit field with additional functionality""" def _get_name(self): # Customize the name return f"Enhanced: {super().name}" def _get_description(self): # Add helpful description return f"Length: {len(self.value)} characters" @script(gesture="kb:NVDA+l") def script_reportLength(self, gesture): """Report the length of text in this field""" length = len(self.value) if self.value else 0 ui.message(f"{length} characters") def initOverlayClass(self): """Called when this overlay is applied to an object""" # Perform initialization pass
import appModuleHandlerfrom NVDAObjects.IAccessible import IAccessibleimport controlTypesclass AppModule(appModuleHandler.AppModule): def chooseNVDAObjectOverlayClasses(self, obj, clsList): """Choose custom classes for objects in this app""" # Check if this is an edit field we want to enhance if ( isinstance(obj, IAccessible) and obj.role == controlTypes.Role.EDITABLETEXT and obj.windowClassName == "Edit" ): # Insert our custom class at the beginning clsList.insert(0, EnhancedEditField)
Insert custom classes at index 0 so they are resolved first in the method resolution order (MRO).
States indicate the current condition of a control:
Using states
import controlTypesdef processObject(obj): states = obj.states # Check for specific states if controlTypes.State.FOCUSED in states: print("Object has focus") if controlTypes.State.CHECKED in states: print("Checkbox is checked") if controlTypes.State.EXPANDED in states: print("Tree item is expanded") # Common states # FOCUSABLE, FOCUSED, SELECTED, SELECTABLE # CHECKED, CHECKABLE, PRESSED, EXPANDED, COLLAPSED # READONLY, DISABLED, INVISIBLE, OFFSCREEN # BUSY, LINKED, VISITED, PROTECTED
# Get a TextInfo objecttextInfo = obj.makeTextInfo(textInfos.POSITION_FIRST)# Move and read texttextInfo.expand(textInfos.UNIT_LINE)line_text = textInfo.text# Get all textstoryInfo = obj.makeTextInfo(textInfos.POSITION_ALL)all_text = storyInfo.text
class ContainerObject(NVDAObject): def _get_focusRedirect(self): """When this object receives focus, redirect to its first child""" return self.firstChild
Properties are cached by default for performance. To disable caching for a specific property:
class MyObject(NVDAObject): _cache_dynamicProperty = False def _get_dynamicProperty(self): # This will be fetched every time return self._getCurrentValue()
Overlay Class Order
Classes in clsList should be ordered from most specific to most general:
def chooseNVDAObjectOverlayClasses(self, obj, clsList): # Most specific first clsList.insert(0, VerySpecificClass) # More general after clsList.append(GeneralClass)
Memory Management
NVDA uses weak references for object relationships. Donโt store strong references unnecessarily:
# Good - uses weak referenceself._parentRef = weakref.ref(parent)# Bad - strong reference creates memory leakself._parent = parent
API-Specific Properties
Only access API-specific properties when you know the object type:
from NVDAObjects.IAccessible import IAccessibleif isinstance(obj, IAccessible): accName = obj.IAccessibleObject.accName(obj.IAccessibleChildID)