Skip to content

第37章 GUI高级开发

学习目标

完成本章学习后,你将能够:

  1. 掌握高级布局:嵌套布局、动态布局、响应式设计
  2. 创建自定义控件:复合控件、绘制控件、动画控件
  3. 实现样式定制:QSS样式表、主题切换、动态样式
  4. 处理多线程界面:信号槽机制、线程安全更新、工作线程
  5. 实现数据绑定:Model-View架构、数据同步、表单验证
  6. 开发复杂组件:表格编辑、树形视图、拖放功能
  7. 实现国际化和本地化:多语言支持、资源管理
  8. 构建企业级应用:插件系统、配置管理、日志集成

37.1 高级布局管理

37.1.1 布局系统原理

python
from PyQt6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
    QFormLayout, QStackedLayout, QSplitter, QTabWidget,
    QMainWindow, QLabel, QPushButton, QLineEdit, QTextEdit,
    QGroupBox, QScrollArea, QFrame, QSizePolicy
)
from PyQt6.QtCore import Qt, QSize, QRect, QPoint
from PyQt6.QtGui import QPainter, QColor, QFont
from typing import Optional, List, Dict, Any
from dataclasses import dataclass
from enum import Enum


class LayoutType(Enum):
    VERTICAL = "vertical"
    HORIZONTAL = "horizontal"
    GRID = "grid"
    FORM = "form"
    STACKED = "stacked"


@dataclass
class LayoutConfig:
    margin: int = 10
    spacing: int = 5
    stretch: int = 0
    alignment: Qt.AlignmentFlag = Qt.AlignmentFlag.AlignTop


class ResponsiveLayout(QWidget):
    def __init__(self, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._layouts: Dict[str, QWidget] = {}
        self._breakpoints = {
            "mobile": 600,
            "tablet": 900,
            "desktop": 1200
        }
        self._current_layout = "desktop"

    def add_responsive_widget(
        self,
        widget: QWidget,
        mobile_visible: bool = True,
        tablet_visible: bool = True,
        desktop_visible: bool = True
    ) -> None:
        self._layouts[widget.objectName() or str(id(widget))] = {
            "widget": widget,
            "mobile_visible": mobile_visible,
            "tablet_visible": tablet_visible,
            "desktop_visible": desktop_visible
        }

    def resizeEvent(self, event) -> None:
        super().resizeEvent(event)
        width = self.width()

        if width < self._breakpoints["mobile"]:
            new_layout = "mobile"
        elif width < self._breakpoints["tablet"]:
            new_layout = "tablet"
        else:
            new_layout = "desktop"

        if new_layout != self._current_layout:
            self._current_layout = new_layout
            self._update_visibility()

    def _update_visibility(self) -> None:
        for name, config in self._layouts.items():
            widget = config["widget"]
            if self._current_layout == "mobile":
                widget.setVisible(config["mobile_visible"])
            elif self._current_layout == "tablet":
                widget.setVisible(config["tablet_visible"])
            else:
                widget.setVisible(config["desktop_visible"])


class FlowLayout(QWidget):
    def __init__(self, parent: Optional[QWidget] = None, spacing: int = 10):
        super().__init__(parent)
        self._items: List[QWidget] = []
        self._spacing = spacing
        self._horizontal_spacing = spacing
        self._vertical_spacing = spacing

    def add_widget(self, widget: QWidget) -> None:
        widget.setParent(self)
        self._items.append(widget)
        self.update_geometry()

    def remove_widget(self, widget: QWidget) -> None:
        if widget in self._items:
            self._items.remove(widget)
            widget.setParent(None)
            self.update_geometry()

    def clear(self) -> None:
        for widget in self._items:
            widget.setParent(None)
        self._items.clear()
        self.update_geometry()

    def update_geometry(self) -> None:
        if not self._items:
            return

        x = 0
        y = 0
        line_height = 0
        width = self.width()

        for widget in self._items:
            size_hint = widget.sizeHint()
            widget_width = size_hint.width()
            widget_height = size_hint.height()

            if x + widget_width > width and x > 0:
                x = 0
                y += line_height + self._vertical_spacing
                line_height = 0

            widget.setGeometry(x, y, widget_width, widget_height)

            x += widget_width + self._horizontal_spacing
            line_height = max(line_height, widget_height)

        total_height = y + line_height
        self.setMinimumHeight(total_height)

    def resizeEvent(self, event) -> None:
        super().resizeEvent(event)
        self.update_geometry()


class BorderLayout(QWidget):
    class Position(Enum):
        NORTH = 0
        SOUTH = 1
        EAST = 2
        WEST = 3
        CENTER = 4

    def __init__(self, parent: Optional[QWidget] = None, spacing: int = 5):
        super().__init__(parent)
        self._widgets: Dict[BorderLayout.Position, QWidget] = {}
        self._spacing = spacing

    def add_widget(self, widget: QWidget, position: Position) -> None:
        if position in self._widgets:
            self._widgets[position].setParent(None)

        widget.setParent(self)
        self._widgets[position] = widget

    def remove_widget(self, position: Position) -> None:
        if position in self._widgets:
            self._widgets[position].setParent(None)
            del self._widgets[position]

    def resizeEvent(self, event) -> None:
        super().resizeEvent(event)
        self._update_layout()

    def _update_layout(self) -> None:
        rect = self.rect()
        center_rect = QRect(rect)

        if BorderLayout.Position.NORTH in self._widgets:
            widget = self._widgets[BorderLayout.Position.NORTH]
            size_hint = widget.sizeHint()
            height = size_hint.height()
            widget.setGeometry(rect.x(), rect.y(), rect.width(), height)
            center_rect.setTop(center_rect.top() + height + self._spacing)

        if BorderLayout.Position.SOUTH in self._widgets:
            widget = self._widgets[BorderLayout.Position.SOUTH]
            size_hint = widget.sizeHint()
            height = size_hint.height()
            widget.setGeometry(rect.x(), rect.bottom() - height, rect.width(), height)
            center_rect.setBottom(center_rect.bottom() - height - self._spacing)

        if BorderLayout.Position.WEST in self._widgets:
            widget = self._widgets[BorderLayout.Position.WEST]
            size_hint = widget.sizeHint()
            width = size_hint.width()
            widget.setGeometry(center_rect.x(), center_rect.y(), width, center_rect.height())
            center_rect.setLeft(center_rect.left() + width + self._spacing)

        if BorderLayout.Position.EAST in self._widgets:
            widget = self._widgets[BorderLayout.Position.EAST]
            size_hint = widget.sizeHint()
            width = size_hint.width()
            widget.setGeometry(center_rect.right() - width, center_rect.y(), width, center_rect.height())
            center_rect.setRight(center_rect.right() - width - self._spacing)

        if BorderLayout.Position.CENTER in self._widgets:
            self._widgets[BorderLayout.Position.CENTER].setGeometry(center_rect)

37.1.2 动态布局

python
class DynamicForm(QWidget):
    def __init__(self, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._main_layout = QVBoxLayout(self)
        self._form_layout = QFormLayout()
        self._main_layout.addLayout(self._form_layout)
        self._fields: Dict[str, QWidget] = {}

    def add_field(
        self,
        name: str,
        label: str,
        widget_type: type = QLineEdit,
        default_value: Any = None
    ) -> QWidget:
        widget = widget_type()
        if default_value is not None:
            if isinstance(widget, QLineEdit):
                widget.setText(str(default_value))
            elif isinstance(widget, QTextEdit):
                widget.setPlainText(str(default_value))

        self._form_layout.addRow(label, widget)
        self._fields[name] = widget
        return widget

    def remove_field(self, name: str) -> None:
        if name in self._fields:
            widget = self._fields[name]
            self._form_layout.removeRow(widget)
            del self._fields[name]

    def get_value(self, name: str) -> Any:
        if name not in self._fields:
            return None

        widget = self._fields[name]
        if isinstance(widget, QLineEdit):
            return widget.text()
        elif isinstance(widget, QTextEdit):
            return widget.toPlainText()
        return None

    def get_all_values(self) -> Dict[str, Any]:
        return {name: self.get_value(name) for name in self._fields}

    def set_value(self, name: str, value: Any) -> None:
        if name not in self._fields:
            return

        widget = self._fields[name]
        if isinstance(widget, QLineEdit):
            widget.setText(str(value))
        elif isinstance(widget, QTextEdit):
            widget.setPlainText(str(value))

    def clear_all(self) -> None:
        for widget in self._fields.values():
            if isinstance(widget, QLineEdit):
                widget.clear()
            elif isinstance(widget, QTextEdit):
                widget.clear()


class CollapsibleSection(QWidget):
    def __init__(self, title: str = "", parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._is_collapsed = False

        self._main_layout = QVBoxLayout(self)
        self._main_layout.setContentsMargins(0, 0, 0, 0)
        self._main_layout.setSpacing(0)

        self._header = QPushButton(title)
        self._header.setCheckable(True)
        self._header.setChecked(True)
        self._header.clicked.connect(self._toggle)
        self._main_layout.addWidget(self._header)

        self._content = QWidget()
        self._content_layout = QVBoxLayout(self._content)
        self._main_layout.addWidget(self._content)

    def _toggle(self) -> None:
        self._is_collapsed = not self._header.isChecked()
        self._content.setVisible(not self._is_collapsed)

    def add_widget(self, widget: QWidget) -> None:
        self._content_layout.addWidget(widget)

    def set_collapsed(self, collapsed: bool) -> None:
        self._is_collapsed = collapsed
        self._header.setChecked(not collapsed)
        self._content.setVisible(not collapsed)

    def is_collapsed(self) -> bool:
        return self._is_collapsed


class TabManager(QWidget):
    def __init__(self, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._main_layout = QVBoxLayout(self)
        self._tab_widget = QTabWidget()
        self._tab_widget.setTabsClosable(True)
        self._tab_widget.tabCloseRequested.connect(self._close_tab)
        self._main_layout.addWidget(self._tab_widget)

        self._tabs: Dict[str, QWidget] = {}

    def add_tab(self, name: str, title: str, widget: QWidget) -> int:
        index = self._tab_widget.addTab(widget, title)
        self._tabs[name] = widget
        return index

    def remove_tab(self, name: str) -> None:
        if name in self._tabs:
            widget = self._tabs[name]
            index = self._tab_widget.indexOf(widget)
            if index >= 0:
                self._tab_widget.removeTab(index)
            del self._tabs[name]

    def get_tab(self, name: str) -> Optional[QWidget]:
        return self._tabs.get(name)

    def _close_tab(self, index: int) -> None:
        widget = self._tab_widget.widget(index)
        for name, w in list(self._tabs.items()):
            if w == widget:
                del self._tabs[name]
                break
        self._tab_widget.removeTab(index)

    def clear_all(self) -> None:
        self._tab_widget.clear()
        self._tabs.clear()

37.2 自定义控件

37.2.1 绘制控件

python
from PyQt6.QtGui import (
    QPainter, QColor, QPen, QBrush, QFont, QFontMetrics,
    QLinearGradient, QRadialGradient, QPainterPath, QPixmap
)
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QPointF, QRectF


class CircularProgress(QWidget):
    valueChanged = pyqtSignal(int)

    def __init__(self, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._value = 0
        self._minimum = 0
        self._maximum = 100
        self._line_width = 8
        self._progress_color = QColor(0, 150, 255)
        self._background_color = QColor(200, 200, 200)
        self._text_color = QColor(50, 50, 50)

        self.setMinimumSize(100, 100)

    def setValue(self, value: int) -> None:
        self._value = max(self._minimum, min(self._maximum, value))
        self.valueChanged.emit(self._value)
        self.update()

    def value(self) -> int:
        return self._value

    def setLineWidth(self, width: int) -> None:
        self._line_width = width
        self.update()

    def setProgressColor(self, color: QColor) -> None:
        self._progress_color = color
        self.update()

    def paintEvent(self, event) -> None:
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        rect = self.rect()
        center = rect.center()
        radius = min(rect.width(), rect.height()) // 2 - self._line_width

        pen = QPen(self._background_color, self._line_width)
        pen.setCapStyle(Qt.PenCapStyle.RoundCap)
        painter.setPen(pen)
        painter.drawEllipse(center, radius, radius)

        if self._value > 0:
            progress = (self._value - self._minimum) / (self._maximum - self._minimum)
            angle = int(360 * progress * 16)

            pen.setColor(self._progress_color)
            painter.setPen(pen)
            rect_arc = QRectF(
                center.x() - radius,
                center.y() - radius,
                radius * 2,
                radius * 2
            )
            painter.drawArc(rect_arc, 90 * 16, -angle)

        painter.setPen(self._text_color)
        font = QFont("Arial", radius // 3)
        painter.setFont(font)
        text = f"{self._value}%"
        painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, text)


class ToggleSwitch(QWidget):
    toggled = pyqtSignal(bool)

    def __init__(self, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._checked = False
        self._animation_offset = 0
        self._track_color_on = QColor(0, 150, 255)
        self._track_color_off = QColor(200, 200, 200)
        self._thumb_color = QColor(255, 255, 255)

        self.setFixedSize(50, 26)
        self.setCursor(Qt.CursorShape.PointingHandCursor)

    def isChecked(self) -> bool:
        return self._checked

    def setChecked(self, checked: bool) -> None:
        if self._checked != checked:
            self._checked = checked
            self.toggled.emit(checked)
            self.update()

    def mousePressEvent(self, event) -> None:
        self.setChecked(not self._checked)

    def paintEvent(self, event) -> None:
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        track_color = self._track_color_on if self._checked else self._track_color_off
        painter.setBrush(QBrush(track_color))
        painter.setPen(Qt.PenStyle.NoPen)
        painter.drawRoundedRect(0, 0, self.width(), self.height(), 13, 13)

        thumb_radius = 10
        thumb_x = self.width() - thumb_radius - 3 if self._checked else 3
        thumb_y = (self.height() - thumb_radius * 2) // 2

        painter.setBrush(QBrush(self._thumb_color))
        painter.drawEllipse(thumb_x, thumb_y, thumb_radius * 2, thumb_radius * 2)


class RatingWidget(QWidget):
    ratingChanged = pyqtSignal(int)

    def __init__(self, max_rating: int = 5, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._rating = 0
        self._max_rating = max_rating
        self._star_size = 24
        self._star_color = QColor(255, 180, 0)
        self._empty_color = QColor(200, 200, 200)
        self._hover_rating = -1

        self.setMouseTracking(True)
        self.setFixedHeight(self._star_size + 10)

    def rating(self) -> int:
        return self._rating

    def setRating(self, rating: int) -> None:
        self._rating = max(0, min(self._max_rating, rating))
        self.ratingChanged.emit(self._rating)
        self.update()

    def sizeHint(self) -> QSize:
        return QSize(self._star_size * self._max_rating + 10, self._star_size + 10)

    def paintEvent(self, event) -> None:
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)

        for i in range(self._max_rating):
            x = i * (self._star_size + 2) + 5
            y = 5

            if i < self._rating or (self._hover_rating >= 0 and i <= self._hover_rating):
                color = self._star_color
            else:
                color = self._empty_color

            self._draw_star(painter, x, y, self._star_size, color)

    def _draw_star(self, painter: QPainter, x: int, y: int, size: int, color: QColor) -> None:
        painter.setBrush(QBrush(color))
        painter.setPen(Qt.PenStyle.NoPen)

        path = QPainterPath()
        points = []
        center_x = x + size / 2
        center_y = y + size / 2
        outer_radius = size / 2
        inner_radius = size / 4

        for i in range(10):
            angle = i * 36 - 90
            radius = outer_radius if i % 2 == 0 else inner_radius
            px = center_x + radius * (2 ** 0.5) * 0.5 * (1 if i % 2 == 0 else 0.5)
            py = center_y + radius * (2 ** 0.5) * 0.5

        painter.drawEllipse(int(x), int(y), size, size)

    def mouseMoveEvent(self, event) -> None:
        x = event.position().x()
        star_width = self._star_size + 2
        self._hover_rating = int((x - 5) / star_width)
        self.update()

    def mousePressEvent(self, event) -> None:
        self.setRating(self._hover_rating + 1)

    def leaveEvent(self, event) -> None:
        self._hover_rating = -1
        self.update()

37.2.2 复合控件

python
class SearchBox(QWidget):
    textChanged = pyqtSignal(str)
    returnPressed = pyqtSignal(str)

    def __init__(self, placeholder: str = "Search...", parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._setup_ui(placeholder)

    def _setup_ui(self, placeholder: str) -> None:
        layout = QHBoxLayout(self)
        layout.setContentsMargins(5, 2, 5, 2)

        self._search_icon = QLabel("🔍")
        layout.addWidget(self._search_icon)

        self._line_edit = QLineEdit()
        self._line_edit.setPlaceholderText(placeholder)
        self._line_edit.textChanged.connect(self.textChanged.emit)
        self._line_edit.returnPressed.connect(
            lambda: self.returnPressed.emit(self._line_edit.text())
        )
        layout.addWidget(self._line_edit)

        self._clear_button = QPushButton("×")
        self._clear_button.setFixedSize(20, 20)
        self._clear_button.clicked.connect(self.clear)
        self._clear_button.setVisible(False)
        layout.addWidget(self._clear_button)

        self._line_edit.textChanged.connect(
            lambda text: self._clear_button.setVisible(bool(text))
        )

    def text(self) -> str:
        return self._line_edit.text()

    def setText(self, text: str) -> None:
        self._line_edit.setText(text)

    def clear(self) -> None:
        self._line_edit.clear()


class ColorPicker(QWidget):
    colorChanged = pyqtSignal(QColor)

    def __init__(self, initial_color: QColor = None, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._color = initial_color or QColor(255, 255, 255)
        self._setup_ui()

    def _setup_ui(self) -> None:
        layout = QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)

        self._color_button = QPushButton()
        self._color_button.setFixedSize(40, 40)
        self._color_button.clicked.connect(self._pick_color)
        self._update_button_color()
        layout.addWidget(self._color_button)

        self._color_label = QLabel(self._color.name())
        layout.addWidget(self._color_label)

    def _pick_color(self) -> None:
        from PyQt6.QtWidgets import QColorDialog
        color = QColorDialog.getColor(self._color, self)
        if color.isValid():
            self._color = color
            self._update_button_color()
            self._color_label.setText(color.name())
            self.colorChanged.emit(color)

    def _update_button_color(self) -> None:
        self._color_button.setStyleSheet(
            f"background-color: {self._color.name()}; border: 1px solid #999;"
        )

    def color(self) -> QColor:
        return self._color

    def setColor(self, color: QColor) -> None:
        self._color = color
        self._update_button_color()
        self._color_label.setText(color.name())


class StatusBar(QWidget):
    def __init__(self, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._setup_ui()

    def _setup_ui(self) -> None:
        self.setStyleSheet("""
            QWidget {
                background-color: #f0f0f0;
                border-top: 1px solid #c0c0c0;
            }
            QLabel {
                padding: 2px 8px;
            }
        """)

        layout = QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)

        self._message_label = QLabel("Ready")
        layout.addWidget(self._message_label)

        layout.addStretch()

        self._position_label = QLabel("Ln 1, Col 1")
        layout.addWidget(self._position_label)

        self._encoding_label = QLabel("UTF-8")
        layout.addWidget(self._encoding_label)

    def showMessage(self, message: str, timeout: int = 0) -> None:
        self._message_label.setText(message)
        if timeout > 0:
            QTimer.singleShot(timeout, lambda: self._message_label.setText("Ready"))

    def setPosition(self, line: int, column: int) -> None:
        self._position_label.setText(f"Ln {line}, Col {column}")

    def setEncoding(self, encoding: str) -> None:
        self._encoding_label.setText(encoding)

37.3 样式定制

37.3.1 QSS样式表

python
class StyleSheetManager:
    DARK_THEME = """
        QWidget {
            background-color: #1e1e1e;
            color: #d4d4d4;
            font-family: 'Segoe UI', Arial, sans-serif;
            font-size: 13px;
        }
        
        QMainWindow {
            background-color: #1e1e1e;
        }
        
        QPushButton {
            background-color: #0e639c;
            color: #ffffff;
            border: none;
            padding: 6px 12px;
            border-radius: 3px;
        }
        
        QPushButton:hover {
            background-color: #1177bb;
        }
        
        QPushButton:pressed {
            background-color: #0d5a8a;
        }
        
        QLineEdit, QTextEdit, QPlainTextEdit {
            background-color: #252526;
            color: #d4d4d4;
            border: 1px solid #3c3c3c;
            padding: 4px 8px;
            border-radius: 3px;
        }
        
        QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus {
            border: 1px solid #0078d4;
        }
        
        QLabel {
            color: #d4d4d4;
            background: transparent;
        }
        
        QComboBox {
            background-color: #3c3c3c;
            color: #d4d4d4;
            border: 1px solid #555555;
            padding: 4px 8px;
            border-radius: 3px;
        }
        
        QComboBox::drop-down {
            border: none;
            width: 20px;
        }
        
        QComboBox::down-arrow {
            image: none;
            border-left: 5px solid transparent;
            border-right: 5px solid transparent;
            border-top: 5px solid #d4d4d4;
        }
        
        QScrollBar:vertical {
            background-color: #1e1e1e;
            width: 12px;
            border-radius: 6px;
        }
        
        QScrollBar::handle:vertical {
            background-color: #424242;
            border-radius: 6px;
            min-height: 30px;
        }
        
        QScrollBar::handle:vertical:hover {
            background-color: #4f4f4f;
        }
        
        QTableWidget, QTableView {
            background-color: #1e1e1e;
            alternate-background-color: #252526;
            gridline-color: #3c3c3c;
        }
        
        QHeaderView::section {
            background-color: #2d2d2d;
            color: #d4d4d4;
            padding: 4px;
            border: 1px solid #3c3c3c;
        }
        
        QMenu {
            background-color: #252526;
            color: #d4d4d4;
            border: 1px solid #3c3c3c;
        }
        
        QMenu::item:selected {
            background-color: #094771;
        }
        
        QTabWidget::pane {
            border: 1px solid #3c3c3c;
            background-color: #1e1e1e;
        }
        
        QTabBar::tab {
            background-color: #2d2d2d;
            color: #969696;
            padding: 6px 12px;
            border: 1px solid #3c3c3c;
        }
        
        QTabBar::tab:selected {
            background-color: #1e1e1e;
            color: #d4d4d4;
            border-bottom-color: #1e1e1e;
        }
    """

    LIGHT_THEME = """
        QWidget {
            background-color: #ffffff;
            color: #333333;
            font-family: 'Segoe UI', Arial, sans-serif;
            font-size: 13px;
        }
        
        QMainWindow {
            background-color: #f5f5f5;
        }
        
        QPushButton {
            background-color: #0078d4;
            color: #ffffff;
            border: none;
            padding: 6px 12px;
            border-radius: 3px;
        }
        
        QPushButton:hover {
            background-color: #106ebe;
        }
        
        QPushButton:pressed {
            background-color: #005a9e;
        }
        
        QLineEdit, QTextEdit, QPlainTextEdit {
            background-color: #ffffff;
            color: #333333;
            border: 1px solid #cccccc;
            padding: 4px 8px;
            border-radius: 3px;
        }
        
        QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus {
            border: 1px solid #0078d4;
        }
        
        QLabel {
            color: #333333;
            background: transparent;
        }
    """

    def __init__(self, app):
        self._app = app
        self._current_theme = "light"

    def apply_theme(self, theme: str) -> None:
        self._current_theme = theme
        if theme == "dark":
            self._app.setStyleSheet(self.DARK_THEME)
        else:
            self._app.setStyleSheet(self.LIGHT_THEME)

    def current_theme(self) -> str:
        return self._current_theme

    def toggle_theme(self) -> None:
        if self._current_theme == "dark":
            self.apply_theme("light")
        else:
            self.apply_theme("dark")


class CustomStyle(QWidget):
    def __init__(self, parent: Optional[QWidget] = None):
        super().__init__(parent)
        self._custom_properties: Dict[str, Any] = {}

    def set_custom_property(self, name: str, value: Any) -> None:
        self._custom_properties[name] = value
        self.setStyleSheet(self._generate_stylesheet())

    def _generate_stylesheet(self) -> str:
        return ""

    def apply_class_style(self, class_name: str, style: str) -> None:
        current_style = self.styleSheet()
        new_style = f".{class_name} {{ {style} }}"
        self.setStyleSheet(current_style + new_style)
        self.setProperty("class", class_name)
        self.style().unpolish(self)
        self.style().polish(self)

37.4 多线程界面

37.4.1 工作线程

python
from PyQt6.QtCore import QThread, pyqtSignal, QMutex, QWaitCondition, QObject


class Worker(QThread):
    started = pyqtSignal()
    finished = pyqtSignal()
    progress = pyqtSignal(int)
    error = pyqtSignal(str)
    result = pyqtSignal(object)

    def __init__(self, parent: Optional[QObject] = None):
        super().__init__(parent)
        self._is_cancelled = False
        self._is_paused = False
        self._mutex = QMutex()
        self._wait_condition = QWaitCondition()

    def run(self) -> None:
        self.started.emit()
        try:
            result = self.do_work()
            if not self._is_cancelled:
                self.result.emit(result)
        except Exception as e:
            self.error.emit(str(e))
        finally:
            self.finished.emit()

    def do_work(self) -> Any:
        return None

    def cancel(self) -> None:
        self._mutex.lock()
        self._is_cancelled = True
        self._mutex.unlock()

    def is_cancelled(self) -> bool:
        self._mutex.lock()
        cancelled = self._is_cancelled
        self._mutex.unlock()
        return cancelled

    def pause(self) -> None:
        self._mutex.lock()
        self._is_paused = True
        self._mutex.unlock()

    def resume(self) -> None:
        self._mutex.lock()
        self._is_paused = False
        self._wait_condition.wakeAll()
        self._mutex.unlock()

    def check_paused(self) -> None:
        self._mutex.lock()
        while self._is_paused and not self._is_cancelled:
            self._wait_condition.wait(self._mutex)
        self._mutex.unlock()


class TaskWorker(Worker):
    def __init__(self, task: callable, *args, **kwargs):
        super().__init__()
        self._task = task
        self._args = args
        self._kwargs = kwargs

    def do_work(self) -> Any:
        return self._task(*self._args, **self._kwargs)


class WorkerPool(QObject):
    def __init__(self, max_workers: int = 4, parent: Optional[QObject] = None):
        super().__init__(parent)
        self._max_workers = max_workers
        self._workers: List[Worker] = []
        self._queue: List[tuple] = []

    def submit(self, task: callable, *args, on_result: callable = None, **kwargs) -> Worker:
        worker = TaskWorker(task, *args, **kwargs)

        if on_result:
            worker.result.connect(on_result)

        worker.finished.connect(lambda: self._on_worker_finished(worker))

        if len(self._workers) < self._max_workers:
            self._workers.append(worker)
            worker.start()
        else:
            self._queue.append(worker)

        return worker

    def _on_worker_finished(self, worker: Worker) -> None:
        if worker in self._workers:
            self._workers.remove(worker)

        if self._queue:
            next_worker = self._queue.pop(0)
            self._workers.append(next_worker)
            next_worker.start()

    def cancel_all(self) -> None:
        for worker in self._workers:
            worker.cancel()
        self._queue.clear()

    def wait_all(self) -> None:
        for worker in self._workers:
            worker.wait()

37.4.2 信号槽进阶

python
from PyQt6.QtCore import pyqtSlot, pyqtSignal, QObject


class SignalBus(QObject):
    _instance = None

    document_saved = pyqtSignal(str)
    document_opened = pyqtSignal(str)
    settings_changed = pyqtSignal(str, object)
    theme_changed = pyqtSignal(str)
    status_message = pyqtSignal(str, int)

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.__init__()
        return cls._instance

    def __init__(self):
        if hasattr(self, "_initialized"):
            return
        super().__init__()
        self._initialized = True


class DataBinding(QObject):
    valueChanged = pyqtSignal(object)

    def __init__(self, initial_value: Any = None):
        super().__init__()
        self._value = initial_value

    def get(self) -> Any:
        return self._value

    def set(self, value: Any) -> None:
        if self._value != value:
            self._value = value
            self.valueChanged.emit(value)

    def bind_to_widget(self, widget: QWidget, property_name: str) -> None:
        self.valueChanged.connect(lambda v: setattr(widget, property_name, v))


class FormValidator(QObject):
    validationChanged = pyqtSignal(bool)

    def __init__(self, parent: Optional[QObject] = None):
        super().__init__(parent)
        self._validators: Dict[str, callable] = {}
        self._errors: Dict[str, str] = {}

    def add_validator(self, field_name: str, validator: callable) -> None:
        self._validators[field_name] = validator

    def validate(self, field_name: str, value: Any) -> bool:
        if field_name not in self._validators:
            return True

        result = self._validators[field_name](value)

        if result is True:
            self._errors.pop(field_name, None)
            return True
        else:
            self._errors[field_name] = result
            return False

    def validate_all(self, data: Dict[str, Any]) -> bool:
        all_valid = True
        for field_name, validator in self._validators.items():
            if not self.validate(field_name, data.get(field_name)):
                all_valid = False

        self.validationChanged.emit(all_valid)
        return all_valid

    def get_errors(self) -> Dict[str, str]:
        return self._errors.copy()

    def has_errors(self) -> bool:
        return len(self._errors) > 0

    @staticmethod
    def required(message: str = "This field is required"):
        def validator(value):
            if value is None or (isinstance(value, str) and not value.strip()):
                return message
            return True
        return validator

    @staticmethod
    def min_length(min_len: int, message: str = None):
        def validator(value):
            if len(str(value)) < min_len:
                return message or f"Minimum length is {min_len}"
            return True
        return validator

    @staticmethod
    def email(message: str = "Invalid email format"):
        import re
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

        def validator(value):
            if re.match(pattern, str(value)):
                return True
            return message
        return validator

37.5 Model-View架构

37.5.1 自定义模型

python
from PyQt6.QtCore import (
    Qt, QAbstractTableModel, QAbstractListModel,
    QModelIndex, QVariant
)
from typing import Any, List, Dict, Optional


class TableModel(QAbstractTableModel):
    def __init__(self, data: List[Dict] = None, headers: List[str] = None):
        super().__init__()
        self._data = data or []
        self._headers = headers or []

    def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
        return len(self._data)

    def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
        return len(self._headers)

    def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
        if not index.isValid():
            return QVariant()

        if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole:
            row = index.row()
            col = index.column()
            if row < len(self._data) and col < len(self._headers):
                key = self._headers[col]
                return str(self._data[row].get(key, ""))
        elif role == Qt.ItemDataRole.TextAlignmentRole:
            return Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter

        return QVariant()

    def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
        if role == Qt.ItemDataRole.DisplayRole:
            if orientation == Qt.Orientation.Horizontal and section < len(self._headers):
                return self._headers[section]
            elif orientation == Qt.Orientation.Vertical:
                return str(section + 1)
        return QVariant()

    def setData(self, index: QModelIndex, value: Any, role: int = Qt.ItemDataRole.EditRole) -> bool:
        if role == Qt.ItemDataRole.EditRole and index.isValid():
            row = index.row()
            col = index.column()
            if row < len(self._data) and col < len(self._headers):
                key = self._headers[col]
                self._data[row][key] = value
                self.dataChanged.emit(index, index)
                return True
        return False

    def flags(self, index: QModelIndex) -> Qt.ItemFlag:
        return super().flags(index) | Qt.ItemFlag.ItemIsEditable

    def insertRows(self, row: int, count: int, parent: QModelIndex = QModelIndex()) -> bool:
        self.beginInsertRows(parent, row, row + count - 1)
        for _ in range(count):
            self._data.insert(row, {key: "" for key in self._headers})
        self.endInsertRows()
        return True

    def removeRows(self, row: int, count: int, parent: QModelIndex = QModelIndex()) -> bool:
        self.beginRemoveRows(parent, row, row + count - 1)
        for _ in range(count):
            if row < len(self._data):
                del self._data[row]
        self.endRemoveRows()
        return True

    def set_data(self, data: List[Dict]) -> None:
        self.beginResetModel()
        self._data = data
        self.endResetModel()

    def get_data(self) -> List[Dict]:
        return self._data.copy()


class ListModel(QAbstractListModel):
    def __init__(self, items: List[Any] = None):
        super().__init__()
        self._items = items or []

    def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
        return len(self._items)

    def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> Any:
        if not index.isValid():
            return QVariant()

        if role == Qt.ItemDataRole.DisplayRole:
            return str(self._items[index.row()])

        return QVariant()

    def insertRows(self, row: int, count: int, parent: QModelIndex = QModelIndex()) -> bool:
        self.beginInsertRows(parent, row, row + count - 1)
        for i in range(count):
            self._items.insert(row + i, None)
        self.endInsertRows()
        return True

    def removeRows(self, row: int, count: int, parent: QModelIndex = QModelIndex()) -> bool:
        self.beginRemoveRows(parent, row, row + count - 1)
        for _ in range(count):
            if row < len(self._items):
                del self._items[row]
        self.endRemoveRows()
        return True

37.6 知识图谱

37.6.1 PyQt/PySide架构体系

┌─────────────────────────────────────────────────────────────────────┐
│                      PyQt/PySide 技术架构                            │
├─────────────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                      应用层 (Application)                     │   │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │   │
│  │  │QApplication│ │QMainWindow│ │QDialog  │ │QWidget  │       │   │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                │                                    │
│  ┌─────────────────────────────┴───────────────────────────────┐   │
│  │                      界面层 (UI Components)                   │   │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │   │
│  │  │ 布局管理  │ │ 控件系统  │ │ 样式系统  │ │ 事件系统  │       │   │
│  │  │QLayout   │ │QWidget   │ │QSS       │ │QEvent    │       │   │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │   │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │   │
│  │  │QVBoxLayou│ │QPushButton│ │QStyle    │ │信号槽    │       │   │
│  │  │QGridLayou│ │QLineEdit │ │主题切换  │ │pyqtSignal│       │   │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                │                                    │
│  ┌─────────────────────────────┴───────────────────────────────┐   │
│  │                      数据层 (Data & Model)                    │   │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │   │
│  │  │ Model    │ │  View    │ │ Delegate │ │ 数据绑定  │       │   │
│  │  │QAbstractM│ │QAbstractV│ │QItemDeleg│ │ 表单验证  │       │   │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                │                                    │
│  ┌─────────────────────────────┴───────────────────────────────┐   │
│  │                      核心层 (Qt Core)                         │   │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │   │
│  │  │QObject   │ │QThread   │ │QTimer    │ │QSettings │       │   │
│  │  │信号槽机制 │ │多线程    │ │定时器    │ │配置存储  │       │   │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                │                                    │
│  ┌─────────────────────────────┴───────────────────────────────┐   │
│  │                      绘图层 (Qt GUI)                          │   │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐       │   │
│  │  │QPainter  │ │QPen/Brush│ │QFont     │ │QPixmap   │       │   │
│  │  │自定义绘制 │ │画笔画刷  │ │字体      │ │图像处理  │       │   │
│  │  └──────────┘ └──────────┘ └──────────┘ └──────────┘       │   │
│  └─────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

37.6.2 信号槽通信机制

┌─────────────────────────────────────────────────────────────────────┐
│                        信号槽通信流程                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   ┌──────────────┐                      ┌──────────────┐           │
│   │   发送者      │                      │   接收者      │           │
│   │   (Sender)    │                      │  (Receiver)  │           │
│   └───────┬──────┘                      └───────┬──────┘           │
│           │                                     │                   │
│           │ 1. emit(signal)                     │                   │
│           ▼                                     │                   │
│   ┌──────────────┐                              │                   │
│   │    信号      │                              │                   │
│   │  pyqtSignal │                              │                   │
│   └───────┬──────┘                              │                   │
│           │                                     │                   │
│           │ 2. 连接检查                         │                   │
│           ▼                                     │                   │
│   ┌──────────────────────────────────────────────────────────┐    │
│   │                    连接类型选择                            │    │
│   │  ┌────────────┐ ┌────────────┐ ┌────────────┐           │    │
│   │  │ Qt.AutoConn│ │Qt.DirectCon│ │Qt.QueuedCon│           │    │
│   │  │ 自动选择   │ │ 直接调用   │ │ 队列调用   │           │    │
│   │  └────────────┘ └────────────┘ └────────────┘           │    │
│   └──────────────────────────────────────────────────────────┘    │
│           │                                     │                   │
│           │ 3. 参数传递                         │                   │
│           ▼                                     ▼                   │
│   ┌──────────────┐                      ┌──────────────┐           │
│   │   参数打包    │ ──────────────────▶ │   槽函数     │           │
│   │  (序列化)    │    跨线程安全传递     │  @pyqtSlot  │           │
│   └──────────────┘                      └──────────────┘           │
│                                                 │                   │
│                                                 │ 4. 执行处理       │
│                                                 ▼                   │
│                                         ┌──────────────┐           │
│                                         │   业务逻辑   │           │
│                                         │   更新UI    │           │
│                                         └──────────────┘           │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

37.7 技术选型指南

37.7.1 GUI框架选型

框架适用场景跨平台许可证学习曲线推荐指数
PyQt6专业桌面应用✅ 全平台GPL/商业中高★★★★★
PySide6商业项目首选✅ 全平台LGPL中高★★★★★
Tkinter简单工具、教学✅ 全平台Python★★★☆☆
wxPython原生外观应用✅ 全平台wxWindows★★★★☆
Kivy跨平台移动端✅ 全平台MIT中高★★★☆☆
Dear PyGui数据可视化工具✅ 全平台MIT★★★☆☆

37.7.2 布局管理器选型

布局类型适用场景特点使用建议
QVBoxLayout垂直排列控件简单直观侧边栏、列表项
QHBoxLayout水平排列控件简单直观工具栏、按钮组
QGridLayout表格形式布局灵活对齐表单、仪表盘
QFormLayout标签-输入配对自动对齐设置对话框
QStackedLayout切换显示页面节省空间向导、多步骤
嵌套布局复杂界面结构高度灵活企业级应用

37.7.3 多线程方案选型

方案适用场景优点缺点
QThread长时间后台任务Qt集成、信号槽通信需要正确管理生命周期
QRunnable短任务池处理线程池管理不能直接发信号
threading简单后台操作Python原生需要信号槽桥接
asyncioI/O密集型任务高效并发与Qt事件循环集成复杂

37.8 常见问题与解决方案

37.8.1 线程安全UI更新

python
from PyQt6.QtCore import QThread, pyqtSignal, QObject, pyqtSlot
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QWidget

class WorkerSignals(QObject):
    """工作线程信号"""
    finished = pyqtSignal(object)
    error = pyqtSignal(str)
    progress = pyqtSignal(int)

class SafeWorker(QThread):
    """线程安全的工作线程"""
    
    def __init__(self, task, *args, **kwargs):
        super().__init__()
        self.task = task
        self.args = args
        self.kwargs = kwargs
        self.signals = WorkerSignals()
    
    def run(self):
        try:
            result = self.task(*self.args, **self.kwargs)
            self.signals.finished.emit(result)
        except Exception as e:
            self.signals.error.emit(str(e))

class ThreadSafeUI(QWidget):
    """线程安全UI更新示例"""
    
    def __init__(self):
        super().__init__()
        self._setup_ui()
    
    def _setup_ui(self):
        layout = QVBoxLayout(self)
        
        self.status_label = QLabel("Ready")
        self.progress_label = QLabel("0%")
        
        layout.addWidget(self.status_label)
        layout.addWidget(self.progress_label)
    
    def start_background_task(self):
        """启动后台任务"""
        worker = SafeWorker(self._long_running_task)
        
        worker.signals.finished.connect(self._on_task_finished)
        worker.signals.error.connect(self._on_task_error)
        worker.signals.progress.connect(self._on_progress)
        
        worker.start()
    
    def _long_running_task(self):
        """长时间运行的任务(在工作线程中执行)"""
        import time
        for i in range(100):
            time.sleep(0.1)
            self.signals.progress.emit(i + 1)
        return "Task completed"
    
    @pyqtSlot(object)
    def _on_task_finished(self, result):
        """槽函数:任务完成(在主线程中执行)"""
        self.status_label.setText(str(result))
    
    @pyqtSlot(str)
    def _on_task_error(self, error):
        """槽函数:任务错误(在主线程中执行)"""
        self.status_label.setText(f"Error: {error}")
    
    @pyqtSlot(int)
    def _on_progress(self, value):
        """槽函数:进度更新(在主线程中执行)"""
        self.progress_label.setText(f"{value}%")

37.8.2 内存泄漏与资源管理

python
from PyQt6.QtCore import QObject, QTimer, pyqtSignal
from PyQt6.QtWidgets import QWidget, QPushButton, QVBoxLayout
import weakref

class ResourceManagedWidget(QWidget):
    """资源管理型控件"""
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self._timers = []
        self._workers = []
        self._connections = []
        self._setup_ui()
    
    def _setup_ui(self):
        layout = QVBoxLayout(self)
        self.button = QPushButton("Start")
        self.button.clicked.connect(self._on_click)
        layout.addWidget(self.button)
    
    def add_managed_timer(self, interval, callback):
        """添加受管理的定时器"""
        timer = QTimer(self)
        timer.timeout.connect(callback)
        timer.start(interval)
        self._timers.append(timer)
        return timer
    
    def add_managed_connection(self, signal, slot):
        """添加受管理的信号连接"""
        connection = signal.connect(slot)
        self._connections.append((signal, slot))
        return connection
    
    def cleanup(self):
        """清理所有资源"""
        for timer in self._timers:
            timer.stop()
            timer.deleteLater()
        self._timers.clear()
        
        for worker in self._workers:
            if hasattr(worker, 'quit'):
                worker.quit()
                worker.wait()
        self._workers.clear()
        
        for signal, slot in self._connections:
            try:
                signal.disconnect(slot)
            except TypeError:
                pass
        self._connections.clear()
    
    def closeEvent(self, event):
        """窗口关闭时清理资源"""
        self.cleanup()
        super().closeEvent(event)


class WeakRefManager:
    """弱引用管理器,防止循环引用"""
    
    @staticmethod
    def create_weak_callback(obj, method_name):
        """创建弱引用回调"""
        weak_obj = weakref.ref(obj)
        
        def callback(*args, **kwargs):
            obj = weak_obj()
            if obj is not None:
                method = getattr(obj, method_name)
                return method(*args, **kwargs)
        
        return callback
    
    @staticmethod
    def safe_connect(signal, obj, method_name):
        """安全的信号连接"""
        callback = WeakRefManager.create_weak_callback(obj, method_name)
        signal.connect(callback)
        return callback

37.8.3 自定义控件事件处理

python
from PyQt6.QtCore import Qt, pyqtSignal, QPointF
from PyQt6.QtGui import QMouseEvent, QKeyEvent, QWheelEvent
from PyQt6.QtWidgets import QWidget

class InteractiveWidget(QWidget):
    """交互式自定义控件"""
    
    clicked = pyqtSignal(QPointF)
    doubleClicked = pyqtSignal(QPointF)
    keyPressed = pyqtSignal(int)
    scrolled = pyqtSignal(float)
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self._last_click_time = 0
        self._double_click_threshold = 300  # ms
        self.setMouseTracking(True)
        self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
    
    def mousePressEvent(self, event: QMouseEvent):
        """鼠标按下事件"""
        if event.button() == Qt.MouseButton.LeftButton:
            pos = event.position()
            self.clicked.emit(pos)
        super().mousePressEvent(event)
    
    def mouseDoubleClickEvent(self, event: QMouseEvent):
        """鼠标双击事件"""
        if event.button() == Qt.MouseButton.LeftButton:
            pos = event.position()
            self.doubleClicked.emit(pos)
        super().mouseDoubleClickEvent(event)
    
    def keyPressEvent(self, event: QKeyEvent):
        """键盘按下事件"""
        key = event.key()
        self.keyPressed.emit(key)
        
        if key == Qt.Key.Key_Escape:
            self._on_escape()
        elif key == Qt.Key.Key_Return:
            self._on_enter()
        
        super().keyPressEvent(event)
    
    def wheelEvent(self, event: QWheelEvent):
        """滚轮事件"""
        delta = event.angleDelta().y()
        self.scrolled.emit(delta)
        super().wheelEvent(event)
    
    def _on_escape(self):
        """ESC键处理"""
        pass
    
    def _on_enter(self):
        """Enter键处理"""
        pass


class DragDropWidget(QWidget):
    """支持拖放的控件"""
    
    filesDropped = pyqtSignal(list)
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAcceptDrops(True)
        self._drag_enter_callback = None
        self._drop_callback = None
    
    def dragEnterEvent(self, event):
        """拖入事件"""
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
        else:
            event.ignore()
    
    def dragMoveEvent(self, event):
        """拖动移动事件"""
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
        else:
            event.ignore()
    
    def dropEvent(self, event):
        """放下事件"""
        mime_data = event.mimeData()
        if mime_data.hasUrls():
            files = [url.toLocalFile() for url in mime_data.urls()]
            self.filesDropped.emit(files)
            if self._drop_callback:
                self._drop_callback(files)
        event.acceptProposedAction()
    
    def set_drop_callback(self, callback):
        """设置放下回调"""
        self._drop_callback = callback

37.8.4 样式动态切换

python
from PyQt6.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout
from PyQt6.QtCore import QSettings

class ThemeManager:
    """主题管理器"""
    
    THEMES = {
        "light": """
            QWidget {
                background-color: #ffffff;
                color: #333333;
            }
            QPushButton {
                background-color: #0078d4;
                color: white;
                border: none;
                padding: 8px 16px;
                border-radius: 4px;
            }
            QPushButton:hover {
                background-color: #106ebe;
            }
        """,
        "dark": """
            QWidget {
                background-color: #1e1e1e;
                color: #d4d4d4;
            }
            QPushButton {
                background-color: #0e639c;
                color: white;
                border: none;
                padding: 8px 16px;
                border-radius: 4px;
            }
            QPushButton:hover {
                background-color: #1177bb;
            }
        """,
        "high_contrast": """
            QWidget {
                background-color: #000000;
                color: #ffffff;
                border: 2px solid #ffffff;
            }
            QPushButton {
                background-color: #ffffff;
                color: #000000;
                border: 2px solid #ffffff;
                padding: 8px 16px;
            }
            QPushButton:hover {
                background-color: #ffff00;
                color: #000000;
            }
        """
    }
    
    def __init__(self, app: QApplication):
        self.app = app
        self.settings = QSettings("MyApp", "ThemeSettings")
        self._current_theme = self.settings.value("theme", "light")
        self.apply_theme(self._current_theme)
    
    def apply_theme(self, theme_name: str):
        """应用主题"""
        if theme_name in self.THEMES:
            self._current_theme = theme_name
            self.app.setStyleSheet(self.THEMES[theme_name])
            self.settings.setValue("theme", theme_name)
    
    def toggle_theme(self):
        """切换主题"""
        themes = list(self.THEMES.keys())
        current_index = themes.index(self._current_theme)
        next_index = (current_index + 1) % len(themes)
        self.apply_theme(themes[next_index])
    
    def current_theme(self) -> str:
        """获取当前主题"""
        return self._current_theme
    
    def available_themes(self) -> list:
        """获取可用主题列表"""
        return list(self.THEMES.keys())
    
    def add_custom_theme(self, name: str, stylesheet: str):
        """添加自定义主题"""
        self.THEMES[name] = stylesheet


class ThemeAwareWidget(QWidget):
    """主题感知控件"""
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self._theme_callbacks = []
    
    def register_theme_callback(self, callback):
        """注册主题变更回调"""
        self._theme_callbacks.append(callback)
    
    def on_theme_changed(self, theme_name: str):
        """主题变更处理"""
        for callback in self._theme_callbacks:
            callback(theme_name)

37.9 本章小结

本章详细介绍了Python GUI高级开发的核心概念和实践:

  1. 高级布局:响应式布局、流式布局、边框布局
  2. 自定义控件:绘制控件、复合控件、动画控件
  3. 样式定制:QSS样式表、主题切换、动态样式
  4. 多线程界面:工作线程、线程池、信号槽机制
  5. 数据绑定:表单验证、数据同步
  6. Model-View:自定义模型、数据展示

练习题

  1. 实现一个可拖拽排序的列表控件
  2. 开发一个自定义日历控件,支持日期范围选择
  3. 实现一个属性面板,支持动态添加和编辑属性
  4. 开发一个多文档界面(MDI)应用程序
  5. 实现一个支持撤销/重做的文本编辑器

扩展阅读

Python技术丛书 - 江苏省宿城中等专业学校