Widgets¶
The @widget decorator and make() factory are the foundation of QtPie. They transform verbose Qt code into clean, declarative definitions.
The @widget Decorator¶
The @widget decorator turns a class into a fully-functional Qt widget with automatic layout and lifecycle management.
Basic Usage¶
This creates a widget with:
- A vertical layout (default)
- An auto-generated object name ("My" - derived from class name)
- Automatic initialization (no __init__ needed)
With or Without Parentheses¶
Both forms work:
Use parentheses when you need to pass parameters.
Declaring Child Widgets¶
Child widgets are declared as typed fields. QtPie automatically adds them to the layout in declaration order.
Using make()¶
The make() factory provides clean syntax for creating widgets:
from qtpie import widget, make
from qtpy.QtWidgets import QLabel, QPushButton, QWidget
@widget
class MyWidget(QWidget):
label: QLabel = make(QLabel, "Hello World")
button: QPushButton = make(QPushButton, "Click Me")
This is much cleaner than the traditional approach:
# Traditional Qt - verbose!
from dataclasses import field
@widget()
class MyWidget(QWidget):
label: QLabel = field(default_factory=lambda: QLabel("Hello World"))
button: QPushButton = field(default_factory=lambda: QPushButton("Click Me"))
Constructor Arguments¶
Pass any arguments the widget's constructor accepts:
from qtpy.QtWidgets import QLineEdit, QSlider
from qtpy.QtCore import Qt
@widget
class MyWidget(QWidget):
# Positional args
label: QLabel = make(QLabel, "Initial Text")
# Keyword args for properties
edit: QLineEdit = make(QLineEdit, placeholderText="Enter name")
# Multiple args
slider: QSlider = make(QSlider, Qt.Orientation.Horizontal)
Any keyword argument that isn't a signal name is set as a property on the widget.
Signal Connections¶
Connect signals right in the make() call - no separate wiring needed.
Method Name (String)¶
@widget
class MyWidget(QWidget):
button: QPushButton = make(QPushButton, "Click", clicked="on_click")
click_count: int = 0
def on_click(self):
self.click_count += 1
The string "on_click" tells QtPie to connect the clicked signal to the on_click method.
Lambda or Callable¶
@widget
class MyWidget(QWidget):
button: QPushButton = make(QPushButton, "Click", clicked=lambda: print("Clicked!"))
Multiple Signals¶
from qtpy.QtWidgets import QSlider
@widget
class MyWidget(QWidget):
slider: QSlider = make(
QSlider,
valueChanged="on_value_changed",
sliderReleased="on_released"
)
def on_value_changed(self, value: int):
print(f"Value: {value}")
def on_released(self):
print("Released!")
Layout Types¶
Control the layout with the layout parameter.
Vertical (Default)¶
@widget # or @widget(layout="vertical")
class MyWidget(QWidget):
first: QLabel = make(QLabel, "Top")
second: QLabel = make(QLabel, "Bottom")
Widgets stack top-to-bottom.
Horizontal¶
@widget(layout="horizontal")
class MyWidget(QWidget):
left: QLabel = make(QLabel, "Left")
right: QLabel = make(QLabel, "Right")
Widgets flow left-to-right.
No Layout¶
No layout is created. Useful when you need manual layout control.
Widget Naming¶
The name parameter sets the widget's objectName (used for QSS styling).
Auto-Generated Name¶
Explicit Name¶
CSS Classes¶
The classes parameter adds CSS-like classes for styling.
@widget(classes=["card", "shadow"])
class MyWidget(QWidget):
label: QLabel = make(QLabel, "Styled!")
This sets a class property on the widget that can be used in QSS selectors. See the Styling guide for details.
Field Naming Conventions¶
QtPie uses underscore conventions to control field behavior:
| Field | Layout | Auto-bind |
|---|---|---|
foo |
✅ Added | to foo |
_foo |
✅ Added | to foo (strips _) |
_foo_ |
❌ Excluded | ❌ None |
Private Fields (_foo)¶
Fields starting with _ are included in the layout and auto-bind with the underscore stripped:
@widget
class MyWidget(QWidget, Widget[Person]):
# Added to layout, auto-binds to record.name
_name: QLineEdit = make(QLineEdit)
# Added to layout, auto-binds to record.age
_age: QSpinBox = make(QSpinBox)
This is useful when you want encapsulation (pyright will warn about external access) but still want the widget in the layout.
Excluded Fields (_foo_)¶
Fields that start AND end with _ are excluded from the layout and do not auto-bind:
@widget
class MyWidget(QWidget):
public: QLabel = make(QLabel, "Visible")
_excluded_: QLabel = make(QLabel, "Not in layout")
Only public will be added to the layout. _excluded_ still exists as an attribute but you control its placement manually.
Non-Widget Fields¶
You can mix widget and non-widget fields:
@widget
class MyWidget(QWidget):
label: QLabel = make(QLabel, "Count: 0")
button: QPushButton = make(QPushButton, "+1", clicked="increment")
# Non-widget fields
count: int = 0
name: str = "Counter"
def increment(self):
self.count += 1
self.label.setText(f"Count: {self.count}")
Only the QLabel and QPushButton are added to the layout. The int and str fields are just regular attributes.
Lifecycle Hook¶
Override setup() to hook into the widget lifecycle:
from typing import override
@widget
class MyWidget(QWidget):
label: QLabel = make(QLabel, "Initial")
@override
def setup(self):
# Called after fields initialized
self.label.setText("Modified in setup")
Accessing Child Widgets¶
All child widgets are available as typed attributes:
@widget
class MyWidget(QWidget):
label: QLabel = make(QLabel, "Hello")
button: QPushButton = make(QPushButton, "Click", clicked="on_click")
def on_click(self):
# Access child widgets directly
current = self.label.text()
self.label.setText(f"{current}!")
The self.label and self.button attributes are fully typed - your IDE will autocomplete their methods.
Complete Example¶
Here's a working counter widget that demonstrates many features:
from qtpie import widget, make
from qtpy.QtWidgets import QLabel, QPushButton, QWidget
@widget(name="Counter", classes=["card"])
class CounterWidget(QWidget):
label: QLabel = make(QLabel, "Count: 0")
increment_btn: QPushButton = make(QPushButton, "+1", clicked="increment")
decrement_btn: QPushButton = make(QPushButton, "-1", clicked="decrement")
reset_btn: QPushButton = make(QPushButton, "Reset", clicked="reset")
count: int = 0
def increment(self):
self.count += 1
self._update_display()
def decrement(self):
self.count -= 1
self._update_display()
def reset(self):
self.count = 0
self._update_display()
def _update_display(self):
self.label.setText(f"Count: {self.count}")
What's Next?¶
- Learn about Layouts - form, grid, and nested layouts
- Explore Signals - advanced signal patterns
- Add Styling - CSS classes and SCSS
For reactive data binding that eliminates manual updates, see Reactive State.