Skip to content

@window

The @window decorator transforms a class into a Qt main window with automatic setup and configuration.

Basic Usage

from qtpy.QtWidgets import QMainWindow
from qtpie import window

@window
class MyWindow(QMainWindow):
    pass

With parentheses and parameters:

@window(title="My Application", size=(1024, 768))
class MyWindow(QMainWindow):
    pass

Parameters

title

Type: str | None Default: None

Sets the window title.

@window(title="My Application")
class MyWindow(QMainWindow):
    pass

# Window title will be "My Application"

Without a title parameter, the window title is empty.

size

Type: tuple[int, int] | None Default: None

Sets the window dimensions as (width, height).

@window(size=(800, 600))
class MyWindow(QMainWindow):
    pass

# Window will be 800 pixels wide, 600 pixels tall

icon

Type: str | None Default: None

Sets the window icon from a file path.

@window(icon="/path/to/icon.png")
class MyWindow(QMainWindow):
    pass

Supports any image format that Qt can load (PNG, JPG, SVG, etc.).

center

Type: bool Default: False

Centers the window on the screen. Must be used with size parameter.

@window(title="Centered", size=(400, 300), center=True)
class MyWindow(QMainWindow):
    pass

# Window will appear centered on screen

name

Type: str | None Default: None (auto-generated from class name)

Sets the widget's objectName for QSS styling.

@window(name="MainAppWindow")
class MyWindow(QMainWindow):
    pass

# objectName is "MainAppWindow"

Auto-naming

If name is not provided, it's derived from the class name:

@window
class SomeWindow(QMainWindow):
    pass

# objectName is "Some" (Window suffix stripped)

@window
class Editor(QMainWindow):
    pass

# objectName is "Editor" (no suffix to strip)

classes

Type: list[str] | None Default: None

Sets CSS-like classes for styling via QSS.

@window(classes=["dark-theme", "main-window"])
class MyWindow(QMainWindow):
    pass

# Can be styled with QSS:
# QMainWindow[class~="dark-theme"] { ... }

The classes are stored as a Qt property named "class":

w = MyWindow()
classes = w.property("class")  # ["dark-theme", "main-window"]

Central Widget

A field named central_widget is automatically set as the window's central widget:

from qtpy.QtWidgets import QTextEdit
from qtpie import window, make

@window
class MyWindow(QMainWindow):
    central_widget: QTextEdit = make(QTextEdit)

# Equivalent to calling: self.setCentralWidget(self.central_widget)

The central widget receives all child content:

from qtpy.QtWidgets import QLabel

@window
class MyWindow(QMainWindow):
    central_widget: QLabel = make(QLabel, "Hello from central!")

w = MyWindow()
assert w.centralWidget() is w.central_widget
assert w.central_widget.text() == "Hello from central!"

If no central_widget field exists, the window has no central widget (which is valid for QMainWindow).

QMenu fields are automatically added to the window's menu bar:

from dataclasses import field
from qtpy.QtWidgets import QMenu

@window
class MyWindow(QMainWindow):
    file_menu: QMenu = field(default_factory=lambda: QMenu("&File"))
    edit_menu: QMenu = field(default_factory=lambda: QMenu("&Edit"))

# Both menus appear in the menu bar

Private Menus

Fields starting with _ are NOT added to the menu bar:

@window
class MyWindow(QMainWindow):
    _hidden_menu: QMenu = field(default_factory=lambda: QMenu("Hidden"))
    visible_menu: QMenu = field(default_factory=lambda: QMenu("Visible"))

# Only visible_menu appears in menu bar

Lifecycle Hook

Override setup() to customize initialization:

@window
class MyWindow(QMainWindow):
    central_widget: QLabel = make(QLabel, "Initial")

    def setup(self) -> None:
        # Called after fields are initialized
        self.central_widget.setText("Modified in setup")

The hook has access to fully initialized child widgets.

Async closeEvent

The @window decorator automatically wraps async closeEvent methods with qasync.asyncClose, allowing cleanup operations to complete before the window is destroyed:

import asyncio

@window(title="My App")
class MyWindow(QMainWindow):
    async def closeEvent(self, event: QCloseEvent) -> None:
        # Async cleanup - runs to completion before window closes
        await self.save_session_async()
        await self.disconnect_from_server()
        event.accept()

This ensures async cleanup completes before the window is destroyed. Without this wrapping, the coroutine would be created but not awaited.

Requirements: Requires qasync to be installed.

Signal Connections

Signals are connected via the make() factory:

from qtpy.QtWidgets import QPushButton

@window
class MyWindow(QMainWindow):
    central_widget: QPushButton = make(QPushButton, "Click", clicked="on_click")

    def on_click(self) -> None:
        print("Button clicked!")

Both method names (strings) and lambdas work:

@window
class MyWindow(QMainWindow):
    central_widget: QPushButton = make(
        QPushButton,
        "Click",
        clicked=lambda: print("Clicked!"),
    )

Field Defaults

Fields can have default values:

@window
class MyWindow(QMainWindow):
    count: int = 42
    name: str = "default"

w = MyWindow()
assert w.count == 42
assert w.name == "default"

Fields can be overridden via constructor kwargs:

@window
class MyWindow(QMainWindow):
    count: int = 0

w = MyWindow(count=99)
assert w.count == 99

Complete Example

from dataclasses import field
from qtpy.QtWidgets import QMainWindow, QTextEdit, QMenu
from qtpie import window, make

@window(
    title="Text Editor",
    size=(1024, 768),
    icon="resources/app_icon.png",
    center=True,
    classes=["main-window"],
)
class EditorWindow(QMainWindow):
    # Central widget
    central_widget: QTextEdit = make(QTextEdit, textChanged="mark_dirty")

    # Menus
    file_menu: QMenu = field(default_factory=lambda: QMenu("&File"))
    edit_menu: QMenu = field(default_factory=lambda: QMenu("&Edit"))

    # Custom fields
    unsaved_changes: bool = False

    def setup(self) -> None:
        # Configure after initialization
        self.statusBar().showMessage("Ready")

    def mark_dirty(self) -> None:
        self.unsaved_changes = True

See Also