Form Layouts¶
Form layouts (QFormLayout) are perfect for data entry forms. They automatically arrange labels and input fields in a two-column layout, creating a clean, professional appearance.
Basic Form Layout¶
Use layout="form" with the @widget decorator, and form_label in make() to specify label text:
from qtpie import widget, make
from qtpy.QtWidgets import QWidget, QLineEdit, QSpinBox
@widget(layout="form")
class PersonForm(QWidget):
name: QLineEdit = make(QLineEdit, form_label="Name")
email: QLineEdit = make(QLineEdit, form_label="Email")
age: QSpinBox = make(QSpinBox, form_label="Age")
This creates a professional form:
How It Works¶
When you use layout="form":
- QtPie creates a
QFormLayoutfor your widget - Each field with
form_labelgets added as a row - Qt creates a
QLabelfor you automatically - Fields are arranged in declaration order
No manual QLabel creation needed - QtPie handles it all.
Real-World Example: Contact Form¶
Here's a complete contact form with various input types:
from qtpie import widget, make, entrypoint
from qtpy.QtWidgets import QWidget, QLineEdit, QSpinBox, QTextEdit, QComboBox
@entrypoint
@widget(layout="form")
class ContactForm(QWidget):
name: QLineEdit = make(QLineEdit, form_label="Full Name", placeholderText="John Doe")
email: QLineEdit = make(QLineEdit, form_label="Email", placeholderText="john@example.com")
age: QSpinBox = make(QSpinBox, form_label="Age", minimum=0, maximum=120)
country: QComboBox = make(QComboBox, form_label="Country")
bio: QTextEdit = make(QTextEdit, form_label="Bio", placeholderText="Tell us about yourself...")
def setup(self) -> None:
self.country.addItems(["USA", "Canada", "UK", "Other"])
Data Binding with Forms¶
Forms work seamlessly with data binding and model widgets:
from dataclasses import dataclass
from qtpie import widget, make, Widget, entrypoint
from qtpy.QtWidgets import QWidget, QLineEdit, QSlider, QLabel
from qtpy.QtCore import Qt
@dataclass
class Dog:
name: str = ""
age: int = 0
@entrypoint
@widget(layout="form")
class DogEditor(QWidget, Widget[Dog]):
name: QLineEdit = make(QLineEdit, form_label="Name")
age: QSlider = make(
QSlider,
Qt.Orientation.Horizontal,
form_label="Age",
minimum=0,
maximum=20
)
info: QLabel = make(QLabel, bind="Name: {name}, Age: {age}")
The name and age fields automatically bind to the model. When you edit the form, the info label updates in real-time.
See Record Widgets for more on data binding.
Fields Without Labels¶
You can omit form_label - the field will still be added to the form, just without a label:
@widget(layout="form")
class MyForm(QWidget):
name: QLineEdit = make(QLineEdit, form_label="Name")
notes: QTextEdit = make(QTextEdit) # No label, full-width field
This is useful for fields that don't need labels, like large text areas or buttons.
Styling Forms¶
Form layouts automatically get a "form" CSS class:
@widget(layout="form", classes=["card", "shadow"])
class MyForm(QWidget):
name: QLineEdit = make(QLineEdit, form_label="Name")
The widget will have both "card" and "form" classes:
This lets you style forms consistently:
QWidget[class~="form"] {
padding: 20px;
background: white;
}
QWidget[class~="form"][class~="card"] {
border: 1px solid #ddd;
border-radius: 8px;
}
See Styling for more on CSS classes.
Field Order¶
Fields appear in the form in declaration order:
@widget(layout="form")
class PersonForm(QWidget):
# These appear in this exact order:
first_name: QLineEdit = make(QLineEdit, form_label="First Name")
last_name: QLineEdit = make(QLineEdit, form_label="Last Name")
email: QLineEdit = make(QLineEdit, form_label="Email")
phone: QLineEdit = make(QLineEdit, form_label="Phone")
Rearrange the field declarations to change the form order.
Field Naming Conventions¶
Fields follow the standard QtPie underscore conventions:
fooand_foo- Added to the form_foo_- Excluded from the form (starts AND ends with_)
@widget(layout="form")
class MyForm(QWidget):
name: QLineEdit = make(QLineEdit, form_label="Name") # In form
_age: QSpinBox = make(QSpinBox, form_label="Age") # In form (private)
_helper_: QLabel = make(QLabel, "Helper") # NOT in form
def setup(self) -> None:
# You can position _helper_ manually
pass
Use _foo_ naming for widgets you want to position manually or use internally.
Combining with Other Features¶
Forms with signals¶
@widget(layout="form")
class LoginForm(QWidget):
username: QLineEdit = make(
QLineEdit,
form_label="Username",
textChanged="on_username_changed"
)
password: QLineEdit = make(
QLineEdit,
form_label="Password",
echoMode=QLineEdit.EchoMode.Password
)
def on_username_changed(self, text: str) -> None:
print(f"Username: {text}")
Forms with validation¶
from qtpie import widget, make, Widget
from qtpy.QtWidgets import QWidget, QLineEdit, QLabel
@widget(layout="form")
class ValidatedForm(QWidget, Widget):
email: QLineEdit = make(QLineEdit, form_label="Email")
error: QLabel = make(QLabel)
def setup(self) -> None:
self.add_validator("email", self.validate_email)
self.is_valid().subscribe(lambda valid: self.update_error(valid))
def validate_email(self, value: str) -> list[str]:
if "@" not in value:
return ["Invalid email address"]
return []
def update_error(self, valid: bool) -> None:
if not valid:
errors = self.validation_for("email")
self.error.setText(errors[0] if errors else "")
else:
self.error.setText("")
See Validation for more on form validation.
When to Use Forms¶
Use forms when:
- Building data entry screens
- Creating settings/preferences dialogs
- Designing login/signup forms
- Making structured input layouts
Consider other layouts when:
- Building toolbars →
layout="horizontal" - Stacking content vertically →
layout="vertical"(default) - Creating calculators or grids →
layout="grid" - Needing custom positioning →
layout="none"
Complete Example: User Profile Editor¶
Here's a complete, runnable example combining forms with data binding, validation, and signals:
from dataclasses import dataclass
from qtpie import widget, make, Widget, entrypoint
from qtpy.QtWidgets import QWidget, QLineEdit, QSpinBox, QPushButton, QLabel
@dataclass
class User:
name: str = ""
email: str = ""
age: int = 18
@entrypoint
@widget(layout="form", classes=["card"])
class UserProfileEditor(QWidget, Widget[User]):
name: QLineEdit = make(QLineEdit, form_label="Name")
email: QLineEdit = make(QLineEdit, form_label="Email")
age: QSpinBox = make(QSpinBox, form_label="Age", minimum=0, maximum=120)
status: QLabel = make(QLabel)
save_btn: QPushButton = make(QPushButton, "Save", clicked="save_profile")
def setup(self) -> None:
# Add validation
self.add_validator("email", self.validate_email)
# Update status when validation state changes
self.is_valid().subscribe(self.update_status)
def validate_email(self, value: str) -> list[str]:
if "@" not in value or "." not in value:
return ["Please enter a valid email address"]
return []
def update_status(self, valid: bool) -> None:
if valid:
self.status.setText("✓ All fields valid")
else:
errors = self.validation_for("email")
self.status.setText(f"✗ {errors[0]}" if errors else "")
def save_profile(self) -> None:
if self.is_valid().value:
# Save the data
self.save_to(self.model)
print(f"Saved: {self.model}")
See Also¶
- Layouts - Overview of all layout types
- Record Widgets - Binding forms to data models
- Validation - Validating form input
- Styling - Styling forms with CSS
- Grid Layouts Guide - For more complex positioning needs