Skip to content

UI Base Package

The winipyside.src.ui.base package provides the foundation framework for all UI components in Winipyside, including lifecycle management, navigation, and utility methods.

Overview

The UI base package contains:

  • Base - Abstract base class for all UI components
  • QABCLoggingMeta - Custom metaclass combining ABC and Qt's metaclass

Base Class

The Base class is the foundation for all UI components (widgets, pages, windows) in Winipyside. It provides a consistent lifecycle pattern and utility methods.

Class Definition

class Base(metaclass=QABCLoggingMeta):
    """Base UI class for a Qt application."""

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        """Initialize the base UI."""
        super().__init__(*args, **kwargs)
        self.base_setup()
        self.pre_setup()
        self.setup()
        self.post_setup()

Key Features

  • 4-phase lifecycle with hooks for initialization
  • Automatic subclass discovery across packages
  • Page navigation methods for stacked layouts
  • SVG icon support from resources
  • Display name generation from class names
  • Custom metaclass for proper ABC and Qt integration

Lifecycle Hooks

All UI components follow a consistent 4-phase initialization pattern. Each phase has a specific purpose:

1. base_setup()

Purpose: Initialize core Qt components required for the UI to function.

When to use:

  • Create Qt widgets (QVBoxLayout, QStackedWidget, etc.)
  • Set window properties (title, size)
  • Initialize Qt-specific attributes

Example:

def base_setup(self) -> None:
    """Setup the base Qt object of the UI."""
    self.setWindowTitle(self.get_display_name())
    self.stack = QStackedWidget()
    self.setCentralWidget(self.stack)

2. pre_setup()

Purpose: Setup operations that must run before main setup.

When to use:

  • Load configuration or settings
  • Initialize data structures
  • Setup connections that other components depend on
  • Prepare resources

Example:

def pre_setup(self) -> None:
    """Setup operations to run before main setup."""
    self.config = self.load_config()
    self.data_cache = {}

3. setup()

Purpose: Main UI initialization logic.

When to use:

  • Add widgets to layouts
  • Configure UI components
  • Set up signal/slot connections
  • Apply styling

Example:

def setup(self) -> None:
    """Setup the main UI components."""
    self.resize(1280, 720)
    self.add_menu_bar()
    self.add_toolbar()

4. post_setup()

Purpose: Finalization after main setup is complete.

When to use:

  • Final adjustments to UI
  • Start background tasks
  • Show initial dialogs
  • Apply final state

Example:

def post_setup(self) -> None:
    """Setup operations to run after main setup."""
    self.restore_window_state()
    self.check_for_updates()

Methods

set_current_page(page_cls: type[BasePage]) -> None

Set the current page in the stacked widget.

Parameters:

  • page_cls (type[BasePage]): The page class to set as current

Example:

from winipyside.src.ui.pages.browser import Browser

# Switch to browser page
self.set_current_page(Browser)

get_page(page_cls: type[BasePage]) -> BasePage

Get a page instance by its class.

Parameters:

  • page_cls (type[BasePage]): The page class to retrieve

Returns:

  • BasePage: The page instance

Example:

browser_page = self.get_page(Browser)
browser_page.browser.load(QUrl("https://example.com"))

get_stack() -> QStackedWidget

Get the stacked widget containing all pages.

Returns:

  • QStackedWidget: The stacked widget

Example:

stack = self.get_stack()
current_index = stack.currentIndex()

Utility Methods

get_display_name() -> str (classmethod)

Get the human-readable display name derived from the class name.

Returns:

  • str: Display name with spaces between words

Example:

class MyCustomWidget(Base, QWidget):
    pass

print(MyCustomWidget.get_display_name())  # "My Custom Widget"

get_subclasses(...) -> list[type[Self]] (classmethod)

Get all non-abstract subclasses of the UI component.

Parameters:

  • package (ModuleType | None): Package to search in. If None, searches main package.

Returns:

  • list[type[Self]]: Sorted list of all non-abstract subclasses

Example:

from winipyside.src.ui.pages.base.base import Base as BasePage

# Get all page classes
all_pages = BasePage.get_subclasses()
for page_cls in all_pages:
    print(page_cls.__name__)

get_svg_icon(name: str, color: str = "white") -> QIcon

Load an SVG icon from resources.

Parameters:

  • name (str): Icon name (without .svg extension)
  • color (str): Icon color (default: "white")

Returns:

  • QIcon: The loaded icon

Example:

icon = self.get_svg_icon("play", color="blue")
button.setIcon(icon)

QABCLoggingMeta

Custom metaclass that combines Python's ABC (Abstract Base Class) functionality with Qt's metaclass.

Why It's Needed

Qt classes use a custom metaclass (type(QObject)), and Python's ABC uses its own metaclass. To use both together, we need a metaclass that inherits from both.

Class Definition

class QABCLoggingMeta(
    ABCLoggingMeta,  # From winiutils
    type(QObject),   # Qt's metaclass
):
    """Metaclass for Qt classes with ABC functionality."""

Usage

The metaclass is automatically applied to the Base class, so all subclasses inherit it:

from winipyside.src.ui.base.base import Base
from PySide6.QtWidgets import QWidget
from abc import abstractmethod

class MyWidget(Base, QWidget):
    @abstractmethod
    def custom_method(self) -> None:
        """This method must be implemented by subclasses."""

Complete Example

Here's a complete example showing all lifecycle hooks and utility methods:

from winipyside.src.ui.base.base import Base
from PySide6.QtWidgets import QWidget, QVBoxLayout, QPushButton
from abc import abstractmethod

class CustomWidget(Base, QWidget):
    """A custom widget demonstrating the Base class."""

    def base_setup(self) -> None:
        """Initialize Qt components."""
        # Create main layout
        self.layout = QVBoxLayout()
        self.setLayout(self.layout)

        # Set window title from class name
        self.setWindowTitle(self.get_display_name())  # "Custom Widget"

    def pre_setup(self) -> None:
        """Setup before main initialization."""
        # Load configuration
        self.config = {"theme": "dark", "font_size": 12}

        # Initialize data structures
        self.data = []

    def setup(self) -> None:
        """Main setup logic."""
        # Add widgets
        self.button = QPushButton("Click Me")
        self.layout.addWidget(self.button)

        # Connect signals
        self.button.clicked.connect(self.on_button_clicked)

        # Apply styling
        self.setStyleSheet("background-color: #2b2b2b;")

    def post_setup(self) -> None:
        """Finalization after setup."""
        # Show initial message
        print(f"{self.get_display_name()} initialized successfully")

    def on_button_clicked(self) -> None:
        """Handle button click."""
        print("Button clicked!")

# Usage
from PySide6.QtWidgets import QApplication

app = QApplication([])
widget = CustomWidget()
widget.show()
app.exec()

Best Practices

1. Always Call Super

When overriding lifecycle methods, always call super() if the parent class has implementation:

def base_setup(self) -> None:
    """Setup the base Qt object."""
    super().base_setup()  # Call parent implementation
    # Your custom setup

2. Keep Lifecycle Methods Focused

Each lifecycle method should have a clear, focused purpose:

# Good - focused responsibilities
def base_setup(self) -> None:
    self.layout = QVBoxLayout()
    self.setLayout(self.layout)

def setup(self) -> None:
    self.add_widgets()
    self.connect_signals()

# Bad - mixing concerns
def base_setup(self) -> None:
    self.layout = QVBoxLayout()
    self.add_widgets()  # Should be in setup()
    self.load_config()  # Should be in pre_setup()

3. Use Abstract Methods for Required Implementation

Mark methods that subclasses must implement as abstract:

from abc import abstractmethod

class CustomBase(Base, QWidget):
    @abstractmethod
    def load_data(self) -> None:
        """Load data - must be implemented by subclasses."""

4. Leverage Display Names

Use get_display_name() for consistent naming:

def base_setup(self) -> None:
    # Automatically generates "My Custom Page" from "MyCustomPage"
    self.setWindowTitle(self.get_display_name())

5. Use Subclass Discovery for Plugins

Use get_subclasses() to discover and load plugins:

# Get all page classes
all_pages = BasePage.get_subclasses()

# Create instances
for page_cls in all_pages:
    page = page_cls(parent=self)

Common Patterns

Pattern 1: Configuration Loading

def pre_setup(self) -> None:
    """Load configuration before setup."""
    self.config = self.load_config()
    self.apply_theme(self.config.get("theme", "light"))

Pattern 2: Lazy Widget Creation

def setup(self) -> None:
    """Setup main UI."""
    # Create widgets on demand
    pass

def get_or_create_widget(self) -> QWidget:
    """Get widget, creating it if necessary."""
    if not hasattr(self, "_widget"):
        self._widget = QWidget()
    return self._widget

Pattern 3: Signal Connection

def setup(self) -> None:
    """Setup and connect signals."""
    self.button.clicked.connect(self.on_button_clicked)
    self.slider.valueChanged.connect(self.on_slider_changed)

def on_button_clicked(self) -> None:
    """Handle button click."""
    pass

def on_slider_changed(self, value: int) -> None:
    """Handle slider change."""
    pass

Pattern 4: Resource Cleanup

def closeEvent(self, event) -> None:
    """Clean up resources on close."""
    # Save state
    self.save_config()

    # Clean up resources
    if hasattr(self, "player"):
        self.player.stop()

    # Accept close event
    event.accept()

See Also