Skip to content

Latest commit

 

History

History
518 lines (411 loc) · 12.3 KB

File metadata and controls

518 lines (411 loc) · 12.3 KB
name pyqt-dialogs
description PyQt/PySide6 dialogs - QFileDialog, QMessageBox, QInputDialog, QColorDialog, custom QDialog patterns
metadata
author version tags
mte90
1.0.0
python
qt
pyqt
pyside
dialogs
ui

PyQt Dialogs

Standard and custom dialog patterns for PyQt/PySide6 applications.

Standard Dialogs

QFileDialog

from PySide6.QtWidgets import QFileDialog

# Open single file
filename, _ = QFileDialog.getOpenFileName(
    self,
    "Open File",
    "/home/user",  # Starting directory
    "Images (*.png *.jpg);;Text Files (*.txt);;All Files (*)"
)

if filename:
    print(f"Selected: {filename}")

# Save file
filename, _ = QFileDialog.getSaveFileName(
    self,
    "Save File",
    "/home/user/untitled.txt",
    "Text Files (*.txt);;All Files (*)"
)

# Select directory
directory = QFileDialog.getExistingDirectory(
    self,
    "Select Directory",
    "/home/user",
    QFileDialog.Option.ShowDirsOnly
)

# Open multiple files
files, _ = QFileDialog.getOpenFileNames(
    self,
    "Open Files",
    "/home/user",
    "Images (*.png *.jpg)"
)

for f in files:
    print(f)

# Options
options = QFileDialog.Option.DontUseNativeDialog  # Use Qt dialog instead of OS dialog
filename, _ = QFileDialog.getOpenFileName(self, "Open", "", "", options=options)

QMessageBox

from PySide6.QtWidgets import QMessageBox

# Question dialog
reply = QMessageBox.question(
    self,
    "Confirm",
    "Are you sure?",
    QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
    QMessageBox.StandardButton.No
)

if reply == QMessageBox.StandardButton.Yes:
    print("User confirmed")

# Information
QMessageBox.information(self, "Info", "Operation completed successfully")

# Warning
QMessageBox.warning(self, "Warning", "This action cannot be undone")

# Critical error
QMessageBox.critical(self, "Error", "Failed to connect to server")

# About
QMessageBox.about(self, "About", "My App v1.0\n\nCopyright 2024")

# About Qt
QMessageBox.aboutQt(self)

# Custom buttons
msg = QMessageBox(self)
msg.setWindowTitle("Custom Dialog")
msg.setText("Continue?")
msg.setIcon(QMessageBox.Icon.Question)
msg.addButton("Yes", QMessageBox.ButtonRole.YesRole)
msg.addButton("No", QMessageBox.ButtonRole.NoRole)
msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)

result = msg.exec()
print(f"Button role: {msg.buttonRole(msg.clickedButton())}")

QInputDialog

from PySide6.QtWidgets import QInputDialog, QLineEdit

# Get text
text, ok = QInputDialog.getText(
    self,
    "Input",
    "Enter name:",
    QLineEdit.EchoMode.Normal,
    "Default value"
)

if ok and text:
    print(f"Name: {text}")

# Get integer
value, ok = QInputDialog.getInt(
    self,
    "Input",
    "Enter age:",
    25,  # Default
    0,   # Min
    120, # Max
    1    # Step
)

if ok:
    print(f"Age: {value}")

# Get double
price, ok = QInputDialog.getDouble(
    self,
    "Input",
    "Enter price:",
    0.0,
    0.0,
    1000.0,
    2  # Decimals
)

if ok:
    print(f"Price: ${price:.2f}")

# Get item from list
items = ["Option 1", "Option 2", "Option 3"]
item, ok = QInputDialog.getItem(
    self,
    "Select",
    "Choose an option:",
    items,
    0,    # Current index
    False # Editable
)

if ok:
    print(f"Selected: {item}")

# Get multiline text
text, ok = QInputDialog.getMultiLineText(
    self,
    "Input",
    "Enter description:",
    "Default\ntext"
)

QColorDialog

from PySide6.QtWidgets import QColorDialog
from PySide6.QtGui import QColor

# Get color
color = QColorDialog.getColor(
    QColor(255, 0, 0),  # Default color
    self,
    "Select Color"
)

if color.isValid():
    print(f"Color: {color.name()}")  # "#ff0000"
    widget.setStyleSheet(f"background-color: {color.name()};")

# With alpha
color = QColorDialog.getColor(
    QColor(255, 0, 0, 128),
    self,
    "Select Color with Alpha",
    QColorDialog.ColorDialogOption.ShowAlphaChannel
)

# Get color with options
options = (
    QColorDialog.ColorDialogOption.ShowAlphaChannel |
    QColorDialog.ColorDialogOption.NoButtons
)
color = QColorDialog.getColor(Qt.white, self, "Color", options)

QFontDialog

from PySide6.QtWidgets import QFontDialog
from PySide6.QtGui import QFont

# Get font
font, ok = QFontDialog.getFont(
    QFont("Arial", 12),  # Default font
    self,
    "Select Font"
)

if ok:
    print(f"Font: {font.family()}, Size: {font.pointSize()}")
    widget.setFont(font)

# With options
font, ok = QFontDialog.getFont(
    QFont(),
    self,
    "Select Font",
    QFontDialog.FontDialogOption.MonospacedFonts
)

Custom Dialogs

Basic Custom Dialog

from PySide6.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QLabel,
    QLineEdit, QDialogButtonBox, QFormLayout
)

class InputDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Input Dialog")
        self.setMinimumWidth(400)
        
        layout = QVBoxLayout(self)
        
        # Form
        form = QFormLayout()
        self.name_edit = QLineEdit()
        self.email_edit = QLineEdit()
        form.addRow("Name:", self.name_edit)
        form.addRow("Email:", self.email_edit)
        layout.addLayout(form)
        
        # Buttons
        buttons = QDialogButtonBox(
            QDialogButtonBox.StandardButton.Ok |
            QDialogButtonBox.StandardButton.Cancel
        )
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addWidget(buttons)
    
    def get_values(self):
        return {
            "name": self.name_edit.text(),
            "email": self.email_edit.text()
        }
    
    def set_values(self, name="", email=""):
        self.name_edit.setText(name)
        self.email_edit.setText(email)

# Usage
dialog = InputDialog(self)
dialog.set_values("John", "john@example.com")

if dialog.exec() == QDialog.DialogCode.Accepted:
    values = dialog.get_values()
    print(f"Name: {values['name']}, Email: {values['email']}")

Dialog with Validation

class ValidatedDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Validated Input")
        
        layout = QVBoxLayout(self)
        
        # Input
        form = QFormLayout()
        self.age_spin = QSpinBox()
        self.age_spin.setRange(0, 120)
        self.email_edit = QLineEdit()
        form.addRow("Age:", self.age_spin)
        form.addRow("Email:", self.email_edit)
        layout.addLayout(form)
        
        # Error label
        self.error_label = QLabel()
        self.error_label.setStyleSheet("color: red;")
        layout.addWidget(self.error_label)
        
        # Buttons
        self.buttons = QDialogButtonBox(
            QDialogButtonBox.StandardButton.Ok |
            QDialogButtonBox.StandardButton.Cancel
        )
        self.buttons.accepted.connect(self.try_accept)
        self.buttons.rejected.connect(self.reject)
        layout.addWidget(self.buttons)
    
    def try_accept(self):
        if not self.validate():
            return
        self.accept()
    
    def validate(self):
        import re
        
        # Validate email
        email = self.email_edit.text()
        if not email:
            self.error_label.setText("Email is required")
            return False
        
        if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
            self.error_label.setText("Invalid email format")
            return False
        
        self.error_label.clear()
        return True
    
    def get_values(self):
        return {
            "age": self.age_spin.value(),
            "email": self.email_edit.text()
        }

Modeless Dialog

class SearchDialog(QDialog):
    """Non-modal (modeless) dialog that stays open."""
    searchRequested = Signal(str)
    
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Search")
        self.setWindowFlags(
            Qt.WindowType.Dialog |
            Qt.WindowType.WindowCloseButtonHint
        )
        
        layout = QVBoxLayout(self)
        
        self.search_edit = QLineEdit()
        self.search_edit.setPlaceholderText("Enter search term...")
        self.search_edit.returnPressed.connect(self.search)
        
        self.search_btn = QPushButton("Search")
        self.search_btn.clicked.connect(self.search)
        
        layout.addWidget(self.search_edit)
        layout.addWidget(self.search_btn)
    
    def search(self):
        term = self.search_edit.text()
        if term:
            self.searchRequested.emit(term)

# Usage
search_dialog = SearchDialog(self)
search_dialog.searchRequested.connect(self.perform_search)
search_dialog.show()  # Use show() instead of exec() for modeless

Best Practices

  1. Use standard dialogs when possible - They're familiar and consistent
  2. Provide sensible defaults - Pre-fill common values
  3. Validate input before accepting - Show clear error messages
  4. Use QDialogButtonBox - Ensures correct button ordering per platform
  5. Set minimum size - Prevent dialogs from being too small
  6. Consider modeless dialogs - For search, find/replace, etc.

Qt 6 Dialogs

QDialog Modern Patterns

# ✅ GOOD: Use QDialog for custom dialogs
class MyDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Settings")
        self.setModal(True)
        
        layout = QVBoxLayout()
        # ... widgets ...
        self.setLayout(layout)
        
        # Confirm/Cancel buttons
        btn_box = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel
        )
        btn_box.accepted.connect(self.accept)
        btn_box.rejected.connect(self.reject)
        layout.addWidget(btn_box)
        
    def get_value(self):
        if self.result() == QDialog.Accepted:
            return self.value
    
    # Qt 6: add native buttons
    # (replace QDialogButtonBox)
    self.addNativeButton(QDialogButtonBox.StandardButton.Ok)

Modal vs Non-modal

# Modal (blocks parent)
dialog = MyDialog(parent)
result = dialog.exec()  # Blocks until closed

# Non-modal (doesn't block)
dialog = MyDialog(parent)
dialog.show()  # Doesn't block

Best Practices (Extended)

# ✅ GOOD: Always have Cancel button
button_box = QDialogButtonBox(
    QDialogButtonBox.Ok | QDialogButtonBox.Cancel
)

# ✅ GOOD: Use QDialogButtonBox for standard buttons
# Avoid manual QDialogButton instances

# ✅ GOOD: Pass parent to dialogs
dialog = QDialog(parent_window)

# ❌ BAD: Creating dialogs without parent
dialog = QDialog()  # No parent = orphaned

Advanced Dialogs

Multi-page Dialog

class MultiPageDialog(QDialog):
    def __init__(self):
        super().__init__()
        self.pages = [Page1(), Page2(), Page3()]
        
        self.current_page = 0
        self.setup_ui()
    
    def setup_ui(self):
        main_layout = QVBoxLayout()
        
        # Page navigation
        page_layout = QHBoxLayout()
        prev_btn = QPushButton("← Prev")
        next_btn = QPushButton("Next →")
        prev_btn.clicked.connect(lambda: self.set_page(-1))
        next_btn.clicked.connect(lambda: self.set_page(1))
        page_layout.addWidget(prev_btn)
        page_layout.addWidget(next_btn)
        
        self.page_container = QWidget()
        self.page_container_layout = QVBoxLayout(self.page_container)
        self.page_container_layout.addWidget(self.pages[0])
        
        main_layout.addLayout(page_layout)
        main_layout.addWidget(self.page_container)
        self.setLayout(main_layout)
    
    def set_page(self, delta):
        self.current_page += delta
        if 0 <= self.current_page < len(self.pages):
            self.page_container_layout.deleteWidget(self.pages[0])
            self.page_container_layout.addWidget(self.pages[self.current_page])
        else:
            self.current_page = max(0, min(len(self.pages)-1, self.current_page))

References