Bootstrap

[Python GUI PyQt] PyQt5快速入门

PyQt5的快速入门

0. 写在前面

  • 本文为博主个人对自己学习PyQt5GUI技术的纲要式的总结,主要的目的是以比较宏观的视角再次对PyQt5这一项技术作一次总结,通过该文可以快速让读者建立起PyQt5最基本的知识体系,了解PyQt5基本界面的设计信号与槽多线程等知识,并简单学会使用Qt Designer工具来实现快速的Python GUI界面设计。
  • 特别感谢王铭东老师在B站上发布的PyQt5快速入门视频教程
  • 博主个人在学习的过程中全程跟敲了代码,现完全开源,感兴趣的同学可以在这里获取。

1. 思维导图

  • 以下是博主个人在学习的过程中绘制的简要的 “辅助读懂PyQt5” 所需掌握的基本知识体系,需要注意的事情是,PyQt5的知识远远不止下图中所展示出来的内容,在实际的项目开发中,需要我们能在掌握必要的常识的基础上,根据实际的项目需求,学会自己主动查找有用的资料,并最终完成自己的开发任务。
    在这里插入图片描述

2. 第一个PyQt5的应用程序

import sys
from PyQt5.QtWidgets import QApplication, QWidget

if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget类
    w = QWidget()
    # 设置窗口标题
    w.setWindowTitle('第一个PyQt5程序')
    # 设置窗口背景色
    w.setStyleSheet("background-color: rgb(255, 255, 255);")
    # 设置窗口显示
    w.show()
    # 程序进行循环等待状态
    app.exec_()
  • 如上文的代码所示,运行后我们可以得到这样的一个窗口:
    在这里插入图片描述

  • from PyQt5.QtWidgets import QApplication, QWidget,我们可以使用from <module> import ....的形式来导入我们在进行Python GUI开发中所需要的库,这些库通常是QtWidgets,QtGuiQtCore
    在这里插入图片描述

  • 在Python的GUI开发中,我们需要了解的是,任何一个PyQt的应用程序都是从app = QApplication(sys.argv)QApplication开始的。每一个应用程序都需要也仅仅需要一个QApplication来管理整个应用程序。

  • QWidgetPyQt中默认的三种重要的窗口之一,我们需要根据我们具体的GUI开发的需求来选择我们使用的基础的窗口类型。这里我们使用w = QWidget()来实例化一个QWidget实例。实例化窗口之后,窗口本身是有很多的属性可以被设置的,这一部分我们会在下文有适当的总结并展开说说。比如,在这个样例中我们主要是使用了w.setWindowTitle('第一个PyQt5程序')来设置了窗口的标题,并且使用了w.setStyleSheet("background-color: rgb(255, 255, 255);")来设置窗口的背景色为白色。

  • 通过w.show()我们可以让我们所定义的窗体的实例对象展示出来。

  • 最后,我们通过app.exec_()来让应用程序进入事件循环(Event loop)。另外,这里之所以使用的是exec_,是为了避免与Python 2中的exec保留字发生冲突。
    在这里插入图片描述

与应用程序的每次交互(无论是按键、单击鼠标还是鼠标移动)都会生成一个事件,该事件放置在事件队列中。在事件循环中,在每次迭代时检查队列,如果找到等待事件,则将事件和控制传递给事件的特定事件处理程序。事件处理程序处理事件,然后将控制权传递回事件循环以等待更多事件。每个应用程序只有一个正在运行的事件循环。

3. PyQt5的常用基本控件和布局

3.1 PyQt5的常用基本控件

在这里插入图片描述

3.1.1 按钮控件 QPushButton
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton

if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget类
    w = QWidget()
    # 设置窗口标题
    w.setWindowTitle('PyQt5程序之按钮')
    # 在窗口里面添加按钮控件
    btn = QPushButton("按钮")
    # 把按钮绑定到对应的窗口,等于是添加到窗口中显示
    btn.setParent(w)
    # 设置窗口背景色
    w.setStyleSheet("background-color: rgb(255, 255, 255);")
    # 设置窗口显示
    w.show()
    # 程序进行循环等待状态
    app.exec_()

在这里插入图片描述

  • 这里其实符合面向对象编程的编程逻辑,这些UI控件本质上都是一类一类的对象,在使用这些对象的时候,我们需要先创建一个对象的实例,然后再来对他们进行操作来实现我们想要实现的功能。
  • 通过btn = QPushButton("按钮") ,我们实现了在窗口里面添加按钮控件,该按钮控件的文本显示为“按钮”
  • 但是仅仅只是创建了控件的实例的话,是没有办法在窗体上显示出来的,还需要我们通过btn.setParent(w)把按钮绑定到对应的窗口,这样我们的按钮才能够在窗口中显示。

这里的QPushButton其实只是Button类型的控件的一个典型的代表,在PyQt5中实际可用的按钮控件还有很多,详细可以在Qt Designer中查看或者在需要的时候查阅文档。
在这里插入图片描述

3.1.2 文本标签控件 QLabel
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel

if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget类
    w = QWidget()
    # 设置窗口标题
    w.setWindowTitle('PyQt5程序之文本')
    # 在窗口里面添加按钮控件,并绑定到w窗口
    btn = QPushButton("按钮", w)
    # 把按钮绑定到对应的窗口,等于是添加到窗口中显示
    # 设置按钮的位置
    btn.move(100, 100)
    # 在窗口里面添加文本标签,并绑定到w窗口
    label = QLabel("文本标签", w)
    # 设置文本标签的位置
    # label.move(100, 150)
    # 设置位置与大小 (x,y)(左上角0,0;横x,纵y) (w,h)(宽度,高度)
    label.setGeometry(100, 150, 150, 50)
    # 设置窗口背景色
    w.setStyleSheet("background-color: rgb(255, 255, 255);")
    # 设置窗口显示
    w.show()
    # 程序进行循环等待状态
    app.exec_()

在这里插入图片描述

  • 除了上文说的xxx.setParent(w)方法,我们还可以在一开始创建控件的时候就把控件绑定到窗体上,这样就简化了代码,如我们可以使用label = QLabel("文本标签", w)实现在窗口里面添加文本标签,并直接绑定到w窗口。
import sys

from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel

if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget类
    w = QWidget()
    w.resize(1000, 500)
    # 设置窗口标题
    w.setWindowTitle('PyQt5程序之QLabel展示图片')
    # 创建一个位图变量
    photo = QPixmap('imgs/Florian.jpg')
    # 按比例缩放至1000*500
    scaled_photo = photo.scaled(1000, 500, Qt.KeepAspectRatio)
    # 创建一个标签控件实例
    label = QLabel(w)
    # 将位图变量绑定到标签控件实例上
    label.setPixmap(scaled_photo)
    # 设置窗口显示
    w.show()
    # 程序进行循环等待状态
    app.exec_()

在这里插入图片描述

  • 特别地,QLabel 不仅仅可以用来显示文字,它还可以用来显示图片。可以使用 QLabel 通过setPixmap()显示图像。这接受一个像素图,您可以通过将图像文件名传递给 QPixmap 类来创建该像素图。比如:widget.setPixmap(QPixmap('otje.jpg'))

这里的QLabel其实只是Display类型的控件的一个典型的代表,在PyQt5中实际可用的按钮控件还有很多,详细可以在Qt Designer中查看或者在需要的时候查阅文档。
在这里插入图片描述

3.1.3 单行输入框控件 QLineEdit
import sys
from PyQt5.QtWidgets import *

if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget类
    w = QWidget()
    # 设置窗口标题
    w.setWindowTitle('PyQt5程序之输入框')
    # 纯文本
    label = QLabel("账号:", w)
    label.setGeometry(100, 100, 50, 30)
    # 文本框
    edit = QLineEdit(w)
    edit.setPlaceholderText("请输入账号")
    edit.setGeometry(150, 100, 200, 30)
    # 在窗口里面添加控件
    btn = QPushButton("注册", w)
    btn.setGeometry(200, 150, 80, 30)
    # 设置窗口显示
    w.show()
    # 程序进行循环等待状态
    app.exec_()

在这里插入图片描述

  • 通常,我们可以使用edit = QLineEdit(w)来创建一个单行的输入框,可以使用edit.setPlaceholderText("请输入账号")来设置未输入数据情况下的提示信息,如果我们想要在该输入行中输入密码的话,我们也可以使用password.setEchoMode(QLineEdit.Password)来将其设置为密文模式。

这里的QLineEdit其实只是Input类型的控件的一个典型的代表,在PyQt5中实际可用的按钮控件还有很多,详细可以在Qt Designer中查看或者在需要的时候查阅文档。
在这里插入图片描述

3.1.4 A Quick Widgets Demo
  • 该部分内容转载自PyQt5 Widgets
  • 在这篇文章中更加详细地距离了PyQt的各种奇妙的控件,有兴趣的读者可以从这里进入,去更进一步地阅读和学习!
  • 我们这里仅举其中的一个比较爆炸性的例子作为展示。
import sys

from PyQt5.QtWidgets import (
    QApplication,
    QCheckBox,
    QComboBox,
    QDateEdit,
    QDateTimeEdit,
    QDial,
    QDoubleSpinBox,
    QFontComboBox,
    QLabel,
    QLCDNumber,
    QLineEdit,
    QMainWindow,
    QProgressBar,
    QPushButton,
    QRadioButton,
    QSlider,
    QSpinBox,
    QTimeEdit,
    QVBoxLayout,
    QWidget,
)


# Subclass QMainWindow to customize your application's main window
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("Widgets App")

        layout = QVBoxLayout()
        widgets = [
            QCheckBox,
            QComboBox,
            QDateEdit,
            QDateTimeEdit,
            QDial,
            QDoubleSpinBox,
            QFontComboBox,
            QLCDNumber,
            QLabel,
            QLineEdit,
            QProgressBar,
            QPushButton,
            QRadioButton,
            QSlider,
            QSpinBox,
            QTimeEdit,
        ]

        for w in widgets:
            layout.addWidget(w())

        widget = QWidget()
        widget.setLayout(layout)

        # Set the central widget of the Window. Widget will expand
        # to take up all the space in the window by default.
        self.setCentralWidget(widget)


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

在这里插入图片描述

Widget功能
QCheckbox多选框
QComboBox下拉列表框
QDateEdit日期编辑框
QDateTimeEdit日期及时间编辑框
QDial可旋转表盘
QDoubleSpinbox浮点数的数字微调器
QFontComboBox字体列表
QLCDNumber简易液晶数字显示屏
QLabel文本标签,不支持交互
QLineEdit单行文本输入框
QProgressBar进度条
QPushButton按钮
QRadioButton单选框
QSlider滑块
QSpinBox整数数字微调器
QTimeEdit时间编辑框

3.2 PyQt5的常用基本控件的基本属性编辑

3.2.1 调整窗口大小
import sys
from PyQt5.QtWidgets import *

if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget类
    w = QWidget()
    # 设置窗口标题
    w.setWindowTitle('PyQt5程序之调整窗口大小')
    # 重置窗口大小
    w.resize(800, 600)
    # 将窗口设置在屏幕的左上角
    # w.move(0, 0)
    # 设置窗口显示
    w.show()
    # 程序进行循环等待状态
    app.exec_()


#pic_center)

  • 在 PyQt 中,我们可以使用w.resize(800, 600)来对窗体或其它控件的大小属性来进行自定义,两个参数分别对应的是widthheight。从上图我们也可以清晰看出,设置后的主窗体的大小确实就是800 * 600
3.2.2 调整窗口位于屏幕中间
import sys
from PyQt5.QtWidgets import *

if __name__ == '__main__':
   # 实例化QApplication类
   # sys.argv是命令行的参数
   app = QApplication(sys.argv)
   # 实例化QWidget类
   w = QWidget()
   # 设置窗口标题
   w.setWindowTitle('PyQt5程序之调整窗口于正中间位置')
   # 重置窗口大小
   w.resize(800, 600)

   # 调整窗口在屏幕中央显示
   # 获取我当前屏幕桌面的中心点的位置信息
   center_pointer = QDesktopWidget().availableGeometry().center()
   print(center_pointer)
   x = center_pointer.x()
   y = center_pointer.y()
   print(x, y)
   # 打印窗口默认的几何信息(位置和大小)
   print(w.frameGeometry())
   print(w.frameGeometry().getRect())
   print(type(w.frameGeometry().getRect()))
   # 获取窗口的默认几何信息
   old_x, old_y, width, height = w.frameGeometry().getRect()
   print(old_x, old_y, width, height)
   # 基于屏幕中心点和窗口的默认大小来调至合适的屏幕中央(自适应)
   w.move(x - int(width / 2), y - int(height / 2))

   # 设置窗口显示
   w.show()
   # 程序进行循环等待状态
   app.exec_()

在这里插入图片描述

  • 在PyQt的坐标系的构建中,是以 左上角为原点,从左向右为正方向的X轴(Width),以及从上到下为正方向的Y轴(Height) 来进行坐标系的构建的。
  • 实现该效果的思路首先是,先使用center_pointer = QDesktopWidget().availableGeometry().center()获取我当前屏幕桌面的中心点的位置信息。
  • 然后使用old_x, old_y, width, height = w.frameGeometry().getRect()获取窗口控件的几何属性,主要是获取它的width和height。
  • 这时候,就只需要用我们所获取到的屏幕的中心点的坐标来分别减去一般的窗体的高度和宽度,这样我们就可以获得窗体相对于屏幕的坐标系中,如果要让窗体显示在屏幕正中的情况下,窗体左上角定位点所应该在的具体的位置坐标了,这时候再使用w.move(x - int(width / 2), y - int(height / 2))重新定位即可。
3.2.3 设置窗体图标
import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget类
    w = QWidget()
    # 设置窗口标题
    w.setWindowTitle('PyQt5程序之设置窗口图标')

    # 设置窗口图标
    w.setWindowIcon(QIcon('imgs/clock.png'))
    # PyQt5隐藏上方的标签栏
    # w.setWindowFlag(Qt.FramelessWindowHint)

    # 设置窗口显示
    w.show()
    # 程序进行循环等待状态
    app.exec_()
    # 设置窗口显示
    w.show()
    # 程序进行循环等待状态
    app.exec_()

在这里插入图片描述

  • 在PyQt中我们可以使用w.setWindowIcon(QIcon('imgs/clock.png'))来给我们自己的窗口添加酷炫的Icon。
3.2.4 隐藏窗口标签栏
import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

if __name__ == '__main__':
   # 实例化QApplication类
   # sys.argv是命令行的参数
   app = QApplication(sys.argv)
   # 实例化QWidget类
   w = QWidget()
   # 设置窗口标题
   w.setWindowTitle('PyQt5程序之隐藏窗口标签栏')

   # PyQt5隐藏上方的标签栏
   w.setWindowFlag(Qt.FramelessWindowHint)

   # 设置窗口显示
   w.show()
   # 程序进行循环等待状态
   app.exec_()
   # 设置窗口显示
   w.show()
   # 程序进行循环等待状态
   app.exec_()

在这里插入图片描述

  • 在PyQt中我们可以使用w.setWindowFlag(Qt.FramelessWindowHint)来隐藏上方的标签栏,这样我们就相当于获得了一个完全空白的窗口面板,我们可以在上面来进行完全自定义的具有个性的GUI界面的设计与实现。

3.3 PyQt5的常用布局

在这里插入图片描述

3.3.1 盒子布局垂直版QVBoxLayout
import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

class MyWindow(QWidget):
    def __init__(self):
        # 切记一定要调用父类的__init__方法,因为它里面有很多对UI控件的初始化操作
        super().__init__()
        # 设置大小
        self.resize(300, 300)
        # 设置窗口标题
        self.setWindowTitle('PyQt5程序之垂直布局')
        # 设置窗口图标
        self.setWindowIcon(QIcon('imgs/clock.png'))

        # 垂直布局(限制)
        layout = QVBoxLayout()
        # 作用是在布局器中增加一个伸缩量,里面的参数表示QSpacerItem的个数,默认值为零
        # 会将你放在layout中的空间压缩成默认的大小
        # 下面的空白区域的比例为:1:1:1:2
        # 加了一个伸缩器(可以理解为一个弹簧)
        layout.addStretch(1)
        # 设置布局的方向
        layout.setDirection(QBoxLayout.TopToBottom)
        # 设置布局的间距
        layout.setContentsMargins(10, 10, 10, 10)
        # 设置布局的内边距
        layout.setSpacing(10)
        # 设置布局
        self.setLayout(layout)
        # 按钮1
        btn1 = QPushButton("按钮1")
        # 添加到布局器中
        layout.addWidget(btn1, Qt.AlignmentFlag.AlignTop)
        # 把按钮添加到布局中
        layout.addWidget(btn1)
        # 加了一个伸缩器(可以理解为一个弹簧)
        layout.addStretch(1)
        # 按钮2
        btn2 = QPushButton("按钮2")
        # 把按钮添加到布局中
        layout.addWidget(btn2)
        # 加了一个伸缩器(可以理解为一个弹簧)
        layout.addStretch(1)
        # 按钮3
        btn3 = QPushButton("按钮3")
        # 把按钮添加到布局中
        layout.addWidget(btn3)
        # 加了一个伸缩器(可以理解为一个弹簧)
        layout.addStretch(2)
        # 让当前的窗口使用这个排列的布局器
        self.setLayout(layout)


if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget子类——MyWindow
    w = MyWindow()
    w.show()
    app.exec_()

在这里插入图片描述

  • 使用布局管理器可以自动调整窗口部件的大小和位置,以适应窗口大小的改变。当你改变窗口的大小时,布局管理器会自动重新计算窗口部件的位置和大小,使得窗口部件能够适应新的窗口大小,避免了手动调整窗口部件大小和位置的繁琐工作。
  • 在使用布局管理器之前,我们需要使用layout = QVBoxLayout()创建一个布局对象。
  • 用布局来管理控件的方式不同于直接把控件绑定到窗口的形式,我们在创建控件的时候也不需要默认指定Parent,取而代之的是layout.addWidget(btn1)把想要添加的控件添加到布局对象中。
  • 为了控制好所添加入布局管理器的控件与控件之前的控件距离,我们可以采用layout.addStretch(2)的方式来自定义控件与控件之前的距离,里面的参数是权重的比例,比如在上图中我们在按钮1、2、3的前后和中间分别设置了1:1:1:2的权重的伸缩器,从最后的页面中我们可以看到从上到下的所有的间隙的大小分布的比例差不多就是1:1:1:2
  • 最后,布局无法直接被显示,需要我们使用self.setLayout(layout)让当前的窗口使用我们定义好的布局器。
3.3.2 盒子布局水平版QHBoxLayout
import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

class MyWindow(QWidget):
    def __init__(self):
        # 切记一定要调用父类的__init__方法,因为它里面有很多对UI控件的初始化操作
        super().__init__()
        # 把对当前的窗口控件的具体的UI布局写到有个专门的方法中,以免初始化的方法内容太臃肿
        self.init_ui()

    def init_ui(self):
        # 设置大小
        self.resize(400, 400)
        # 设置窗口标题
        self.setWindowTitle('PyQt5程序之水平布局')
        # 设置窗口图标
        self.setWindowIcon(QIcon('imgs/clock.png'))

        # 垂直布局
        # outer_container = QVBoxLayout()
        outer_container = QHBoxLayout()

        # 垂直布局器中有两个组,一个是爱好组,另一个是性别组
        hobby_group = QGroupBox("爱好")
        # 创建一个垂直布局器
        v_inner_layout = QVBoxLayout()
        # 在爱好组中加入可选的爱好
        choice1 = QRadioButton("文明6")
        choice2 = QRadioButton("云顶之弈")
        choice3 = QRadioButton("毕业设计")
        # 把组件添加到布局器中
        v_inner_layout.addWidget(choice1)
        v_inner_layout.addWidget(choice2)
        v_inner_layout.addWidget(choice3)
        # 将布局器绑定到组
        hobby_group.setLayout(v_inner_layout)
        # 把当前的爱好组加入到垂直布局中
        outer_container.addWidget(hobby_group)

        gender_group = QGroupBox("性别")
        # 创建一个水平布局器
        h_inner_layout = QHBoxLayout()
        # 在爱好组中加入可选的爱好
        boy = QRadioButton("男")
        girl = QRadioButton("女")
        # 把组件添加到布局器中
        h_inner_layout.addWidget(boy)
        h_inner_layout.addWidget(girl)
        # 将布局器绑定到组
        gender_group.setLayout(h_inner_layout)
        # 把当前的性别组加入到垂直布局中
        outer_container.addWidget(gender_group)

        # 让当前的窗口使用布局器
        self.setLayout(outer_container)


if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget子类——MyWindow
    w = MyWindow()
    w.show()
    app.exec_()
  • 当最顶层布局为垂直布局
    在这里插入图片描述
  • 当最顶层布局为水平布局
    在这里插入图片描述
  • 从技术角度来说,水平布局的布局管理器和垂直布局的布局管理器是类似的不同的仅仅是从空间上来看,它约束的空间关系是一种水平的布局。都是先使用QVBoxLayout()或者QHBoxLayout()创建一个垂直布局器或者水平布局器,然后再使用addWidget()把控件添加到布局器上,最后使用setLayout()让布局绑定窗口或者控件。
  • 然而这个案例中所不同的地方是,布局是可以嵌套的。当然,布局是无法直接嵌套在另外一层布局上的,所以就需要中间的容器。在当前的例子中,内部的爱好和性别的两个组控件,就是这中间的容器,QGroupBox()对外层的容器充当控件,又作为内层的布局的容器,分别添加了垂直方向和水平方向的布局管理器。
3.3.3 网格布局QGridLayout
import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

class MyWindow(QWidget):
    def __init__(self):
        # 切记一定要调用父类的__init__方法,因为它里面有很多对UI控件的初始化操作
        super().__init__()
        # 把对当前的窗口控件的具体的UI布局写到有个专门的方法中,以免初始化的方法内容太臃肿
        self.init_ui()

    def init_ui(self):
        # 设置大小
        self.resize(300, 300)
        # 设置窗口标题
        self.setWindowTitle('PyQt5程序之QGridLayout——计算器')
        # 设置窗口图标
        self.setWindowIcon(QIcon('imgs/clock.png'))

        # 最外层是一个垂直布局布局
        outer_container = QVBoxLayout()
        # 创建一个输入框
        input_box = QLineEdit()
        input_box.setPlaceholderText('请输入内容')
        # 把输入框和按钮放到布局中
        outer_container.addWidget(input_box)
        # 创建计算器的网络布局
        grid = QGridLayout()
        # 计算器网格布局的数据准备(这里刻意使用一种类似json格式的键值对的数据形式)
        data = {
            0: ['7', '8', '9', '+', '('],
            1: ['4', '5', '6', '-', ')'],
            2: ['1', '2', '3', '*', '<-'],
            3: ['0', '.', '=', '/', 'C']
        }
        # 把网格布局添加到外层的垂直布局中
        outer_container.addLayout(grid)
        for key_row, numbers in data.items():
            for index_column, value in enumerate(numbers):
                btn = QPushButton(value)
                grid.addWidget(btn, key_row, index_column)
        # 让当前的窗口使用布局器
        self.setLayout(outer_container)


if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget子类——MyWindow
    w = MyWindow()
    w.show()
    app.exec_()

在这里插入图片描述

  • 这是一个计算器的布局样例。其中的按键是通过GridLayout来实现的。创建网格布局的方式是grid = QGridLayout()和其它的布局方式不同的地方主要是,对于网格布局,我们在添加控件addWidget()的时候,还需要指定当前的控件填在网格布局中的行列坐标。坐标是从0开始计数。
  • 当然,有时候我们也会遇到可能一个控件需要跨多个网格的情况,这时候就需要在添加控件的时候再额外指定它所跨的行的数量和所跨的列的数量。 比如layout.addWidget(button, row, col, 1, 2) ,它表示当前的按钮会占据1行2列
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QPushButton


class Calculator(QWidget):
    def __init__(self):
        super().__init__()

        # 创建网格布局
        layout = QGridLayout(self)
        # 创建按钮并添加到布局中
        buttons = [
            '7', '8', '9', '/',
            '4', '5', '6', '*',
            '1', '2', '3', '-',
            '0', '=', '+'
        ]
        row = 0
        col = 0
        for btnText in buttons:
            if btnText == '0':
                # 为数字“0”的按钮设置 columnSpan 为 2
                button = QPushButton(btnText, self)
                layout.addWidget(button, row, col, 1, 2)  # 占据1行2列
                col += 2  # 跳过下一列
            else:
                button = QPushButton(btnText, self)
                layout.addWidget(button, row, col)
                col += 1
                # 换行条件:当到达每行的最后一个按钮时
            if col == 4 and btnText != '0':
                row += 1
                col = 0
                # 设置窗口的基本属性
        self.setLayout(layout)
        self.setWindowTitle('计算器示例')


if __name__ == '__main__':
    app = QApplication(sys.argv)
    calc = Calculator()
    calc.show()
    sys.exit(app.exec_())

在这里插入图片描述

3.3.4 表单布局QFormLayout
import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

class MyWindow(QWidget):
    def __init__(self):
        # 切记一定要调用父类的__init__方法,因为它里面有很多对UI控件的初始化操作
        super().__init__()
        # 把对当前的窗口控件的具体的UI布局写到有个专门的方法中,以免初始化的方法内容太臃肿
        self.init_ui()

    def init_ui(self):
        # 设置大小
        self.resize(400, 300)
        # 设置窗口标题
        self.setWindowTitle('PyQt5程序之QFormLayout')
        # 设置窗口图标
        self.setWindowIcon(QIcon('imgs/clock.png'))

        # 创建一个垂直布局作为窗口的主要布局
        mainLayout = QVBoxLayout()
        # 创建一个表单布局(本质上就是一行一行的部分)
        formLayout = QFormLayout()

        # 创建标签和对应的字段控件
        # 姓名行及其输入框
        nameLabel = QLabel('姓名:')
        nameLineEdit = QLineEdit()
        formLayout.addRow(nameLabel, nameLineEdit)
        # 电子邮箱行及其输入框
        emailLabel = QLabel('电子邮箱:')
        emailLineEdit = QLineEdit()
        formLayout.addRow(emailLabel, emailLineEdit)
        # 将表单布局添加到主布局中
        mainLayout.addLayout(formLayout)

        # 创建一个提交按钮,并添加到主布局中
        submitButton = QPushButton('提交')
        mainLayout.addWidget(submitButton)

        # 设置窗口的主布局
        self.setLayout(mainLayout)


if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget子类——MyWindow
    w = MyWindow()
    w.show()
    app.exec_()

在这里插入图片描述

  • 表单布局最特别的地方在于表单布局的控件的添加不是我们所熟悉的addWidget()它不是一个一个控件来添加的,它是使用诸如formLayout.addRow(nameLabel, nameLineEdit)的形式来一行一行来进行控件的添加的。QFormLayout被设计用来创建一个两列的表单布局,其中左列通常包含标签(QLabel),右列包含与之相关的控件(如QLineEdit、QCheckBox、QPushButton等)。
  • 但是,这并不意味着我们必须遵循这种标签-控件的模式;我们可以根据需要添加任何类型的控件到左列或右列。例如,在同一行的左列添加一个QLabel,而在右列添加一个QPushButton。或者,你也可以在左列和右列都添加QPushButton或其他类型的控件。QFormLayout会自动处理控件的对齐和间距,使得布局看起来整洁且有序。
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QFormLayout, QLabel, QLineEdit, QPushButton


class FormLayoutExample(QWidget):
    def __init__(self):
        super().__init__()

        # 创建表单布局
        layout = QFormLayout()

        # 添加控件到布局中
        layout.addRow("Name:", QLineEdit())
        layout.addRow("Age:", QLineEdit())

        # 在同一行添加不同类型的控件
        button1 = QPushButton("Submit")
        button2 = QPushButton("Cancel")
        layout.addRow(button1, button2)

        # 设置窗口的基本属性
        self.resize(400, 100)
        self.setLayout(layout)
        self.setWindowTitle('QFormLayout 示例')


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = FormLayoutExample()
    ex.show()
    sys.exit(app.exec_())

在这里插入图片描述

3.3.5 栈布局QStackLayout
import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

class Window1(QWidget):
    def __init__(self):
        super().__init__()
        label = QLabel("这里是窗口1", self)
        self.setStyleSheet("background-color: red")
        # 设置标签内的字体的属性
        font = QFont()
        font.setPointSize(20)
        label.setFont(font)


class Window2(QWidget):
    def __init__(self):
        super().__init__()
        label = QLabel("这里是窗口2", self)
        self.setStyleSheet("background-color: green")
        # 设置标签内的字体的属性
        font = QFont()
        font.setPointSize(20)
        label.setFont(font)

class MyWindow(QWidget):
    def __init__(self):
        # 切记一定要调用父类的__init__方法,因为它里面有很多对UI控件的初始化操作
        super().__init__()
        # 把对当前的窗口控件的具体的UI布局写到有个专门的方法中,以免初始化的方法内容太臃肿
        self.windows_stack = None
        self.init_ui()

    def init_ui(self):
        # 设置大小
        self.resize(400, 400)
        # 设置窗口标题
        self.setWindowTitle('PyQt5程序之QStackLayout')
        # 设置窗口图标
        self.setWindowIcon(QIcon('imgs/clock.png'))

        # 垂直布局
        outer_container = QVBoxLayout()
        # 展示区控件
        display = QWidget()
        display.setStyleSheet("background-color:grey")
        # 为展示区控件绑定抽屉布局(加上self的含义是让它变成一个类变量)
        self.windows_stack = QStackedLayout()
        # 创建两个新的窗口对象
        window1 = Window1()
        window2 = Window2()
        self.windows_stack.addWidget(window1)
        self.windows_stack.addWidget(window2)
        # 将抽屉布局绑定到展示控件上
        display.setLayout(self.windows_stack)
        # 按钮控件
        btn1 = QPushButton("切换到窗口一")
        btn2 = QPushButton("切换到窗口二")
        # 为按钮设置单击事件
        btn1.clicked.connect(self.show_window1)
        btn2.clicked.connect(self.show_window2)
        # 绑定控件
        outer_container.addWidget(display)
        outer_container.addWidget(btn1)
        outer_container.addWidget(btn2)

        # 让当前的窗口使用布局器
        self.setLayout(outer_container)
        
    def show_window1(self):
        self.windows_stack.setCurrentIndex(0)
        
    def show_window2(self):
        self.windows_stack.setCurrentIndex(1)


if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget子类——MyWindow
    w = MyWindow()
    w.show()
    app.exec_()
  • 这是点击窗口二按钮的显示情况:
    在这里插入图片描述
  • 这是点击窗口一按钮的显示情况(也是默认的情况):
    在这里插入图片描述
  • 其实本例子在使用窗口切换的按钮中已经用到和后面要讲解的信号和槽相关的内容了,这里暂时先按下不表,作为一个引子抛砖引玉,大家可以先对信号和槽的使用有一个大概的印象。
  • 这里有一个具体的实现细节是display = QWidget(),栈布局本身是作为一种布局存在,而不是作为一种UI控件存在,所以要使得栈布局能够正常发挥自己的作用,需要给他找一个控件介质,这里用的是QWidget()窗口控件。
  • 栈布局也叫抽屉布局,“抽屉”的这个喻体很象形地体现出这种布局的特点,它就像是一个多层的抽屉,不同层的抽屉可以放、不同的“窗口”,但是当们从上往下看的时候,同一个时候只能看到一个拉开的抽屉,也即只能看到一个窗口。使用这种“抽屉”布局能够实现同一空间区域的多样化的应用。一个形象的例子是我们电脑上的微信的窗口,左侧是不同的好友,而右侧则是我们和好友的聊天的页面,我们点击不同的好友的时候,右侧的区域会转变成不同的好友相关的聊天区,但是空间依然还是那一块空间。在栈布局中,不同的显示窗口的显示主要用setCurrentIndex(0)来实现,通过指定想要展示的窗体的下标,来实现展示的窗体的切换。下标从0开始计数。

3.4 PyQt5常用的三种窗口

在这里插入图片描述

3.4.1 PyQt5之QWidget
import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

class MyQWidget(QWidget):
    def __init__(self):
        # 切记一定要调用父类的__init__方法,因为它里面有很多对UI控件的初始化操作
        super().__init__()
        # 把对当前的窗口控件的具体的UI布局写到有个专门的方法中,以免初始化的方法内容太臃肿
        self.init_ui()

    def init_ui(self):
        # 设置大小
        self.resize(700, 400)
        # 设置窗口标题
        self.setWindowTitle('PyQt5程序之QWidget')
        # 设置窗口图标
        self.setWindowIcon(QIcon('imgs/clock.png'))

        # 创建一个标签
        label = QLabel("我是PyQt5的三种重要窗口之一,"
                       "\n是QWidget窗口的示例~"
                       "\n我的特点是自定义的程度高。", self)
        # 调整label的字体大小为30,颜色为green
        label.setFont(QFont("微软雅黑", 25, QFont.Bold))
        label.setStyleSheet("color: green")


if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget子类——MyQWidget
    w = MyQWidget()
    w.show()
    app.exec_()

在这里插入图片描述

  • QWidget窗口的使用方法之一是直接让我们所创建的自定义的窗口类继承它,这样我们所自定义的窗口就是QWidget的子类,就能够获得这种窗口的特点。在PyQt中,QWidget是所有用户界面对象的基类。
3.4.2 PyQt5之QMainWindow
import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

class MyQMainWindow(QMainWindow):
    def __init__(self):
        # 切记一定要调用父类的__init__方法,因为它里面有很多对UI控件的初始化操作
        super().__init__()
        # 把对当前的窗口控件的具体的UI布局写到有个专门的方法中,以免初始化的方法内容太臃肿
        self.init_ui()

    def init_ui(self):
        # 设置大小
        self.resize(700, 400)
        # 设置窗口标题
        self.setWindowTitle('PyQt5程序之QMainWindow')
        # 设置窗口图标
        self.setWindowIcon(QIcon('imgs/clock.png'))

        # 创建菜单和动作
        file_menu = QMenu('文件', self)
        new_action = QAction('新建', self)
        open_action = QAction('打开', self)
        save_action = QAction('保存', self)
        exit_action = QAction('退出', self)
        file_menu.addAction(new_action)
        file_menu.addAction(open_action)
        file_menu.addAction(save_action)
        file_menu.addSeparator()  # 创建了一个分割的横线
        file_menu.addAction(exit_action)

        # 创建菜单栏并添加菜单
        menu_bar = QMenuBar(self)
        menu_bar.addMenu(file_menu)
        self.setMenuBar(menu_bar)

        # 创建工具栏
        tool_bar = self.addToolBar('工具')
        edit_action = QAction('编辑', self)
        tool_bar.addAction(edit_action)

        # 设置中心部件
        central_MainWidget = QWidget(self)  # 使得中心区域可以容纳多个以上的组件。
        v_layout = QVBoxLayout()
        # 创建一个文本编辑框
        text_edit = QTextEdit(self)
        # 创建一个标签
        label = QLabel("我是PyQt5的三种重要窗口之一,"
                       "\n是QMainWindow窗口的示例~"
                       "\n我的特点是自带菜单栏,工具栏,状态栏和中心内容区。", self)
        # 调整label的字体大小为30,颜色为green
        label.setFont(QFont("微软雅黑", 20, QFont.Bold))
        label.setStyleSheet("color: green")
        # 创建一个按钮,点击时更新状态栏信息
        self.button = QPushButton('点击更新状态栏', self)
        # 将控件绑定到布局
        v_layout.addWidget(text_edit)
        v_layout.addWidget(label)
        v_layout.addWidget(self.button)
        # 将布局绑定到主控件
        central_MainWidget.setLayout(v_layout)
        # 将主控件绑定到QMainWindow的中心内容区
        self.setCentralWidget(central_MainWidget)

        # 创建状态栏
        self.status_bar = QStatusBar(self)
        self.status_bar.showMessage('欢迎使用应用程序')
        # 设置状态栏的永久显示项
        permanent_label = QLabel('永久信息:当前模式')
        self.status_bar.addPermanentWidget(permanent_label)
        self.setStatusBar(self.status_bar)

        # 连接动作的信号和槽
        # (连接上了事件,有事件相关的控件都加了self变成了当前类的属性:类变量,
        # 这样才可以在当前类的其它方法中直接使用。)
        exit_action.triggered.connect(self.close)
        self.button.clicked.connect(self.update_statusbar)

    """
    * 这是中心内容区的按钮点击了之后用来更新状态栏的信息的方法。
    """
    def update_statusbar(self):
        # 更新状态栏的临时信息
        self.status_bar.showMessage('新的风暴已经出现!', 5000)  # 5秒后消失


if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QMainWindow子类——MyQMainWindow
    w = MyQMainWindow()
    w.show()
    app.exec_()

在这里插入图片描述

  • QMainWindow是一个提供主应用程序窗口的类,通常包含一个菜单栏工具栏状态栏和一个中心部件(通常是一个 QWidget 或其子类)。QMainWindow是用于构建复杂应用程序的主要窗口类,因为它提供了构建多组件、多面板界面的结构。

  • 这是展开了顶部的菜单栏QMainWindow
    在这里插入图片描述

  • 一般使用 menu_bar = QMenuBar(self)来创建一个顶部菜单栏,并绑定到当前的QMainWindow

  • 菜单栏中可以容纳多个菜单,一般使用file_menu = QMenu('文件', self)来创建一个菜单。

  • 菜单中可以有多个菜单项,在PyQt中我们称为QAction,比如我们可以用new_action = QAction('新建', self)来创建一个名为“新建”的QAction,QAction是用来表示窗口上的动作或事件的。它包含了多种信息,如图标、菜单文字、快捷键、状态栏文字和浮动帮助等。它可以和信号一样可以通过连接槽函数来实现特定的功能。

  • 对于创建好的QAction我们需要file_menu.addAction(new_action)将其添加到菜单中。

  • 菜单中除了可以添加事件之外,还可以添加file_menu.addSeparator()以创建一个分割的横线。

  • 当然,我们所创建的菜单也需要添加到菜单栏中:menu_bar.addMenu(file_menu)

  • 并将我们创建的菜单栏绑定到当前的QMainWindowself.setMenuBar(menu_bar)

  • 当前选中的这一条就是工具栏
    在这里插入图片描述

  • QAction的一个关键特性是其能够根据添加的位置改变自己的显示方式。例如,如果将其添加到菜单中,它会显示为一个菜单项; 而如果添加到工具栏,则会显示为一个按钮。 这种灵活性使得QAction成为在Qt中创建交互性窗口界面的重要工具。

  • QAction可以直接添加到工具栏上。

  • 更新了状态栏信息的QMainWindow
    在这里插入图片描述

  • 使用self.status_bar = QStatusBar(self)创建一个状态栏。

  • 使用self.status_bar.showMessage('欢迎使用应用程序')在左下脚短暂显示状态信息。

  • 也可以自己创建一个标签permanent_label = QLabel('永久信息:当前模式'),然后把这个标签设置为永久显示的信息: self.status_bar.addPermanentWidget(permanent_label)

  • 最后记得,把状态栏绑定到当前的主窗口:self.setStatusBar(self.status_bar)

  • 当前选中的这一部分就是中心部件,在这个中心部件中,我们放置了一个QTextEdit、一个QLabel和一个QPushBotton,用垂直布局将这三个控件整合起来。
    在这里插入图片描述

  • 值得额外注意的是,对于QMainWindow而言,它的中心部件其实可以看成是一个独立的窗口,就像是我们前面学习的QWidget事实上,它也确实是这样做的,用一个QWidget的实例来作为容纳的介质,所有其它的控件或者布局都放在这个QWidget中,最后再self.setCentralWidget(central_MainWidget)将主控件绑定到QMainWindow的中心内容区即可。

3.4.3 PyQt5之QDialog
import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *


class CustomDialog(QDialog):
    def __init__(self):
        super().__init__()
        # 设置大小
        self.resize(200, 200)
        self.setWindowTitle('自定义对话框')

        # 创建布局
        layout = QVBoxLayout()

        # 创建标签
        self.label = QLabel('请输入一些文本:', self)
        layout.addWidget(self.label)

        # 创建按钮
        self.ok_button = QPushButton('确定', self)
        self.ok_button.clicked.connect(self.accept)  # 点击按钮时接受对话框
        layout.addWidget(self.ok_button)

        # 设置对话框的布局
        self.setLayout(layout)

    def get_text(self):
        # 此处仅为示例,实际上你可能需要从某个输入框获取文本
        return "示例文本"  # 假设这是用户输入的文本

class MyQMainWindow(QMainWindow):
    def __init__(self):
        # 切记一定要调用父类的__init__方法,因为它里面有很多对UI控件的初始化操作
        super().__init__()
        # 把对当前的窗口控件的具体的UI布局写到有个专门的方法中,以免初始化的方法内容太臃肿
        self.init_ui()

    def init_ui(self):
        # 设置大小
        self.resize(400, 400)
        # 设置窗口标题
        self.setWindowTitle('PyQt5程序之QDialog')
        # 设置窗口图标
        self.setWindowIcon(QIcon('imgs/clock.png'))

        # 添加一个按钮用于打开对话框
        self.open_dialog_button = QPushButton('打开对话框', self)
        self.open_dialog_button.clicked.connect(self.show_dialog)
        self.setCentralWidget(self.open_dialog_button)

    def show_dialog(self):
        # 创建并显示自定义对话框
        dialog = CustomDialog()
        if dialog.exec_():  # 执行对话框,如果用户点击了确定按钮则返回True
            text = dialog.get_text()  # 获取用户输入的文本
            print(f"用户输入的文本是: {text}")


if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QMainWindow子类——MyQMainWindow
    w = MyQMainWindow()
    w.show()
    app.exec_()
  • 这是主界面:
    在这里插入图片描述
  • 点击打开对话框后,弹出正式的QDialog
    在这里插入图片描述
  • QDialogPyQt中是一个通用的对话框类,用于创建模态或非模态对话框窗口。模态对话框会阻止用户与其他窗口交互,直到对话框被关闭;非模态对话框则允许用户同时与多个窗口交互。在我们这里的例子中,我们创建的是一个模态对话框

4. PyQt5的信号和槽

在这里插入图片描述

4.1 信号与槽之接收信号

import sys

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *

class MyWindow(QWidget):
    def __init__(self):
        # 切记一定要调用父类的__init__方法,因为它里面有很多对UI控件的初始化操作
        super().__init__()
        # 把对当前的窗口控件的具体的UI布局写到有个专门的方法中,以免初始化的方法内容太臃肿
        self.init_ui()

    def init_ui(self):
        # 设置大小
        self.resize(500, 300)
        # 设置窗口标题
        self.setWindowTitle('PyQt5程序之信号和槽1——接收信号')
        # 设置窗口图标
        self.setWindowIcon(QIcon('imgs/clock.png'))

        # 创建了一个按钮
        btn = QPushButton("点我点我!", self)
        # 设置窗口的宽度和高度
        btn.setGeometry(200, 200, 100, 30)
        # 为按钮的单击信号绑定对应的槽函数
        btn.clicked.connect(self.on_btn_clicked)

    def on_btn_clicked(self, arg):
        print('点我点我!', arg)


if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget子类——MyWindow
    w = MyWindow()
    w.show()
    app.exec_()

在这里插入图片描述

  • 这是一个用来演示信号与槽的最基本的交互效果的一个示例,在示例中,我们通过多次点击窗体中的“点我点我!”的按钮,可以看到在控制台输出了“点我点我!”的字样。
  • 在上述的代码中,主要体现信号与槽的相关知识的代码是btn.clicked.connect(self.on_btn_clicked),这是将一个按钮控件btn的单击信号clicked与我们定义的类函数(本质上是槽函数,是我们写的对于btn按钮的单击事件的响应函数)连接起来。一般来说,响应函数(槽函数)我们都会写成是窗口类的类方法。
  • 至于为什么在“点我点我!”字段后面害有一个False的原因是对于QPushButtonclicked信号,它并不传递任何数据,所以通常不需要为槽函数定义参数。但是我们其实定义了一个参数arg,所以PyQt会自动传入一个默认值(对于没有数据的信号,通常是 False)。

4.2 信号与槽之自定义信号

import sys
import time

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *


class MyWindow(QWidget):
    # 自定义一个信号,自定义的信号只可以放在类变量这里
    # 后面的str表示该信号将会传递一个str类型的数据变量
    my_signal = pyqtSignal(str)

    def __init__(self):
        # 切记一定要调用父类的__init__方法,因为它里面有很多对UI控件的初始化操作
        super().__init__()
        # 把对当前的窗口控件的具体的UI布局写到有个专门的方法中,以免初始化的方法内容太臃肿
        self.init_ui()

    def init_ui(self):
        # 设置大小
        self.resize(500, 300)
        # 设置窗口标题
        self.setWindowTitle('PyQt5程序之信号和槽2——自定义信号')
        # 设置窗口图标
        self.setWindowIcon(QIcon('imgs/clock.png'))

        # 创建了一个按钮
        btn = QPushButton("测试自定义信号", self)
        # 设置窗口的宽度和高度
        btn.setGeometry(200, 200, 100, 30)

        # 为按钮的单击信号绑定对应的槽函数
        btn.clicked.connect(self.on_btn_clicked)
        # 为自定义的信号绑定他对应的槽函数
        self.my_signal.connect(self.find_error)

    def on_btn_clicked(self):
        for index, ip in enumerate(["192.168.21.%d" % x for x in range(1, 255)]):
            print(f"正在检查:{ip}...", end="")
            if index % 5 == 0:
                self.my_signal.emit("【发现错误!!】")  # 相当于啊调用了自定义信号my_signal的槽函数
            else:
                self.my_signal.emit("")
            # 延时0.01s
            time.sleep(0.01)

    def find_error(self, msg):
        print(msg)


if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget子类——MyWindow
    w = MyWindow()
    w.show()
    app.exec_()

  • 在上一个例子中,我们所使用的信号是按钮控件自带的clicked单击信号,但是当我们需要完成更加复杂的用户交互的时候,有些时候我们所需要的信号并不一定有现成的,这时候就需要我们进行信号的自定义。
  • 我们可以使用形如my_signal = pyqtSignal(str)来自定义一个信号,其中后面的str表示该信号将会传递一个str类型的数据变量。当我们需要传入的参数很多的时候,一般我们也是只写一个str,而具体的字符串用JSON格式的字符串来封装。
  • 和预定义好的信号不同的是,对于我们自定义的信号,我们得使用self.my_signal.emit("【发现错误!!】"),在代码中主动进行信号的发射来调用了自定义信号my_signal的槽函数。
  • 当然,相同的是,自定义的信号和预定义的信号一样,都需要我们为信号连接上相对应的响应函数:self.my_signal.connect(self.find_error)

4.3 信号与槽之实例练习

import sys
import time

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *


class MyWindow(QWidget):
    # 自定义一个信号,自定义的信号只可以放在类变量这里
    # 后面的str表示该信号将会传递一个str类型的数据变量
    my_signal = pyqtSignal(str)
    # 用于更新显示框处的信息的文本变量
    info = ""

    def __init__(self):
        # 切记一定要调用父类的__init__方法,因为它里面有很多对UI控件的初始化操作
        super().__init__()
        # 把对当前的窗口控件的具体的UI布局写到有个专门的方法中,以免初始化的方法内容太臃肿
        self.init_ui()

    def init_ui(self):
        # 设置大小
        self.resize(500, 350)
        # 设置窗口标题
        self.setWindowTitle('PyQt5程序之信号和槽3——练习')
        # 设置窗口图标
        self.setWindowIcon(QIcon('imgs/clock.png'))

        # 创建了一个多行文本框
        self.text = QTextEdit(self)
        # 设置 QTextEdit 为只读
        self.text.setReadOnly(True)
        # 设置字体大小为20
        font = QFont()
        font.setPointSize(10)
        self.text.setFont(font)
        # 设置多行文本框的窗口的宽度和高度
        self.text.setGeometry(50, 20, 400, 250)

        # 创建了一个按钮
        btn = QPushButton("测试自定义信号", self)
        # 设置窗口的宽度和高度
        btn.setGeometry(200, 280, 100, 30)

        # 为按钮的单击信号绑定对应的槽函数
        btn.clicked.connect(self.on_btn_clicked)
        # 为自定义的信号绑定他对应的槽函数
        self.my_signal.connect(self.find_error)

    def on_btn_clicked(self):
        # 每次启动前先清空输入框
        self.info = ""
        self.text.setText("")
        for index, ip in enumerate(["192.168.21.%d" % x for x in range(1, 255)]):
            signal_line = "模拟,正在检查:" + ip + "..."
            if index % 5 == 0:
                self.my_signal.emit(signal_line + "【发现错误!!】")  # 相当于啊调用了自定义信号my_signal的槽函数
            else:
                self.my_signal.emit(signal_line)
            # 延时0.01s
            time.sleep(0.01)

    def find_error(self, msg):
        self.info += msg + '\n'
        self.text.setText(self.info)
        # self.text.append(msg+'\n')
        print(msg)


if __name__ == '__main__':
    # 实例化QApplication类
    # sys.argv是命令行的参数
    app = QApplication(sys.argv)
    # 实例化QWidget子类——MyWindow
    w = MyWindow()
    w.show()
    app.exec_()

在这里插入图片描述

  • 在实例练习中,和上一个实例不一样的地方是,除了仅仅是在控制行中输出内容之外,我们还尝试把内容输出到窗口的展示区。
  • 相比于在上一个实例中我们在单击事件的响应函数中既传参又进行具体的操作不同,在这个实例中,我们在单击事件的响应函数中不执行操作,只通过信号发射传递参数。而所有的打印和显示到UI上的操作我们都留到自定义信号的响应函数中才去具体执行。
  • 但是,依然出现的一个问题是,UI的文本的更新并没有如我们设想的如控制行的输出区一样一行一行地输出和更新,相反他需要等控制行中的所有的文本都打印完毕之后,他才会一次性把所有的文本更新到UI展示区,而会产生这样的现象的问题是因为这些所有的操作都是在主线程中执行的,所以UI的更新会阻塞等待控制行的操作先处理完之后才一起更新。
  • 而如果想要实现后台操作和UI更新能够同步进行的话,就需要我们学会使用多线程技术

5. PyQt5的多线程

5.1 PyQt5多线程基础语法实例

"""
动态加载ui文件
"""
import sys
import time

from PyQt5.QtCore import QThread
from PyQt5.QtWidgets import *
from PyQt5 import uic


class MyWidget(QWidget):

    def __init__(self):
        super().__init__()
        self.myThread = None
        self.ui = None
        self.ui_thread_btn = None
        self.QThread_btn = None
        self.text_edit = None
        self.init_ui()

    def init_ui(self):
        self.ui = uic.loadUi("ui/QThread-1.ui")
        self.ui_thread_btn = self.ui.pushButton
        self.QThread_btn = self.ui.pushButton_2
        self.text_edit = self.ui.textEdit

        # 给登录按钮绑定信号和槽函数
        self.ui_thread_btn.clicked.connect(self.click1)
        # 给忘记密码按钮绑定信号和槽函数
        self.QThread_btn.clicked.connect(self.click2)

    def click1(self):
        for i in range(5):
            time.sleep(1)
            print(f"正在使用UI线程,......{i+1}")

    def click2(self):
        # 这里的子线程必须要写成self的属性!!否则无法实现异步的效果!
        # 因为我们并不希望myThread线程在click2函数运行结束的时候就被销毁,我们希望它可以根据它的需求继续留存着。
        self.myThread = MyQThread()
        self.myThread.start()


class MyQThread(QThread):
    def __init__(self):
        super().__init__()

    def run(self):
        for i in range(5):
            time.sleep(1)
            print(f"正在使用子线程,......{i+1}")


if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = MyWidget()
    # 展示窗口
    # w.show()  # 在这种情况下这是一个错误示范
    w.ui.show()  # 这才是正确的操作
    sys.exit(app.exec_())

在这里插入图片描述

  • 当我点击的是左边的按钮的时候,这时候是没有使用多线程的情况,在命令行中输出UI线程的时候,我是无法同时对输入行内的数字进行编辑的。
    在这里插入图片描述
  • 而当我点击的是右边的按钮的时候,这时候是使用多线程的情况,在命令行中输出子线程的时候,我可以同时对输入行内的数字进行编辑。这就达到了我们使用多线程技术来实现对操作和UI更新的分离的效果。
  • PyQt中使用多线程的方法并不难,主要是继承PyQt中本身自带的QThread类,重写run方法,在run方法中书写具体的业务逻辑即可。在使用的时候,只需要在对应的响应的槽函数中实例化一个线程实例,然后再启动线程的start()方法来运行run方法就行。
  • 这里使用的UI是使用Qt Designer进行设计的,至于具体要如何使用Qt Desiner来进行GUI的设计,以及对于已经设计好的UI文件,我们要如何集成到Python的GUI项目中,在下一部分我们会具体展开讲解。

6. PyQt5的界面设计神器——Qt Designer快速入门

6.1 Qt Designer界面介绍

在这里插入图片描述

  • 在进入Qt Designer的时候,程序首先会让我们选择窗体,这里面一共有5种可以选择的窗体,但是其实本质上是三大类窗体,就是我们在上面所介绍的QWidgetQMainWindowQDialog
    在这里插入图片描述
  • 在选择窗体的类型之后,我们就得到一个带有点阵的可以用于直接设计的GUI界面,其中左侧有一系列的可以通过直接拖拽来移动到窗体中的UI控件和布局。
    在这里插入图片描述
  • 每当我们拖拽控件到窗体上的时候,对象查看器中,就会多出来一个UI控件的对象。
    在这里插入图片描述
  • 当我们选中一个UI对象的时候,属性编辑器中就会显示出当前的对象的一些基本的属性,我们可以通过属性编辑器来直接对属性进行编辑。
    在这里插入图片描述
  • 我们也可以使用Qt Designer来进行简单的信号和槽的编辑和设置,但是这里面并不提供自定义的槽函数的实现,所以对于Qt Designer来说,他很适合用来进行GUI的设计,但是他并不是适合用来为Python GUI程序注入灵魂——即进行信号与槽的编辑。
  • 此外,Qt Designer也支持对我们已经设计好的GUI进行预览的功能。
  • 当我们完成了UI的设计之后,我们保存记得得到一份.ui文件。

6.2 将Qt Designer的UI文件集成到Python GUI项目

"""
动态加载ui文件
"""
import sys
import time

from PyQt5.QtWidgets import *
from PyQt5 import uic

class MyWidget(QWidget):

    def __init__(self):
        super().__init__()
        self.ui = None
        self.user_name = None
        self.password = None
        self.login_btn = None
        self.forget_btn = None
        self.text_browser = None
        self.init_ui()

    def init_ui(self):
        self.ui = uic.loadUi("ui/login.ui")
        # self.explain_function()  # 这是一个用来对ui文件进行输出测试的测试方法
        self.user_name = self.ui.lineEdit  # 这是获取用户名输入框
        self.password = self.ui.lineEdit_2  # 这是获取用户密码输入框
        self.login_btn = self.ui.pushButton  # 这是获取登录按钮
        self.forget_btn = self.ui.pushButton_2  # 这是获取忘记密码按钮
        self.text_browser = self.ui.textBrowser  # 这是获取文本浏览器

        # 给登录按钮绑定信号和槽函数
        self.login_btn.clicked.connect(self.login)
        # 给忘记密码按钮绑定信号和槽函数
        self.forget_btn.clicked.connect(self.forget)
        # 给文本浏览器绑定信号和槽函数
        self.text_browser.textChanged.connect(self.show_text)

    def login(self):
        user_name = self.user_name.text()
        password = self.password.text()
        print("您当前输入的用户名为:" + user_name)
        print("您当前输入的密码为:" + password)
        print("登录成功!")
        # self.text_browser.setText("登录成功!")

    def forget(self):
        print("找回密码成功!")
        # self.text_browser.setText("找回密码成功!")

    def show_text(self):
        # 获取当前时间的字符串
        self.text_browser.setText("文本信息改变" + time.gmtime().__str__())

    def explain_function(self):
        # 往下是使用print输出来尝试了解我们所载入的ui对象中到底有些什么东西?
        print(self.ui)  # 打印ui文件中的最顶层的对象
        print(self.ui.__dict__)  # 获取ui文件中最顶层的对象中的所有的属性
        """
        以下是ui文件中最顶层的对象中的所有的属性,以键值对的方式给出:
        {'label': <PyQt5.QtWidgets.QLabel object at 0x0000014D5EE89750>,
         'lineEdit': <PyQt5.QtWidgets.QLineEdit object at 0x0000014D5EE89870>, 
         'lineEdit_2': <PyQt5.QtWidgets.QLineEdit object at 0x0000014D5EE89900>, 
         'label_2': <PyQt5.QtWidgets.QLabel object at 0x0000014D5EE89990>, 
         'pushButton': <PyQt5.QtWidgets.QPushButton object at 0x0000014D5EE89A20>, 
         'pushButton_2': <PyQt5.QtWidgets.QPushButton object at 0x0000014D5EE89AB0>, 
         'textBrowser': <PyQt5.QtWidgets.QTextBrowser object at 0x0000014D5EE89B40>}
        """
        print(self.ui.label.text())  # ui文件中最顶层的对象中的label对象的text()方法


if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = MyWidget()
    # 展示窗口
    # w.show()  # 在这种情况下这是一个错误示范
    w.ui.show()  # 这才是正确的操作
    sys.exit(app.exec_())

在这里插入图片描述

  • 以我这里设计的这一份简单的登录界面为例:不同于以往的需要自己去定义各种各样的UI控件,结合了Qt Designer的UI设计,我们只需要通过self.ui = uic.loadUi("ui/login.ui")载入我们已经设计好的UI界面即可。
  • 而UI界面中的控件,本质上,其实我们是通过管理对象实例来对这些控件对象进行操作的,从我们载入的UI对象中,我们可以根据我们在UI设计中对他们的命名来获取这些UI对象,进而可以像直接用代码编辑UI的时候那样,通过对对象的处理来实现各种业务逻辑。

7. PyQt5综合案例

  • 至此,关于PyQt5的基本知识已经介绍完毕,有了这些基本知识,我们就可以着手去尝试我们所需要的Python GUI程序的编写了,当然,只学会这些还是远远不够的,如果想要更近一步的学习,可以参考我列出的进一步学习中的学习链接,如果对于上面的知识想要进一步的巩固,可以尝试去仔细阅读和学习我在参考资料中列出的学习链接。
  • 此外,在我的Gitee代码仓库中还有其它的未完全给出的学习代码,其中还包含一个完整的登录功能的综合案例,欢迎大家进一步学习。
  • 最后的最后,诚挚感谢王铭东老师在B站的无私分享!

参考资料

进一步学习

;