freelsn / notes

0 stars 0 forks source link

PyQt 基础 #2

Open freelsn opened 1 year ago

freelsn commented 1 year ago

PyQt 最基本的程序

利用 PyQt 创建一个空窗口,这里的代码是任何 PyQt 程序都需要包含的。

'basic_window.py'

import sys

from PyQt5.QtWidgets import QApplication, QWidget

class EmptyWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setGeometry(200, 100, 400, 300)  # 设置窗口的初始位置和大小
        self.setWindowTitle('Empty Window in PyQt')  # 设置窗口标题
        self.show()  # 显示窗口

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = EmptyWindow()
    sys.exit(app.exec())

执行命令 python basic_window.py,将弹出如图所示的窗口。

image

QApplication is responsible for managing the application’s main event loop and widget initialization and finalization. The main event loop is where user interactions in the GUI window, such as clicking on a button, are managed. 它的输入是 list,支持从命令行输入参数,比如执行 python basic_window.py -style Fusion。如果不需要接受命令行参数,可以写成 app=QApplication([])

The method exec() starts the application’s event loop and will remain here until you quit the application. The function sys.exit() ensures a clean exit.

在创建一个窗口时,一般会创建一个 class,并继承 QMainWindowQWidgetQDialog 其中一个。

freelsn commented 1 year ago

QLabel

A QLabel object acts as a non-editable placeholder to display plain or rich text, hyperlinks, images, or GIFs.

QPixmap

QPixmap is a Qt class that is optimized for showing images on the screen and is useful for displaying an image on a QLabel object.

Note

Qt is filled with numerous class methods called accessors, also referred to as getters, for retrieving values and mutators, also called setters, for changing values. You have already seen two setter examples. to change the size of a widget or widget, you can use the setter setGeometry(). If you wanted to retrieve that value at any time, you could use the getter geometry(). Setter and getter methods follow that pattern in pyQt, where setters have the word set in the method name, and the getter removes the word set and replaces the first letter with a lowercase one.

'labels.py'

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel
from PyQt5.QtGui import QPixmap

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setGeometry(100, 100, 250, 250)
        self.setWindowTitle('QLabel Example')
        self.setUpMainWindow()
        self.show()

    def setUpMainWindow(self):
        hello_label = QLabel(self)
        hello_label.setText('Hello')
        hello_label.move(105, 15)  # 调整label的位置

        image = 'images/world.png'
        try:
            with open(image):
                world_label = QLabel(self)
                pixmap = QPixmap(image)
                world_label.setPixmap(pixmap)
                world_label.move(25, 40)
        except FileNotFoundError as error:
            print(f'Image not found.\n Error: {error}')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
'user_profile.py'

import sys
from tarfile import POSIX_MAGIC
from PyQt5.QtWidgets import QApplication, QWidget, QLabel
from PyQt5.QtGui import QFont, QPixmap

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setGeometry(50, 50, 250, 400)
        self.setWindowTitle('User Profile GUI')
        self.setUpMainWindow()
        self.show()

    def setUpMainWindow(self):
        self.createImageLabels()

        user_label = QLabel(self)
        user_label.setText('John Doe')
        user_label.setFont(QFont('Arial', 20))
        user_label.move(85, 140)

        bio_label = QLabel(self)
        bio_label.setText('Biography')
        bio_label.setFont(QFont('Arial', 17))
        bio_label.move(15, 170)

        about_label = QLabel(self)
        about_label.setText("I'm a software Engineer with 10 years\
            experience creating awesome code.")
        about_label.setWordWrap(True)
        about_label.move(15, 190)

        skills_label = QLabel(self)
        skills_label.setText('Skills')
        skills_label.setFont(QFont('Arial', 17))
        skills_label.move(15, 240)

        languages_label = QLabel(self)
        languages_label.setText('Python | PHP | SQL | JavaScript')
        languages_label.move(15, 260)

        experience_label = QLabel(self)
        experience_label.setText('Experience')
        experience_label.setFont(QFont('Arial', 17))
        experience_label.move(15, 290)

        developer_label = QLabel(self)
        developer_label.setText('Python Developer')
        developer_label.move(15, 310)

        dev_dates_label = QLabel(self)
        dev_dates_label.setText('Mar 2011 - Present')
        dev_dates_label.setFont(QFont('Arial', 10))
        dev_dates_label.move(15, 330)

        driver_label = QLabel(self)
        driver_label.setText('Pizza Delivery Driver')
        driver_label.move(15, 350)

        driver_dates_label = QLabel(self)
        driver_dates_label.setText('Aug 2015 - Dec 2017')
        driver_dates_label.setFont(QFont('Arial', 10))
        driver_dates_label.move(15, 370)

    def createImageLabels(self):
        bg_label = QLabel(self)
        bg_label.setPixmap(QPixmap('images/skyblue.png'))
        profile_label = QLabel(self)
        profile_label.setPixmap(QPixmap('images/profile_image.png'))

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
freelsn commented 1 year ago

设计良好交互界面的理念

清晰

使用较大的尺寸、较深的颜色便于观察。

简洁

提供的按钮或文字不是越多越好,够用就行。

一致

整体和部分的设计风格要一致,包括颜色搭配等。

高效

设计高效的操作逻辑。

熟悉

要符合用户的使用习惯,也就是要遵循大部分UI的设计逻辑,比如先输入用户名再输入密码。

反馈

当用户执行操作后,要提供适当的反馈。比如按钮颜色的变化、弹出输入错误提示、声音提示等。

freelsn commented 1 year ago

如果 VSCode 无法自动补全,需要执行 pip install pyqt5-stubs

freelsn commented 1 year ago

事件处理

GUI 是事件驱动的。意思是当某个事件被触发时,会发出一个信号,被主程序监听到,根据信号的内容进行响应,整个过程称为事件处理。调用 exec() 后,主程序开始持续监听信号,直到整个程序结束。很多 widget 都可以发出信号,比如 QPushButton,点击按钮后,发出 clicked 信号。在程序中将一个没有输入的函数绑定到 clicked,这样在信号触发后就可以调用这个函数,此函数称为 slot。

freelsn commented 1 year ago

QPushButton

'buttons.py'

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton
from PyQt5.QtCore import Qt  # Qt Namespace

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setGeometry(100, 100, 250, 150)
        self.setWindowTitle('QPushButton Example')
        self.setUpMainWindow()
        self.show()

    def setUpMainWindow(self):
        self.times_pressed = 0

        self.name_label = QLabel("Don't push the button.", self)
        self.name_label.setAlignment(Qt.AlignCenter)  #
        self.name_label.move(60, 30)

        self.button = QPushButton('Push Me', self)
        self.button.move(80, 70)
        self.button.clicked.connect(self.buttonClicked)  #

    def buttonClicked(self):
        self.times_pressed += 1

        if self.times_pressed == 1:
            self.name_label.setText("Why'd you press me?")
        if self.times_pressed == 2:
            self.name_label.setText("I'm warning you.")
            self.button.setText("Feelin' Lucky?")
            self.button.adjustSize()
            self.button.move(70, 70)
        if self.times_pressed == 3:
            print('The window has been closed.')
            self.close()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
freelsn commented 1 year ago

QLineEdit

当用户需要输入信息时,比如输入用户名或者密码时,可以使用此组件。此组件提供一行输入栏,支持常用操作包括:

'line_edits.py'

import sys
from PyQt5.QtWidgets import (QApplication,
                             QWidget,
                             QLabel,
                             QLineEdit,
                             QPushButton)

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setMaximumSize(310, 130)  #
        self.setWindowTitle('QLineEdit Example')
        self.setUpMainWindow()
        self.show()

    def setUpMainWindow(self):
        QLabel('Please enter you name below.', self).move(70, 10)
        name_label = QLabel('Name', self)
        name_label.move(20, 50)

        self.name_edit = QLineEdit(self)
        self.name_edit.resize(210, 20)
        self.name_edit.move(70, 50)

        clear_button = QPushButton('Clear', self)
        clear_button.move(140, 90)
        clear_button.clicked.connect(self.clearText)

        accept_button = QPushButton('OK', self)
        accept_button.move(210, 90)
        accept_button.clicked.connect(self.acceptText)

    def clearText(self):
        self.name_edit.clear()

    def acceptText(self):
        print(self.name_edit.text())
        self.close()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
freelsn commented 1 year ago

QCheckBox

The QCheckBox widget is a selectable button that generally has two states: on and off.

the checkboxes in QCheckBox are not mutually exclusive, meaning you can select more than one checkbox at a time. to make them mutually exclusive, add the checkboxes to a QButtonGroup object or consider using QRadioButton.

'checkboxes.py'

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QCheckBox, QLabel

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setGeometry(100, 100, 250, 150)
        self.setWindowTitle('QCheckBox Example')
        self.setUpMainWindow()
        self.show()

    def setUpMainWindow(self):
        header_label = QLabel('Which shifts can you work? \
            (Please check all that apply)', self)
        header_label.setWordWrap(True)
        header_label.move(20, 10)

        morning_cb = QCheckBox('Morning [8 AM - 2 AM]', self)
        morning_cb.move(40, 60)
        # morning_cb.toggle()  # Uncomment to start checked
        morning_cb.toggled.connect(self.printSelected)

        after_cb = QCheckBox('Afternoon [1 PM - 8 PM]', self)
        after_cb.move(40, 80)
        after_cb.toggled.connect(self.printSelected)

        night_cb = QCheckBox('Night [7 PM - 3 AM]', self)
        night_cb.move(40, 100)
        night_cb.toggled.connect(self.printSelected)

    def printSelected(self, checked):
        # With so many widgets connected to the same slot,
        # it can be hard to determine which widget is being
        # interacted with and emitting the signal.
        # Thankfully, the QObject method sender() returns
        # which object (the widget) is sending the signal.
        sender = self.sender()
        if checked:
            print(f'{sender.text()} Selected.')
        else:
            print(f'{sender.text()} Deselected.')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
freelsn commented 1 year ago

QMessageBox

有时用户执行操作后希望得到对话框形式的反馈,目的包括二次确认、进一步输入信息等。此时可以使用 dialog box。被弹出的窗口叫做 dialog,弹出前的窗口是 dialog 的 parent。Parent 一般决定 dialog 的默认位置。

Dialog 主要分两种:

  1. Modal dialogs:当窗口弹出后,只能操作 dialog,不能操作其它窗口。属于阻塞式的。
  2. Modaless dialogs:当窗口弹出后,可以操作任意其它窗口。属于非阻塞式的。

QMessageBox 是 dialog 的一种,主要通过以下四种静态方法创建:

'message_boxes.py'

import sys
from PyQt5.QtWidgets import (QApplication,
                             QWidget,
                             QLabel,
                             QMessageBox,
                             QLineEdit,
                             QPushButton)
from PyQt5.QtGui import QFont

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setGeometry(100, 100, 340, 140)
        self.setWindowTitle('QMessageBox Example')
        self.setUpMainWindow()
        self.show()

    def setUpMainWindow(self):
        catalogue_label = QLabel('Author Catalogue', self)
        catalogue_label.move(100, 10)
        catalogue_label.setFont(QFont('Arial', 18))

        search_label = QLabel('Search the index for an author:', self)
        search_label.move(20, 40)

        author_label = QLabel('Name:', self)
        author_label.move(20, 74)

        self.author_edit = QLineEdit(self)
        self.author_edit.move(70, 70)
        self.author_edit.resize(240, 24)
        self.author_edit.setPlaceholderText('Enter names as: First Last')  #

        search_button = QPushButton('Search', self)
        search_button.move(140, 100)
        search_button.clicked.connect(self.searchAuthors)

    def searchAuthors(self):
        authors = ('Name 1', 'Name 2', 'Name 3')
        if self.author_edit.text() in authors:
            QMessageBox.information(
                self,  # parent
                'Author Found',  # title
                'Author found in catalogue!',  # text
                QMessageBox.Ok  # buttons
            )
        else:
            answer = QMessageBox.question(
                self,
                'Author Not Found',
                """<p>Author not found in catalogue.</p>
                <p>Do you wish to continue?</p>""",
                QMessageBox.Yes | QMessageBox.No,
                QMessageBox.No  # defaultButton
            )
            if answer == QMessageBox.No:
                print('Closing application.')
                self.close()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
freelsn commented 1 year ago

When a QWidget is closed in PyQt, a QCloseEvent object is generated. However, widgets and windows are not actually closed. Rather, they are hidden from view if the event is accepted. The reason that the application actually quits when the LoginWindow instance is closed is due to a signal that is emitted when the last main window (one with no parent) is no longer visible. That signal is QApplication.lastWindowClosed(), which is already handled by PyQt.

freelsn commented 1 year ago
'login_gui.py'

import sys
from PyQt5.QtWidgets import (QApplication,
                             QWidget,
                             QDialog,
                             QLabel,
                             QLineEdit,
                             QPushButton,
                             QCheckBox,
                             QMessageBox)
from PyQt5.QtGui import QFont, QPixmap

class LoginWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setFixedSize(360, 220)
        self.setWindowTitle('Login GUI')
        self.setUpWindow()
        self.show()

    def setUpWindow(self):
        self.login_is_successful = False

        login_label = QLabel('Login', self)
        login_label.setFont(QFont('Arial', 20))
        login_label.move(160, 10)

        username_label = QLabel('Username:', self)
        username_label.move(20, 54)

        self.username_edit = QLineEdit(self)
        self.username_edit.resize(250, 24)
        self.username_edit.move(90, 50)

        password_label = QLabel('Password:', self)
        password_label.move(20, 86)

        self.password_edit = QLineEdit(self)
        self.password_edit.setEchoMode(QLineEdit.Password)  # hide text
        self.password_edit.resize(250, 24)
        self.password_edit.move(90, 82)

        self.show_password_cb = QCheckBox('Show Password', self)
        self.show_password_cb.move(90, 110)
        self.show_password_cb.toggled.connect(self.displayPasswordIfChecked)

        login_button = QPushButton('Login', self)
        login_button.resize(320, 34)
        login_button.move(20, 140)
        login_button.clicked.connect(self.clickLoginButton)

        not_member_label = QLabel('Not a member?', self)
        not_member_label.move(20, 186)

        sign_up_button = QPushButton('Sign Up', self)
        sign_up_button.move(120, 180)
        sign_up_button.clicked.connect(self.createNewUser)

    def clickLoginButton(self):
        users = {
            'admin': 'password',
            'User1234': 'CoolGuy',
        }
        username = self.username_edit.text()
        password = self.password_edit.text()

        if (username, password) in users.items():
            QMessageBox.information(self,
                                    'Login Successful!',
                                    'Login Successful!',
                                    QMessageBox.Ok,
                                    QMessageBox.Ok)
            self.login_is_successful = True
            # It is worth noting that close() does not actually
            # close the window like you may think.
            # The window is merely hidden from view.
            self.close()
            self.openApplicationWindow()
        else:
            QMessageBox.warning(self,
                                'Error Message',
                                'The username or password is incorrect.',
                                QMessageBox.Close,
                                QMessageBox.Close)

    def displayPasswordIfChecked(self, checked):
        if checked:
            self.password_edit.setEchoMode(QLineEdit.Normal)
        else:
            self.password_edit.setEchoMode(QLineEdit.Password)

    def createNewUser(self):
        self.create_new_user_window = NewUserDialog()
        self.create_new_user_window.show()

    def openApplicationWindow(self):
        self.main_window = MainWindow()
        self.main_window.show()

    def closeEvent(self, event):
        if self.login_is_successful:
            event.accept()
        else:
            answer = QMessageBox.question(self,
                                          'Quit Application?',
                                          'Are you sure you want to QUIT?',
                                          QMessageBox.No | QMessageBox.Yes,
                                          QMessageBox.Yes)
            if answer == QMessageBox.Yes:
                event.accept()
            if answer == QMessageBox.No:
                event.ignore()

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setMinimumSize(640, 426)
        self.setWindowTitle('Main Window')
        self.setUpMainWindow()

    def setUpMainWindow(self):
        QLabel('Registered User')

class NewUserDialog(QDialog):
    def __init__(self):
        super().__init__()
        self.setModal(True)
        self.initializeUI()

    def initializeUI(self):
        self.setFixedSize(360, 320)
        self.setWindowTitle('Registration GUI')
        self.setUpWindow()

    def setUpWindow(self):
        login_label = QLabel('Create New Account', self)
        login_label.setFont(QFont('Arial', 20))
        login_label.move(90, 20)

        user_label = QLabel(self)
        user_label.setPixmap(QPixmap('images/new_user_icon.png'))
        user_label.move(150, 60)

        name_label = QLabel('Username:', self)
        name_label.move(20, 144)

        self.name_edit = QLineEdit(self)
        self.name_edit.resize(250, 24)
        self.name_edit.move(90, 140)

        full_name_label = QLabel('Full Name:', self)
        full_name_label.move(20, 174)

        full_name_edit = QLineEdit(self)
        full_name_edit.resize(250, 24)
        full_name_edit.move(90, 170)

        new_pswd_label = QLabel('Password:', self)
        new_pswd_label.move(20, 204)

        self.new_pswd_edit = QLineEdit(self)
        self.new_pswd_edit.setEchoMode(QLineEdit.Password)
        self.new_pswd_edit.resize(250, 24)
        self.new_pswd_edit.move(90, 200)

        confirm_label = QLabel('Confirm:', self)
        confirm_label.move(20, 234)

        self.confirm_edit = QLineEdit(self)
        self.confirm_edit.setEchoMode(QLineEdit.Password)
        self.confirm_edit.resize(250, 24)
        self.confirm_edit.move(90, 230)

        sign_up_button = QPushButton('Sign Up', self)
        sign_up_button.resize(320, 32)
        sign_up_button.move(20, 270)
        sign_up_button.clicked.connect(self.confirmSignUp)

    def confirmSignUp(self):
        name_text = self.name_edit.text()
        pswd_text = self.new_pswd_edit.text()
        confirm_text = self.confirm_edit.text()

        if name_text == '' or pswd_text == '':
            QMessageBox.warning(self,
                                'Error Message',
                                'Please enter username or password values.',
                                QMessageBox.Close,
                                QMessageBox.Close)
        elif pswd_text != confirm_text:
            QMessageBox.warning(self,
                                'Error Message',
                                'The passwords you entered do not match.',
                                QMessageBox.Close,
                                QMessageBox.Close)
        else:
            print(name_text)
            print(pswd_text)
            self.close()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = LoginWindow()
    sys.exit(app.exec())
freelsn commented 1 year ago

Layout Management

前面我们都在使用 .move(x, y) 调整组件的布局,这种方法叫做绝对位置布局。当缩放窗口大小时,绝对位置布局无法自动调整组件的位置和间距,因此无法充分利用窗口的空间。为了解决这个问题,我们应该使用 PyQt 提供的 layout manager。

如果希望组件垂直分布,可以使用 QVBoxLayout()

v_box = QVBoxLayout()  # Create layout manager instance
v_box.addWidget(label)  # Add widgets to the layout
v_box.addWidget(line_edit)
parent_widget.setLayout(v_box)  # Set the layout for the parent

Layout manager 也可以嵌套使用。

h_box = QHBoxLayout()
v_box.addLayout(h_box)

addWidget() 主要有三个参数,addWidget(widget, stretch, alignment)

The stretch parameter refers to the stretch factor, or how much the widgets will stretch in relation to other widgets in the row or column. The value for stretch is an int, where 0 uses a widget's default parameters to set the stretch factor. Widgets are laid out proportionally, and ones with larger stretch values will use more space. Widgets can also be aligned in a row or column using the alignment argument.

freelsn commented 1 year ago

QHBoxLayout

'horizontal_box.py'

import sys
from PyQt5.QtWidgets import (QApplication,
                             QWidget,
                             QLabel,
                             QLineEdit,
                             QPushButton,
                             QHBoxLayout)

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setMinimumWidth(500)  #
        self.setFixedHeight(60)  #
        self.setWindowTitle('QHBoxLayout Example')
        self.setUpMainWindow()
        self.show()

    def setUpMainWindow(self):
        name_label = QLabel('New Username:')

        name_edit = QLineEdit()
        name_edit.setClearButtonEnabled(True)
        name_edit.textEdited.connect(self.checkUserInput)

        self.accept_button = QPushButton('Confirm')
        self.accept_button.setEnabled(False)
        self.accept_button.clicked.connect(self.close)

        main_h_box = QHBoxLayout()
        main_h_box.addWidget(name_label)
        main_h_box.addWidget(name_edit)
        main_h_box.addWidget(self.accept_button)
        self.setLayout(main_h_box)

    def checkUserInput(self, text):
        if len(text) > 0 and all(t.isalpha() or t.isdigit() for t in text):
            self.accept_button.setEnabled(True)
        else:
            self.accept_button.setEnabled(False)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
freelsn commented 1 year ago

QButtonGroup

You may often have a few checkboxes or buttons that need to be grouped together to make it easier to manage them. Thankfully, PyQt has the QButtonGroup class to help manage associated buttons while also making them mutually exclusive. This can be helpful if you only want one checkbox to be checked at a time.

'vertical_box.py'

import sys
from PyQt5.QtWidgets import (QApplication,
                             QWidget,
                             QLabel,
                             QCheckBox,
                             QPushButton,
                             QButtonGroup,
                             QVBoxLayout)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setMinimumSize(350, 200)
        self.setWindowTitle('QVBoxLayout Example')
        self.setUpMainWindow()
        self.show()

    def setUpMainWindow(self):
        header_label = QLabel('Chez PyQt')
        header_label.setFont(QFont('Arial', 18))
        header_label.setAlignment(Qt.AlignCenter)

        question_label = QLabel('How would you rate your service?')
        question_label.setAlignment(Qt.AlignTop)

        ratings = ['Satisfied', 'Average', 'Not Satisfied']
        ratings_group = QButtonGroup(self)  #
        ratings_group.buttonClicked.connect(self.checkboxClicked)

        self.confirm_button = QPushButton('Confirm')
        self.confirm_button.setEnabled(False)
        self.confirm_button.clicked.connect(self.close)

        main_v_box = QVBoxLayout()
        main_v_box.addWidget(header_label)
        main_v_box.addWidget(question_label)

        for rating in ratings:
            rating_cb = QCheckBox(rating)
            ratings_group.addButton(rating_cb)
            main_v_box.addWidget(rating_cb)

        main_v_box.addWidget(self.confirm_button)
        self.setLayout(main_v_box)

    def checkboxClicked(self, button):
        print(button.text())
        self.confirm_button.setEnabled(True)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
freelsn commented 1 year ago

Rather than using a QLineEdit widget for inputting information, sometimes, you may want a user to only be allowed to select from a list of predetermined values or numerical ranges.

QSpinBox

QSpinBox creates an object that is similar to a text box but allows the user to select integer values by either typing a value into the widget or by clicking on up and down arrows. You can also edit the range of the values, set the step size when the arrow is clicked, set a starting value, or even add prefixes or suffixes in the box. There are classes similar to QSpinBox that provide similar functionality for different situations. QDoubleSpinBox is used for selecting floating-point numbers. QDateTimeEdit or one of its variations is useful for selecting date and time values.

QComboBox

The QComboBox widget displays a drop-down list of options for the user to select when a user clicks on the widget’s arrow button. Combo boxes are handy for displaying a large amount of options in the least amount of space.

'nested.py'

import sys
from PyQt5.QtWidgets import (QApplication,
                             QWidget,
                             QLabel,
                             QComboBox,
                             QSpinBox,
                             QHBoxLayout,
                             QVBoxLayout)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setMinimumSize(400, 160)
        self.setWindowTitle('Nested Layout Example')
        self.setUpMainWindow()
        self.show()

    def setUpMainWindow(self):
        info_label = QLabel('Select 2 items from lunch and their prices.')
        info_label.setFont(QFont('Arial', 16))
        info_label.setAlignment(Qt.AlignCenter)

        food_list = [
            'egg', 'turkey sandwich', 'ham sandwich',
            'cheese', 'hummus', 'yogurt', 'apple', 'banana',
            'orange', 'waffle', 'carrots', 'bread', 'pasta',
            'crackers', 'pretzels', 'coffee', 'soda', 'water',
        ]
        food_combo1 = QComboBox()
        food_combo1.addItems(food_list)
        food_combo2 = QComboBox()
        food_combo2.addItems(food_list)

        self.price_sb1 = QSpinBox()
        self.price_sb1.setRange(0, 100)
        self.price_sb1.setPrefix('$')
        self.price_sb1.valueChanged.connect(self.calculateTotal)

        self.price_sb2 = QSpinBox()
        self.price_sb2.setRange(0, 100)
        self.price_sb2.setPrefix('$')
        self.price_sb2.valueChanged.connect(self.calculateTotal)

        item1_h_box = QHBoxLayout()
        item1_h_box.addWidget(food_combo1)
        item1_h_box.addWidget(self.price_sb1)

        item2_h_box = QHBoxLayout()
        item2_h_box.addWidget(food_combo2)
        item2_h_box.addWidget(self.price_sb2)

        self.totals_label = QLabel('Total Spent: $')
        self.totals_label.setFont(QFont('Arial', 16))
        self.totals_label.setAlignment(Qt.AlignRight)

        main_v_box = QVBoxLayout()
        main_v_box.addWidget(info_label)
        main_v_box.addLayout(item1_h_box)
        main_v_box.addLayout(item2_h_box)
        main_v_box.addWidget(self.totals_label)

        self.setLayout(main_v_box)

    def calculateTotal(self, value):
        total = self.price_sb1.value() + self.price_sb2.value()
        self.totals_label.setText(f'Total Spent: ${total}')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
freelsn commented 1 year ago

QGridLayout

从 layout 中获取 widget 可以使用 .itemAtPosition(row, col)。如果是 box layout,可以使用 .itemAt(index)

QTextEdit

适合输入多行、比较长的文字。

'grid.py'

import os, sys, json
from PyQt5.QtWidgets import (QApplication,
                             QWidget,
                             QLabel,
                             QLineEdit,
                             QCheckBox,
                             QTextEdit,
                             QGridLayout)
from PyQt5.QtCore import Qt, QDate
from PyQt5.QtGui import QFont

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setMinimumSize(500, 300)
        self.setWindowTitle('QGridLayout Example')
        self.setUpMainWindow()
        self.loadWidgetValuesFromFile()
        self.show()

    def setUpMainWindow(self):
        header_label = QLabel('Simple Daily Planner')
        header_label.setFont(QFont('Arial', 20))
        header_label.setAlignment(Qt.AlignLeft)

        today_label = QLabel("Today's Focus")
        today_label.setFont(QFont('Arial', 14))
        self.today_tedit = QTextEdit()

        notes_label = QLabel('Notes')
        notes_label.setFont(QFont('Arial', 14))
        self.notes_tedit = QTextEdit()

        self.main_grid = QGridLayout()
        self.main_grid.addWidget(header_label, 0, 0)
        self.main_grid.addWidget(today_label, 1, 0)
        self.main_grid.addWidget(self.today_tedit, 2, 0, 3, 1)
        self.main_grid.addWidget(notes_label, 5, 0)
        self.main_grid.addWidget(self.notes_tedit, 6, 0, 3, 1)

        today = QDate.currentDate().toString(Qt.ISODate)
        date_label = QLabel(today)
        date_label.setFont(QFont('Arial', 18))
        date_label.setAlignment(Qt.AlignRight)

        todo_label = QLabel('To Do')
        todo_label.setFont(QFont('Arial', 14))

        self.main_grid.addWidget(date_label, 0, 2)
        self.main_grid.addWidget(todo_label, 1, 1, 1, 2)

        for row in range(2, 9):
            item_cb = QCheckBox()
            item_edit = QLineEdit()
            self.main_grid.addWidget(item_cb, row, 1)
            self.main_grid.addWidget(item_edit, row, 2)

        self.setLayout(self.main_grid)

    def saveWidgetValues(self):
        details = {
            'focus': self.today_tedit.toPlainText(),
            'notes': self.notes_tedit.toPlainText(),
        }
        remaining_todo = []

        for row in range(2, 9):
            item = self.main_grid.itemAtPosition(row, 1)  #
            # When widgets are added to layouts,
            # they are added as QLayoutItem objects
            widget = item.widget()
            if not widget.isChecked():
                item = self.main_grid.itemAtPosition(row, 2)
                widget = item.widget()
                text = widget.text()
                if text != '':
                    remaining_todo.append(text)
        details['todo'] = remaining_todo

        with open('details.txt', 'w') as f:
            f.write(json.dumps(details, indent=4))

    def closeEvent(self, event):
        self.saveWidgetValues()

    def loadWidgetValuesFromFile(self):
        if not os.path.exists('details.txt'):
            return
        details = json.load(open('details.txt'))
        self.today_tedit.setText(details['focus'])
        self.notes_tedit.setText(details['notes'])
        for row, txt in enumerate(details['todo']):
            item = self.main_grid.itemAtPosition(row+2, 2)
            item.widget().setText(txt)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
freelsn commented 1 year ago

QFormLayout

For situations where you need to create a form to collect information from a user, PyQt provides the QFormLayout class. It is a layout manager that arranges its children widgets into a two-column layout, the left column consisting of labels and the right one consisting of entry field widgets such as QLineEdit or QSpinBox. The QFormLayout class makes designing these kinds of GUIs very convenient.

Form layout 认为信息是一行一行显示的,且每一行包括两个部分:label 和 field。这两部分可以有多种组合,主要有:

'form.py'

import sys
from PyQt5.QtWidgets import (QApplication,
                             QWidget,
                             QLabel,
                             QPushButton,
                             QDateEdit,
                             QLineEdit,
                             QTextEdit,
                             QComboBox,
                             QFormLayout,
                             QHBoxLayout)
from PyQt5.QtCore import Qt, QRegularExpression, QDate
from PyQt5.QtGui import QFont, QRegularExpressionValidator

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setMinimumSize(500, 400)
        self.setWindowTitle('QFormLayout Example')
        self.setUpMainWindow()
        self.show()

    def setUpMainWindow(self):
        header_label = QLabel('Appointment Form')
        header_label.setFont(QFont('Arial', 18))
        header_label.setAlignment(Qt.AlignCenter)

        self.first_name_edit = QLineEdit()
        self.first_name_edit.setPlaceholderText('First')
        self.first_name_edit.textEdited.connect(self.clearText)
        self.last_name_edit = QLineEdit()
        self.last_name_edit.setPlaceholderText('Last')
        self.last_name_edit.textEdited.connect(self.clearText)

        name_h_box = QHBoxLayout()
        name_h_box.addWidget(self.first_name_edit)
        name_h_box.addWidget(self.last_name_edit)

        gender_combo = QComboBox()
        gender_combo.addItems(['Male', 'Female'])

        self.phone_edit = QLineEdit()
        self.phone_edit.setInputMask('(999) 999-999;_')  #
        self.phone_edit.textEdited.connect(self.clearText)

        self.birthdate_edit = QDateEdit()
        self.birthdate_edit.setDisplayFormat('MM/dd/yyyy')
        self.birthdate_edit.setMaximumDate(QDate.currentDate())
        self.birthdate_edit.setCalendarPopup(True)  #
        self.birthdate_edit.setDate(QDate.currentDate())

        self.email_edit = QLineEdit()
        self.email_edit.setPlaceholderText('<username>@<domain>.com')
        regex = QRegularExpression(
            '\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[com]{3}\\b',
            QRegularExpression.CaseInsensitiveOption,
        )
        self.email_edit.setValidator(QRegularExpressionValidator(regex))
        self.email_edit.textEdited.connect(self.clearText)

        extra_info_tedit = QTextEdit()
        self.feedback_label = QLabel()
        submit_button = QPushButton('SUBMIT')
        submit_button.setMaximumWidth(140)
        submit_button.clicked.connect(self.checkFormInformation)

        submit_h_box = QHBoxLayout()
        submit_h_box.addWidget(self.feedback_label)
        submit_h_box.addWidget(submit_button)

        main_form = QFormLayout()
        main_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        main_form.setFormAlignment(Qt.AlignHCenter | Qt.AlignTop)
        main_form.setLabelAlignment(Qt.AlignLeft)
        main_form.addRow(header_label)
        main_form.addRow('Name', name_h_box)
        main_form.addRow('Gender', gender_combo)
        main_form.addRow('Date of Birth', self.birthdate_edit)
        main_form.addRow('Phone', self.phone_edit)
        main_form.addRow('Email', self.email_edit)
        main_form.addRow(QLabel('Comments or Messages'))
        main_form.addRow(extra_info_tedit)
        main_form.addRow(submit_h_box)

        self.setLayout(main_form)

    def clearText(self, text):
        self.feedback_label.clear()

    def checkFormInformation(self):
        if self.first_name_edit.text() == '' or self.last_name_edit.text() == '':
            self.feedback_label.setText('[INFO] Missing names.')
        elif not self.phone_edit.hasAcceptableInput():
            self.feedback_label.setText('[INFO] Phone number incorrect.')
        elif not self.email_edit.hasAcceptableInput():
            self.feedback_label.setText('[INFO] Email incorrect.')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())
freelsn commented 1 year ago

QStackedLayout

有时需要在同一个窗口实现页面的切换功能,比如浏览器窗口可以切换多个页面,此时可以使用 stacked layout。

Stacked layout 可以保存多个 page,在任一时刻只显示一个 page,page 可以通过选择菜单进行切换。一个 page 就是一个完整的 widget,可以由多个子 widget 或 layout 构成。Stacked layout 支持 page 动态添加和删除。

'stacked.py'

import sys
from PyQt5.QtWidgets import (QApplication,
                             QWidget,
                             QLabel,
                             QLineEdit,
                             QTextEdit,
                             QComboBox,
                             QSpinBox,
                             QDoubleSpinBox,
                             QStackedLayout,
                             QFormLayout,
                             QVBoxLayout)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.initializeUI()

    def initializeUI(self):
        self.setFixedSize(300, 340)
        self.setWindowTitle('QStackedLayout Example')
        self.setUpMainWindow()
        self.show()

    def setUpMainWindow(self):
        page_combo = QComboBox()
        page_combo.addItems(['Image', 'Description', 'Additional Info'])
        page_combo.activated.connect(self.switchPage)

        # Image page
        profile_image = QLabel()
        profile_image.setPixmap(QPixmap('images/cat.png'))
        profile_image.setScaledContents(True)

        # Description page
        pg2_form = QFormLayout()
        pg2_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        pg2_form.setFormAlignment(Qt.AlignHCenter | Qt.AlignTop)
        pg2_form.setLabelAlignment(Qt.AlignLeft)
        pg2_form.addRow('Breed:', QLabel('Norwegian Forest cat'))
        pg2_form.addRow(QLabel('Description:'))
        text = """Have a long, sturdy body, long legs
        and a bushy tail. They are friendly, intelligent,
        and generally good with people."""
        pg2_form.addRow(QTextEdit(text))
        pg2_container = QWidget()
        pg2_container.setLayout(pg2_form)

        # Additional Info page
        pg3_form = QFormLayout()
        pg3_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)
        pg3_form.setFormAlignment(Qt.AlignHCenter | Qt.AlignTop)
        pg3_form.setLabelAlignment(Qt.AlignLeft)
        pg3_form.addRow(QLabel("Enter your cat's info"))
        pg3_form.addRow('Name:', QLineEdit())
        pg3_form.addRow('Color:', QLineEdit())
        age_sb = QSpinBox()
        age_sb.setRange(0, 30)
        pg3_form.addRow('Age:', age_sb)
        weight_dsb = QDoubleSpinBox()
        weight_dsb.setRange(0.0, 30.0)
        pg3_form.addRow('Weight (kg):', weight_dsb)
        pg3_container = QWidget()
        pg3_container.setLayout(pg3_form)

        # group all
        self.stacked_layout = QStackedLayout()
        self.stacked_layout.addWidget(profile_image)
        self.stacked_layout.addWidget(pg2_container)
        self.stacked_layout.addWidget(pg3_container)
        main_v_box = QVBoxLayout()
        main_v_box.addWidget(page_combo)
        main_v_box.addLayout(self.stacked_layout)

        self.setLayout(main_v_box)

    def switchPage(self, index):
        self.stacked_layout.setCurrentIndex(index)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())