design.md 16.5 KB

Design: PySide6 Migration Architecture

Overview

This document describes the architectural design for migrating from tkinter to PySide6, including class structure, layout design, and implementation patterns.

Architecture

Application Structure

image_generator.py (Qt-based)
├── LoginDialog (QDialog)
│   ├── Database authentication
│   ├── Remember me functionality
│   └── Returns: (success, username, remember_user, remember_password, password_hash)
│
├── ImageGeneratorWindow (QMainWindow)
│   ├── Central widget with main layout
│   ├── Reference images section
│   ├── Prompt section
│   ├── Settings section
│   ├── Action buttons
│   └── Preview area
│
└── Worker Threads (QThread)
    └── ImageGenerationWorker
        └── Handles async image generation

LoginDialog Design

Class Structure

class LoginDialog(QDialog):
    """
    Qt-based login dialog
    Replaces: LoginWindow (tkinter)
    """

    def __init__(self, db_config, last_user="", saved_password_hash=""):
        super().__init__()
        self.db_config = db_config
        self.last_user = last_user
        self.saved_password_hash = saved_password_hash

        self.success = False
        self.authenticated_user = ""
        self.password_changed = False
        self.current_password_hash = ""

        self.setup_ui()

    def setup_ui(self):
        """Build UI using Qt layouts"""
        # Will use QVBoxLayout for vertical stacking
        # QFormLayout for label+input pairs
        # QHBoxLayout for checkbox row

Layout Design

┌─────────────────────────────────────┐
│          LoginDialog                │
│  ┌───────────────────────────────┐  │
│  │   QVBoxLayout (main)          │  │
│  │  ┌─────────────────────────┐  │  │
│  │  │  QLabel: "登录"          │  │  │
│  │  │  (title, centered)       │  │  │
│  │  └─────────────────────────┘  │  │
│  │                               │  │
│  │  ┌─────────────────────────┐  │  │
│  │  │ QFormLayout:            │  │  │
│  │  │  "用户名:" - QLineEdit   │  │  │
│  │  │  "密码:"   - QLineEdit   │  │  │
│  │  └─────────────────────────┘  │  │
│  │                               │  │
│  │  ┌─────────────────────────┐  │  │
│  │  │ QHBoxLayout:            │  │  │
│  │  │  ☑ 记住用户名            │  │  │
│  │  │  ☑ 记住密码              │  │  │
│  │  └─────────────────────────┘  │  │
│  │                               │  │
│  │  ┌─────────────────────────┐  │  │
│  │  │ QPushButton: "登录"      │  │  │
│  │  └─────────────────────────┘  │  │
│  │                               │  │
│  │  ┌─────────────────────────┐  │  │
│  │  │ QLabel: error_label     │  │  │
│  │  └─────────────────────────┘  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

Styling Approach

Use QSS (Qt Style Sheets) for consistent styling:

def apply_styles(self):
    self.setStyleSheet("""
        QDialog {
            background-color: #ffffff;
        }
        QLabel#title {
            font-size: 20pt;
            font-weight: bold;
            color: #1d1d1f;
        }
        QLabel#field_label {
            font-size: 10pt;
            color: #666666;
        }
        QLineEdit {
            padding: 8px;
            border: 1px solid #e5e5e5;
            border-radius: 4px;
            background-color: #fafafa;
            font-size: 11pt;
        }
        QPushButton#login_button {
            background-color: #007AFF;
            color: white;
            font-size: 10pt;
            font-weight: bold;
            padding: 10px 20px;
            border: none;
            border-radius: 6px;
        }
        QPushButton#login_button:hover {
            background-color: #0051D5;
        }
        QLabel#error_label {
            color: #ff3b30;
            font-size: 9pt;
        }
    """)

ImageGeneratorWindow Design

Class Structure

class ImageGeneratorWindow(QMainWindow):
    """
    Qt-based main application window
    Replaces: ImageGeneratorApp (tkinter)
    """

    def __init__(self):
        super().__init__()
        self.api_key = ""
        self.uploaded_images = []  # List of file paths
        self.generated_image_data = None
        self.generated_image_bytes = None
        self.saved_prompts = []

        self.load_config()
        self.setup_ui()

    def setup_ui(self):
        """Build main window UI"""
        # Central widget with QVBoxLayout
        # Sections organized with QGroupBox or QFrame

Layout Design

┌──────────────────────────────────────────────────────────┐
│  ImageGeneratorWindow (QMainWindow)                      │
│  ┌────────────────────────────────────────────────────┐  │
│  │ QVBoxLayout (central widget)                       │  │
│  │                                                    │  │
│  │  ┌──────────────────────────────────────────────┐  │  │
│  │  │ QGroupBox: "参考图片"                         │  │  │
│  │  │  ┌────────────────────────────────────────┐  │  │  │
│  │  │  │ QHBoxLayout:                           │  │  │  │
│  │  │  │   QPushButton: "+ 添加图片"             │  │  │  │
│  │  │  │   QLabel: "已选择 X 张"                 │  │  │  │
│  │  │  └────────────────────────────────────────┘  │  │  │
│  │  │  ┌────────────────────────────────────────┐  │  │  │
│  │  │  │ QScrollArea (horizontal):              │  │  │  │
│  │  │  │   Image previews with delete buttons   │  │  │  │
│  │  │  └────────────────────────────────────────┘  │  │  │
│  │  └──────────────────────────────────────────────┘  │  │
│  │                                                    │  │
│  │  ┌──────────────────────────────────────────────┐  │  │
│  │  │ QHBoxLayout (content row)                    │  │  │
│  │  │  ┌────────────────┐  ┌──────────────────┐   │  │  │
│  │  │  │ QGroupBox:     │  │ QGroupBox:       │   │  │  │
│  │  │  │ "提示词"        │  │ "生成设置"        │   │  │  │
│  │  │  │  - Toolbar     │  │  - 宽高比         │   │  │  │
│  │  │  │  - QTextEdit   │  │  - 图片尺寸       │   │  │  │
│  │  │  └────────────────┘  └──────────────────┘   │  │  │
│  │  └──────────────────────────────────────────────┘  │  │
│  │                                                    │  │
│  │  ┌──────────────────────────────────────────────┐  │  │
│  │  │ QHBoxLayout (action buttons)                 │  │  │
│  │  │   QPushButton: "生成图片"                      │  │  │
│  │  │   QPushButton: "下载图片"                      │  │  │
│  │  │   QLabel: status                             │  │  │
│  │  └──────────────────────────────────────────────┘  │  │
│  │                                                    │  │
│  │  ┌──────────────────────────────────────────────┐  │  │
│  │  │ QGroupBox: "预览"                             │  │  │
│  │  │  ┌────────────────────────────────────────┐  │  │  │
│  │  │  │ QLabel (image display)                 │  │  │  │
│  │  │  │  - Scaled pixmap                       │  │  │  │
│  │  │  │  - Double-click to open                │  │  │  │
│  │  │  └────────────────────────────────────────┘  │  │  │
│  │  └──────────────────────────────────────────────┘  │  │
│  └────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────┘

Threading Design

Qt uses QThread for background operations:

class ImageGenerationWorker(QThread):
    """
    Worker thread for image generation
    Replaces: threading.Thread
    """
    # Signals for communication
    finished = Signal(bytes)  # Image bytes
    error = Signal(str)  # Error message
    progress = Signal(str)  # Status updates

    def __init__(self, api_key, prompt, images, config):
        super().__init__()
        self.api_key = api_key
        self.prompt = prompt
        self.images = images
        self.config = config

    def run(self):
        """Execute in background thread"""
        try:
            # Generate image
            image_bytes = self.generate_image()
            self.finished.emit(image_bytes)
        except Exception as e:
            self.error.emit(str(e))

# Usage in main window:
def generate_image_async(self):
    self.worker = ImageGenerationWorker(...)
    self.worker.finished.connect(self.on_image_generated)
    self.worker.error.connect(self.on_generation_error)
    self.worker.progress.connect(self.update_status)
    self.worker.start()

Benefits over threading.Thread:

  • Thread-safe signal/slot mechanism
  • No need for root.after() for UI updates
  • Automatic cleanup
  • Better error handling

Data Flow

Configuration Management

# Same as tkinter version
def load_config(self):
    """Load from config.json"""
    # Identical logic, just different widget updates

def save_config(self):
    """Save to config.json"""
    # Identical logic

Authentication Flow

# In main.py or __main__:
app = QApplication(sys.argv)

# Show login dialog
login_dialog = LoginDialog(db_config, last_user, saved_password_hash)
if login_dialog.exec() == QDialog.Accepted:
    # Login successful
    main_window = ImageGeneratorWindow()
    main_window.show()
    sys.exit(app.exec())
else:
    # Login cancelled or failed
    sys.exit(0)

Styling Strategy

Approach 1: QSS (Recommended)

# Define styles in QSS (similar to CSS)
app.setStyleSheet("""
    QGroupBox {
        font-weight: bold;
        border: 1px solid #e5e5e5;
        border-radius: 6px;
        margin-top: 6px;
        padding-top: 10px;
    }
    QGroupBox::title {
        color: #1d1d1f;
    }
""")

Benefits:

  • Centralized styling
  • Easy to modify
  • Similar to web CSS
  • Can load from external file

Approach 2: Programmatic Styling

# Set properties directly
button = QPushButton("生成图片")
button.setStyleSheet("background-color: #007AFF; color: white;")
font = QFont("Segoe UI", 10, QFont.Bold)
button.setFont(font)

Recommendation: Use QSS for consistent styling, programmatic for dynamic changes.

Image Handling

Display Images

# Tkinter used PIL ImageTk.PhotoImage
# Qt uses QPixmap

# Load and display
pixmap = QPixmap(file_path)
scaled_pixmap = pixmap.scaled(
    width, height,
    Qt.KeepAspectRatio,
    Qt.SmoothTransformation
)
image_label.setPixmap(scaled_pixmap)

Thumbnails

# Create thumbnail
thumbnail = pixmap.scaled(
    100, 100,
    Qt.KeepAspectRatio,
    Qt.SmoothTransformation
)

Migration Compatibility

Preserve Functionality

All existing features must work identically:

Feature tkinter Implementation Qt Implementation
Login tk.Toplevel + mainloop() QDialog.exec()
Remember me tk.BooleanVar QCheckBox.isChecked()
Password hash String storage Identical
Config save JSON file Identical
Image upload filedialog.askopenfilenames QFileDialog.getOpenFileNames
Image generation threading.Thread QThread
Status updates root.after() Signal.emit()
Image display PhotoImage QPixmap

Error Handling

# Qt has better exception handling in threads
class ImageGenerationWorker(QThread):
    error = Signal(str)

    def run(self):
        try:
            # ... generation code ...
        except Exception as e:
            self.error.emit(str(e))

# In main window:
def on_generation_error(self, error_msg):
    QMessageBox.critical(self, "错误", f"生成失败: {error_msg}")

Testing Strategy

Unit Testing

# Qt has QTest framework
from PySide6.QtTest import QTest

def test_login_dialog():
    dialog = LoginDialog(db_config)

    # Simulate user input
    dialog.username_entry.setText("test_user")
    dialog.password_entry.setText("password")

    # Simulate button click
    QTest.mouseClick(dialog.login_button, Qt.LeftButton)

    assert dialog.success == True

Manual Testing Checklist

  • macOS: All UI elements visible
  • Windows: Native look and feel
  • Login: Authentication works
  • Image generation: API calls work
  • File operations: Upload/download work
  • Config: Save/load works
  • Threading: No UI freezing

Performance Considerations

Qt Advantages:

  • Native rendering (faster than tkinter)
  • Better memory management
  • Hardware acceleration support
  • Efficient image handling

Optimization:

  • Use QPixmap cache for repeated images
  • Lazy-load image thumbnails
  • Use QThread for all I/O operations
  • Implement loading indicators

Future Enhancements

With Qt, we can easily add:

  • Drag-and-drop image upload
  • Progress bars for generation
  • Animations and transitions
  • Custom window chrome
  • System tray integration
  • Better keyboard shortcuts
  • Undo/redo functionality
  • More sophisticated layouts

Conclusion

The Qt architecture provides:

  1. Reliability: Native rendering solves macOS issues
  2. Maintainability: Cleaner code structure
  3. Extensibility: Easy to add features
  4. Performance: Better than tkinter
  5. Cross-platform: Consistent behavior

This design preserves all existing functionality while providing a solid foundation for future improvements.