PyQt5教程汇总

 本文整理汇总了网上对PyQt5的资料,重新整合了一篇教程,希望可以帮助到大家:

PyQt5 (yuque.com)这是本文原文,大家可以来我的语雀空间查看详细内容

All Classes | Qt 5.15官方文档icon-default.png?t=O83Ahttps://doc.qt.io/qt-5/classes.html

快速掌握PyQt5 - 知乎带你快速学会使用PyQt5icon-default.png?t=O83Ahttps://www.zhihu.com/column/pyqt5

快速掌握PyQticon-default.png?t=O83Ahttps://blog.csdn.net/la_vie_est_belle/category_9279128.html

PyQt5新手教程(六万字)-CSDN博客文章浏览阅读3.3w次,点赞125次,收藏730次。PyQt是Python编程语言的一个GUI(图形用户界面)工具包,它允许开发人员使用Python语言创建桌面应用程序。PyQt是基于Qt库的Python封装,Qt是一个流行的C++框架,用于开发跨平台的应用程序。_pyqthttps://blog.csdn.net/shinuone/article/details/131895764

一、开始

1.安装:

Windows上安装:

pip install pyqt5

Linux上安装:

sudo apt-get install python3-pyqt5

MacOS上安装:

pip3 install pyqt5

安装Qdesignr:

pip install PyQt5Designer

2.程序运行起点

通过下方代码就可以呈现一个非常简单的PyQt5程序。

import sys
from PyQt5.QtWidgets import QApplication, QLabel

if __name__ == '__main__':
    app = QApplication(sys.argv)  # 1
    label = QLabel('Hello World') # 2
    ''' 2 相当于:
    label = QLabel()
    label.setText('Hello World')'''
    label.show()                  # 3
    sys.exit(app.exec_())         # 4

1. 想要创建应用必须先实例化一个QApplication,并将sys.argv作为参数传入;

2. 实例化一个QLabel控件,该控件用来展示文字或图片,这里用于展示文本。可以像上方代码一样直接传入‘Hello World’进行实例化,也可以先实例化,再调用setText()方法来设置文本:

3. 通过调用show()方法使控件可见(默认是隐藏);

4. app.exec_()是执行应用,让应用开始运转循环,直到窗口关闭返回0给sys.exit(),退出整个程序。 有些小伙伴可能发现还有exec(),在Python2中exec是关键字,所以PyQt5就使用exec_()而不是exec() 。不过exec在Python3中已经不再是关键字了,所以如果读者使用的是Python3的话那在上述代码中用exec()也完全没关系。

import sys
from PyQt5.QtWidgets import QApplication, QLabel

if __name__ == '__main__':
    app = QApplication(sys.argv)
    label = QLabel('<font color="red">Hello</font> <h1>World</h1>')
    # label.setText('<font color="red">Hello</font> <h1>World</h1>')
    label.show()
    sys.exit(app.exec_())

二、信号与槽

信号(signal)与槽(slot)机制很重要。在这里我把信号视作裁判鸣枪,而用于行动的槽函数则视作选手开跑,当裁判鸣枪后(即信号发出),选手就开始往前跑(槽函数启动)。PyQt5中各个对象间或各个对象自身就是通过信号与槽机制来相互通信的

1.一个信号连接一个槽(通过按钮来改变文本)

很多程序上是有“开始”按钮的,按下去后按钮上的文本就变成了“停止”。下面就是一个示例(之后的代码都会用类来呈现):

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton


class Demo(QWidget):                                            # 1
    def __init__(self):
        super(Demo, self).__init__()
        self.button = QPushButton('Start', self)                # 2
        self.button.clicked.connect(self.change_text)           # 3

    def change_text(self):
        print('change text')
        self.button.setText('Stop')                             # 4
        self.button.clicked.disconnect(self.change_text)        # 5


if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = Demo()                                               # 6
    demo.show()                                                 # 7
    sys.exit(app.exec_())

1. 该类继承QWidget,可以将QWidget看作是一种毛坯房,还没有装修,而我们往其中放入QPushButton、QLabel等控件就相当于在装修这间毛坯房。类似的毛坯房还有QMainWindow和QDialog,之后章节再讲述;

2. 实例化一个QPushButton,因为继承于QWidget,所以self不能忘了(相当于告诉程序这个QPushButton是放在QWidget这个房子中的);

3. 连接信号与槽函数。self.button就是一个控件,clicked(按钮被点击)是该控件的一个信号,connect()即连接,self.change_text即下方定义的函数(我们称之为槽函数)。所以通用的公式可以是:widget.signal.connect(slot);

4. 将按钮文本从‘Start’改成‘Stop’;

5. 信号和槽解绑,解绑后再按按钮你会发现控制台不会再输出‘change text’,如果把这行解绑的代码注释掉,你会发现每按一次按钮,控制台都会输出一次‘change text’;

6. 实例化Demo类;

7. 使demo可见,其中的控件自然都可见(除非某控件刚开始设定隐藏)

现在用鸣枪和开跑来分析下上面这个例子:按钮控件是裁判,他鸣枪发出信号(clicked),change_text()槽函数运行就是选手开跑。

2.多个信号连接一个槽
class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.button = QPushButton('Start', self)
        self.button.pressed.connect(self.change_text)     # 1
        self.button.released.connect(self.change_text)    # 2

    def change_text(self):
        if self.button.text() == 'Start':                 # 获取按钮内的字
            self.button.setText('Stop')
        else:
            self.button.setText('Start')
3.一个信号与另外一个信号连接
self.button.pressed.connect(self.button.released)
self.button.released.connect(self.change_text)
4.一个信号连接多个槽

信号都为clicked,然后再多定义几个槽函数:

f

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.resize(300, 300)                           #初始化函数中将窗口大小
        self.setWindowTitle('demo')                     #窗口名称设置为‘demo’
        self.button = QPushButton('Start', self)
        self.button.clicked.connect(self.change_text)
        self.button.clicked.connect(self.change_window_size)    # 2 槽函数多了两个
        self.button.clicked.connect(self.change_window_title)   # 3

    def change_text(self):
        print('change text')
        self.button.setText('Stop')
        self.button.clicked.disconnect(self.change_text)

    def change_window_size(self):                               # 修改窗口大小
        print('change window size')
        self.resize(500, 500)
        self.button.clicked.disconnect(self.change_window_size)

    def change_window_title(self):                              # 修改窗口名称
        print('change window title')
        self.setWindowTitle('window title changed')
        self.button.clicked.disconnect(self.change_window_title)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = Demo()                                               
    demo.show()                                                 
    sys.exit(app.exec_())
5.自定义信号
import sys
from PyQt5.QtCore import pyqtSignal                            # 需要先导入pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QLabel

class Demo(QWidget):
    my_signal = pyqtSignal()                                   # 实例化一个自定义的信号

    def __init__(self):
        super(Demo, self).__init__()
        self.label = QLabel('Hello World', self)
        self.my_signal.connect(self.change_text)               # 将自定义的信号连接到槽函数

    def change_text(self):
        if self.label.text() == 'Hello World':
            self.label.setText('Hello PyQt5')
        else:
            self.label.setText('Hello World')

    def mousePressEvent(self, QMouseEvent):                    # 监测鼠标是否有按下
        self.my_signal.emit()                                   

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

自定义信号的高级用法:

《PyQt5高级编程实战》自定义信号详解_pyqt5 自定义信号-CSDN博客

如果要连接带参数的槽,可以用lambda: 函数(参数) 实现

三、布局管理

把各个控件摆摆好,让整个界面更加有序好看,这就是布局管理器的作用。

1 垂直布局QVBoxLayout
self.v_layout = QVBoxLayout()           # 实例化一个垂直布局管理器QVBoxLayout;
self.v_layout.addWidget(self.user_label)# 将控件self.user_label添加到垂直布局中
self.v_layout.addWidget(self.pwd_label) # 最先添加的出现在最上方
self.setLayout(self.v_layout)           # 将self.v_layout设为整个窗口的最终布局方式。

该布局方式就是将各个控件按从上到下垂直的方式摆放,下面看一个例子:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout
 
class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.user_label = QLabel('Username:', self)
        self.pwd_label = QLabel('Password:', self)
 
        self.v_layout = QVBoxLayout()           # 实例化一个垂直布局管理器QVBoxLayout;
        self.v_layout.addWidget(self.user_label)# 将控件添加到垂直布局中
        self.v_layout.addWidget(self.pwd_label) # 最先添加的出现在最上方
        self.setLayout(self.v_layout)          # 将self.v_layout设为整个窗口的最终布局方式。
 
if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = Demo()
    demo.show()
    sys.exit(app.exec_())

运行截图如下,可以看出两个标签文本是垂直对齐排列的:

2 水平布局管理 QHBoxLayout
self.h_layout = QHBoxLayout()

将控件从左到右依次水平摆放:

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

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.user_label = QLabel('Username:', self)
        self.user_line = QLineEdit(self)        # QLineEdit是一个单行文本输入框

        self.h_layout = QHBoxLayout()           # 实例化一个水平布局管理器
        self.h_layout.addWidget(self.user_label)# 添加到水平布局管理器中
        self.h_layout.addWidget(self.user_line) # 先添加的出现在左边
        self.setLayout(self.h_layout)     # 将self.h_layout设为整个窗口的最终布局方式

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

3 混合使用
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, \
    QHBoxLayout, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()

        self.user_label = QLabel('Username:', self)
        self.pwd_label = QLabel('Password:', self)
        self.user_line = QLineEdit(self)
        self.pwd_line = QLineEdit(self)
        self.login_button = QPushButton('Log in', self)
        self.signin_button = QPushButton('Sign in', self)

        self.user_h_layout = QHBoxLayout()                      
        self.pwd_h_layout = QHBoxLayout()                       
        self.button_h_layout = QHBoxLayout()                     
        self.all_v_layout = QVBoxLayout()                        

        self.user_h_layout.addWidget(self.user_label)
        self.user_h_layout.addWidget(self.user_line)
        self.pwd_h_layout.addWidget(self.pwd_label)
        self.pwd_h_layout.addWidget(self.pwd_line)
        self.button_h_layout.addWidget(self.login_button)
        self.button_h_layout.addWidget(self.signin_button)
        self.all_v_layout.addLayout(self.user_h_layout)
        self.all_v_layout.addLayout(self.pwd_h_layout)
        self.all_v_layout.addLayout(self.button_h_layout)

        self.setLayout(self.all_v_layout)

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

4 表单布局 QFormLayout

表单布局可以将控件以两列的形式进行排布,左列控件为文本标签,右列为输入型的控件,如QLineEdit。用这个布局管理器我们可以更加快速方便地构写有表单的界面。我们用QFormLayout来改写下上面的代码:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, \
    QHBoxLayout, QVBoxLayout, QFormLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()

        self.user_label = QLabel('Username:', self)
        self.pwd_label = QLabel('Password:', self)
        self.user_line = QLineEdit(self)
        self.pwd_line = QLineEdit(self)
        self.login_button = QPushButton('Log in', self)
        self.signin_button = QPushButton('Sign in', self)

        self.f_layout = QFormLayout()                    # 实例化一个QFormLayout控件
        self.button_h_layout = QHBoxLayout()                     
        self.all_v_layout = QVBoxLayout()                        

        self.f_layout.addRow(self.user_label, self.user_line)# 调用addRow()方法传入控件
        self.f_layout.addRow(self.pwd_label, self.pwd_line)
        self.button_h_layout.addWidget(self.login_button)
        self.button_h_layout.addWidget(self.signin_button)
        self.all_v_layout.addLayout(self.f_layout)          #  将表单布局添加到总布局中
        self.all_v_layout.addLayout(self.button_h_layout)

        self.setLayout(self.all_v_layout)

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

可以发现代码比之前的更加简洁了。

5 网格布局QGridLayout
self.grid_layout = QGridLayout()
self.grid_layout.addWidget(widget, row, column, rowSpan, columnSpan)
'''
widget就是要添加的控件;row为第几行,0代表第一行;column为第几列;
rowSpan表示要让这个控件去占用几行(默认一行);columnSpan表示要让这个控件去占用几列(默认一列)。'''

当使用该布局管理器的时候,你可以把整个窗体想象成带有坐标的,然后只用把各个控件放在相应的坐标就好了,请看示例(还是上方的登录框):

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QLineEdit, QPushButton, \
    QGridLayout, QVBoxLayout, QHBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()

        self.user_label = QLabel('Username:', self)
        self.pwd_label = QLabel('Password:', self)
        self.user_line = QLineEdit(self)
        self.pwd_line = QLineEdit(self)
        self.login_button = QPushButton('Log in', self)
        self.signin_button = QPushButton('Sign in', self)

        self.grid_layout = QGridLayout()             # 实例化一个QGridLayout布局管理器
        self.h_layout = QHBoxLayout()
        self.v_layout = QVBoxLayout()

        self.grid_layout.addWidget(self.user_label, 0, 0, 1, 1)    # 添加控件
        self.grid_layout.addWidget(self.user_line, 0, 1, 1, 1)
        self.grid_layout.addWidget(self.pwd_label, 1, 0, 1, 1)
        self.grid_layout.addWidget(self.pwd_line, 1, 1, 1, 1)
        self.h_layout.addWidget(self.login_button)                     
        self.h_layout.addWidget(self.signin_button)
        self.v_layout.addLayout(self.grid_layout)
        self.v_layout.addLayout(self.h_layout)    

        self.setLayout(self.v_layout)

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

这里混合使用QVBoxLayout、QHBoxLayout和QGridLayout来完成布局。

在上方程序中,我们将self.user_label放在(0, 0)这个坐标,也就是第一行第一列,占用一行一列;将self.user_line放在(0, 1),即第一行第二列,也就是self.user_label的右边,占用一行一列;将self.pwd_label放在(1, 0),即第二行第一列,在self.user_label的正下方,占用一行一列;最后我们将self.pwd_line放在(1, 1),即第二行第二列,占用一行一列。

因为默认都是一行一列,所以也可以写成:

self.grid_layout.addWidget(self.user_label, 0, 0)        
self.grid_layout.addWidget(self.user_line, 0, 1)
self.grid_layout.addWidget(self.pwd_label, 1, 0)
self.grid_layout.addWidget(self.pwd_line, 1, 1)
6 堆叠布局管理器 QStackedLayout

在一个窗口中,管理多个窗口,但同一时刻只能显示一个窗口(如:选项卡界面)

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QPushButton, QLabel, QVBoxLayout, QStackedLayout

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Stacked Layout Example")
        
        self.stacked_layout = QStackedLayout()
        page1 = self.create_page("Page 1 Content", "Switch to Page 2")
        page2 = self.create_page("Page 2 Content", "Switch to Page 1")
        self.stacked_layout.addWidget(page1)
        self.stacked_layout.addWidget(page2)

        central_widget = QWidget()
        central_widget.setLayout(self.stacked_layout)
        self.setCentralWidget(central_widget)

    def create_page(self, content_text, switch_button_text):
        layout = QVBoxLayout()
        content_label = QLabel(content_text)
        switch_button = QPushButton(switch_button_text)
        switch_button.clicked.connect(self.switch_page)
        layout.addWidget(content_label)
        layout.addWidget(switch_button)

        page = QWidget()
        page.setLayout(layout)
        return page

    def switch_page(self):
        # 切换页面
        current_index = self.stacked_layout.currentIndex()
        next_index = (current_index + 1) % 2  # 切换到下一页(循环切换)
        self.stacked_layout.setCurrentIndex(next_index)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

PyQt常用组件

四、消息框

1.各种类型的消息框
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QMessageBox

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.button = QPushButton('information', self)
        self.button.clicked.connect(self.show_messagebox)

    def show_messagebox(self):
        QMessageBox.information(self, 'Title', 'Content', 
            QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)  # 2↓

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

在槽函数中我们创建了一个信息框(information),基本用法如下:

QMessageBox.information(QWidget, 'Title', 'Content', buttons)
  • 第一个参数填self,表示该信息框属于我们这里的Demo窗口;
  • 第二个参数类型为字符串,填入的是该信息框的标题;
  • 第三个参数类型也是字符串,填入的是信息框的提示内容;
  • 第四个参数为信息框上要添加的按钮,在示例代码中我们添加了Yes、No和Cancel三个按钮,多个按钮之间用 | 来连接,常见的按钮种类有以下几种:
  • QMessageBox.Ok
  • QMessageBox.Yes
  • QMessageBox.No
  • QMessageBox.Close
  • QMessageBox.Cancel
  • QMessageBox.Open
  • QMessageBox.Save

如果没有指定按钮,信息框会自己默认加上合适的按钮上去:

QMessageBox.information(self, 'Title', 'Content')

最后运行截图如下:

点击后显示提示框:

除了信息框(information),还有以下几种,用法都是类似的(请注意消息框上的图标变化):
QMessageBox.question 问答框

QMessageBox.warning 警告框

QMessageBox.critical 错误框

QMessageBox.about 关于框

2 与消息框交互

在上面的示例中,不管用户按了哪个按钮,程序都用关闭消息框来作出反应。然而用户会希望点击不同按钮,程序作出的反应不同。下面举一个简单的示例:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QMessageBox

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.button = QPushButton('Click Me!', self)
        self.button.clicked.connect(self.show_messagebox)

    def show_messagebox(self):
        choice = QMessageBox.question(self, 'Change Text?', 'Would you like to change the button text?',  
            QMessageBox.Yes | QMessageBox.No)  # 点击消息框上的某个按钮后,会返回这个按钮

        if choice == QMessageBox.Yes:                   # 按下Yes,则改变按钮的文字;
            self.button.setText('Changed!')
        elif choice == QMessageBox.No:                  # 按下了No,则什么都不做
            pass

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

运行截图如下:

点击Yes之后,按钮文本改变:

五、完善登录注册页面

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QDialog, QLabel, QLineEdit, QPushButton, \
    QGridLayout, QVBoxLayout, QHBoxLayout, QMessageBox

USER_PWD = {
        'la_vie': 'password'
    }

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.resize(300, 100)

        self.user_label = QLabel('Username:', self)
        self.pwd_label = QLabel('Password:', self)
        self.user_line = QLineEdit(self)
        self.pwd_line = QLineEdit(self)
        self.login_button = QPushButton('Log in', self)
        self.signin_button = QPushButton('Sign in', self)

        self.grid_layout = QGridLayout()
        self.h_layout = QHBoxLayout()
        self.v_layout = QVBoxLayout()

        self.lineedit_init()
        self.pushbutton_init()
        self.layout_init()
        self.signin_page = SigninPage()     # 实例化SigninPage()

    def layout_init(self):
        self.grid_layout.addWidget(self.user_label, 0, 0, 1, 1)
        self.grid_layout.addWidget(self.user_line, 0, 1, 1, 1)
        self.grid_layout.addWidget(self.pwd_label, 1, 0, 1, 1)
        self.grid_layout.addWidget(self.pwd_line, 1, 1, 1, 1)
        self.h_layout.addWidget(self.login_button)
        self.h_layout.addWidget(self.signin_button)
        self.v_layout.addLayout(self.grid_layout)
        self.v_layout.addLayout(self.h_layout)

        self.setLayout(self.v_layout)

    def lineedit_init(self):
        self.user_line.setPlaceholderText('Please enter your username')
        self.pwd_line.setPlaceholderText('Please enter your password')
        self.pwd_line.setEchoMode(QLineEdit.Password)

        self.user_line.textChanged.connect(self.check_input_func)
        self.pwd_line.textChanged.connect(self.check_input_func)

    def pushbutton_init(self):
        self.login_button.setEnabled(False)
        self.login_button.clicked.connect(self.check_login_func)
        self.signin_button.clicked.connect(self.show_signin_page_func)

    def check_login_func(self):
        if USER_PWD.get(self.user_line.text()) == self.pwd_line.text():
            QMessageBox.information(self, 'Information', 'Log in Successfully!')
        else:
            QMessageBox.critical(self, 'Wrong', 'Wrong Username or Password!')

        self.user_line.clear()
        self.pwd_line.clear()

    def show_signin_page_func(self):
        self.signin_page.exec_()

    def check_input_func(self):
        if self.user_line.text() and self.pwd_line.text():
            self.login_button.setEnabled(True)
        else:
            self.login_button.setEnabled(False)

class SigninPage(QDialog):
    def __init__(self):
        super(SigninPage, self).__init__()
        self.signin_user_label = QLabel('Username:', self)
        self.signin_pwd_label = QLabel('Password:', self)
        self.signin_pwd2_label = QLabel('Password:', self)
        self.signin_user_line = QLineEdit(self)
        self.signin_pwd_line = QLineEdit(self)
        self.signin_pwd2_line = QLineEdit(self)
        self.signin_button = QPushButton('Sign in', self)

        self.user_h_layout = QHBoxLayout()
        self.pwd_h_layout = QHBoxLayout()
        self.pwd2_h_layout = QHBoxLayout()
        self.all_v_layout = QVBoxLayout()

        self.lineedit_init()
        self.pushbutton_init()
        self.layout_init()

    def layout_init(self):
        self.user_h_layout.addWidget(self.signin_user_label)
        self.user_h_layout.addWidget(self.signin_user_line)
        self.pwd_h_layout.addWidget(self.signin_pwd_label)
        self.pwd_h_layout.addWidget(self.signin_pwd_line)
        self.pwd2_h_layout.addWidget(self.signin_pwd2_label)
        self.pwd2_h_layout.addWidget(self.signin_pwd2_line)

        self.all_v_layout.addLayout(self.user_h_layout)
        self.all_v_layout.addLayout(self.pwd_h_layout)
        self.all_v_layout.addLayout(self.pwd2_h_layout)
        self.all_v_layout.addWidget(self.signin_button)

        self.setLayout(self.all_v_layout)

    def lineedit_init(self):
        self.signin_pwd_line.setEchoMode(QLineEdit.Password)
        self.signin_pwd2_line.setEchoMode(QLineEdit.Password)

        self.signin_user_line.textChanged.connect(self.check_input_func)
        self.signin_pwd_line.textChanged.connect(self.check_input_func)
        self.signin_pwd2_line.textChanged.connect(self.check_input_func)

    def pushbutton_init(self):
        self.signin_button.setEnabled(False)
        self.signin_button.clicked.connect(self.check_signin_func)

    def check_input_func(self):
        if self.signin_user_line.text() and self.signin_pwd_line.text() and self.signin_pwd2_line.text():
            self.signin_button.setEnabled(True)
        else:
            self.signin_button.setEnabled(False)

    def check_signin_func(self):
        if self.signin_pwd_line.text() != self.signin_pwd2_line.text():
            QMessageBox.critical(self, 'Wrong', 'Two Passwords Typed Are Not Same!')
        elif self.signin_user_line.text() not in USER_PWD:
            USER_PWD[self.signin_user_line.text()] = self.signin_pwd_line.text()
            QMessageBox.information(self, 'Information', 'Register Successfully')
            self.close()
        else:
            QMessageBox.critical(self, 'Wrong', 'This Username Has Been Registered!')

        self.signin_user_line.clear()
        self.signin_pwd_line.clear()
        self.signin_pwd2_line.clear()

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

用到的新内容:

self.signin_page = SigninPage()     # 实例化SigninPage()
self.signin_page.exec_()			# ① 打开页面(阻塞原页面,原页面将不可用)
self.signin_page.show()				# ② 不影响原页面使用
self.user_line.setPlaceholderText('Please enter your username')
self.pwd_line.setEchoMode(QLineEdit.Password)
self.user_line.textChanged.connect(self.check_input_func)#绑定槽和信号
self.user_line.text()#获取输入框内容
self.login_button.setEnabled(False)
self.signin_user_line.clear()

六、文本编辑框

单行文本框
self.lineText = QLineEdit(self)#实例化

self.lineText.setPlaceholderText('Please enter your username')#设置背景文字

self.lineText.setEchoMode(QLineEdit.Password)#设置输入框隐藏密码
#此外还有 QLineEdit.PasswordEchoOnEdit 在编辑时显示其它时隐藏
# QLineEdit.NoEcho 不显示          QLineEdit.Normal 正常显示

self.lineText.setFont()#设置字体

self.lineText.textChanged.connect(self.check_input_func)#连接槽函数,内容改变时调用

txt=self.lineText.text()#获取内容
多行文本框
self.text_edit = QTextEdit(self)
self.text_browser = QTextBrowser(self)
self.text_edit.textChanged.connect(self.show_text_func)
print(self.text_edit.toPlainText())
self.text_browser.setText(self.text_edit.toPlainText())  #toHtml
self.text_edit.textCursor().removeSelectedText()	#删除选中的内容
self.text_edit.textCursor().selection().toHtml()	#获取选中的内容
self.text_edit.insertHtml()
self.text_edit.setFont(font)
self.text_edit.setTextColor(color)
'''
def font_func(self):
        font, ok = QFontDialog.getFont()
        if ok:
            self.text_edit.setFont(font)

    def color_func(self):
        color = QColorDialog.getColor()
        if color.isValid():
            self.text_edit.setTextColor(color)
'''
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QTextEdit, QTextBrowser, QHBoxLayout, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.edit_label = QLabel('QTextEdit', self)
        self.browser_label = QLabel('QTextBrowser', self)
        self.text_edit = QTextEdit(self)
        self.text_browser = QTextBrowser(self)

        self.edit_v_layout = QVBoxLayout()
        self.browser_v_layout = QVBoxLayout()
        self.all_h_layout = QHBoxLayout()

        self.layout_init()
        self.text_edit_init()

    def layout_init(self):
        self.edit_v_layout.addWidget(self.edit_label)
        self.edit_v_layout.addWidget(self.text_edit)

        self.browser_v_layout.addWidget(self.browser_label)
        self.browser_v_layout.addWidget(self.text_browser)

        self.all_h_layout.addLayout(self.edit_v_layout)
        self.all_h_layout.addLayout(self.browser_v_layout)

        self.setLayout(self.all_h_layout)

    def text_edit_init(self):
        self.text_edit.textChanged.connect(self.show_text_func)  # 1

    def show_text_func(self):
        self.text_browser.setText(self.text_edit.toPlainText())  # 2

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

程序非常简单。通过实例化两个QLabel、一个QTextEdit以及一个QTextBrowser再通过垂直布局和水平布局就可以完成整个界面。关键点是在信号和槽的连接上。

1. 将self.text_edit的textChanged信号连接到自定义的槽函数上。也就是说当self.text_edit中的文本发生改变的时候,就会发出textChanged信号,然后调用show_text_func()槽函数。

2. 在槽函数中我们通过setText()方法将self.text_browser的文本设为self.text_edit的文本,而self.text_edit的文本通过toPlainText()获取,而不是text().

有趣的是,当我们在编辑框中输入Html代码时,右边的浏览框会对其执行:

七、各种按钮

1 QPushButton

通过之前的章节,已经对该按钮有一定了解,下面再介绍该按钮的其他方法:.............................................

import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.test_button = QPushButton('Test', self)
        self.test_button.setCheckable(True)                         # 1
        self.test_button.setIcon(QIcon('button.png'))               # 2
        self.test_button.toggled.connect(self.button_state_func)    # 3

    def button_state_func(self):
        print(self.test_button.isChecked())                         # 4

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

1. 按钮有标记和非标记两种状态,这两种状态下的按钮显示的样子不同。通过setCheckable(True)方法可以将按钮设置为可标记的,此时按钮就有了标记和非标记两种状态。可通过isCheckable()方法来判断按钮是否是可标记的

2. 通过setIcon()方法给按钮设置一个图标,传入的参数为QIcon();

免费图标搜索引擎 | 免费PNG图标 | 免版权SVG图标

Vector Icons and Stickers - PNG, SVG, EPS, PSD and CSS 或者点这个

3. toggled信号是专门用来配合按钮标记状态变化的,也就是说按钮标记状态发生变化就会发出toggled信号。在这里将toggled信号和自定义的槽函数连接了起来;

类似的还有 clicked、pressed、released

4. 通过isChecked()方法来判断按钮是否为标记状态,返回True、False。所以该槽函数会在按钮标记状态发生改变的时候启动,并打印True和False。

(运行前记得将button.png放在项目目录下)

2 QToolButton

QToolButton是与工具操作相关的按钮,通常和QToolBar搭配使用。QToolButton一般不用来显示文本,而显示图标QIcon (关于QToolBar我们会在后续介绍QMainWindow的时候再详细讲解)。

不过在方法使用上,QToolButton跟QPushButton还是很像的:

import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QWidget, QToolButton

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.test_button = QToolButton(self)                        # 1
        self.test_button.setCheckable(True)
        self.test_button.setIcon(QIcon('button.png'))
        self.test_button.toggled.connect(self.button_state_func)
        self.test_button.isCheckable()

    def button_state_func(self):
        print(self.test_button.isChecked())

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

不能在QToolButton实例化的时候直接传入文本字符串,因为该控件没有相应的初始化函数。如果要设置文本的话得通过setText()方法。但是setText()方法和setIcon()方法都使用的话,只会显示图标。

3 QRadioButton 单选

该控件为单选按钮,也就是说默认每次只有一个按钮会被选中。下面我们来完成一个开关灯泡的小程序:

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

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.off_button = QRadioButton('off', self)                 # 1
        self.on_button = QRadioButton('on', self)                   # 2

        self.pic_label = QLabel(self)                               # 3

        self.button_h_layout = QHBoxLayout()
        self.pic_h_layout = QHBoxLayout()
        self.all_v_layout = QVBoxLayout()

        self.layout_init()
        self.radiobutton_init()
        self.label_init()

    def layout_init(self):
        self.pic_h_layout.addStretch(1)                             # 4
        self.pic_h_layout.addWidget(self.pic_label)
        self.pic_h_layout.addStretch(1)
        self.button_h_layout.addWidget(self.off_button)
        self.button_h_layout.addWidget(self.on_button)
        self.all_v_layout.addLayout(self.pic_h_layout)
        self.all_v_layout.addLayout(self.button_h_layout)

        self.setLayout(self.all_v_layout)

    def radiobutton_init(self):
        self.off_button.setChecked(True)                            # 5
        self.off_button.toggled.connect(self.on_off_bulb_func)      # 6
        # self.on_button.toggled.connect(self.on_off_bulb_func)

    def label_init(self):
        self.pic_label.setPixmap(QPixmap('off.png'))                # 7

    def on_off_bulb_func(self):                                     # 8
        if self.off_button.isChecked():
            self.pic_label.setPixmap(QPixmap('off.png'))
        else:
            self.pic_label.setPixmap(QPixmap('on.png'))

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

1-2. 实例化两个单选按钮,一个off,另一个为on;

3. 这里准备用QLabel控件来显示图片;

4. 在管理布局的函数中有一个addStretch(int)方法,该方法就是添加一个伸缩项,而占位符的大小就是其中填入的数字。我们可以看到在示例中先添加了一个大小为1的占位符,然后再添加用于显示图片的QLabel,最后再加了一个大小为1的占位符。这样做的目的就是为了让这个QLabel控件居中。若将添加的第一个占位符(即左边的占位符)大小设为2,那么QLabel就会偏右,因为左边的占位符大小比QLabel右边的占位符大;

5. 将off单选按钮设为选中状态;

6. 若单选按钮的状态发生改变,则会发出toggled信号。在这里将toggled信号和自定义的槽函数进行连接;

7. 初始状态灯泡是不亮的,所以通过setPixmap()给QLabel设置off.png,即灯泡不亮的图片。setPixmap()方法接受一个QPixmap()对象;

8. 在该槽函数中,我们判断off按钮是否处于选中状态,若是,则灯泡不亮,否则给QLabel设置灯泡发亮的图片。


 

在上方radiobutton_init()函数中,我们只给off_button连接了信号和槽,而on_button没有。正如开始时提到的一点:默认每次只有一个单选按钮会被选中,而这里只有两个按钮,所以一个按钮的状态发生变化,另一个必然也会。

免费图标搜索引擎 | 免费PNG图标 | 免版权SVG图标

4 QCheckBox 复选框

复选框一共有三种状态:全选中、半选中和无选中。若一个父选项的子选项全部为选中状态,则该父选项为全选中;若子选项全部为无选中状态,则该父选项为无选中状态;若子选项既有全选中和无选中状态,则该父选项为半选中状态。

import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QCheckBox, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.checkbox1 = QCheckBox('Checkbox 1', self)
        self.checkbox2 = QCheckBox('Checkbox 2', self)
        self.checkbox3 = QCheckBox('Checkbox 3', self)

        self.v_layout = QVBoxLayout()

        self.checkbox_init()
        self.layout_init()

    def layout_init(self):
        self.v_layout.addWidget(self.checkbox1)
        self.v_layout.addWidget(self.checkbox2)
        self.v_layout.addWidget(self.checkbox3)
        self.setLayout(self.v_layout)

    def checkbox_init(self):
        self.checkbox1.setChecked(True)                                                             # 1
        # self.checkbox1.setCheckState(Qt.Checked)                                                  # 2
        self.checkbox1.stateChanged.connect(lambda: self.on_state_change_func(self.checkbox1))      # 3

        self.checkbox2.setChecked(False)
        # self.checkbox2.setCheckState(Qt.Unchecked)
        self.checkbox2.stateChanged.connect(lambda: self.on_state_change_func(self.checkbox2))

        self.checkbox3.setTristate(True)                                                            # 4
        self.checkbox3.setCheckState(Qt.PartiallyChecked)                                           # 5
        self.checkbox3.stateChanged.connect(lambda: self.on_state_change_func(self.checkbox3))      

    def on_state_change_func(self, checkbox):                                                       # 6
        print('{} was clicked, and its current state is {}'.format(checkbox.text(), checkbox.checkState()))

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

1-2. 通过setChecked()方法传入True或者False可以将复选框设为选中或无选中状态;另外一种替代的方法是setCheckState(),传入的参数可以是选中状态Qt.Checked, 无选中状态Qt.Unchecked和半选中状态Qt.PartiallyChecked;

3. stateChanged信号会在复选框状态发生改变的时候发出。这里我们发现槽函数是带参数的,可以通过lambda表达式来将参数传入槽函数。若单纯使用self.on_state_change_func(self.checkbox2)则会报错;

4-5. 如果要让一个复选框拥有三种状态,则必须通过setTristate(True)方法来实现。在这里我们让第三个复选框拥有三种状态;

6. checkState()方法可以获取当前复选框的状态,返回值为int类型,0为无选中状态,1为半选中状态,2位选中状态。

八、下拉选择框QCombox和数字调节框QSpinBox

1 QComboBox 下拉选框
self.combobox_1 = QComboBox(self)		#实例化
self.combobox_2 = QFontComboBox(self)	#字体框

self.combobox_1.addItem(self.choice)         # 添加一个选项
self.combobox_1.addItems(self.choice_list)   # 添加一个序列

self.combobox_1.currentIndexChanged.connect(lambda: self.on_combobox_func(self.combobox_1))
#self.combobox_1.currentTextChanged.connect(lambda: self.on_combobox_func(self.combobox_1))#另一个方式,效果相同

combobox.currentIndex()#获取所选索引
combobox.currentText() #获取所选文本

combobox.currentFont() #获取所选字体
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QComboBox, QFontComboBox, QLineEdit, QMessageBox, QVBoxLayout

class Demo(QWidget):
    choice = 'a'
    choice_list = ['b', 'c', 'd', 'e']

    def __init__(self):
        super(Demo, self).__init__()

        self.combobox_1 = QComboBox(self)                   # 1
        self.combobox_2 = QFontComboBox(self)               # 2

        self.lineedit = QLineEdit(self)                     # 3

        self.v_layout = QVBoxLayout()

        self.layout_init()
        self.combobox_init()

    def layout_init(self):
        self.v_layout.addWidget(self.combobox_1)
        self.v_layout.addWidget(self.combobox_2)
        self.v_layout.addWidget(self.lineedit)

        self.setLayout(self.v_layout)

    def combobox_init(self):
        self.combobox_1.addItem(self.choice)              # 4
        self.combobox_1.addItems(self.choice_list)        # 5
        self.combobox_1.currentIndexChanged.connect(lambda: self.on_combobox_func(self.combobox_1))   # 6
        # self.combobox_1.currentTextChanged.connect(lambda: self.on_combobox_func(self.combobox_1))  # 7

        self.combobox_2.currentFontChanged.connect(lambda: self.on_combobox_func(self.combobox_2))
        # self.combobox_2.currentFontChanged.connect(lambda: self.on_combobox_func(self.combobox_2))

    def on_combobox_func(self, combobox):                                                             # 8
        if combobox == self.combobox_1:
            QMessageBox.information(self, 'ComboBox 1', '{}: {}'.format(combobox.currentIndex(), combobox.currentText()))
        else:
            self.lineedit.setFont(combobox.currentFont())

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

1-2. 实例化QComboBoxQFontComboBox,前者是普通的下拉框,框里是没有内容的,需要添加。而QFontComboBox是字体下拉框,继承于QComboBox,该字体下拉框里会默认有许多字体供选择;

3. 实例化一个单行文本输入框,用于测试从字体下拉框中选择一项时,输入框中字体发生的变化;

4-5. addItem()方法是添加一个选项,而addItems()接收一个可循环参数,这里传入了列表self.choice_list;

6-7. 当下拉框当前选项发生变化变化的话,则会触发序号变化currentIndexChanged信号和文本变化currentTextChanged信号,我们在这里进行了信号与槽的连接,注意槽函数是带参数的,所以我们用lambda表达式进行处理;

8. 在自定义的槽函数中,我们通过判断combobox的种类,若是self.combobox_1的话,则出现信息框,并且显示当前文本和及文本序号,currentIndex()方法获取当前文本序号,currentText()方法获取当前文本。若是self.combobox_2的话,则通过setFont()方法将输入框的字体设为当前选中的字体,currentFont()获取字体下拉框的当前字体。

点击第一个下拉框,改变选项,出现信息框,1为序号,b为文本:

点击第二个下拉框换种字体,然后输入框中的文本就会有相应的字体:

2 QSpinBox 数字调节框
self.spinbox = QSpinBox(self)				#实例化,整数型
self.double_spinbox = QDoubleSpinBox(self) 	#小数型

self.spinbox.setRange(-99, 99)				#设置数值范围
self.spinbox.setSingleStep(1) 				#设置步长
self.spinbox.setValue(66) 					#设置初始值

self.spinbox.valueChanged.connect(self.value_change_func) #连接槽函数

self.double_spinbox.value()					#获取值
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QSpinBox, QDoubleSpinBox, QHBoxLayout

class Demo(QWidget):
  def __init__(self):
    super(Demo, self).__init__()
    self.spinbox = QSpinBox(self)
    self.spinbox.setRange(-99, 99)                                               # 1
    self.spinbox.setSingleStep(1)                                                # 2
    self.spinbox.setValue(66)                                                    # 3
    self.spinbox.valueChanged.connect(self.value_change_func)                    # 4

    self.double_spinbox = QDoubleSpinBox(self)                                   # 5
    self.double_spinbox.setRange(-99.99, 99.99)
    self.double_spinbox.setSingleStep(0.01)
    self.double_spinbox.setValue(66.66)

    self.h_layout = QHBoxLayout()
    self.h_layout.addWidget(self.spinbox)
    self.h_layout.addWidget(self.double_spinbox)
    self.setLayout(self.h_layout)

  def value_change_func(self):
    decimal_part = self.double_spinbox.value() - int(self.double_spinbox.value())# 6
    self.double_spinbox.setValue(self.spinbox.value() + decimal_part)            # 7

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

1. 给实例化的QSpinBox设置范围,如果不设置的话QSpinBox默认范围为0-99;

2. 设置步长,即每次点击递增或递减多少值;

3. 设置初始显示值;

4. 每次数字发生变化都会触发valueChanged信号;

5. QSpinBox为整型数字调节框,而QDoubleSpinBox为浮点型数字调节框。QDoubleSpinBox的默认范围为0.00-99.99,而小数位数默认是两位,不过可以通过setDecimals(int)方法来设置小数位数;

6-7. 该槽函数主要是在QSpinBox数值发生变化时,将QDoubleSpinBox的整数部分设置成QSpinBox的值,小数部分保持不变。所以要首先获取QDoubleSpinBox的小数部分再进行设置。通过setValue()方法可以设置调节框的值,而value()方法是获取值。

点击改变左边QSpinBox的值,右边QDoubleSpinBox值的整数部分也会相应改变:

九、滑动条和表盘

1 QSlider 滑块
self.slider = QSlider(Qt.Horizontal, self)  #实例化水平滑动条
self.slider = QSlider(Qt.Vertical, self)	#实例化竖直滑动条

self.slider.setRange(0, 100)				#设置滑动范围
self.slider_2.setMinimum(0)
self.slider_2.setMaximum(100)
self.slider_1.setValue()					#设置值

self.slider_1.valueChanged.connect(lambda: self.on_change_func(self.slider_1))#连接槽函数,并传参

self.slider_2.value()						#获取值
self.label = QLabel('0', self)                      #实例化
self.label.setFont(QFont('Arial Black', 20))		#设置字体
self.label.setText('hello world')					#改变文字
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QApplication, QWidget, QSlider, QLabel, QVBoxLayout, QHBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.slider_1 = QSlider(Qt.Horizontal, self)                                       # 1
        self.slider_1.setRange(0, 100)                                                     # 2
        self.slider_1.valueChanged.connect(lambda: self.on_change_func(self.slider_1))     # 3

        self.slider_2 = QSlider(Qt.Vertical, self)
        self.slider_2.setMinimum(0)                                                        # 4
        self.slider_2.setMaximum(100)                                                      # 5
        self.slider_2.valueChanged.connect(lambda: self.on_change_func(self.slider_2))

        self.label = QLabel('0', self)                                                     # 6
        self.label.setFont(QFont('Arial Black', 20))

        self.h_layout = QHBoxLayout()
        self.v_layout = QVBoxLayout()

        self.h_layout.addWidget(self.slider_2)
        self.h_layout.addStretch(1)
        self.h_layout.addWidget(self.label)
        self.h_layout.addStretch(1)

        self.v_layout.addWidget(self.slider_1)
        self.v_layout.addLayout(self.h_layout)
        
        self.setLayout(self.v_layout)

    def on_change_func(self, slider):                                                       # 7
        if slider == self.slider_1:
            self.slider_2.setValue(self.slider_1.value())
            self.label.setText(str(self.slider_1.value()))
        else:
            self.slider_1.setValue(self.slider_2.value())
            self.label.setText(str(self.slider_2.value()))

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

1. 通过传入Qt.Hrizontal可以实例化一个水平的滑动条,传入Qt.Vertical的话可以实例化一个垂直的滑动条;

2. 通过setRange()方法可以设置滑动条的范围;

3. 当滑动时,数值发生改变,触发valueChanged信号;

4-5. 除了setRange()方法,还可以使用setMinimum()和setMaximum()方法来设置最小值和最大值;

6. 这里实例化的QLabel是为了显示出QSlider当前的数值;

7. 在自定义的槽函数中,将两个滑动条的数值同步,然后用QLabel显示出当前数值。

不管移动哪个滑动条,另一个滑动条的数值会同步,QLabel也会显示出相应的数值:

2 QDial 旋钮
self.dial = QDial(self)					#实例化
self.dial.setFixedSize(100, 100)		#设置固定的大小,否则会根据页面空间变化
self.dial.setRange(0, 100)				#设置范围
self.dial.setNotchesVisible(True)		#设置刻度线
self.dial.valueChanged.connect(self.on_change_func)#连接槽函数
import sys
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QApplication, QWidget, QDial, QLabel, QHBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.setWindowTitle('QDial')                            # 1

        self.dial = QDial(self)
        self.dial.setFixedSize(100, 100)                        # 2
        self.dial.setRange(0, 100)                              # 3
        self.dial.setNotchesVisible(True)                       # 4
        self.dial.valueChanged.connect(self.on_change_func)     # 5

        self.label = QLabel('0', self)
        self.label.setFont(QFont('Arial Black', 20))

        self.h_layout = QHBoxLayout()
        self.h_layout.addWidget(self.dial)
        self.h_layout.addWidget(self.label)

        self.setLayout(self.h_layout)

    def on_change_func(self):
        self.label.setText(str(self.dial.value()))

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

1. setWindowTitle()设置窗口标题;

2. 实例化一个QDial控件后,通过setFixedSize()方法来固定QDial的大小。 如果不设置该方法的话,我们会发现在改变表盘数值时,表盘的大小会发生改变;

3. 使用setRange()方法来设置表盘数值范围,当然也可以使用setMinimum()和setMaximum()方法;

4. setNotchesVisible(True)可以显示刻度,刻度会根据我们设置的数值自动调整;

5. 当改变表盘数值时,会触发valueChanged信号,在槽函数中我们让QLabel显示出当前表盘数值。

十、定时器QTimer和进度条QProgressBar

1 QTimer

以下这个程序中,按钮被点击后,QLabel显示的数字会不断增加:

self.timer = QTimer(self) 						#s实例化
self.timer.timeout.connect(self.update_func)	#连接槽函数
self.timer.start(1000)							#开始计时,每1000毫秒调用一次槽函数
self.timer.stop()								#停止计时

self.timer.setSingleShot(True)					#设置是否是只计时一次,默认False
self.timer.setInterval(100)						#设置时间
self.timer.setTimerTypr()						#设置定时器精度,默认Qt.CoarseTimer,其他如下表

self.timer.isActive()							#判断是否在计师状态
self.timer.isSingleShot()
self.timer.interval()							#获取设置的延时时长
self.timer.timerType()

常量

描述

Qt::PreciseTimer

0

精确的定时器,尽量保持毫秒精度,补偿程序运行时间

Qt::CoarseTimer

1

粗略的定时器,5%,不考虑程序运行时间

Qt::VeryCoarseTimer

2

很粗略的定时器,只保留完整的第二精度。

import sys
from PyQt5.QtCore import QTimer, Qt
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.label = QLabel('0', self)                          # 1
        self.label.setAlignment(Qt.AlignCenter)                 

        self.step = 0                                           # 2

        self.timer = QTimer(self)                               # 3
        self.timer.timeout.connect(self.update_func)

        self.ss_button = QPushButton('Start', self)             # 4
        self.ss_button.clicked.connect(self.start_stop_func)

        self.v_layout = QVBoxLayout()
        self.v_layout.addWidget(self.label)
        self.v_layout.addWidget(self.ss_button)

        self.setLayout(self.v_layout)

    def start_stop_func(self):                      
        if not self.timer.isActive():
            self.ss_button.setText('Stop')
            self.timer.start(100)
        else:
            self.ss_button.setText('Start')
            self.timer.stop()

    def update_func(self):
        self.step += 1
        self.label.setText(str(self.step))

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

1. 首先实例化一个QLabel,并将文本设为0。setAlignment(Qt.AlignCenter)可以让QLabel控件在窗口中居中显示,而之前我们是通过addStretch(int)方法来让一个控件在布局中居中的,显然通过setAlignment(Qt.AlignCenter)方法更加方便:

self.h_layout.addStretch(1)
self.h_layout.addWidget(self.label)
self.h_layout.addStretch(1)
# =
self.label.setAlignment(Qt.AlignCenter)

2. step变量用于计数,QLabel控件显示的就是这里的step,程序会通过QTimer来不断增加step的值;

3. 其次实例化一个QTimer,并将timeout信号连接到自定义的槽函数update_func()上:

def update_func(self):
    self.step += 1
    self.label.setText(str(self.step))

每次调用该槽函数就会将step值加1,并且用QLabel显示当前值;

4. 最后我们实例化一个QPushButton按钮来控制定时器的启动的停止,连接的自定义的槽函数如下:

def start_stop_func(self):                      
    if not self.timer.isActive():
        self.ss_button.setText('Stop')
        self.timer.start(100)
    else:
        self.ss_button.setText('Start')
        self.timer.stop()

在槽函数中通过isActive()方法来判断定时器是否处于激活状态,若没有激活,则将按钮文字变成Stop并通过start(100)方法来启动定时器,100表示100毫秒定时器就会触发timeout信号,并执行update_func()槽函数;若已经处于激活状态,则将按钮文字变回Start并通过stop()方法停止定时器。

若想在触发timeout信号后只调用一次update_func(),那么我们可以通过setSingleShot(True)方法来设置。

2 QProgressBar
self.progressbar = QProgressBar(self)			#实例化
self.progressbar.setOrientation(Qt.Vertical)    #设至为竖向进度条,默认为横向      
self.progressbar.setMinimum(0)					#设置范围
self.progressbar.setMaximum(100)
self.progressbar.setRange(0, 100)
self.progressbar.setValue(self.step)			#设置值

将上面的QLabel用QProgressBar来代替:

import sys
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import QApplication, QWidget, QProgressBar, QPushButton, QHBoxLayout, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.progressbar = QProgressBar(self)                   # 1
        # self.progressbar.setOrientation(Qt.Vertical)          
        self.progressbar.setMinimum(0)                          # 2
        self.progressbar.setMaximum(100)
        # self.progressbar.setRange(0, 100)
        
        self.step = 0                                           # 3
        
        self.timer = QTimer(self)                               # 4
        self.timer.timeout.connect(self.update_func)

        self.ss_button = QPushButton('Start', self)             # 5
        self.ss_button.clicked.connect(self.start_stop_func)
        self.reset_button = QPushButton('Reset', self)          # 6
        self.reset_button.clicked.connect(self.reset_func)
        
        self.h_layout = QHBoxLayout()
        self.v_layout = QVBoxLayout()

        self.h_layout.addWidget(self.ss_button)
        self.h_layout.addWidget(self.reset_button)
        self.v_layout.addWidget(self.progressbar)
        self.v_layout.addLayout(self.h_layout)

        self.setLayout(self.v_layout)

    def start_stop_func(self):
        if self.ss_button.text() == 'Start':
            self.ss_button.setText('Stop')
            self.timer.start(100)
        else:
            self.ss_button.setText('Start')
            self.timer.stop()

    def update_func(self):
        self.step += 1
        self.progressbar.setValue(self.step)

        if self.step >= 100:
            self.ss_button.setText('Start')
            self.timer.stop()
            self.step = 0

    def reset_func(self):
        self.progressbar.reset()
        self.ss_button.setText('Start')
        self.timer.stop()
        self.step = 0

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

1. 实例化一个QProgressBar,默认是水平的,但是我们可以通过setOrientation(Qt.Vertical)方法来让进度条垂直显示;

2. 通过setMinimum()和setMaximum()方法来设置范围,也可以单单用setRange()方法来实现,这里我们将范围设为0-100;

3. 这里的step变量用于计数,之后QProgressBar会将值设为step;

4. 实例化一个QTimer,并将timeout信号连接到update_func()槽函数上:

def update_func(self):
    self.step += 1
    self.progressbar.setValue(self.step)

    if self.step >= 100:
        self.ss_button.setText('Start')
        self.timer.stop()
        self.step = 0

每次触发timeout都会调用该槽函数,在这里我们将step值加1,并将progressbar的值设为step,当step值达到pregress的最大值时(也就是说进度条达到100%),将按钮文本重新设为Start,停止定时器并将step值重设为0;

5. 实例化一个QPushButton按钮来控制QTimer的启动与停止,这里将它的clicked信号和start_stop_func()槽函数连接起来:

def start_stop_func(self):
    if self.ss_button.text() == 'Start':
        self.ss_button.setText('Stop')
        self.timer.start(100)
    else:
        self.ss_button.setText('Start')
        self.timer.stop()

在槽函数中,我们通过按钮文字来进行判断,若为Start,则说明定时器没有启动,所以将按钮文字设为Stop,并且通过start(100)方法来启动,100表示100毫秒,即0.1秒。也就是说之后每隔0.1秒就会触发timeout信号并调用update_func()槽函数;若按钮文字为Stop,则将其设为Start并停止定时器 (通过定时器isActive()的方法也可以使用);

6. 该实例化的按钮用于重置进度条:

def reset_func(self):
    self.progressbar.reset()
    self.ss_button.setText('Start')
    self.timer.stop()
    self.step = 0

其所连接的槽函数中通过reset()方法来进行重置,还有将按钮文字设为Start,停止定时器以及将step值设为0。

十一、液晶数字显示屏

self.lcd_1 = QLCDNumber(self)			#实例化
self.lcd_1.setDigitCount(10)			#设置字符位数
self.lcd_1.display(1234567890)			#显示
self.lcd_2.setSegmentStyle(QLCDNumber.Flat)#显示样式 ,详见下表
self.lcd_2.setSmallDecimalPoint(True)	#设置小数点显示方式,True为在两个数字之间,False为单独占一个位置
self.lcd_4.setMode(QLCDNumber.Hex)		#设置显示进制
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QLCDNumber, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.resize(600, 600)

        self.lcd_1 = QLCDNumber(self)                                   # 1
        self.lcd_1.setDigitCount(10)
        self.lcd_1.display(1234567890)

        self.lcd_2 = QLCDNumber(self)                                   # 2
        self.lcd_2.setSegmentStyle(QLCDNumber.Flat)
        # self.lcd_2.setSmallDecimalPoint(True)
        self.lcd_2.setDigitCount(10)
        self.lcd_2.display(0.123456789)

        self.lcd_3 = QLCDNumber(self)                                   # 3
        self.lcd_3.setSegmentStyle(QLCDNumber.Filled)
        self.lcd_3.display('HELLO')

        self.lcd_4 = QLCDNumber(self)                                   # 4
        self.lcd_4.setSegmentStyle(QLCDNumber.Outline)
        self.lcd_4.setMode(QLCDNumber.Hex)
        self.lcd_4.setDigitCount(6)
        self.lcd_4.display(666)

        self.v_layout = QVBoxLayout()
        self.v_layout.addWidget(self.lcd_1)
        self.v_layout.addWidget(self.lcd_2)
        self.v_layout.addWidget(self.lcd_3)
        self.v_layout.addWidget(self.lcd_4)

        self.setLayout(self.v_layout)

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

1. 实例化一个QLCDNumber控件,然后通过setDiditCount()方法来设置一共可以显示多少位数字;

2. lcd_2显示浮点型数字。通过setSegmentStyle()可以设置显示屏数字样式,可传入的参数有:

setSmallDecimalPoint(bool)方法可以设置小数点的显示方式:若为True,那么小数点就会在两个数字之间显示出来,而不会单独占一个位置。如果为False,那就会单独占位(默认为False)。

3. lcd_3显示的为字符串,可以显示的字母种类有限:A, B, C, D, E, F, h, H, L, o, P, r, u, U, Y, O/0, S/5, g/9;

4. 可以通过setMode()方法来更改数字显示方式,这里用传入QLCDNumber.Hex让数字以16进制方式显示,总共可以传入以下参数:

运行截图如下:

十二、与日期相关的控件

1 QCalendarWidget
self.calendar = QCalendarWidget(self)				#实例化
self.calendar.setMinimumDate(QDate(1, 2, 14))       #设置日期范围
self.calendar.setMaximumDate(QDate(6666, 6, 6))
self.calendar.setDateRange(QDate(1946, 2, 14), QDate(6666, 6, 6))
self.calendar.setFirstDayOfWeek(Qt.Monday) 			#设置一周的第一天,默认周日
self.calendar.setSelectedDate(QDate(1946, 2, 14))	#设置预选日期,默认今天
self.calendar.setGridVisible(0)						#设置日期网格是否可见
self.calendar.clicked.connect(self.show_emotion_func)#连接槽函数,在点击某日期时调用
self.calendar.minimumDate()							#获取最小可选日期
self.calendar.maximumDate().toString('ddd')			#获取最大可选日期 
#		通过.toString('ddd')获取周几的简称,yyyy为年,MM为月,dd为日,可自由组合如'yyyy-MM-dd (ddd)'
self.calendar.selectedDate()						#获取所选日期
import sys
from PyQt5.QtCore import QDate, Qt
from PyQt5.QtWidgets import QApplication, QWidget, QCalendarWidget, QLabel, QVBoxLayout

EMOTION = {                                                                     # 1 
    'Mon': '(╯°Д°)╯︵ ┻━┻',
    'Tue': '(╯ ̄Д ̄)╯╘═╛',
    'Wed': '╭( ̄▽ ̄)╯╧═╧',
    'Thu': '_(:з」∠)_',
    'Fri': '(๑•̀ㅂ•́) ✧',
    'Sat': '( ˘ 3˘)♥',
    'Sun': '(;′༎ຶД༎ຶ`)',
    '周一': '(╯°Д°)╯︵ ┻━┻',
    '周二': '(╯ ̄Д ̄)╯╘═╛',
    '周三': '╭( ̄▽ ̄)╯╧═╧',
    '周四':'_(:з」∠)_',
    '周五': '(๑•̀ㅂ•́) ✧',
    '周六': '( ˘ 3˘)♥',
    '周日': '(;′༎ຶД༎ຶ`)'
}

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.calendar = QCalendarWidget(self)
        self.calendar.setMinimumDate(QDate(1946, 2, 14))                        # 2
        self.calendar.setMaximumDate(QDate(6666, 6, 6))                         # 3
        # self.calendar.setDateRange(QDate(1946, 2, 14), QDate(6666, 6, 6))
        # self.calendar.setFirstDayOfWeek(Qt.Monday)                            # 4
        # self.calendar.setSelectedDate(QDate(1946, 2, 14))                     # 5
        self.calendar.setGridVisible(True)                                      # 6
        self.calendar.clicked.connect(self.show_emotion_func)                   # 6

        print(self.calendar.minimumDate())                                      # 7
        print(self.calendar.maximumDate())
        print(self.calendar.selectedDate())

        self.label = QLabel(self)                                               # 8
        self.label.setAlignment(Qt.AlignCenter)

        weekday = self.calendar.selectedDate().toString('ddd')                  # 9
        self.label.setText(EMOTION[weekday])

        self.v_layout = QVBoxLayout()
        self.v_layout.addWidget(self.calendar)
        self.v_layout.addWidget(self.label)
        
        self.setLayout(self.v_layout)
        self.setWindowTitle('QCalendarWidget')

    def show_emotion_func(self):                                                # 10
        weekday = self.calendar.selectedDate().toString('ddd')
        self.label.setText(EMOTION[weekday])

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

1. 设置一个字典,并将各个星期及对应的颜文字分别作为键值输入;

2. 通过setMinimumDate()和setMaximumDate()可以设置日历的最小和最大日期(可用setDateRange()替代),传入的参数为QDate。

3. setFirstDayOfWeek()方法可以设置一个星期的第一天,默认第一天为星期天,可传入的参数有:

  • Qt.Monday
  • Qt.Tuesday
  • Qt.Wednesday
  • Qt.Thursday
  • Qt.Friday
  • Qt.Saturday
  • Qt.Sunday

4. setSelectedDate()方法可以设置日历初始化时所显示的日期,如果不设置,则默认是当天日期;

5. setGridVisible(bool)方法可以设置是否在日历上显示网格;

6. 当点击到日历上的某个日期时,clicked信号就会被触发。

7. minimumDate()、maximumDate()和selectedDate()分别获取日历的最早日期,最后日期和当前所选日期,类型为QDate;

8. 实例化一个QLabel控件用于显示颜文字;

9. 首先通过selectedDate()方法获取到当前所选日期,接着通过toString(‘ddd‘)方法获取星期的缩写,然后作为字典的键获取对应的值(注:笔者系统语言为英语,读者的系统语言为中文的话,则会获取到中文的星期名,那么此时应该将开头字典的键换成中文);

yyyy代表年份,MM月份,dd日,ddd周

10. 在槽函数中同理,获取到对应的值后,让QLabel控件进行显示。

2 QDateTimeEdit

QDateTimeEdit是QDateEdit和QTimeEdit的父类,看名字就知道QDateTimeEdit可以编辑日期和时间,QDateEdit只能编辑日期(年月日),而QTimeEdit只能编辑时间(时分秒),三种控件用法十分类似,以下重点讲QDateTimeEdit:

self.datetime_1 = QDateTimeEdit(self) 								#实例化
self.datetime_2 = QDateTimeEdit(QDateTime.currentDateTime(), self)	#实例化并传入当前时间
self.date = QDateEdit(QDate.currentDate(), self)    
self.time = QTimeEdit(QTime.currentTime(), self)    

self.datetime_2.setDisplayFormat('yyyy-MM-dd HH:mm:ss')				#设置显示格式
self.datetime_3.setCalendarPopup(True)								#设置日历弹窗

self.datetime_2.date()												#获取日期
self.datetime_2.time()
self.datetime_2.dateTime()

self.datetime_3.dateTimeChanged.connect(lambda: print('DateTime Changed!')#连接槽函数
import sys
from PyQt5.QtCore import QDate, QTime, QDateTime
from PyQt5.QtWidgets import QApplication, QWidget, QDateTimeEdit, QDateEdit, QTimeEdit, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()    
        self.datetime_1 = QDateTimeEdit(self)                                           # 1
        self.datetime_1.dateChanged.connect(lambda: print('Date Changed!'))

        self.datetime_2 = QDateTimeEdit(QDateTime.currentDateTime(), self)              # 2
        self.datetime_2.setDisplayFormat('yyyy-MM-dd HH:mm:ss')
        self.datetime_2.timeChanged.connect(lambda: print('Time Changed!'))
        print(self.datetime_2.date())
        print(self.datetime_2.time())
        print(self.datetime_2.dateTime())

        self.datetime_3 = QDateTimeEdit(QDateTime.currentDateTime(), self)              # 3
        self.datetime_3.dateTimeChanged.connect(lambda: print('DateTime Changed!'))
        self.datetime_3.setCalendarPopup(True)

        self.datetime_4 = QDateTimeEdit(QDate.currentDate(), self)                      # 4
        self.datetime_5 = QDateTimeEdit(QTime.currentTime(), self)

        self.date = QDateEdit(QDate.currentDate(), self)                                # 5
        self.date.setDisplayFormat('yyyy/MM/dd')
        print(self.date.date())

        self.time = QTimeEdit(QTime.currentTime(), self)                                # 6
        self.time.setDisplayFormat('HH:mm:ss')
        print(self.time.time())

        self.v_layout = QVBoxLayout()
        self.v_layout.addWidget(self.datetime_1)
        self.v_layout.addWidget(self.datetime_2)
        self.v_layout.addWidget(self.datetime_3)
        self.v_layout.addWidget(self.datetime_4)
        self.v_layout.addWidget(self.datetime_5)
        self.v_layout.addWidget(self.date)
        self.v_layout.addWidget(self.time)

        self.setLayout(self.v_layout)

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

1. 实例化一个QDateTimeEdit控件,并将其dateChanged信号与print('Date Changed!')打印函数连接起来,也就是说每当用户改变该控件上的日期(不是时间)时,就会触发dateChanged信号,而控制台就会打印'Date Changed!';

2. 实例化一个QDateTimeEdit控件并将日期时间设置为当前的日期和时间,如果没有设置(像1中的QdateTimeEdit一样),那么就会显示默认日期时间2000/1/1 12:00 AM。通过setDisplayFormat()方法可以设置日期时间的显示格式。这里还将timeChanged信号和打印函数进行了连接,也就是说每当用户改变时间(不是日期)时,就会触发timeChanged信号,而控制台就会打印'Time Changed!',通过调用date()、time()和dateTime()可以分别获取到日期、时间以及合并的日期时间;

3. 该QDateTimeEdit控件的dateTimeChanged信号和打印函数连接了起来,也就是说用户不管是改了日期还是时间,都会触发该信号,从而打印'DateTime Changed!',setCalendarPopup(True)方法可以设置日历弹窗;

4. self.datetime_4只传入了日期参数,没有时间;而self.datetime_5只传入了时间参数,没有日期;

5-6. 分别实例化了一个QDateEdit和QTimeEdit控件,用法和QDateTimeEdit控件极为类似。

十三、组合框QGroupBox和工具箱QToolBox

1 QGroupBox 组合框
self.groupbox_1 = QGroupBox('On and Off', self)		#实例化
self.groupbox_1.setLayout(self.h1_layout)			#添加内容
self.groupbox_1.setFlat(True)						#True可以让边界消失
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QApplication, QWidget, QGroupBox, QRadioButton, QLabel, QHBoxLayout, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.groupbox_1 = QGroupBox('On and Off', self)                       # 1
        self.groupbox_2 = QGroupBox('Change Color', self)

        self.red = QRadioButton('red', self)                                  # 2
        self.blue = QRadioButton('blue', self)
        self.green = QRadioButton('green', self)
        self.yellow = QRadioButton('yellow', self)
        self.color_list = [self.red, self.blue, self.green, self.yellow]

        self.on = QRadioButton('On', self)                                    # 3
        self.off = QRadioButton('Off', self)

        self.pic_label = QLabel(self)                                         # 4

        self.h1_layout = QHBoxLayout()
        self.h2_layout = QHBoxLayout()
        self.h3_layout = QHBoxLayout()
        self.all_v_layout = QVBoxLayout()

        self.layout_init()
        self.radiobutton_init()
        self.label_init()

    def layout_init(self):
        self.h1_layout.addWidget(self.on)
        self.h1_layout.addWidget(self.off)
        self.groupbox_1.setLayout(self.h1_layout)

        self.h2_layout.addWidget(self.red)
        self.h2_layout.addWidget(self.blue)
        self.h2_layout.addWidget(self.green)
        self.h2_layout.addWidget(self.yellow)
        self.groupbox_2.setLayout(self.h2_layout)

        self.h3_layout.addWidget(self.groupbox_1)
        self.h3_layout.addWidget(self.groupbox_2)

        self.all_v_layout.addWidget(self.pic_label)
        self.all_v_layout.addLayout(self.h3_layout)

        self.setLayout(self.all_v_layout)

    def radiobutton_init(self):                                              
        self.yellow.setChecked(True)                                         # 5
        for btn in self.color_list:
            btn.clicked.connect(self.change_color_func)

        self.off.setChecked(True)                                            # 6
        self.off.toggled.connect(self.on_and_off_func)

    def label_init(self):                                                    # 7
        self.pic_label.setPixmap(QPixmap('images/off.png'))
        self.pic_label.setAlignment(Qt.AlignCenter)

    def change_color_func(self):
        if self.on.isChecked():
            path = 'images/{}.png'.format([btn.text() for btn in self.color_list if btn.isChecked()][0])
            self.pic_label.setPixmap(QPixmap(path))

    def on_and_off_func(self):
        if self.on.isChecked():
            path = 'images/{}.png'.format([btn.text() for btn in self.color_list if btn.isChecked()][0])
            self.pic_label.setPixmap(QPixmap(path))
        else:
            self.pic_label.setPixmap(QPixmap('images/off.png'))

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

1. 实例化两个QGroupBox组合框,第一个用来放置On和Off单选按钮,第二个用来放置各种颜色按钮;

2. 实例化各个颜色按钮,并将它们放在一个列表中,方便之后使用列表推导式来简化代码;

3. 实例化On和Off单选按钮;

4. 实例化一个QLabel控件,用于显示图片;

5. 在radiobutton_init()函数中,我们先将yellow单选按钮设置为已点击状态,然后用各个颜色按钮的clicked信号与自定义的槽函数change_color_func()连接起来。

在下面这个槽函数中,我们先判断On单选按钮是否是已点击状态(因为如果电源没开的话,灯泡颜色是不会变化的) ,如果是的话则获取图片路径path,然后将QLabel设为相应的图片,这里我们详细分析下。

def change_color_func(self):
    if self.on.isChecked():
        path = 'images/{}.png'.format([btn.text() for btn in self.color_list if btn.isChecked()][0])
        self.pic_label.setPixmap(QPixmap(path))

6. 将Off按钮设为点击状态,然后将它的toggled信号与自定义的槽函数连接起来(槽函数原理与5中类似,不再讲解);

7. 将刚开始显示的QLabel图片设置为Off.png,然后让其居中显示。

左边的On和Off单选按钮与右边的各颜色按钮互不影响(单选按钮就是每次只能有一个按钮被选中),这就是QGroupBox带来的效果,左右两边各司其职。

2 QToolBox 工具箱

QToolBox工具箱,这个工具箱有很多抽屉,每次只能打开一个,抽屉里可以放很各种各样的东西。我们知道QQ上有很多分组,然后每个分组下可以放很多联系人,点开我的好友分组,该分组下放着相应的联系人:

self.toolbox=QToolBox(self)			#实例化
self.toolbox.addItem(self.groupbox_1, 'Couple One')		#添加一组元素
self.toolbox.currentChanged.connect(self.print_index_func)#连接槽函数
self.toolbox.currentIndex()				#获取当前展开的是哪一个
self.toolbox.setCurrentIndex(1)			#设置展开哪一个

我们现在用QToolBox工具来实现类似样式,代码如下:

import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QToolBox, QGroupBox, QToolButton, QVBoxLayout

class Demo(QToolBox):                                           # 1
    def __init__(self):
        super(Demo, self).__init__()
        self.groupbox_1 = QGroupBox(self)                       # 2
        self.groupbox_2 = QGroupBox(self)
        self.groupbox_3 = QGroupBox(self)

        self.toolbtn_f1 = QToolButton(self)                     # 3
        self.toolbtn_f2 = QToolButton(self)
        self.toolbtn_f3 = QToolButton(self)
        self.toolbtn_m1 = QToolButton(self)
        self.toolbtn_m2 = QToolButton(self)
        self.toolbtn_m3 = QToolButton(self)

        self.v1_layout = QVBoxLayout()
        self.v2_layout = QVBoxLayout()
        self.v3_layout = QVBoxLayout()

        self.addItem(self.groupbox_1, 'Couple One')             # 4
        self.addItem(self.groupbox_2, 'Couple Two')
        self.addItem(self.groupbox_3, 'Couple Three')
        self.currentChanged.connect(self.print_index_func)      # 5

        self.layout_init()
        self.groupbox_init()
        self.toolbtn_init()

    def layout_init(self):
        self.v1_layout.addWidget(self.toolbtn_f1)
        self.v1_layout.addWidget(self.toolbtn_m1)
        self.v2_layout.addWidget(self.toolbtn_f2)
        self.v2_layout.addWidget(self.toolbtn_m2)
        self.v3_layout.addWidget(self.toolbtn_f3)
        self.v3_layout.addWidget(self.toolbtn_m3)

    def groupbox_init(self):                                    # 6
        self.groupbox_1.setFlat(True)
        self.groupbox_2.setFlat(True)
        self.groupbox_3.setFlat(True)
        self.groupbox_1.setLayout(self.v1_layout)
        self.groupbox_2.setLayout(self.v2_layout)
        self.groupbox_3.setLayout(self.v3_layout)

    def toolbtn_init(self):                                     # 7
        self.toolbtn_f1.setIcon(QIcon('images/f1.ico'))
        self.toolbtn_f2.setIcon(QIcon('images/f2.ico'))
        self.toolbtn_f3.setIcon(QIcon('images/f3.ico'))
        self.toolbtn_m1.setIcon(QIcon('images/m1.ico'))
        self.toolbtn_m2.setIcon(QIcon('images/m2.ico'))
        self.toolbtn_m3.setIcon(QIcon('images/m3.ico'))

    def print_index_func(self):
        couple_dict = {
            0: 'Couple One',
            1: 'Couple Two',
            2: 'Couple Three'
        }
        sentence = 'You are looking at {}.'.format(couple_dict.get(self.currentIndex()))
        print(sentence)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = Demo()
    demo.show()
    sys.exit(app.exec_())
import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QToolBox, QGroupBox, QToolButton, QVBoxLayout,QLabel

class Demo(QLabel):                                           # 1
    def __init__(self):
        super(Demo, self).__init__()
        self.toolbox=QToolBox(self)
        self.groupbox_1 = QGroupBox(self)                       # 2
        self.groupbox_2 = QGroupBox(self)
        self.groupbox_3 = QGroupBox(self)

        self.toolbtn_f1 = QToolButton(self)                     # 3
        self.toolbtn_f2 = QToolButton(self)
        self.toolbtn_f3 = QToolButton(self)
        self.toolbtn_m1 = QToolButton(self)
        self.toolbtn_m2 = QToolButton(self)
        self.toolbtn_m3 = QToolButton(self)

        self.v1_layout = QVBoxLayout()
        self.v2_layout = QVBoxLayout()
        self.v3_layout = QVBoxLayout()

        self.toolbox.addItem(self.groupbox_1, 'Couple One')             # 4
        self.toolbox.addItem(self.groupbox_2, 'Couple Two')
        self.toolbox.addItem(self.groupbox_3, 'Couple Three')
        self.toolbox.currentChanged.connect(self.print_index_func)      # 5

        self.layout_init()
        self.groupbox_init()
        self.toolbtn_init()

    def layout_init(self):
        self.v1_layout.addWidget(self.toolbtn_f1)
        self.v1_layout.addWidget(self.toolbtn_m1)
        self.v2_layout.addWidget(self.toolbtn_f2)
        self.v2_layout.addWidget(self.toolbtn_m2)
        self.v3_layout.addWidget(self.toolbtn_f3)
        self.v3_layout.addWidget(self.toolbtn_m3)

    def groupbox_init(self):                                    # 6
        self.groupbox_1.setFlat(True)
        self.groupbox_2.setFlat(True)
        self.groupbox_3.setFlat(True)
        self.groupbox_1.setLayout(self.v1_layout)
        self.groupbox_2.setLayout(self.v2_layout)
        self.groupbox_3.setLayout(self.v3_layout)
        self.vlayout=QVBoxLayout()
        self.vlayout.addWidget(self.toolbox)
        self.setLayout(self.vlayout)

    def toolbtn_init(self):                                     # 7
        self.toolbtn_f1.setIcon(QIcon('images/female1.png'))
        self.toolbtn_f2.setIcon(QIcon('images/female2.png'))
        self.toolbtn_f3.setIcon(QIcon('images/female3.png'))
        self.toolbtn_m1.setIcon(QIcon('images/male1.png'))
        self.toolbtn_m2.setIcon(QIcon('images/male2.png'))
        self.toolbtn_m3.setIcon(QIcon('images/male3.png'))

    def print_index_func(self):
        couple_dict = {
            0: 'Couple One',
            1: 'Couple Two',
            2: 'Couple Three'
        }
        sentence = 'You are looking at {}.'.format(couple_dict.get(self.toolbox.currentIndex()))
        print(sentence)

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

1. 因为要显示的最终控件也就是QToolBox,所以这里就直接继承QToolBox;

2. 实例化三个QGroupBox控件,目的是将6个头像分别放入这三个QGroupBox中;

3. QToolButton用于显示头像;

4. 通过addItem(QWidget, Str)方法将QGroupBox添加到QToolBox中,第一个参数为要添加的控件,第二个参数是给每个QToolBox抽屉设定的名称;

5. 每当用户点击不同抽屉时,都会触发currentChanged信号,我们将该信号连接到自定义的槽函数上:

def print_index_func(self):
    couple_dict = {
        0: 'Couple One',
        1: 'Couple Two',
        2: 'Couple Three'
    }
    sentence = 'You are looking at {}.'.format(couple_dict.get(self.currentIndex()))
    print(sentence)

通过currentIndex()方法可以获取到当前所点击的抽屉序号,序号从0开始。这里通过字典来获取相应的抽屉名称,然后将其打印出来;

6. setFlat(True)方法可以让QGroupBox的边框消失;

7. 通过setIcon(QIcon)方法来设置QToolButton的图标;

十四、窗口坐标

坐标体系

不管从显示屏屏幕还是程序窗口来看,左上角都为原点(0, 0),向右为x轴正向,向下为y轴正向。

Qt官方文档上关于窗口坐标的一张图:

我们可以把窗口分成三块:标题栏、边框和客户区,这样进行分割后我们才能很好地理解不同的方法所获取到的坐标有什么不同。

以上方法理解如下:

  • x()——得到窗口左上角在显示屏屏幕上的x坐标;
  • y()——得到窗口左上角在显示屏屏幕上的y坐标;
  • pos()——得到窗口左上角在显示屏屏幕上的x和y坐标,类型为QPoint();
  • geometry().x()——得到客户区左上角在显示屏屏幕上的x坐标;
  • geometry().y()——得到客户区左上角在显示屏屏幕上的y坐标;
  • geometry()——得到客户区左上角在显示屏屏幕上的x和y坐标,以及客户区的宽度和长度,类型为QRect();
  • width()——得到客户区的宽度;
  • height()——得到客户区的高度;
  • geometry().width()——得到客户区的宽度;
  • geometry().height()——得到客户区的高度;
  • frameGeometry().width()——得到窗口的宽度;
  • frameGeometry().height()——得到窗口的高度;

补充

  • frameGeometry().x()——即x(),得到窗口左上角在显示屏屏幕上的x坐标;
  • frameGeometry().y()——即y(),得到窗口左上角在显示屏屏幕上的y坐标;
  • frameGeometry()——即pos(),得到窗口左上角在显示屏屏幕上的x和y坐标,以及窗口的宽度和长度,类型为QRect();

注:通过geometry()和frameGeometry()获取坐标的相关方法需要在窗口调用show()方法之后才能使用,否则获取的将是无用数据。

import sys
from PyQt5.QtWidgets import QApplication, QWidget

if __name__ == '__main__':
    app = QApplication(sys.argv)
    widget = QWidget()
    widget.resize(200, 200)                    # 1
    widget.move(100, 100)                      # 2
    # widget.setGeometry(100, 100, 200, 200)   # 3
    widget.show()

    print('-----------------x(), y(), pos()-----------------')
    print(widget.x())
    print(widget.y())
    print(widget.pos())

    print('-----------------width(), height()-----------------')
    print(widget.width())
    print(widget.height())
    print(widget.size())

    print('-----------------geometry().x(), geometry.y(), geometry()-----------------')
    print(widget.geometry().x())
    print(widget.geometry().y())
    print(widget.geometry())

    print('-----------------geometry.width(), geometry().height()-----------------')
    print(widget.geometry().width())
    print(widget.geometry().height())

    print('-----------------frameGeometry().x(), frameGeometry().y(), frameGeometry(), '
          'frameGeometry().width(), frameGeometry().height()-----------------')
    print(widget.frameGeometry().x())
    print(widget.frameGeometry().y())
    print(widget.frameGeometry())
    print(widget.frameGeometry().width())
    print(widget.frameGeometry().height())

    sys.exit(app.exec_())

窗口调节:

widget.resize(200, 200)
widget.move(100, 100) 
widget.setGeometry(100, 100, 200, 200)

    def hide(self) -> None: ...
    def show(self) -> None: ...
    def setHidden(self, hidden: bool) -> None: ...
    def setVisible(self, visible: bool) -> None: ...
    def setFixedSize(self, w: int, h: int) -> None: ...
    def setBaseSize(self, basew: int, baseh: int) -> None: ...
    def setMaximumHeight(self, maxh: int) -> None: ...
    def setMaximumSize(self, maxw: int, maxh: int) -> None: ...
    def setEnabled(self, a0: bool) -> None: ...#设置能不能操作
#查看信息
.isHidden(self) -> bool: ...
.isVisible(self) -> bool: ...
.underMouse(self) -> bool: ...
.maximumHeight(self) -> int: ...
.maximumWidth(self) -> int: ...
.minimumHeight(self) -> int: ...
.minimumWidth(self) -> int: ...
.isFullScreen(self) -> bool: ...
.isMaximized(self) -> bool: ...
.isMinimized(self) -> bool: ...

十五、事件处理

1 窗口关闭事件
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTextEdit, QPushButton, QMessageBox, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()

        self.is_saved = True                                            # 1

        self.textedit = QTextEdit(self)                                 # 2
        self.textedit.textChanged.connect(self.on_textchanged_func)

        self.button = QPushButton('Save', self)                         # 3
        self.button.clicked.connect(self.on_clicked_func)

        self.v_layout = QVBoxLayout()   
        self.v_layout.addWidget(self.textedit)
        self.v_layout.addWidget(self.button)
        self.setLayout(self.v_layout)

    def on_textchanged_func(self):                      
        if self.textedit.toPlainText(): 
            self.is_saved = False
        else:
            self.is_saved = True

    def on_clicked_func(self):
        self.save_func(self.textedit.toPlainText())
        self.is_saved = True

    def save_func(self, text):          
        with open('saved.txt', 'w') as f:
            f.write(text)

    def closeEvent(self, QCloseEvent):                                  # 4
        if not self.is_saved:
            choice = QMessageBox.question(self, '', 'Do you want to save the text?',
                                          QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
            if choice == QMessageBox.Yes:
                self.save_func(self.textedit.toPlainText())
                QCloseEvent.accept()
            elif choice == QMessageBox.No:
                QCloseEvent.accept()
            else:
                QCloseEvent.ignore()

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

1. is_saved变量用于记录用户是否已经进行保存;

2. 实例化一个QTextEdit控件用于文本编辑,将其textChanged信号和自定义的槽函数连接起来:

def on_textchanged_func(self):                      
    if self.textedit.toPlainText(): 
        self.is_saved = False
    else:
        self.is_saved = True

在槽函数中我们判断在每次文本内容发生变化时,textedit中是否还有文本,若有的话则将is_saved变量设为False,即未保存;若无文本,则将其设为True(如果没有文本的话,那可以直接关闭窗口,程序不会询问是否需要保存,因为没必要)。

3. 实例化一个按钮用于保存操作,将clicked信号与自定义的槽函数进行连接:

def on_clicked_func(self):
    self.save_func(self.textedit.toPlainText())
    self.is_saved = True

每次点击该按钮就进行保存,不管文本编辑框中是否有文本,文本保存通过save_fcun()函数完成:

def save_func(self, text):          
    with open('saved.txt', 'w') as f:
        f.write(text)

我们将内容保存在当前目录下的saved.txt函数中。PyQt5提供一个QFileDialog类可以让我们更好的完成保存操作,后续章节会涉及,这里只是先简单的完成下保存功能。

4. 这里我们重新定义了QWidget的窗口关闭函数closeEvent(),如果用户还没有进行保存,则弹出一个QMessageBox窗口询问是否保存,若用户点击Yes,则调用save_func()函数进行保存,然后通过accept()方法来接收关闭窗口操作,窗口随之关闭;若点击No,则不进行保存,但同样得关闭窗口;若点击cancel,也不进行保存,并通过ignore()方法来忽略这次关闭窗口的操作。

2 鼠标事件

鼠标移动、按下或者释放动作都会唤起相应的鼠标事件:

import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.button_label = QLabel('No Button Pressed', self)              # 1
        self.xy_label = QLabel('x:0, y:0', self)                           # 2
        self.global_xy_label = QLabel('global x:0, global y:0', self)      # 3

        self.button_label.setAlignment(Qt.AlignCenter)
        self.xy_label.setAlignment(Qt.AlignCenter)
        self.global_xy_label.setAlignment(Qt.AlignCenter)

        self.v_layout = QVBoxLayout()
        self.v_layout.addWidget(self.button_label)
        self.v_layout.addWidget(self.xy_label)
        self.v_layout.addWidget(self.global_xy_label)
        self.setLayout(self.v_layout)

        self.resize(300, 300)                                          
        self.setMouseTracking(True)                                        # 4

    def mouseMoveEvent(self, QMouseEvent):                                 # 5
        x = QMouseEvent.x()
        y = QMouseEvent.y()
        global_x = QMouseEvent.globalX()
        global_y = QMouseEvent.globalY()

        self.xy_label.setText('x:{}, y:{}'.format(x, y))
        self.global_xy_label.setText('global x:{}, global y:{}'.format(global_x, global_y))

    def mousePressEvent(self, QMouseEvent):                                # 6
        if QMouseEvent.button() == Qt.LeftButton:
            self.button_label.setText('Left Button Pressed')
        elif QMouseEvent.button() == Qt.MidButton:
            self.button_label.setText('Middle Button Pressed')
        elif QMouseEvent.button() == Qt.RightButton:
            self.button_label.setText('Right Button Pressed')

    def mouseReleaseEvent(self, QMouseEvent):                              # 7
        if QMouseEvent.button() == Qt.LeftButton:
            self.button_label.setText('Left Button Released')
        elif QMouseEvent.button() == Qt.MidButton:
            self.button_label.setText('Middle Button Released')
        elif QMouseEvent.button() == Qt.RightButton:
            self.button_label.setText('Right Button Released')

    def mouseDoubleClickEvent(self, QMouseEvent):                          # 8
        if QMouseEvent.button() == Qt.LeftButton:
            self.button_label.setText('Left Button Double Clikced')
        elif QMouseEvent.button() == Qt.MidButton:
            self.button_label.setText('Middle Button Double Clicked')
        elif QMouseEvent.button() == Qt.RightButton:
            self.button_label.setText('Right Button Double Clikced')

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

1. button_label用来显示鼠标的点击和释放动作;

2. xy_label用于记录鼠标相对于QWidget窗口的坐标;

3. global_xy_label用于记录鼠标相对于显示屏屏幕的坐标;

4. setMouseTracking(True)方法可以让窗口始终追踪鼠标,否则只能每次等鼠标被点击后,窗口才会开始记录鼠标的动作变化;而鼠标释放后,窗口又会不进行记录了,这样比较麻烦。

5. mouseMoveEvent()为鼠标移动时所触发的响应函数,在函数中,我们通过x()和y()方法获取鼠标相对于QWidget窗口的坐标,用globalX()和globalY()方法获取鼠标相对于显示屏屏幕的坐标;

6. mousePressEvent()为鼠标被按下时所触发的响应函数,在函数中,我们通过button()方法来确认被按下的键,然后用button_label显示被按下的键;

7. mouseReleaseEvent()为鼠标释放时所触发的响应函数,同理,在函数中,我们通过button()方法来确认被释放的键,然后用button_label显示被释放的键;

8. mouseDoubleClickEvent()为鼠标被双击时所触发的响应函数,同理,在函数中,我们通过button()方法来确认被双击的键,然后用button_label显示被双击的键;


 

运行截图如下,刚开始坐标都为(0, 0),鼠标也没有键被按下:

3 键盘事件

按下或释放键盘上的任意键时都会触发键盘事件:

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

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.pic_label = QLabel(self)                                   # 1
        self.pic_label.setPixmap(QPixmap('images/keyboard.png'))
        self.pic_label.setAlignment(Qt.AlignCenter)
    
        self.key_label = QLabel('No Key Pressed', self)                 # 2
        self.key_label.setAlignment(Qt.AlignCenter)

        self.v_layout = QVBoxLayout()
        self.v_layout.addWidget(self.pic_label)
        self.v_layout.addWidget(self.key_label)
        self.setLayout(self.v_layout)
    
    def keyPressEvent(self, QKeyEvent):                                 # 3
        if QKeyEvent.key() == Qt.Key_Up:
            self.pic_label.setPixmap(QPixmap('images/up.png'))
            self.key_label.setText('Key Up Pressed')
        elif QKeyEvent.key() == Qt.Key_Down:
            self.pic_label.setPixmap(QPixmap('images/down.png'))
            self.key_label.setText('Key Down Pressed')
        elif QKeyEvent.key() == Qt.Key_Left:
            self.pic_label.setPixmap(QPixmap('images/left.png'))
            self.key_label.setText('Key Left Pressed')
        elif QKeyEvent.key() == Qt.Key_Right:
            self.pic_label.setPixmap(QPixmap('images/right.png'))
            self.key_label.setText('Key Right Pressed')
        else: print(QKeyEvent.key())
    
    def keyReleaseEvent(self, QKeyEvent):                               # 4
        self.pic_label.setPixmap(QPixmap('images/keyboard.png'))
        self.key_label.setText('Key Released')

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

1. pic_label用于设置图片,先将初始化的图片设为keyboard.png;

2. key_label用于记录按键状态;

3. keyPressEvent()为键盘某个键被按下时所触发的响应函数,在这个函数中我们判断被按下的键种类,并将pic_label设为相应的箭头图片,将key_label设为相应的文本;

4. keyReleasedEvent()在键盘上的任意键被释放时所触发的响应函数,在这个函数中,我们将pic_label设为初始图片keyboard.png,并将key_label文本设为‘Key Released'。

高级事件用法:

《PyQt5高级编程实战》事件处理深入应用_pyqt5 里self.lines = []什么意思-CSDN博客
十六、拖放与剪切板

拖放和剪贴板的功能原理基础都是QMimeData类。QMimeData当然与MIME相关:MIME是描述消息内容类型的因特网标准,可以简单理解为对文件扩展名的详细解释,通过该解释,程序就可以知道应该以何种方式处理该数据。每个MIME类型由两部分组成,前面是数据的大类别,后面定义具体的种类,例如扩展名为.png的MIME类型为image/png;而QMimeData则给记录自身MIME类型的数据提供了一个容器,用于专门处理MIME类型数据。

详细的MIME类型请参考:MIME参考手册

针对常见的MIME类型,QMimeData类提供了很方便的函数用于处理MIME类型数据:

1 文件拖放

拖放分为拖动和放下两个动作,它们涉及到以下事件:

  • DragEnterEvent: 所拖动目标进入接收该事件的窗口或控件时触发;
  • DragMoveEvent: 所拖动目标进入窗口或控件后,继续被拖动时触发;
  • DragLeaveEvent: 所拖动目标离开窗口或控件时触发;
  • DropEvent: 所拖动目标被放下时触发。

下面我们完成一个可以实现拖放txt文件并读取的小程序:

import sys
from PyQt5.QtWidgets import QApplication, QTextBrowser

class Demo(QTextBrowser):                                           # 1
    def __init__(self):
        super(Demo, self).__init__()
        self.setAcceptDrops(True)                                   # 2

    def dragEnterEvent(self, QDragEnterEvent):                      # 3
        print('Drag Enter')
        if QDragEnterEvent.mimeData().hasText():
            QDragEnterEvent.acceptProposedAction()

    def dragMoveEvent(self, QDragMoveEvent):                        # 4
        print('Drag Move')

    def dragLeaveEvent(self, QDragLeaveEvent):                      # 5
        print('Drag Leave')

    def dropEvent(self, QDropEvent):                                # 6
        print('Drag Drop')
        # MacOS
        #txt_path = QDropEvent.mimeData().text().replace('file:///', '/')

        # Linux
        # txt_path = QDropEvent.mimeData().text().replace('file:///', '/').strip()

        # Windows
        txt_path = QDropEvent.mimeData().text().replace('file:///', '')

        with open(txt_path, 'r') as f:
            self.setText(f.read())

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

1. 继承QTextBrowser,也就是说下面来实现该控件的拖放事件响应函数;

2. setAcceptDrops(True)方法可以让该控件接收放下(Drop)事件,默认True

3. 当拖动目标进入QTextBrowser的那一刹那,触发dragEnterEvent事件,在该响应函数中,我们先判断所拖动目标的MIME类型是否为text/plain,若是的话则调用acceptProposedAction()方法来表明可以在QTextBrowser上进行拖放动作;

4. 当目标进入窗体后,如果不放下而是继续移动的话,则会触发dragMoveEvent事件;

5. 将进入控件后的目标再次拖动到控件之外时,就会触发dragLeaveEvent()事件;

6. 将目标在QTextBrowser中放下后,我们先通过QDropEvent.mimeData().text()方法获取到该文件的URI路径,replace()方法将其中的file:///替换为/,这样得到的值才是我们想要的本地文件路径。最后打开my.txt文件进行读取,并用setText()方法将QTextBrowser的文本设为该my.txt的内容。

注:不同系统使用的路径写法也不同,请注意区分。在Linux上获取的路径中存在'\r\n'所以用strip()去除。

2 剪贴板

通常我们在Windows或Linux上使用复制都是按ctrl+c然后按ctrl+v进行粘贴(Mac上为command+c和command+v),这其中就涉及到了剪贴板,当进行复制时,其实是将要复制的内容放到了一个无形的剪贴板上,要粘贴时,再将剪贴板上的内容放到界面上。

当我们浏览CSDN博客,在碰到代码想要复制的时候,右上角会出现一个复制按钮,点击后就可以将所有代码复制到剪贴板上:

我们用PyQt5来实现下类似功能,代码如下:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTextEdit, QTextBrowser, QPushButton, QGridLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.text_edit = QTextEdit(self)
        self.text_browser = QTextBrowser(self)
        
        self.clipboard = QApplication.clipboard()                               # 1
        self.clipboard.dataChanged.connect(lambda: print('Data Changed'))    

        self.copy_btn = QPushButton('Copy', self)                               # 2
        self.copy_btn.clicked.connect(self.copy_func)               

        self.paste_btn = QPushButton('Paste', self)                             # 3
        self.paste_btn.clicked.connect(self.paste_func)

        self.g_layout = QGridLayout()
        self.g_layout.addWidget(self.text_edit, 0, 0, 1, 1)
        self.g_layout.addWidget(self.text_browser, 0, 1, 1, 1)
        self.g_layout.addWidget(self.copy_btn, 1, 0, 1, 1)
        self.g_layout.addWidget(self.paste_btn, 1, 1, 1, 1)
        self.setLayout(self.g_layout)

    def copy_func(self):
        self.clipboard.setText(self.text_edit.toPlainText())

    def paste_func(self):
        self.text_browser.setText(self.clipboard.text())

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

1. 实例化一个剪贴板,并将其dataChanged信号和打印函数连接起来。每当剪贴板内容发生变化的时候,都会触发dataChanged信号。在这里也就是说每当发生变化,控制台都会打印出Data Changed;

2. 当点击copy_btn后,槽函数copy_func()就会启动:

def copy_func(self):
    self.clipboard.setText(self.text_edit.toPlainText())

在槽函数中,我们将text_edit中的文本获取过来并通过setText()方法将其设置为剪贴板的文本;

3. 当点击paste_btn后,槽函数paste_func()启动:

def paste_func(self):
    self.text_browser.setText(self.clipboard.text())

在槽函数中,我们将text_browser的文本设为剪贴板的文本。当然该槽函数还有另一种实现方法:

def paste_func(self):
    mime = self.clipboard.mimeData()
    if mime.hasText():
        self.text_browser.setText(mime.text())

首先通过mimeData()方法获取剪贴板内容的MIME类型,然后判断mime类型是否为text/plain,是的话则通过text()方法获取,并设为text_browser文本。

当然以上只是针对文本内容,当然还可以复制图片等文件,而剪贴板当然也有相应的方法,以下列出常用的:

运行截图如下,在左侧输入文本,点击Copy,再点击Paste,右侧显示相应文本:

十七、列表控件、树形控件、表格控件

列表控件可以让我们以列表形式呈现内容,使界面更加有序美观。QListWidget列表控件应当与QListWidgetItem一起使用,后者作为项被添加入列表控件中,也就是说列表控件中的每一项都是一个QListWidgetItem。这也是为什么我们说QListWidget是一个基于项(Item-based)的控件了。

同样基于项的控件还有QTreeWidget树形控件和QTableWidget表格控件,前者以树状方式呈现内容,并与QTreeWidgetItem搭配使用;后者以表格形式呈现内容,并与QTableWidgetItem一起使用。

1 列表控件QListWidget
self.listwidget_1 = QListWidget(self)			#实例化列表控件
self.listwidget_1.clicked.connect()				#单机列表元素的信号
self.listwidget_1.doubleClicked.connect()		#双击列表元素的信号

self.item_1 = QListWidgetItem('Item 1', self.listwidget_1)#实例化元素并加入列表
self.item = QListWidgetItem('Item 2')			#实例化列表元素,并设置文字
self.listwidget_1.addItem(self.item)			#将元素添加进列表
self.listwidget_1.insertItem(8, self.item_8)	#将元素插入进列表(索引)
self.listwidget_1.takeItem(8)					#从列表去除元素(索引)

self.listwidget_1.addItem('Item 7')             #直接将文字添加进列表(但这样可能没有更多功能)
str_list = ['Item 9', 'Item 10']
self.listwidget_1.addItems(str_list)

self.listwidget_1.currentItem()					#获取当前选中的元素
self.listwidget_1.currentRow()					#获取选中元素的索引
self.listwidget_1.count()						#获取列表中有多少元素
def row(self, item: typing.Optional[QListWidgetItem]) -> int: ...#获取元素的索引
def item(self, row: int) -> typing.Optional[QListWidgetItem]: ...#获取索引对应的元素
def setCurrentItem(self, item: typing.Optional[QListWidgetItem]) -> None: ...#设置选中的元素
def setCurrentRow(self, row: int) -> None: ...#设置选中的行
def sortItems(self, order: QtCore.Qt.SortOrder = ...) -> None: ...#排序
def clear(self) -> None: ...
import sys
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QListWidget, QListWidgetItem, QHBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.pic_label = QLabel(self)                       # 1
        self.pic_label.setPixmap(QPixmap('arrow.png'))

        self.listwidget_1 = QListWidget(self)               # 2
        self.listwidget_2 = QListWidget(self) 
        self.listwidget_1.doubleClicked.connect(lambda: self.change_func(self.listwidget_1))
        self.listwidget_2.doubleClicked.connect(lambda: self.change_func(self.listwidget_2))

        for i in range(6):                                  # 3
            text = 'Item {}'.format(i)
            self.item = QListWidgetItem(text)
            self.listwidget_1.addItem(self.item)

        self.item_6 = QListWidgetItem('Item 6', self.listwidget_1)  # 4

        self.listwidget_1.addItem('Item 7')                         # 5
        str_list = ['Item 9', 'Item 10']
        self.listwidget_1.addItems(str_list)

        self.item_8 = QListWidgetItem('Item 8')                     # 6
        self.listwidget_1.insertItem(8, self.item_8)
        # self.listwidget_1.insertItem(8, 'Item 8')

        self.h_layout = QHBoxLayout()
        self.h_layout.addWidget(self.listwidget_1)
        self.h_layout.addWidget(self.pic_label)
        self.h_layout.addWidget(self.listwidget_2)
        self.setLayout(self.h_layout)

    def change_func(self, listwidget):                              # 7
        if listwidget == self.listwidget_1:
            item = QListWidgetItem(self.listwidget_1.currentItem())
            self.listwidget_2.addItem(item)
            print(self.listwidget_2.count())
        else:
            self.listwidget_2.takeItem(self.listwidget_2.currentRow())
            print(self.listwidget_2.count())

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

1. pic_label用于显示图片;

2. 实例化两个QListWidget,listwidget_1放在左边用于显示可选的内容,listwidget_2放在右边用于显示被双击的项。然后将这两个QListWidget控件的doubleClicked信号和自定义的槽函数连接起来,每当双击QListWidget中的某项时,就会触发该槽函数。

3. 循环创建六个QListWidgetItem,并通过调用addItem(QListWidgetItem)将其添加到listwidget_1中;

4. 当然也可以通过实例化时直接指定父类的方式进行添加;

5. 也可以不用QListWidgetItem,直接调用addItem(str)方法来添加一项内容。也可以使用addItem(Iterable)来添加一组项内容(不过若要让项呈现更多功能的话,还是应该选择QListWidgetItem);

6. 通过insertItem(row, QListWidgetItem)方法可以在指定行中加入一项内容;

7. 接下来我们讲一下槽函数:

def change_func(self, listwidget):
    if listwidget == self.listwidget_1:
        item = QListWidgetItem(self.listwidget_1.currentItem())
        self.listwidget_2.addItem(item)
        print(self.listwidget_2.count())
    else:
        self.listwidget_2.takeItem(self.listwidget_2.currentRow())
        print(self.listwidget_2.count())

在槽函数中,我们判断信号是哪一个QListWidget发出的,如果是listwidget_1的话,我们先通过currentItem()获取到当前被双击的项,之后实例化为QListWidgetItem,再通过addItem(QListWidgetItem)方法加入listwidget_2中,count()方法用于获取项数量,这里我们打印出listwidget_2中一共有多少项内容。若信号是listwidget_2发出的话,则将当前被双击项的行数currentRow()传给takeItem(int)方法来进行删除,然后也打印下项数量。

双击左边的某项,会发现右边的列表控件会显示出来:

双击右边的某项将其删除:

2 树形控件QTreeWidget
self.tree = QTreeWidget(self)				#实例化树形控件
self.tree.setColumnCount(2)					#设置列数,默认为1
self.tree.setHeaderLabels(['Instal','Test'])#设置列名
self.tree.itemClicked.connect(self.change_f)#连接槽函数

self.preview = QTreeWidgetItem(self.tree)	#实例化元素,并设为控件的顶级选项
self.tree.addTopLevelItem(self.preview)		#如上面没有设置,也可以单独设置
self.preview.addChild(self.qt5112)			#添加子级

self.preview.setText(0, 'Preview')			#设置它第0列文字
self.qt5112.setCheckState(0, Qt.Unchecked)	#设置元素是否被选中,不调用该函数则该元素没有复选框
self.tree.expandItem(self.qt5112)			#展开一个级
self.tree.expandAll()						#展开所有

self.qt5112.text(column)							#获取元素第column列的文字
self.qt5112.checkState(0)							#获取第0列是否被选中Qt.Checked/Qt.Unchecked/Qt.PartiallyChecked
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QTreeWidget, QTreeWidgetItem, QLabel, QHBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.resize(500, 300)
        self.label = QLabel('No Click')                         # 1

        self.tree = QTreeWidget(self)                           # 2
        self.tree.setColumnCount(2)
        self.tree.setHeaderLabels(['Install Components', 'Test'])
        self.tree.itemClicked.connect(self.change_func)

        self.preview = QTreeWidgetItem(self.tree)               # 3
        self.preview.setText(0, 'Preview')

        # self.preview = QTreeWidgetItem()
        # self.preview.setText(0, 'Preview')
        # self.tree.addTopLevelItem(self.preview)

        self.qt5112 = QTreeWidgetItem()                         # 4
        self.qt5112.setText(0, 'Qt 5.11.2 snapshot')
        self.qt5112.setCheckState(0, Qt.Unchecked)
        self.preview.addChild(self.qt5112)

        choice_list = ['macOS', 'Android x86', 'Android ARMv7', 'Sources', 'iOS']
        self.item_list = []
        for i, c in enumerate(choice_list):                     # 5
            item = QTreeWidgetItem(self.qt5112)
            item.setText(0, c)
            item.setCheckState(0, Qt.Unchecked)
            self.item_list.append(item)

        self.test_item = QTreeWidgetItem(self.qt5112)           # 6
        self.test_item.setText(0, 'test1')
        self.test_item.setText(1, 'test2')

        self.tree.expandAll()                                   # 7

        self.h_layout = QHBoxLayout()
        self.h_layout.addWidget(self.tree)
        self.h_layout.addWidget(self.label)
        self.setLayout(self.h_layout)

    def change_func(self, item, column):
        self.label.setText(item.text(column))                   # 8

        print(item.text(column))
        print(column)
        if item == self.qt5112:                                 # 9
            if self.qt5112.checkState(0) == Qt.Checked:
                [x.setCheckState(0, Qt.Checked) for x in self.item_list]
            else:
                [x.setCheckState(0, Qt.Unchecked) for x in self.item_list]
        else:                                                   # 10
            check_count = 0
            for x in self.item_list:
                if x.checkState(0) == Qt.Checked:
                    check_count += 1

            if check_count == 5:
                self.qt5112.setCheckState(0, Qt.Checked)
            elif 0 < check_count < 5:
                self.qt5112.setCheckState(0, Qt.PartiallyChecked)
            else:
                self.qt5112.setCheckState(0, Qt.Unchecked)

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

1. QLabel控件用于显示每个QTreeWidgetItem的文本;

2. 实例化一个QTreeWidget,通过setColumnCount(int)将该树状控件的列数设为2(默认为1列)。通过setHeaderLabels(Iterable)设置每列的标题,如果只有一列的话,则应该通过setHeaderLabel(str)方法设置。接着将itemClicked信号与自定义的槽函数连接起来,每当点击QTreeWidget中的任意一项时,都会触发itemClicked信号。QtAssistant中对该信号的说明如下:

这是一个带参数的信号,而文档中解释说:每当一项内容被点击时,参数item就是被点击的项,参数column就是被点击项所在的列,这两个参数会自动传给我们的槽函数。

3. 实例化一个QTreeWidgetItem,并将其父类设为self.tree,表示self.preview为最外层(最顶层)的项,接着通过setText(int, str)方法来设置文本,第一个int类型参数为该文本所在的列,0表示放在第一列。当然我们也可以在实例化时不指定父类,并让self.tree调用addTopLevelItem()方法来设置最顶层的项;

4. setCheckState(int, CheckState)方法可以让该项以复选框形式呈现出来,addChild(QTreeWidgetItem)方法可以添加子项,这里让self.preview添加一个self.qt5112选项;

5. 实例化5个子项,将他们添加到self.qt5112中,并以复选框形式显示;

6. 这里的self.test项只是拿来作为对比,好让读者知道将QTreeWidget设为两列时的样子;

7. 调用expandAll()方法可以让QTreeWidget所有的项都是以打开状态显示的。注意必须要在所有项都已经实例化好之后再调用该方法,如果一开始就调用则会没有效果;

8. 在槽函数中,self.label显示对应的项文本,item就是被点击的项,我们调用text(int)传入列数,获得文本(该text()函数用法跟我们之前使用的有所不同)。

9. 如果被点击的项为qt5112,则我们判断是否其被选中,若是的话,将它的所有子项都设为选中状态,若为无选中状态的话,则将其子项全部设为无选中状态;

10. 若被点击是qt5112的子项时,我们判断有多少个子项已经被选中了,若数量为5,则设置qt5112为选中状态,若为0-5之间,则设为半选中状态,若等于0,则设为无选中状态。

3 表格控件QTableWidget

核心都是类似的,搭配QTableWidgetItem使用,下面就来讲一下常用的方法:

self=QTableWidget()
self.setRowCount(6)                             # 设置行列数
self.setColumnCount(6)
self.table = QTableWidget(6, 6, self)
print(self.rowCount())                          # 获取行列数
print(self.columnCount())
self.setHorizontalHeaderLabels(['h1', 'h2', 'h3', 'h4', ' h5', 'h6'])   # 设置行标题
self.setVerticalHeaderLabels(['t1', 't2', 't3', 't4', 't5', 't6'])		#列标题
self.setShowGrid(False)							#设置是否显示网格,默认True

self.item_1 = QTableWidgetItem('Hi')            # 向网格(0,0)位置添加单元格
self.setItem(0, 0, self.item_1)
self.item_2.setTextAlignment(Qt.AlignCenter)	#设置单元格显示为居中显示(其他见下面,一次只能传入一个参数)
self.setSpan(2, 2, 2, 2)						#合并单元格
print(self.findItems('Hi', Qt.MatchExactly))    # 寻找元素,方法详见下面
print(self.findItems('B', Qt.MatchContains))
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QTableWidget, QTableWidgetItem

class Demo(QTableWidget):                               # 1
    def __init__(self):
        super(Demo, self).__init__()
        self.setRowCount(6)                             # 2
        self.setColumnCount(6)
        # self.table = QTableWidget(6, 6, self)

        print(self.rowCount())                          # 3
        print(self.columnCount())

        self.setColumnWidth(0, 30)                      # 4
        self.setRowHeight(0, 30)

        self.setHorizontalHeaderLabels(['h1', 'h2', 'h3', 'h4', ' h5', 'h6'])   # 5
        self.setVerticalHeaderLabels(['t1', 't2', 't3', 't4', 't5', 't6'])

        # self.setShowGrid(False)                       # 6

        self.item_1 = QTableWidgetItem('Hi')            # 7
        self.setItem(0, 0, self.item_1)

        self.item_2 = QTableWidgetItem('Bye')           # 8
        self.item_2.setTextAlignment(Qt.AlignCenter)
        self.setItem(2, 2, self.item_2)

        self.setSpan(2, 2, 2, 2)                        # 9

        print(self.findItems('Hi', Qt.MatchExactly))    # 10
        print(self.findItems('B', Qt.MatchContains))

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

1. 这里我们直接继承QTableWidget来实现程序;

2. setRowCount(int)设置表格的行数,setColumnCount(int)设置列数。或者可以选择在实例化的时候直接指定行列数;

3. rowCount()获取行数,columnCount()获取列数;

4. setColumnWidth(int, int)设置列款,第一个参数填列序号,第二个参数填宽度值。setRowCount(int, int)设置行宽,参数同理;

5. setHorizontalHeaderLabels(iterable)设置行的标题,而setVerticalHeaderLabels设置列的标题;

6. setShowGird(bool)设置是否显示表格上的网格线,True为显示,False不显示;

7. 实例化一个单元格,并用setItem(int, int, QTableWidgetItem)将该单元格添加到表格中。前两个int类型参数分别为行序号和列序号;

8. setTextAlignment()设置单元格的文本对齐方式,QtAssistant中输入Qt::Alignment就可以找到各种对齐方式:

9. setSpan(int, int, int, int)方法用来合并单元格,前两个int参数分别为行序号和列序号,后两个分别为要合并的行数和列数;

10. findItems(str, Qt.MatchFlag)方法用来进行查找,前一个参数为用来匹配的字符串,后一个参数为匹配方式。在代码中我们用了两种匹配方式,第一种为Qt.MatchExactly,表示精确匹配。第二种为Qt.MatchContains,表示包含匹配。在QtAssistant中输出Qt::MatchFlag即可了解各种匹配方式:

运行截图如下:

高级内容:

《PyQt5高级编程实战》学会使用视图委托_pyqt5 qstyleditemdelegate-CSDN博客

十八、列表视图、树形视图、表格视图

前一章的列表控件QListWidget,树形控件QTreeWidget和表格控件QTableWidget是基于项(item-based)的控件,它们分别与QListWidgetItem,QTreeWidgetItem以及QTableWidgetItem一起使用。在基于项的控件中,数据是存储于项中再由对应的控件添加进去并显示的。而这章的列表视图QListView,树形视图QTreeView和表格视图QTableView是基于模型的(model-based),也就是说有关数据的获取或者存储操作都是跟模型有关。当我们在处理大量数据的时候,采用基于模型的控件可以让程序的处理速度更快,性能更好。

需要一提的是QListView,QTreeView和QTableView分别是QListWidget,QTreeWidget和QTableWidget的父类,后三者的目的主要是为了使用方便,让开发更加快速(准确来说后三者也可以是视图,不过笔者还是习惯将前三者称作视图,后三者称为控件,比较好区分)。在高端复杂的程序中,还是建议使用前三者。

在分别讲解三个视图前,我们先来了解一下PyQt5中所提供的模型种类及用处:

接下来我们结合模型和视图进行讲解(有关SQL的模型会在讲解数据库时再涉及)。

1 列表视图QListView
self.item_list = ['item %s' % i for i in range(11)]         # 1
self.model_1 = QStringListModel(self)						#实例化一个模型
self.model_1.setStringList(self.item_list)					#将内容放入模型
self.listview_1 = QListView(self)							#实例化一个列表
self.listview_1.setModel(self.model_1)						#设置模型
self.listview_1.setEditTriggers()							#设置编辑格式,参数详见下表
self.listview_1.doubleClicked.connect()						#连接槽函数

self.model_2.rowCount()										#获取模型总行数
self.model_2.insertRows(a,b)								#在模型第a行添加b个空行
self.listview_1.currentIndex().data()						#获取列表选中内容
self.model_2.index()										#由行数获取索引
self.model_2.setData(index, data)							#根据索引设置模型的内容
self.model_2.removeRows(a, b)								#在模型第a行删除b行

我们用QStringListModel和QListView来实现前一章中QListWidget时所完成的程序:

import sys
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import QStringListModel
from PyQt5.QtWidgets import QApplication, QWidget, QListView, QLabel, QHBoxLayout, QAbstractItemView

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()

        self.item_list = ['item %s' % i for i in range(11)]         # 1
        self.model_1 = QStringListModel(self)
        self.model_1.setStringList(self.item_list)

        self.model_2 = QStringListModel(self)                       # 2

        self.listview_1 = QListView(self)                           # 3
        self.listview_1.setModel(self.model_1)
        self.listview_1.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.listview_1.doubleClicked.connect(lambda: self.change_func(self.listview_1))

        self.listview_2 = QListView(self)                           # 4
        self.listview_2.setModel(self.model_2)
        self.listview_2.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.listview_2.doubleClicked.connect(lambda: self.change_func(self.listview_2))

        self.pic_label = QLabel(self)                               # 5
        self.pic_label.setPixmap(QPixmap('arrow.png'))

        self.h_layout = QHBoxLayout()
        self.h_layout.addWidget(self.listview_1)
        self.h_layout.addWidget(self.pic_label)
        self.h_layout.addWidget(self.listview_2)
        self.setLayout(self.h_layout)

    def change_func(self, listview):                                
        if listview == self.listview_1:                             # 6
            self.model_2.insertRows(self.model_2.rowCount(), 1)

            data = self.listview_1.currentIndex().data()
            index = self.model_2.index(self.model_2.rowCount()-1)
            self.model_2.setData(index, data)
        else:                                                       # 7
            self.model_2.removeRows(self.listview_2.currentIndex().row(), 1)

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

1. 首先用列表推导式生成11个item,然后实例化一个QStringListModel并用setStringList(iterable)方法将数据添加到到模型中;

2. model_1用于listview_1,model_2用于listview_2;

3. 实例化一个列表视图listview_1,并调用setModel(model)方法将model_1作为参数传入,此时,列表视图就会显示出模型中的数据,模型数据发生任何改变,视图也会自动作出相应改变。setEditTriggers()方法可设置视图的编辑规则,可传入的参数如下(当然QListWidget也有相同方法):

接着我们将视图的doubleClicked信号与自定义的槽函数change_func()连接了起来;

4. 同理,实例化一个listview_2,设置模型和编辑规则并将信号和槽函数进行连接;

5. pic_label用于显示图片;

6. 在槽函数中我们判断触发doubleClicked信号的视图,如果是listview_1的话,则我们通过insertRows(int, int)方法先在model_2中插入一个空行,该方法的第一个参数为行序号,即要将数据插入哪一行,第二个为要插入的行数量。在这里rowCount()方法获取总行数(在这里正好可以当做行序号,表示我们将新行添加到最后一行),1表示我们在每次双击时都只插入一行。

由于插入的只是空行,所以我们要调用setData(QModelindex, data)方法将该空行设为相应的内容。通过currentIndex().data()方法可以获取到被双击行的数据,比如双击在'Item 0'上,那么我们就获取了‘Item 0’这个文本。而调用QStringListModel的index(int)方法获取到指定的QModelIndex,传入的参数为行序号(请注意序号都是从0开始计算);

7. 如果是listview_2触发doubleClicked信号的话,那我们就通过调用removeRows(int, int),方法将被双击的行给删除掉。currentIndex().row()方法获取被双击行的行序号,1表示我们只删除一行。

2 树形视图QTreeView

树形视图常被用来显示文件目录:

self.model = QDirModel(self)                            # 实例化一个文件目录模型
self.model.setReadOnly(False)							# 设置是否只读
self.model.setSorting(QDir.Name | QDir.IgnoreCase)		# 设置排序方式,详见下表

self.tree = QTreeView(self)                             # 实例化视图
self.tree.setModel(self.model)							#设置模型
self.tree.clicked.connect(self.show_info)				#连接槽函数
QDir.currentPath()										#获取工作文件路径
self.index = self.model.index(path)						#通过路径获取索引
self.tree.expand(index)									#展开索引
self.tree.scrollTo(index)								#滚动窗口到

self.tree.currentIndex()								#获取选中的索引
file_name = self.model.fileName(index)					#通过索引获取文件名
file_path = self.model.filePath(index)					#通过索引获取路径
import sys
from PyQt5.QtCore import QDir
from PyQt5.QtWidgets import QApplication, QWidget, QTreeView, QDirModel, QLabel, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.resize(600, 300)
        self.model = QDirModel(self)                            # 1
        self.model.setReadOnly(False)
        self.model.setSorting(QDir.Name | QDir.IgnoreCase)

        self.tree = QTreeView(self)                             # 2
        self.tree.setModel(self.model)
        self.tree.clicked.connect(self.show_info)
        self.index = self.model.index(QDir.currentPath())       
        self.tree.expand(self.index)
        self.tree.scrollTo(self.index)

        self.info_label = QLabel(self)                          # 3

        self.v_layout = QVBoxLayout()
        self.v_layout.addWidget(self.tree)
        self.v_layout.addWidget(self.info_label)
        self.setLayout(self.v_layout)

    def show_info(self):                                        # 4
        index = self.tree.currentIndex()
        file_name = self.model.fileName(index)
        file_path = self.model.filePath(index)
        file_info = 'File Name: {}\nFile Path: {}'.format(file_name, file_path)
        self.info_label.setText(file_info)

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

1. 实例化一个QDirModel,通过setReadOnly(False)方法可以让我们在QTreeView中直接对文件进行编辑,若设为True,则表示只读模式。setSorting()方法可以让显示的文件或文件夹按照指定要求进行排序,可传入的参数如下:

2. 实例化一个树形视图,将其模型设为self.model。接着将clicked信号和自定义的槽函数连接起来。然后我们通过调用self.model的index()方法传入一个QDir.currentPath()来获取当前路径的QModelIndex索引值,再调用self.tree的expand()方法进行展开显示,scrollTo()方法则是将当前视图的视口滚动到self.index索引处。

3. info_label用于显示文件信息;

4. 在槽函数中,我们首先通过currentIndex()方法获取当前鼠标所点击的QModelIndex索引值,然后分别传入self.model的fileName()和filePath()函数中,最后用info_label显示出来:

3 表格视图QTableView

这里我们将QStandItemModel和QTableView搭配使用:

self.model = QStandardItemModel(6, 6, self)            # 实例化模型并设置行列
self.model = QStandardItemModel(self)					#实例化不设置
self.model.setColumnCount(6)							#再设置行列
self.model.setRowCount(6)

item = QStandardItem('')								#将字符转换成standardItem
self.model.setItem(row, column, item)					#将元素添加到模型
self.model.appendRow(self.item_list)					#追加行,参数为QStandardItem组成的列表
self.model.insertRow(7, self.item_list)					#插入行
self.model.setHorizontalHeaderLabels(['a','b','c'])		#列标题
self.table = QTableView(self)                           # 实例化表格
self.table.setModel(self.model)							#将模型传入
self.table.horizontalHeader().setStretchLastSection(True)#设置让表格最后一列拉伸填满整个窗口
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)#拉伸整个表格填满窗口
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)#设置编辑表格内容,参数上面已经提到过
self.table.clicked.connect(self.show_info)				#连接槽函数

row = self.table.currentIndex().row()					#获取选中的行号
column = self.table.currentIndex().column()
data = self.table.currentIndex().data()					#获取选中的单元格内容
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import QApplication, QWidget, QTableView, QAbstractItemView, QLabel, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.resize(650, 300)3

        self.model = QStandardItemModel(6, 6, self)            # 1
        # self.model = QStandardItemModel(self)
        # self.model.setColumnCount(6)
        # self.model.setRowCount(6)

        for row in range(6):                                   # 2
            for column in range(6):
                item = QStandardItem('({}, {})'.format(row, column))
                self.model.setItem(row, column, item)

        self.item_list = [QStandardItem('(6, {})'.format(column)) for column in range(6)]
        self.model.appendRow(self.item_list)                   # 3

        self.item_list = [QStandardItem('(7, {})'.format(column)) for column in range(6)]
        self.model.insertRow(7, self.item_list)                # 4

        self.table = QTableView(self)                          # 5
        self.table.setModel(self.model)
        self.table.horizontalHeader().setStretchLastSection(True)
        self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.table.clicked.connect(self.show_info)

        self.info_label = QLabel(self)                         # 6
        self.info_label.setAlignment(Qt.AlignCenter)

        self.v_layout = QVBoxLayout()
        self.v_layout.addWidget(self.table)
        self.v_layout.addWidget(self.info_label)
        self.setLayout(self.v_layout)

    def show_info(self):                                       # 7
        row = self.table.currentIndex().row()
        column = self.table.currentIndex().column()
        print('({}, {})'.format(row, column))

        data = self.table.currentIndex().data()
        self.info_label.setText(data)

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

1. 实例化一个QStandItemModel,可直接在实例化时传入行数和列数,或者也可以通过setRowCount()和setColumn()方法来设置。

2. QStandItemModel与QStandardItem搭配使用,这里通过循环实例化36个QStandardItem(每一个item代表一个单元格),接着调用setItem()方法将每一个Item放在相应的位置;

3. appendRow()方法可以把新行添加到表格最后,也就是说目前有7行;

4. insertRow()方法可以在指定方法添加一行,这里我们在最后一行插入一行,现在有8行;

5. 实例化一个QTableView,设置模型。self.table.horizontalHeader().setStretchLastSection(True)可以让表格填满整个窗口,如果拉伸窗口的话则为了填满窗口表格最后列会改变尺寸。setEditTriggers()方法设置编辑规则,这里我们设置无法编辑。最后将clicked信号和自定义的槽函数连接起来;

6. info_label用于显示单元格文本;

7. currentIndex().row()可以获取当前点击单元格所在的行序号,currentIndex().column()可以获取当前点击单元格所在的列序号。currentIndex().data()可以获取到当前点击单元格的文本;

更多高级内容:

《PyQt5高级编程实战》学会使用视图委托_pyqt5 qstyleditemdelegate-CSDN博客

十九、滚动区域QScrollArea和滚动条QScrollBar

想象下,假如你的程序中用到了100个按钮,那你会选择将这100个按钮全部同时显示在界面上吗?当然不能这么做,不然界面得多拥挤。我们可以将这100个按钮放在一个滚动区域QScrollArea中,用户刚开始只能看见几个按钮,但滚动条可以让他们操作其余的按钮。 通过QScrollArea我们可以让界面显得更加整洁友好。

QScrollArea本身自带滚动条,但是有很多时候,开发人员可能想要往滚动条上加入更多功能来控制界面,或者说有时候只是需要用到滚动条而已。QScrollBar类可以让我们设计自己想要的滚动条。

1. 滚动区域QScrollArea

2. 滚动条QScrollBar

3. Zoom in按钮和Zoom out按钮分别用于放大缩小图片

self.label.setScaledContents(True)				#设置内容和label同步缩放
self.scroll_area = QScrollArea(self)			#实例化滚动区域
self.scroll_area.setWidget(self.label)
self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)	#关闭横向滚动条,详见下方
self.scroll_area.horizontalScrollBar().maximum()	#获取区域的滚动条最大值
self.scroll_area.horizontalScrollBar().setValue()#设置滚动区域滚动条的值

self.scrollbar = QScrollBar(Qt.Horizontal, self)       # 实例化横向滚动条
self.scrollbar.setMaximum(value)					   #设置滚动条最大值
self.scrollbar.valueChanged.connect(self.sync_func)		#连接槽函数
self.scrollbar.value()									#获取值
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QScrollArea, QScrollBar, \
                            QHBoxLayout, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.label = QLabel(self)                              # 1
        self.label.setPixmap(QPixmap('image.jpg'))
        self.label.setScaledContents(True)

        self.scroll_area = QScrollArea(self)                   # 2
        self.scroll_area.setWidget(self.label)
        self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        self.scrollbar = QScrollBar(Qt.Horizontal, self)       # 3
        self.scrollbar.setMaximum(self.scroll_area.horizontalScrollBar().maximum())

        self.bigger_btn = QPushButton('Zoom in', self)         # 4
        self.smaller_btn = QPushButton('Zoom out', self)

        self.bigger_btn.clicked.connect(self.bigger_func)      # 5
        self.smaller_btn.clicked.connect(self.smaller_func)
        self.scrollbar.valueChanged.connect(self.sync_func)

        self.h_layout = QHBoxLayout()                        
        self.h_layout.addWidget(self.bigger_btn)
        self.h_layout.addWidget(self.smaller_btn)

        self.v_layout = QVBoxLayout()
        self.v_layout.addWidget(self.scroll_area)
        self.v_layout.addWidget(self.scrollbar)
        self.v_layout.addLayout(self.h_layout)
        self.setLayout(self.v_layout)

    def bigger_func(self):
        self.label.resize(int(self.label.width()*1.2), int(self.label.height()*1.2))
        self.scrollbar.setMaximum(self.scroll_area.horizontalScrollBar().maximum())

    def smaller_func(self):
        self.label.resize(int(self.label.width() * 0.8), int(self.label.height() * 0.8))
        self.scrollbar.setMaximum(self.scroll_area.horizontalScrollBar().maximum())

    def sync_func(self):
        self.scroll_area.horizontalScrollBar().setValue(self.scrollbar.value())

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

1. 实例化一个QLabel控件用于显示大图。setScaledContents(True)方法可以让图片随着QLabel控件大小变化而变化,即自适应;

2. 实例化一个QScrollArea控件,调用setWidget()方法将QLabel滚动区域中的控件。而以下这行代码的含义就是要将滚动区域自带的横向滚动条给隐藏掉,因为我们要使用寄己的滚动条:

self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

Qt.ScrollBarAlwaysOff Qt.ScrollBarAlwaysOn Qt.ScrollBarAsNeeded

显然,如果要隐藏纵向滚动条的话,则使用:

self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

3. 实例化一个横向滚动条,并调用setMaximum()方法设置最大值。而它的最大值应该跟QScrollArea被隐藏掉的横向滚动条的最大值一样;

4. 实例化两个按钮用于放大缩小QLabel控件(图片也会相应的放大缩小);

5. 信号和槽函数连接。在bigger_func()槽函数中,我们将QLabel控件放大20%,同时设置QScrollBar的最大值为QScrollArea横向滚动条的最大值;在smaller_func()槽函数中,我们将QLabel控件缩小20%,同样要更新QScrollBar的最大值;在sync_func()槽函数中,我们让QScrollArea横向滚动条的当前值和QScrollBar的值同步。这样一来就相当于我们在用自己实例化的QScrollBar来控制滚动区域中的图片(相信某些读者会有这样的需求)。

运行截图如下:

Tips:

1. 可以将QLabel换成一个QWidget,而这个QWidget中包含许多子控件,这样做可以节省许多界面空间;

2. QScrollBar当然不一定要跟QScrollArea一起使用,我们也可以将它看成一个QSlider;

3. 以上例子只是QScrollArea和QScrollBar用法的一小部分,可以通过文档了解到更多用法(一定要多查文档)。

二十、各种对话框

1 颜色对话框和字体对话框
color = QColorDialog.getColor()
font, ok = QFontDialog.getFont()

这两种对话框分别可以让用户进行颜色和字体选择。两者用法相似,所以就放在一起了:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTextEdit, QColorDialog, QFontDialog, QPushButton, \
                            QHBoxLayout, QVBoxLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.text_edit = QTextEdit(self)                 # 1

        self.color_btn = QPushButton('Color', self)      # 2
        self.font_btn = QPushButton('Font', self)
        self.color_btn.clicked.connect(lambda: self.open_dialog_func(self.color_btn))
        self.font_btn.clicked.connect(lambda: self.open_dialog_func(self.font_btn))

        self.h_layout = QHBoxLayout()
        self.h_layout.addWidget(self.color_btn)
        self.h_layout.addWidget(self.font_btn)
        self.v_layout = QVBoxLayout()
        self.v_layout.addWidget(self.text_edit)
        self.v_layout.addLayout(self.h_layout)
        self.setLayout(self.v_layout)

    def open_dialog_func(self, btn):
        if btn == self.color_btn:                        # 3
            color = QColorDialog.getColor()
            if color.isValid():
                self.text_edit.setTextColor(color)
        else:                                            # 4
            font, ok = QFontDialog.getFont()
            if ok:
                self.text_edit.setFont(font)

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

1. QTextEdit控件用于显示文本颜色和字体变化;

2. 实例化两个按钮分别用于打开颜色对话框和字体对话框,然后进行信号和槽的连接;

3. 如果是color_btn被按下的话,则调用QColorDialog的getColor()方法显示颜色对话框,当选择一种颜色后其十六进制的值会保存在color变量中,但如果点击对话框中的取消(Cancel)按钮的话,则color为无效值。通过isValid()方法判断color是否有效,若有效的话则通过setTextColor()方法设置QTextEdit的文本颜色;

4. 如果是font_btn被按下的话,则调用QFontDialog的getFont()方法显示字体对话框,该方法返回两个值,分别为QFont和bool值,如果用户选择了一种字体并按下确定(Ok)的话,则font保存所选择的QFont值,并且ok为True。若按下取消(Cancel)的话,则bool为False。当ok为True时,调用setFont()方法设置QTextEdit的文本字体。

注:字体对话框QFontDialog在Mac上不起作用。

2 输入对话框

输入对话框一共有五种输入方法:

下面请看示例:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QInputDialog, QLineEdit, QTextEdit, QPushButton, \
                            QGridLayout

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.name_btn = QPushButton('Name', self)                                       
        self.gender_btn = QPushButton('Gender', self)
        self.age_btn = QPushButton('Age', self)
        self.score_btn = QPushButton('Score', self)
        self.info_btn = QPushButton('Info', self)

        self.name_btn.clicked.connect(lambda: self.open_dialog_func(self.name_btn))
        self.gender_btn.clicked.connect(lambda: self.open_dialog_func(self.gender_btn))
        self.age_btn.clicked.connect(lambda: self.open_dialog_func(self.age_btn))
        self.score_btn.clicked.connect(lambda: self.open_dialog_func(self.score_btn))
        self.info_btn.clicked.connect(lambda: self.open_dialog_func(self.info_btn))

        self.name_line = QLineEdit(self)
        self.gender_line = QLineEdit(self)
        self.age_line = QLineEdit(self)
        self.score_line = QLineEdit(self)
        self.info_textedit = QTextEdit(self)

        self.g_layout = QGridLayout()
        self.g_layout.addWidget(self.name_btn, 0, 0, 1, 1)
        self.g_layout.addWidget(self.name_line, 0, 1, 1, 1)
        self.g_layout.addWidget(self.gender_btn, 1, 0, 1, 1)
        self.g_layout.addWidget(self.gender_line,1, 1, 1, 1)
        self.g_layout.addWidget(self.age_btn, 2, 0, 1, 1)
        self.g_layout.addWidget(self.age_line, 2, 1, 1, 1)
        self.g_layout.addWidget(self.score_btn, 3, 0, 1, 1)
        self.g_layout.addWidget(self.score_line, 3, 1, 1, 1)
        self.g_layout.addWidget(self.info_btn, 4, 0, 1, 1)
        self.g_layout.addWidget(self.info_textedit, 4, 1, 1, 1)
        self.setLayout(self.g_layout)

    def open_dialog_func(self, btn):
        if btn == self.name_btn:                    # 1
            name, ok = QInputDialog.getText(self, 'Name Input', 'Please enter the name:')
            if ok:
                self.name_line.setText(name)
        elif btn == self.gender_btn:                # 2
            gender_list = ['Female', 'Male']
            gender, ok = QInputDialog.getItem(self, 'Gender Input', 'Please choose the gender:', gender_list, 0, False)
            if ok:
                self.gender_line.setText(gender)    
        elif btn == self.age_btn:                   
            age, ok = QInputDialog.getInt(self, 'Age Input', 'Please select the age:')
            if ok:
                self.age_line.setText(str(age))
        elif btn == self.score_btn:                
            score, ok = QInputDialog.getDouble(self, 'Score Input', 'Please select the score:')
            if ok:
                self.score_line.setText(str(score))
        else:                                      
            info, ok = QInputDialog.getMultiLineText(self, 'Info Input', 'Please enter the info:')
            if ok:
                self.info_textedit.setText(info)

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

前面就是实例化按钮、单行输入框和文本编辑框并通过布局管理器进行排列,我们重点来看下槽函数:

1. 如果是name_btn被点击的话,则调用QInputDialog的getText(parent, str, str)方法,第一个参数为指定的父类,第二个为输入框的标题,第三个为输入框提示。方法会返回一个字符串和一个布尔值,若点击输入框的ok按钮,则变量ok就为True,接着我们调用QLineEdit的setText()方法将其文本设为所输入的内容;

2. getItem(parent, str, str, iterable, int, bool)方法需要多设置几个参数,前三个与getText()相同,第四个参数为要加入的选项内容,这里我们传入了item_list列表,可以让用户选择男性或女性。第五个参数为最初显示的选项,0代表刚开始显示第一个选项,即Female。最后一个参数是选项内容是否可编辑,这里设为False,不可编辑。

其他方法的使用都是类似的,这里就进行省略了。

点击Name按钮输入姓名:

3 文件对话框
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QTextEdit, QPushButton, QMessageBox, QVBoxLayout, \
                            QHBoxLayout, QFileDialog

class Demo(QWidget):
    def __init__(self):
        super(Demo, self).__init__()

        self.is_saved = True
        self.is_saved_first = True                      # 1
        self.path = ''                                  # 2

        self.textedit = QTextEdit(self)
        self.textedit.textChanged.connect(self.on_textchanged_func)

        self.button = QPushButton('Save', self)
        self.button.clicked.connect(self.on_clicked_func)
        self.button_2 = QPushButton('Open', self)       # 3
        self.button_2.clicked.connect(self.open_file_func)

        self.h_layout = QHBoxLayout()
        self.h_layout.addWidget(self.button)
        self.h_layout.addWidget(self.button_2)
        self.v_layout = QVBoxLayout()
        self.v_layout.addWidget(self.textedit)
        self.v_layout.addLayout(self.h_layout)
        self.setLayout(self.v_layout)

    def on_textchanged_func(self):
        if self.textedit.toPlainText():
            self.is_saved = False
        else:
            self.is_saved = True

    def on_clicked_func(self):                          # 4
        if self.is_saved_first:
            self.save_as_func(self.textedit.toPlainText())
        else:
            self.save_func(self.textedit.toPlainText())

    def save_func(self, text):
        with open(self.path, 'w') as f:
            f.write(text)
        self.is_saved = True

    def save_as_func(self, text):
        self.path, _ = QFileDialog.getSaveFileName(self, 'Save File', './', 'Files (*.txt *.log)')
        if self.path:
            with open(self.path, 'w') as f:
                f.write(text)
            self.is_saved = True
            self.is_saved_first = False

    def open_file_func(self):                           # 5
        file, _ = QFileDialog.getOpenFileName(self, 'Open File', './', 'Files (*.txt *.log)')
        if file:
            with open(file, 'r') as f:
                self.textedit.clear()
                self.textedit.setText(f.read())
                self.is_saved = True

    def closeEvent(self, QCloseEvent):
        if not self.is_saved:
            choice = QMessageBox.question(self, '', 'Do you want to save the text?',
                                          QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
            if choice == QMessageBox.Yes:               # 6
                self.on_clicked_func()
                QCloseEvent.accept()
            elif choice == QMessageBox.No:
                QCloseEvent.accept()
            else:
                QCloseEvent.ignore()


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

1. is_saved_first变量用于判断是否是第一次进行保存,如果是的话则会打开文件对话框让用户选择路径,填好文件名然后保存(即另存为)。如果不是第一次保存就说明之前保存过,那用户再点击保存按钮时程序就直接将文本保存在第一次保存时的路径;

2. path变量用于保存路径;

3. 新增了一个打开按钮,点击后可以打开文件对话框用于选择并打开文件;

4. 在on_clicked_func()槽函数中,首先判断是不是第一次保存,是的话调用save_as_func()函数进行另存为操作:

def save_as_func(self, text):
    self.path, _ = QFileDialog.getSaveFileName(self, 'Save File', './', 'Files (*.txt *.log)')
    if self.path:
        with open(self.path, 'w') as f:
            f.write(text)
        self.is_saved = True
        self.is_saved_first = False

在该函数中调用QFileDialog的getSaveFileName(parent, str, strt, str)方法,第一个参数为指定父类,第二个为文件对话框的标题,第三个为对话框打开时显示的路径,最后一个为文件扩展名过滤器——对话框只会显示该过滤器所提供的扩展名。当用户在对话框中选择好要保存的路径和文件名后,对话框返回绝对路径(我们保存在path中)和扩展过滤器,这里我们用不到后者,所以直接就保存在_中。有了绝对路径后我们就可以使用with open()方法保存文本内容了。

如果不是第一次保存的话,那说明path已经保存了第一次保存时的路径了,那我们就直接使用with open()方法进行保存就行了。

请注意要改变is_saved和is_saved_first的值。

5. 在open_file_func()槽函数中,我们调用QFileDialog的getOpenFileName(parent, str, str, str)方法,用法跟getSaveFileName()类似,这里不再详细讲解。当用户选择了要开打的文件后,file值保存绝对路径,我们用with open()方法打开读取,并将textedit的文本设为该文件内容。请小伙伴们思考下为什么这里要将is_save设为True。

6. 在关闭窗口时,若用户点击Yes,则保存逻辑跟on_clicked_func()槽函数一样,所以直接调用该槽函数就可以了(不过还是有点区别,因为此时is_saved和is_saved_first变量没必要修改了)。

二十一、主窗口

在较为大型复杂,功能较多的应用程序中,我们通常继承QMainWindow类来进行开发。该主窗口为搭建应用用户界面提供了非常好的框架,请看下图:

主窗口类提供了菜单栏(Menu Bar)、工具栏(Tool Bar)、控件停靠区域(Dock Widgets)和状态栏(Status Bar),可以往其中加入很多自己想要的东西,这也使得我们可以快速地开发一个功能复杂并且界面友好的应用程序。

本章主要介绍菜单栏、工具栏和状态栏,控件停靠区域会放在后续章节。

记事本应用:

import sys
import time
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtCore import QMimeData, Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QTextEdit, QAction, QFileDialog, QMessageBox,\
                            QFontDialog, QColorDialog, QSplashScreen

class Demo(QMainWindow):
    is_saved = True
    is_saved_first = True
    path = ''

    def __init__(self):
        super(Demo, self).__init__()
        self.file_menu = self.menuBar().addMenu('File')
        self.edit_menu = self.menuBar().addMenu('Edit')
        self.help_menu = self.menuBar().addMenu('Help')

        self.file_toolbar = self.addToolBar('File')
        self.edit_toolbar = self.addToolBar('Edit')

        self.status_bar = self.statusBar()

        self.new_action = QAction('New', self)
        self.open_action = QAction('Open', self)
        self.save_action = QAction('Save', self)
        self.save_as_action = QAction('Save As', self)
        self.close_action = QAction('Close', self)
        self.cut_action = QAction('Cut', self)
        self.copy_action = QAction('Copy', self)
        self.paste_action = QAction('Paste', self)
        self.font_action = QAction('Font', self)
        self.color_action = QAction('Color', self)
        self.about_action = QAction('Qt', self)

        self.text_edit = QTextEdit(self)

        self.mime_data = QMimeData()
        self.clipboard = QApplication.clipboard()

        self.setCentralWidget(self.text_edit)
        self.resize(450, 600)

        self.menu_init()
        self.toolbar_init()
        self.status_bar_init()
        self.action_init()
        self.text_edit_int()

    def menu_init(self):
        self.file_menu.addAction(self.new_action)
        self.file_menu.addAction(self.open_action)
        self.file_menu.addAction(self.save_action)
        self.file_menu.addAction(self.save_as_action)
        self.file_menu.addSeparator()
        self.file_menu.addAction(self.close_action)

        self.edit_menu.addAction(self.cut_action)
        self.edit_menu.addAction(self.copy_action)
        self.edit_menu.addAction(self.paste_action)
        self.edit_menu.addSeparator()
        self.edit_menu.addAction(self.font_action)
        self.edit_menu.addAction(self.color_action)

        self.help_menu.addAction(self.about_action)

    def toolbar_init(self):
        self.file_toolbar.addAction(self.new_action)
        self.file_toolbar.addAction(self.open_action)
        self.file_toolbar.addAction(self.save_action)
        self.file_toolbar.addAction(self.save_as_action)

        self.edit_toolbar.addAction(self.cut_action)
        self.edit_toolbar.addAction(self.copy_action)
        self.edit_toolbar.addAction(self.paste_action)
        self.edit_toolbar.addAction(self.font_action)
        self.edit_toolbar.addAction(self.color_action)

    def status_bar_init(self):
        self.status_bar.showMessage('Ready to compose')

    def action_init(self):
        self.new_action.setIcon(QIcon('images/new.ico'))
        self.new_action.setShortcut('Ctrl+N')
        self.new_action.setToolTip('Create a new file')
        self.new_action.setStatusTip('Create a new file')
        self.new_action.triggered.connect(self.new_func)

        self.open_action.setIcon(QIcon('images/open.ico'))
        self.open_action.setShortcut('Ctrl+O')
        self.open_action.setToolTip('Open an existing file')
        self.open_action.setStatusTip('Open an existing file')
        self.open_action.triggered.connect(self.open_file_func)

        self.save_action.setIcon(QIcon('images/save.ico'))
        self.save_action.setShortcut('Ctrl+S')
        self.save_action.setToolTip('Save the file')
        self.save_action.setStatusTip('Save the file')
        self.save_action.triggered.connect(lambda: self.save_func(self.text_edit.toHtml()))

        self.save_as_action.setIcon(QIcon('images/save_as.ico'))
        self.save_as_action.setShortcut('Ctrl+A')
        self.save_as_action.setToolTip('Save the file to a specified location')
        self.save_as_action.setStatusTip('Save the file to a specified location')
        self.save_as_action.triggered.connect(lambda: self.save_as_func(self.text_edit.toHtml()))

        self.close_action.setIcon(QIcon('images/close.ico'))
        self.close_action.setShortcut('Ctrl+E')
        self.close_action.setToolTip('Close the window')
        self.close_action.setStatusTip('Close the window')
        self.close_action.triggered.connect(self.close_func)

        self.cut_action.setIcon(QIcon('images/cut.ico'))
        self.cut_action.setShortcut('Ctrl+X')
        self.cut_action.setToolTip('Cut the text to clipboard')
        self.cut_action.setStatusTip('Cut the text')
        self.cut_action.triggered.connect(self.cut_func)

        self.copy_action.setIcon(QIcon('images/copy.ico'))
        self.copy_action.setShortcut('Ctrl+C')
        self.copy_action.setToolTip('Copy the text')
        self.copy_action.setStatusTip('Copy the text')
        self.copy_action.triggered.connect(self.copy_func)

        self.paste_action.setIcon(QIcon('images/paste.ico'))
        self.paste_action.setShortcut('Ctrl+V')
        self.paste_action.setToolTip('Paste the text')
        self.paste_action.setStatusTip('Paste the text')
        self.paste_action.triggered.connect(self.paste_func)

        self.font_action.setIcon(QIcon('images/font.ico'))
        self.font_action.setShortcut('Ctrl+T')
        self.font_action.setToolTip('Change the font')
        self.font_action.setStatusTip('Change the font')
        self.font_action.triggered.connect(self.font_func)

        self.color_action.setIcon(QIcon('images/color.ico'))
        self.color_action.setShortcut('Ctrl+R')
        self.color_action.setToolTip('Change the color')
        self.color_action.setStatusTip('Change the color')
        self.color_action.triggered.connect(self.color_func)

        self.about_action.setIcon(QIcon('images/about.ico'))
        self.about_action.setShortcut('Ctrl+Q')
        self.about_action.setToolTip('What is Qt?')
        self.about_action.setStatusTip('What is Qt?')
        self.about_action.triggered.connect(self.about_func)

    def text_edit_int(self):
        self.text_edit.textChanged.connect(self.text_changed_func)

    def text_changed_func(self):
        if self.text_edit.toPlainText():
            self.is_saved = False
        else:
            self.is_saved = True

    def new_func(self):
        if not self.is_saved and self.text_edit.toPlainText():
            choice = QMessageBox.question(self, '', 'Do you want to save the text?',
                                          QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
            if choice == QMessageBox.Yes:
                self.save_func(self.text_edit.toHtml())
                self.text_edit.clear()
                self.is_saved_first = True
            elif choice == QMessageBox.No:
                self.text_edit.clear()
            else:
                pass
        else:
            self.text_edit.clear()
            self.is_saved = False
            self.is_saved_first = True

    def open_file_func(self):
        if not self.is_saved:
            choice = QMessageBox.question(self, '', 'Do you want to save the text?',
                                          QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
            if choice == QMessageBox.Yes:
                self.save_func(self.text_edit.toHtml())
                file, _ = QFileDialog.getOpenFileName(self, 'Open File', './', 'Files (*.html *.txt *.log)')
                if file:
                    with open(file, 'r',encoding='utf-8') as f:
                        self.text_edit.clear()
                        self.text_edit.setText(f.read())
                        self.is_saved = True
            elif choice == QMessageBox.No:
                file, _ = QFileDialog.getOpenFileName(self, 'Open File', './', 'Files (*.html *.txt *.log)')
                if file:
                    with open(file, 'r',encoding='utf-8') as f:
                        self.text_edit.clear()
                        self.text_edit.setText(f.read())
                        self.is_saved = True
            else:
                pass
        else:
            file, _ = QFileDialog.getOpenFileName(self, 'Open File', './', 'Files (*.html *.txt *.log)')
            if file:
                with open(file, 'r',encoding='utf-8') as f:
                    self.text_edit.clear()
                    self.text_edit.setText(f.read())
                    self.is_saved = True

    def save_func(self, text):
        if self.is_saved_first:
            self.save_as_func(text)
        else:
            with open(self.path, 'w',encoding='utf-8') as f:
                f.write(text)
            self.is_saved = True

    def save_as_func(self, text):
        self.path, _ = QFileDialog.getSaveFileName(self, 'Save File', './', 'Files (*.html *.txt *.log)')
        if self.path:
            with open(self.path, 'w',encoding='utf-8') as f:
                f.write(text)
            self.is_saved = True
            self.is_saved_first = False

    def close_func(self):
        if not self.is_saved:
            choice = QMessageBox.question(self, 'Save File', 'Do you want to save the text?',
                                          QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
            if choice == QMessageBox.Yes:
                self.save_func(self.text_edit.toHtml())
                self.close()
            elif choice == QMessageBox.No:
                self.close()
            else:
                pass

    def closeEvent(self, QCloseEvent):
        if not self.is_saved:
            choice = QMessageBox.question(self, 'Save File', 'Do you want to save the text?',
                                          QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
            if choice == QMessageBox.Yes:
                self.save_func(self.text_edit.toHtml())
                QCloseEvent.accept()
            elif choice == QMessageBox.No:
                QCloseEvent.accept()
            else:
                QCloseEvent.ignore()

    def cut_func(self):
        self.mime_data.setHtml(self.text_edit.textCursor().selection().toHtml())
        self.clipboard.setMimeData(self.mime_data)
        self.text_edit.textCursor().removeSelectedText()

    def copy_func(self):
        self.mime_data.setHtml(self.text_edit.textCursor().selection().toHtml())
        self.clipboard.setMimeData(self.mime_data)

    def paste_func(self):
        self.text_edit.insertHtml(self.clipboard.mimeData().html())

    def font_func(self):
        font, ok = QFontDialog.getFont()
        if ok:
            self.text_edit.setFont(font)

    def color_func(self):
        color = QColorDialog.getColor()
        if color.isValid():
            self.text_edit.setTextColor(color)

    def about_func(self):
        QMessageBox.aboutQt(self, 'About Qt')


if __name__ == '__main__':
    app = QApplication(sys.argv)
    demo = Demo()
    demo.show()
    sys.exit(app.exec_())
self.file_menu = self.menuBar().addMenu('File')			#在菜单栏创建一组菜单
self.file_toolbar = self.addToolBar('File')				#在工具栏创建一组工具
self.status_bar = self.statusBar()						#创建一个状态栏
self.status_bar.showMessage('Ready to compose')			#状态栏显示内容
self.setCentralWidget(self.text_edit)					#设置中央区域的内容

self.new_action = QAction('New', self)					#创建一个命令
self.file_menu.addAction(self.new_action)				#在菜单栏/工具栏的一个分组组中添加动作
self.file_menu.addSeparator()							#添加分割线

self.new_action.setIcon(QIcon('images/new.ico'))		#设置命令的图标
self.new_action.setShortcut('Ctrl+N')					#设置快捷键
self.new_action.setToolTip('Create a new file')			#设置提示(鼠标停在上面,显示一个气泡)
self.new_action.setStatusTip('Create a new file')		#按下时在状态栏显示
self.new_action.triggered.connect(self.new_func)		#连接槽函数

二十二、装入更多控件

1 拆分窗口 QSplitter
QSplitter(self)		#实例化
.setOrientation(Qt.Vertical)	#设置为竖向拆分,(默认横向)
.addWidget(self.list_view)		#添加
.insertWidget(0, self.table_view)#插入窗口
.setSizes([300, 200, 200])		#设置每个的宽度
.count()						#返回拆分的数量

将QListView、QTreeView和QTableView放到拆分窗口中,三个视图全都只用QDirModel来显示文件目录。请看下方代码:

import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QSplitter, QListView, QTreeView, QTableView, QDirModel

class Demo(QSplitter):                                          # 1
    def __init__(self):
        super(Demo, self).__init__()
        self.dir_model = QDirModel(self)                        # 2

        self.list_view = QListView(self)                        # 3
        self.tree_view = QTreeView(self)
        self.table_view = QTableView(self)
        self.list_view.setModel(self.dir_model)
        self.tree_view.setModel(self.dir_model)
        self.table_view.setModel(self.dir_model)

        self.tree_view.doubleClicked.connect(self.show_func)    # 4

        # self.setOrientation(Qt.Vertical)                      # 5
        self.addWidget(self.list_view)
        self.addWidget(self.tree_view)
        self.insertWidget(0, self.table_view)
        self.setSizes([300, 200, 200])
        print(self.count())

    def show_func(self, index):
        self.list_view.setRootIndex(index)
        self.table_view.setRootIndex(index)

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

1. 继承QSplitter;

2. 实例化QDirModel模型;

3. 分别实例化QListView、QTreeView和QTableView,然后将这三个视图的模型设为dir_model;

4. 将QTreeView的doubleClicked信号和自定义的槽函数连接起来,QtAssistant中对该信号解释如下:

所以我们知道每当双击时,被双击项的索引就会保存在index中,而这个参数会传给槽函数:

def show_func(self, index):
    self.list_view.setRootIndex(index)
    self.table_view.setRootIndex(index)

在槽函数中我们调用setRootIndex()并传入index值,也就是说,每当我们双击QTreeView中的某项时,QListView和QTableView就会将该项的索引设为自身的根索引,并显示相应的目录结构;

5. 拆分窗口默认是水平的,可以调用setOrientation(Qt.Vertical)方法将其设为垂直方向。调用addWidget()方法将视图添加到拆分窗口中。insrtWidget(int, widget)可以将控件插入到相应的位置,第一个参数为要插入的索引位置,第二个参数为控件。setSizes(iterable)可以设置各个子控件的宽度(如果拆分窗口为垂直方向的话,则该方法会设置高度)。count()方法返回拆分窗口中的控件数量。

拖动拆分线可以随意改变子控件大小:

2 标签页(选项卡)窗口 QTabWidget
self.tab=QTabWidget()						#实例化
self.tab.addTab(self.tab1, 'Basic Info')	#添加一个标签页,名称为Basic info,内容为tab1
self.tab.addTab(self.tab3, QIcon('info.ico'), 'More Info')#带图标的标签
self.tab.currentIndex()						#获取当前所在的页面序号

我们来用QTabWidget来完成一个简单的信息填写程序:

import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QWidget, QTabWidget, QLabel, QLineEdit, QDateEdit,\
                            QComboBox, QTextEdit, QGridLayout

class Demo(QTabWidget):
    def __init__(self):
        super(Demo, self).__init__()
        self.tab1 = QWidget()                   # 1
        self.tab2 = QWidget()
        self.tab3 = QTextEdit()

        self.tab1_init()                        # 2
        self.tab2_init()

        self.addTab(self.tab1, 'Basic Info')    # 3
        self.addTab(self.tab2, 'Contact Info')
        self.addTab(self.tab3, QIcon('info.ico'), 'More Info')

        self.currentChanged.connect(lambda: print(self.currentIndex()))    # 4

    def tab1_init(self):
        name_label = QLabel('Name:', self.tab1)
        gender_label = QLabel('Gender:', self.tab1)
        bd_label = QLabel('Birth Date:', self.tab1)

        name_line = QLineEdit(self.tab1)
        items = ['Please choose your gender', 'Female', 'Male']
        gender_combo = QComboBox(self.tab1)
        gender_combo.addItems(items)
        bd_dateedit = QDateEdit(self.tab1)

        g_layout = QGridLayout()
        g_layout.addWidget(name_label, 0, 0, 1, 1)
        g_layout.addWidget(name_line, 0, 1, 1, 1)
        g_layout.addWidget(gender_label, 2, 0, 1, 1)
        g_layout.addWidget(gender_combo, 2, 1, 1, 1)
        g_layout.addWidget(bd_label, 3, 0, 1, 1)
        g_layout.addWidget(bd_dateedit, 3, 1, 1, 1)

        self.tab1.setLayout(g_layout)

    def tab2_init(self):
        tel_label = QLabel('Tel:', self.tab2)
        mobile_label = QLabel('Mobile:', self.tab2)
        add_label = QLabel('Address:', self.tab2)

        tel_line = QLineEdit(self.tab2)
        mobile_line = QLineEdit(self.tab2)
        add_line = QLineEdit(self.tab2)

        g_layout = QGridLayout()
        g_layout.addWidget(tel_label, 0, 0, 1, 1)
        g_layout.addWidget(tel_line, 0, 1, 1, 1)
        g_layout.addWidget(mobile_label, 1, 0, 1, 1)
        g_layout.addWidget(mobile_line, 1, 1, 1, 1)
        g_layout.addWidget(add_label, 2, 0, 1, 1)
        g_layout.addWidget(add_line, 2, 1, 1, 1)

        self.tab2.setLayout(g_layout)

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

1. 实例化两个QWidget窗口和一个QTextEdit控件,之后我们会在tab1_init()中往tab1窗口上添加控件并完成布局,在tab2_int()中往tab2窗口上添加控件并完成布局;

2. 在tab1_init()函数中,我们实例化三个QLabel控件分别显示‘Name:’,‘Gender:’和‘Birth Date:’文本。相应的,我们实例化力了一个QLineEdit控件用于输入姓名,一个QComboBox控件用于选择性别,还有一个QDateEdit控件用于选择出生日期。之后用网格布局管理器QGridLayout完成布局。而在tab2_init()函数中我们就实例化了三个QLabel控件和三个QLineEdit控件。用户可以在此输入电话,手机和地址信息;

3. 通过调用addTab(widget, str)方法就可以将控件添加到QTabWidget中并设置标签页的名字,当然我们也可以调用addTab(widget, QIcon, str)同时设置标签页的图标:self.addTab(self.tab3, QIcon('info.ico'), 'More Info')。除了addTab()方法,当然还有insertTab()方法(add和insert通常一起存在);

4. 每当用户点击不同标签页时,都会触发currentChanged信号,我们在这里进行信号和槽的连接,每当触发信号时,打印当前标签页的索引,而索引可通过currentIndex()方法获取。

运行截图如下:

3 堆叠窗口QStackedWidget

QStackedWidget用法跟QTabWdidget用法思想相似,只是界面样式不同。我们经常将QStackedWidget和QListWidget或者QListView搭配使用,请看示例:

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QStackedWidget, QLabel, QLineEdit, QDateEdit,\
                            QComboBox, QTextEdit, QListWidget, QGridLayout, QHBoxLayout

class Demo(QWidget):                                    # 1
    def __init__(self):
        super(Demo, self).__init__()
        self.stack1 = QWidget()
        self.stack2 = QWidget()
        self.stack3 = QTextEdit()

        self.stack1_init()
        self.stack2_init()

        self.stacked_widget = QStackedWidget(self)      # 2
        self.stacked_widget.addWidget(self.stack1)
        self.stacked_widget.addWidget(self.stack2)
        self.stacked_widget.addWidget(self.stack3)
        self.stacked_widget.currentChanged.connect(lambda: print(self.stacked_widget.currentIndex()))

        self.list_widget = QListWidget(self)            # 3
        self.list_widget.addItem('Basic Info')
        self.list_widget.addItem('Contact Info')
        self.list_widget.addItem('More Info')
        self.list_widget.clicked.connect(self.change_func)

        self.h_layout = QHBoxLayout()
        self.h_layout.addWidget(self.list_widget)
        self.h_layout.addWidget(self.stacked_widget)

        self.setLayout(self.h_layout)

    def stack1_init(self):
        name_label = QLabel('Name:', self.stack1)
        gender_label = QLabel('Gender:', self.stack1)
        bd_label = QLabel('Birth Date:', self.stack1)

        name_line = QLineEdit(self.stack1)
        items = ['Please choose your gender', 'Female', 'Male']
        gender_combo = QComboBox(self.stack1)
        gender_combo.addItems(items)
        bd_dateedit = QDateEdit(self.stack1)

        g_layout = QGridLayout()
        g_layout.addWidget(name_label, 0, 0, 1, 1)
        g_layout.addWidget(name_line, 0, 1, 1, 1)
        g_layout.addWidget(gender_label, 2, 0, 1, 1)
        g_layout.addWidget(gender_combo, 2, 1, 1, 1)
        g_layout.addWidget(bd_label, 3, 0, 1, 1)
        g_layout.addWidget(bd_dateedit, 3, 1, 1, 1)

        self.stack1.setLayout(g_layout)

    def stack2_init(self):
        tel_label = QLabel('Tel:', self.stack2)
        mobile_label = QLabel('Mobile:', self.stack2)
        add_label = QLabel('Address:', self.stack2)

        tel_line = QLineEdit(self.stack2)
        mobile_line = QLineEdit(self.stack2)
        add_line = QLineEdit(self.stack2)

        g_layout = QGridLayout()
        g_layout.addWidget(tel_label, 0, 0, 1, 1)
        g_layout.addWidget(tel_line, 0, 1, 1, 1)
        g_layout.addWidget(mobile_label, 1, 0, 1, 1)
        g_layout.addWidget(mobile_line, 1, 1, 1, 1)
        g_layout.addWidget(add_label, 2, 0, 1, 1)
        g_layout.addWidget(add_line, 2, 1, 1, 1)

        self.stack2.setLayout(g_layout)

    def change_func(self):
        self.stacked_widget.setCurrentIndex(self.list_widget.currentIndex().row())

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

1. 这里我们继承QWidget,将QStackedWidget和QListWidget放在该窗口中;

2. 实例化一个QStackedWidget窗口,将我们已经在stack1_init()和stack2_init()函数中设置好的窗口以及QTextEdit控件添加进去,并且进行信号和槽的连接,每当堆叠层发生变化时,打印相应的索引;

3. 实例一个QListWidget列表控件,添加三个子项'Basic Info',‘Contact Info’和‘More Info’,并且将clicked信号和槽函数连接起来:

def change_func(self):
    self.stacked_widget.setCurrentIndex(self.list_widget.currentIndex().row())

每当用户点击列表控件中不同行时,QStackedWidget调用setCurrentIndex(int)方法传入索引将显示界面设为相应的堆叠层;

4 停靠窗口QDockWidget

看过第二十三章的小伙伴应该知道了QDockWidget是要和QMainWindow一起搭配使用的。我们再次放上这张图:

可以看出浅蓝色部分为停靠窗口可以停靠的区域,不过我们得调用QDockWidget的setAllowedAreas()来可停靠的位置,可以传入的参数有:

除了设置停靠窗口的停靠位置,我们也还可以调用setFeatures()方法来设置停靠窗口的功能属性,可传入的参数有:

下面我们使用停靠窗口来完成以下界面:

import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QDockWidget, QTextEdit

class Demo(QMainWindow):
    def __init__(self):
        super(Demo, self).__init__()
        # 1
        self.dock1 = QDockWidget('Dock Window 1', self)
        self.dock2 = QDockWidget('Dock Window 2', self)
        self.dock3 = QDockWidget('Dock Window 3', self)

        # 2
        self.dock1.setAllowedAreas(Qt.RightDockWidgetArea | Qt.LeftDockWidgetArea)
        self.dock2.setAllowedAreas(Qt.RightDockWidgetArea | Qt.TopDockWidgetArea)
        self.dock3.setAllowedAreas(Qt.NoDockWidgetArea)

        # 3
        self.dock1.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable)
        self.dock2.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable)
        self.dock3.setFeatures(QDockWidget.DockWidgetClosable)

        # 4
        self.text1 = QTextEdit()
        self.text2 = QTextEdit()
        self.text3 = QTextEdit()

        self.dock1.setWidget(self.text1)
        self.dock2.setWidget(self.text2)
        self.dock3.setWidget(self.text3)

        # 5
        self.addDockWidget(Qt.RightDockWidgetArea, self.dock1)
        self.addDockWidget(Qt.RightDockWidgetArea, self.dock2)
        self.addDockWidget(Qt.RightDockWidgetArea, self.dock3)

        # 6
        self.center_text = QTextEdit()
        self.setCentralWidget(self.center_text)


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

1. 实例化三个停靠窗口,在实例化的时候直接设置好窗口的标题;

2. 设置三个停靠窗口的可停靠区域;

3. 设置三个停靠窗口的功能属性,因为我们在第二步中对dock3设置为不可停靠,所在这步中就只用设置可关闭的功能属性就好了;

4. 实例化三个文本编辑框QTextEdit,然后调用QDockWidget的setWidget()方法将文本编辑框放入停靠窗口中;

5. 调用主窗口的addDockWidget()方法将停靠窗口加入到主窗口中,注意要规定停靠窗口刚开始的停靠区域;

6. 设置主窗口的中央控件。


 

运行截图如下,我们改变第一个和第二个停靠窗口的停靠位置:

停靠窗口还可以合体,只需将其中一个窗口放在另一个上就可以了(〃 ̄︶ ̄)人( ̄︶ ̄〃):

5 多文档界面QMdiArea

当使用多文档界面功能时,我们是将QMdiArea作为主窗口的中央部件,然后在这个中央部件中,我们可以同时打开很多个子窗口QMdiSubWindow,就像下面这种多窗口状态:

import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QMainWindow, QMdiArea, QMdiSubWindow, QAction, QTextEdit

class Demo(QMainWindow):
    def __init__(self):
        super(Demo, self).__init__()
        self.mdi_area = QMdiArea(self)                                     # 1
        self.setCentralWidget(self.mdi_area)

        self.toolbar = self.addToolBar('Tool Bar')
    
        self.new_action = QAction(QIcon('images/new.ico'), 'New', self)    # 2
        self.close_action = QAction(QIcon('images/close.ico'), 'Close', self)
        self.close_all_action = QAction(QIcon('images/close_all.ico'), 'Close All', self)
        self.mode1_action = QAction(QIcon('cascade.ico'), 'Cascade', self)
        self.mode2_action = QAction(QIcon('tile.ico'), 'Tile', self)

        self.new_action.triggered.connect(self.new_func)                   # 3
        self.close_action.triggered.connect(self.mdi_area.closeActiveSubWindow)
        self.close_all_action.triggered.connect(self.mdi_area.closeAllSubWindows)
        self.mode1_action.triggered.connect(self.mdi_area.cascadeSubWindows)
        self.mode2_action.triggered.connect(self.mdi_area.tileSubWindows)

        self.toolbar.addAction(self.new_action)                            # 4
        self.toolbar.addAction(self.close_action)
        self.toolbar.addAction(self.close_all_action)
        self.toolbar.addAction(self.mode1_action)
        self.toolbar.addAction(self.mode2_action)

    def new_func(self):
        text = QTextEdit()
        sub = QMdiSubWindow()
        sub.setWidget(text)
        self.mdi_area.addSubWindow(sub)
        sub.show()

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

1. 实例化一个QMdiArea控件,并调用setCentralWidget()方法将其设为主窗口的中央部件;

2. 实例化五个动作分别用来新建窗口,关闭当前子窗口,关闭所有子窗口,将子窗口布局方式改为层叠布局以及将子窗口布局方式改为平铺布局;

3. 将各个动作进行信号和槽的连接。new_action的信号连接的槽函数为new_func():

def new_func(self):
    text = QTextEdit()
    sub = QMdiSubWindow()
    sub.setWidget(text)
    self.mdi_area.addSubWindow(sub)
    sub.show()

在该槽函数中,我们首先实例化一个文本编辑框QTextEdit和一个子窗口QMdiSubWindow,然后调用子窗口的setWidget()方法将文本编辑框放入子窗口中(类似QDockWidget用法),最后调用QMdiArea的addSubWindow()方法将子窗口加入到中央部件中,当然我们还要用show()方法将子窗口显示出来。close_action的信号连接的槽函数为QMdiArea的closeActiveSubWindow()方法,该方法可以关闭当前激活的窗口(最新的或者正在使用的窗口)。以下三个连接的槽函数分别为QMdiArea的closeAllSubWindow(),cascadeSubWindow()以及tileSubWindow()方法。第一个方法用来关闭所有的子窗口,第二个方法是将当前子窗口的布局改成层叠布局,而第三个方法是将子窗口的布局改成平铺布局;

4. 在这里我们只用调用addAction()方法将动作加入到工具栏上就行了。

运行截图如下,新建三个子窗口:

将布局方式改为层叠布局:

将布局方式改为平铺布局:

关闭一个激活的子窗口:

关闭所有子窗口:

二十三、Pyinstaller打包

二十四、数据库

二十五、多线程

二十六、QSS 样式表

二十七、国际化

二十八、网页交互

二十九、绘图与打印

三十、动画

三十一、音频与视频

三十二、图形视图框架

三十三、网络应用

三十六、用PyQtGraph绘制可视化数据图表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值