2.2消息框控件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
#实例化qpushbotton按钮控件
button = QPushButton('信息框')
button.clicked.connect(self.show_information)

h_layout = QHBoxLayout()
h_layout.addWidget(button)
self.setLayout(h_layout)

def show_information(self):
QMessageBox.information(self,'标题','内容',QMessageBox.Yes) # information(父类,信息框标题,信息框内容)
#如果显示多个按钮:QMessageBox.information(self, '标题', '内容', QMessageBox.Yes|QMessageBox.No),
#默认不填则yes。 Ok,Yes,No,Close,Cancel,Open,Save。
#询问框:QMessageBox.question()
#信息框:QMessageBox.information()
#警告框:QMessageBox.warning()
#错误框:QMessageBox.critical()
#关于框:QMessageBox.about()



if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())#关闭窗口时,app.exec()返回0并穿给sys.exit(),使python解释器正常退出。

2.2.2与消息框交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
#实例化qpushbotton按钮控件
self.button = QPushButton('信息框')
self.button.clicked.connect(self.show_information)

h_layout = QHBoxLayout()
h_layout.addWidget(self.button)#比2.2多了self
self.setLayout(h_layout)

def show_information(self):
choice = QMessageBox.question(self,'标题','内容',QMessageBox.Yes|QMessageBox.No) # information(父类,信息框标题,信息框内容)
#如果显示多个按钮:QMessageBox.information(self, '标题', '内容', QMessageBox.Yes|QMessageBox.No),
#默认不填则yes。 Ok,Yes,No,Close,Cancel,Open,Save。
#询问框:QMessageBox.question()
#信息框:QMessageBox.information()
#警告框:QMessageBox.warning()
#错误框:QMessageBox.critical()
#关于框:QMessageBox.about()
if choice == QMessageBox.Yes:
self.button.setText('hao')

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())#关闭窗口时,app.exec()返回0并穿给sys.exit(),使python解释器正常退出。

2.2.3编写带中文按钮的消息框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

#自定义消息框
class QuestionMessageBox(QMessageBox):
def __init__(self, parent, title, content): #初始化
super(QuestionMessageBox, self).__init__(parent)
self.setWindowTitle(title)
self.setText(content)
self.setIcon(QMessageBox.Question) #设置图标
#问号图标:QMessageBox.Question
#信息图标:QMessageBox.Information
#警告图标:QMessageBox.Warning
#错误图标:QMessageBox.Critical
#无图标: QMessageBox.NoIcon
self.addButton('是', QMessageBox.YesRole) #自定义按钮
self.addButton('否', QMessageBox.NoRole) #
#ok按钮:QMessageBox.AcceptRole
#cancel按钮:QMessageBox.RejectRole
#yes按钮:QMessageBox.YesRole
#No按钮:QMessageBox.NoRole

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
#实例化qpushbotton按钮控件
self.button = QPushButton('信息框')
self.button.clicked.connect(self.show_information)

h_layout = QHBoxLayout()
h_layout.addWidget(self.button)#比2.2多了self
self.setLayout(h_layout)

def show_information(self):
choice = QuestionMessageBox(self,'标题','内容') # information(父类,信息框标题,信息框内容)
choice.exec()#调用exec方法让消息框显示出来
if choice.clickedButton().text()=='是':
self.button.setText('hao')


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())#关闭窗口时,app.exec()返回0并穿给sys.exit(),使python解释器正常退出。

2.3文本框控件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.username_line = QLineEdit() #注释1开始
self.password_line = QLineEdit()

h_layout1 = QHBoxLayout()
h_layout2 = QHBoxLayout()
v_layout = QVBoxLayout() #注释1结束
h_layout1.addWidget(QLabel('Username:')) # 2
h_layout1.addWidget(self.username_line)
h_layout2.addWidget(QLabel('Password:'))
h_layout2.addWidget(self.password_line)
v_layout.addLayout(h_layout1)
v_layout.addLayout(h_layout2)
self.setLayout(v_layout)

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

2.4.4复选框按钮控件QCheckBox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.check_box1 = QCheckBox('Check 1')
self.check_box2 = QCheckBox('Check 2')
self.check_box3 = QCheckBox('Check 3')

self.check_box1.setChecked(True) #复选框默认选中和未选中
self.check_box2.setChecked(False)
self.check_box3.setTristate(True) #增加状态
self.check_box3.setCheckState(Qt.PartiallyChecked) #设置半选中状态,也可用来设置按钮的选中和未选中状态
#Qt.Uncheckd 未选中状态
#Qt.PartiallyChecked 半选中状态
#Qt.Checked 选中状态

self.check_box1.stateChanged.connect(self.show_state) #注释2开始
self.check_box2.stateChanged.connect(self.show_state)
self.check_box3.stateChanged.connect(self.show_state) #注释2结束

v_layout = QVBoxLayout()
v_layout.addWidget(self.check_box1)
v_layout.addWidget(self.check_box2)
v_layout.addWidget(self.check_box3)
self.setLayout(v_layout)

def show_state(self):
print(self.sender().checkState())


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

2.4.5可编辑的下拉框按钮控件QcomboBox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.combo_box = QComboBox()
self.combo_box.addItem('Louis')
self.combo_box.addItems(['Mike', 'Mary', 'John'])
self.combo_box.currentIndexChanged.connect(self.show_choice)

self.combo_box.setEditable(True) #设置可编辑的下拉框
self.line_edit = self.combo_box.lineEdit() #获取下拉框的内容
self.line_edit.textChanged.connect(self.show_edited_text) # 2

h_layout = QHBoxLayout()
h_layout.addWidget(self.combo_box)
self.setLayout(h_layout)

def show_choice(self):
print(self.combo_box.currentIndex())
print(self.combo_box.currentText())

def show_edited_text(self):
print(self.line_edit.text())


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

2.5.1液晶数字控件QLCDNumber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.lcd1 = QLCDNumber()
self.lcd1.setDigitCount(5) # 设置QLCDNumber控件的最大长度
self.lcd1.display(12345) # 通过display设置要显示的数字
self.lcd1.setMode(QLCDNumber.Dec) # 设置数字的进制
#QLCDNumber.Hex 十六进制
#QLCDNumber.Dec 十进制
#QLCDNumber.Oct 八进制
#QLCDNumber.Bin 二进制

self.lcd2 = QLCDNumber()
self.lcd2.setDigitCount(5)
self.lcd2.display(0.1234)
self.lcd2.setSegmentStyle(QLCDNumber.Flat) # 设置浮点数样式
#QLCDNumber.Outline 片段凸起,并用背景颜色填充
#QLCDNumber.Filled 片段凸起,并用前景颜色填充
#QLCDNumber.Flat 片段扁平,并用背景颜色填充

self.lcd3 = QLCDNumber()
self.lcd3.setDigitCount(5)
self.lcd3.display(123456789) # 显示对象大于所设置的长度,则显示为0

self.lcd4 = QLCDNumber()
self.lcd4.display('HELLO') # QLCNumber控件只能显示这些字符:
#A、B、C、D、E、F、h、H、L、o、P、r、u、U、Y、O/0、S/5、g/9。

v_layout = QVBoxLayout()
v_layout.addWidget(self.lcd1)
v_layout.addWidget(self.lcd2)
v_layout.addWidget(self.lcd3)
v_layout.addWidget(self.lcd4)
self.setLayout(v_layout)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

2.5.2数字调节框控件QSpinBox和QDoubleSpinBox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.spinbox = QSpinBox() # 调节整数
self.spinbox.setRange(-99, 99) # 调节整数范围
self.spinbox.setSingleStep(2) # 调节整数步长
self.spinbox.setValue(66) # 设置初始值
self.spinbox.valueChanged.connect(self.show_spinbox_value)

self.db_spinbox = QDoubleSpinBox() # 调节浮点数
self.db_spinbox.setRange(-99.99, 99.99)
self.db_spinbox.setSingleStep(1.5)
self.db_spinbox.setValue(66.66)
self.db_spinbox.valueChanged.connect(self.show_db_spinbox_value)

v_layout = QVBoxLayout()
v_layout.addWidget(self.spinbox)
v_layout.addWidget(self.db_spinbox)
self.setLayout(v_layout)

def show_spinbox_value(self):
print(self.spinbox.value())

def show_db_spinbox_value(self):
print(self.db_spinbox.value())


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

2.5.3滑动条控件QSlider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

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


class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.slider1 = QSlider() # 滑动条控件
self.slider1.setRange(0, 99) # 范围
self.slider1.setValue(66) # 初始值
self.slider1.setSingleStep(2) # 步长
self.slider1.valueChanged.connect(self.show_value)

self.slider2 = QSlider()
self.slider2.setOrientation(Qt.Horizontal) # 设置滑动条方向为水平
# Qt.Horizontal 水平方向
# Qt.Vertical 垂直方向

self.slider2.setMinimum(0)
self.slider2.setMaximum(99) #设置最大最小值,同setRange

self.slider3 = QSlider(Qt.Horizontal) # 实例化时直接传入滑动条方向
self.slider3.setRange(0, 99)
self.slider3.setTickPosition(QSlider.TicksBelow) # 增加刻度线
# QSlider.NoTicks 不添加刻度线
# QSlider.TicksBothSides 在滑动条两侧都添加刻度线
# QSlider.TicksAbove 在水平滑动条上方添加刻度线
# QSlider.TicksBelow 在水平滑动条方添加刻度线
# QSlider.TicksLeft 在垂直滑动条左侧添加刻度线
# QSlider.TicksRight 在垂直滑动条右侧添加刻度线
self.slider3.setTickInterval(10) # 设置刻度间隔

v_layout = QVBoxLayout()
v_layout.addWidget(self.slider1)
v_layout.addWidget(self.slider2)
v_layout.addWidget(self.slider3)
self.setLayout(v_layout)

def show_value(self):
print(self.slider1.value())

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

2.5.3滑动条控件QSlider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.dial = QDial()
self.dial.setRange(0, 365) #设置范围
self.dial.valueChanged.connect(self.show_value)

self.dial.setNotchesVisible(True) #是否显示刻度线
self.dial.setNotchTarget(10.5) #设置刻度之间的像素间隔(默认3.7像素)
#self.dial.setWrapping(True) #加这行代码可以让刻度线对仪表盘360°包裹

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

def show_value(self):
print(self.dial.value())

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

2.6.1日历控件QCalendarWidge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
"""
爬虫任务是耗时的,所以我们不能把爬虫代码放在UI主线程中执行,否则会导致界面无响应。正确的做法是将爬虫任务交给子线程来完成
"""

import requests
from parsel import Selector
from PyQt5.QtCore import *


class CrawlThread(QThread):
log_signal = pyqtSignal(str)
finish_signal = pyqtSignal()
data_signal = pyqtSignal(list)

def __init__(self, window):
super(CrawlThread, self).__init__()
self.window = window
self.flag = True

def run(self):
page_count = 0
total_page = self.window.page_spin_box.value()

self.flag = True
while page_count < total_page:
if self.flag:
page_count += 1
self.crawl_page(page_count)
else:
break

self.finish_signal.emit() # 传递爬取日志

def crawl_page(self, page_num):
self.log_signal.emit(f'当前正在爬取第{page_num}页')

page_url = f'https://quotes.toscrape.com/page/{page_num}/'
response = requests.get(page_url)

if 'No quotes found!' in response.text:
self.log_signal.emit('当前页面上没有名言了,不再继续爬取。')
self.stop()
return

selector = Selector(response.text)
quotes = selector.xpath('//div[@class="quote"]')

for quote in quotes:
content = quote.xpath('./span/text()').extract_first()
author = quote.xpath('./span/small/text()').extract_first()
tags = quote.xpath('./div[@class="tags"]/a/text()').extract()
tags = ';'.join(tags)
print([content, author, tags])
self.data_signal.emit([content, author, tags]) # 传递数据

def stop(self):
self.flag = False

2.6.2日期时间控件QDateTimeEdit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.datetime_edit = QDateTimeEdit(QDateTime.currentDateTime())
self.datetime_edit.setDisplayFormat('yyyy-MM-dd HH:mm:ss') # 1
self.datetime_edit.setDateRange(QDate(1949, 10, 1), QDate(6666, 6, 6))
self.datetime_edit.setCalendarPopup(True) # 2
self.datetime_edit.dateTimeChanged.connect(self.show_text) # 3

self.date_edit = QDateEdit(QDate.currentDate())
self.date_edit.setDisplayFormat('yyyy-MM-dd')
self.date_edit.setDateRange(QDate(1949, 10, 1), QDate(6666, 6, 6))
self.date_edit.dateChanged.connect(self.show_text)

self.time_edit = QTimeEdit(QTime.currentTime())
self.time_edit.setDisplayFormat('HH:mm:ss')
self.time_edit.setTimeRange(QTime(6, 6, 6), QTime(8, 8, 8))
self.date_edit.timeChanged.connect(self.show_text)

v_layout = QVBoxLayout()
v_layout.addWidget(self.datetime_edit)
v_layout.addWidget(self.date_edit)
v_layout.addWidget(self.time_edit)
self.setLayout(v_layout)

def show_text(self):
print(self.sender().text())

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

2.7.1定时器控件QTimer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.lcd = QLCDNumber() #显示时间
self.lcd.setSegmentStyle(QLCDNumber.Flat)
self.lcd.setDigitCount(20) #显示最大字符数
self.update_date_time() #显示当前时间(不会动)

self.timer = QTimer() #注释2开始
self.timer.start(1000) #每隔1000ms发射一次timeout信号
self.timer.timeout.connect(self.update_date_time) #链接的槽函数将新时间显示到QLCDNumber控件上

h_layout = QHBoxLayout()
h_layout.addWidget(self.lcd)
self.setLayout(h_layout)

def update_date_time(self):
date_time = QDateTime.currentDateTime().toString('yyyy-M-d hh:mm:ss')
self.lcd.display(date_time)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

2.7.2进度条控件QProgressBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.value = 0 # 用于保存当前进度值

self.timer = QTimer()
self.timer.start(100)
self.timer.timeout.connect(self.update_progress)

self.progress_bar1 = QProgressBar() #进度条1
self.progress_bar1.setRange(0, 100)
#self.progress_bar1.setRange(0, 0) #使进度条显示繁忙

self.progress_bar2 = QProgressBar()
self.progress_bar2.setTextVisible(False) #隐藏进度条2的数字
self.progress_bar2.setMinimum(0)
self.progress_bar2.setMaximum(100)
self.progress_bar2.setInvertedAppearance(True) #使进度条从右向左填满

v_layout = QVBoxLayout()
v_layout.addWidget(self.progress_bar1)
v_layout.addWidget(self.progress_bar2)
self.setLayout(v_layout)

def update_progress(self):
self.value += 1
self.progress_bar1.setValue(self.value)
self.progress_bar2.setValue(self.value)

if self.value == 100:
self.timer.stop()

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.1.1分组框控件QGroupBox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.letter_group_box = QGroupBox('字母') #直接放入名称
self.number_group_box = QGroupBox()
self.number_group_box.setTitle('数字') #使用setTitle放入名称

self.letter1 = QLabel('a')
self.letter2 = QLabel('b')
self.letter3 = QLabel('c')
self.number1 = QLabel('1')
self.number2 = QLabel('2')
self.number3 = QLabel('3')

letter_v_layout = QVBoxLayout()
number_v_layout = QVBoxLayout()
window_v_layout = QVBoxLayout()
letter_v_layout.addWidget(self.letter1)
letter_v_layout.addWidget(self.letter2)
letter_v_layout.addWidget(self.letter3)
number_v_layout.addWidget(self.number1)
number_v_layout.addWidget(self.number2)
number_v_layout.addWidget(self.number3)

self.letter_group_box.setLayout(letter_v_layout) #注释2开始
self.number_group_box.setLayout(number_v_layout) #注释2结束
window_v_layout.addWidget(self.letter_group_box) #注释3开始
window_v_layout.addWidget(self.number_group_box)
self.setLayout(window_v_layout) #注释3结束


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.1.2工具箱控件QToolBox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.letter_group_box = QGroupBox()
self.number_group_box = QGroupBox()

self.letter1 = QLabel('a')
self.letter2 = QLabel('b')
self.letter3 = QLabel('c')
self.number1 = QLabel('1')
self.number2 = QLabel('2')
self.number3 = QLabel('3')

letter_v_layout = QVBoxLayout()
number_v_layout = QVBoxLayout()
window_v_layout = QVBoxLayout()
letter_v_layout.addWidget(self.letter1)
letter_v_layout.addWidget(self.letter2)
letter_v_layout.addWidget(self.letter3)
number_v_layout.addWidget(self.number1)
number_v_layout.addWidget(self.number2)
number_v_layout.addWidget(self.number3)
self.letter_group_box.setLayout(letter_v_layout)
self.number_group_box.setLayout(number_v_layout)

self.tool_box = QToolBox()
self.tool_box.addItem(self.letter_group_box, '字母')
self.tool_box.insertItem(0, self.number_group_box, '数字')#0表示插入位置
self.tool_box.setItemIcon(0, QIcon('number.png')) #加图标
self.tool_box.setItemIcon(1, QIcon('letter.png')) #加图标
self.tool_box.currentChanged.connect(self.show_current_text) #信号会在抽屉被切换时发射

window_v_layout.addWidget(self.tool_box)
self.setLayout(window_v_layout)

def show_current_text(self):
index = self.tool_box.currentIndex()
print(self.tool_box.itemText(index))

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.2.1控件滚动区域QScrollArea

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.pic_scroll_area = QScrollArea() #实例化对象
self.btn_scroll_area = QScrollArea()

pic_label = QLabel()
pic_label.setPixmap(QPixmap('pyqt.jpg'))
self.pic_scroll_area.setWidget(pic_label) #显示图片
self.pic_scroll_area.ensureVisible(750, 750, 100, 100)#(坐标x,坐标y,边距x,边距y)

widget_for_btns = QWidget() #注释3开始
btn_h_layout = QHBoxLayout()
for i in range(100):
btn = QPushButton(f'按钮{i+1}')
btn_h_layout.addWidget(btn)
widget_for_btns.setLayout(btn_h_layout)
self.btn_scroll_area.setWidget(widget_for_btns)
self.btn_scroll_area.setAlignment(Qt.AlignCenter) #注释3结束

window_v_layout = QVBoxLayout()
window_v_layout.addWidget(self.pic_scroll_area)
window_v_layout.addWidget(self.btn_scroll_area)
self.setLayout(window_v_layout)

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.2.2滚动条控件QScrollBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
笔者会隐藏掉QScrollArea自带的水平滚动条,并实例化一个新的QScrollBar对象来代替它
"""

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

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.scroll_area = QScrollArea()
self.original_bar = self.scroll_area.horizontalScrollBar()

self.pic_label = QLabel()
self.pic_label.setPixmap(QPixmap('pyqt.jpg'))
self.scroll_area.setWidget(self.pic_label)

self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)# 隐藏水平滚动条
# Qt.ScrollBarAsNeeded 滚动条在鼠标有滚动操作时才会显示(macOS和Linux系统上的默认显示模式。
# Qt.ScrollBarAlwaysOff 不显示滚动条
# Qt.ScrollBarAlwaysOn 滚动条会一直显示(windows系统上的默认显示模式)

self.scroll_bar = QScrollBar() #实例化QScrollBar控件对象
self.scroll_bar.setOrientation(Qt.Horizontal) #设置水平滚动条,默认是垂直滚动条
self.scroll_bar.valueChanged.connect(self.move_bar)
self.scroll_bar.setMinimum(self.original_bar.minimum())
self.scroll_bar.setMaximum(self.original_bar.maximum())#实例化一个新的QScrollBar控件对象,用setOrientation(Qt.Horizontal)将它设置成水平滚动条(默认是垂直滚动条)。valuedChanged信号会在滚动条移动时发射。QScrollBar还有一个sliderMoved信号,它只有在用户使用鼠标按住并移动滚动条时才会发射,用鼠标滚轮移动时不会发射。槽函数通过value()方法获取到滚动条的值后,将其传递给了自带的滚动条original_bar。这样在移动scroll_bar时,original_bar会跟着移动(虽然看不见),图片也就会跟着移动了。自定义的scroll_bar的最大值、最小值要和原先自带的original_bar的一样,所以需要用setMinimum()和setMaximum()方法进行设置
# self.scroll_area.setHorizontalScrollBar(self.scroll_bar)# 3

v_layout = QVBoxLayout()
v_layout.addWidget(self.scroll_area)
v_layout.addWidget(self.scroll_bar)
self.setLayout(v_layout)

def move_bar(self):
value = self.scroll_bar.value()
self.original_bar.setValue(value)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.3.1拆分窗口控件QSplitter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.splitter = QSplitter()

self.text_edit1 = QTextEdit()
self.text_edit2 = QTextEdit()
self.text_edit3 = QTextEdit()
self.text_edit1.setPlaceholderText('edit 1')
self.text_edit2.setPlaceholderText('edit 2')
self.text_edit3.setPlaceholderText('edit 3')

self.splitter.addWidget(self.text_edit1)
self.splitter.insertWidget(0, self.text_edit2) #需要指定插入位置的索引0
self.splitter.addWidget(self.text_edit3)
self.splitter.setSizes([300, 200, 100]) # 列表中元素为各个控件宽度,没有设宽度的控件则不显示
self.splitter.setOpaqueResize(False) # False:拖拽动作防守后控件大小才改变 True:控件大小实时改变
self.splitter.setOrientation(Qt.Vertical) #垂直布局各个控件

window_h_layout = QHBoxLayout()
window_h_layout.addWidget(self.splitter)
self.setLayout(window_h_layout)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.3.2 标签页控件QTabWidget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
QTabWidget用来分页显示内容,它上面有一些标签。用户每单击一个标签就能够显示一个选项卡,
这样多个选项卡就可以共享一块区域,可以节省很多空间
"""

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

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.tab_widget = QTabWidget()

self.text_edit1 = QTextEdit()
self.text_edit2 = QTextEdit()
self.text_edit3 = QTextEdit()
self.text_edit1.setPlaceholderText('edit 1')
self.text_edit2.setPlaceholderText('edit 2')
self.text_edit3.setPlaceholderText('edit 3')

self.tab_widget.addTab(self.text_edit1, 'edit 1') #(控件,选项卡文本)
self.tab_widget.insertTab(0, self.text_edit2, 'edit 2')#(索引,控件,选项卡文本)
self.tab_widget.addTab(self.text_edit3, QIcon('edit.png'), 'edit 3') #(控件,图标,选项卡文本)

self.tab_widget.currentChanged.connect(self.show_tab_name)
self.tab_widget.setTabShape(QTabWidget.Triangular) # 设置选项卡形状,
# QTabWidget.Rounded 圆角(默认形状)
# QTabWidget.Triangular 三角
self.tab_widget.setTabPosition(QTabWidget.South)#设置选项卡位置,无这行代码则默认选项卡在上方
# QTabWidget.North 选项卡在窗口上方
# QTabWidget.West 选项卡在窗口左方
# QTabWidget.East 选项卡在窗口右方
# QTabWidget.South 选项卡在窗口下方
h_layout = QHBoxLayout()
h_layout.addWidget(self.tab_widget)
self.setLayout(h_layout)

def show_tab_name(self):
index = self.tab_widget.currentIndex()#获取当前选项卡的索引
print(self.tab_widget.tabText(index))

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.3.3 堆栈控件QStackedWidget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
堆栈控件和标签页控件相似,可以让多个界面共享同一块区域,不同的是,
堆栈控件不提供选项卡,而是将各个界面按照层级顺序上下摆放的。
QStackedWidget通常需要搭配其他控件来实现切换效果
"""

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

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.stacked_widget = QStackedWidget()

self.text_edit1 = QTextEdit()
self.text_edit2 = QTextEdit()
self.text_edit3 = QTextEdit()
self.text_edit1.setPlaceholderText('edit 1')
self.text_edit2.setPlaceholderText('edit 2')
self.text_edit3.setPlaceholderText('edit 3')

self.stacked_widget.addWidget(self.text_edit1) #注释1, QStackedWidget同样用addWidget()和insertWidget()添加控件
self.stacked_widget.insertWidget(0, self.text_edit2)
self.stacked_widget.addWidget(self.text_edit3) #注释1结束
self.stacked_widget.currentChanged.connect(self.show_text)# 2

self.btn1 = QPushButton('show edit 1') #注释3开始
self.btn2 = QPushButton('show edit 2')
self.btn3 = QPushButton('show edit 3')
self.btn1.clicked.connect(self.change_edit)
self.btn2.clicked.connect(self.change_edit)
self.btn3.clicked.connect(self.change_edit) #注释3结束

btn_h_layout = QHBoxLayout()
window_v_layout = QVBoxLayout()
btn_h_layout.addWidget(self.btn1)
btn_h_layout.addWidget(self.btn2)
btn_h_layout.addWidget(self.btn3)
window_v_layout.addLayout(btn_h_layout)
window_v_layout.addWidget(self.stacked_widget)
self.setLayout(window_v_layout)

def show_text(self):
edit = self.stacked_widget.currentWidget()
print(edit.placeholderText()) #打印占位符

def change_edit(self):
btn = self.sender()
if btn.text() == 'show edit 1':
self.stacked_widget.setCurrentIndex(1)
elif btn.text() == 'show edit 2':
self.stacked_widget.setCurrentIndex(0)
else:
self.stacked_widget.setCurrentIndex(2)

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.3.4 多文档区域控件QMdiArea

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
多文档区域控件QMdiArea(Multi-document Interface,MDI)提供了一块可以显示多个窗口的区域。
区域上的每一个窗口都属于QMdiSubWindow类,我们可以在各个窗口上设置各种控件
"""

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

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.mdi_area = QMdiArea() #实例化了一个多文档区域控件对象和4个按钮控件对象

self.new_btn = QPushButton('新建窗口')
self.close_btn = QPushButton('关闭全部')
self.tile_btn = QPushButton('平铺布局')
self.cascade_btn = QPushButton('层叠布局') #注释1结束
self.new_btn.clicked.connect(self.add_new_edit) # 2
self.close_btn.clicked.connect(self.close_all) # 3
self.tile_btn.clicked.connect(self.mdi_area.tileSubWindows)#注释4开始
self.cascade_btn.clicked.connect(self.mdi_area.cascadeSubWindows)#注释4结束
#多文档区域中的窗口有两种布局方式:
# 一种是平铺布局,窗口就像瓦片一样铺满整个区域,可以用tileSubWindows()方法实现;
# 另一种是层叠布局,一个窗口放在另一个窗口上,有遮挡关系,可以用cascadeSubWindows()方法实现。
v_layout = QVBoxLayout()
v_layout.addWidget(self.new_btn)
v_layout.addWidget(self.close_btn)
v_layout.addWidget(self.cascade_btn)
v_layout.addWidget(self.tile_btn)
v_layout.addWidget(self.mdi_area)
self.setLayout(v_layout)

def add_new_edit(self):
new_edit = QTextEdit()
sub_window = QMdiSubWindow()
sub_window.setWidget(new_edit)
self.mdi_area.addSubWindow(sub_window)
sub_window.show()

def close_all(self):
self.mdi_area.closeAllSubWindows() #该方法只是关闭窗口而已,窗口对象还是占内存的,所以要用deleterLater()方法将其彻底销毁
all_windows = self.mdi_area.subWindowList()
for window in all_windows:
window.deleteLater()
#QMdiArea区域中的所有窗口可以通过subWindowList()获取,该方法返回一个列表,其中的窗口元素默认按照创建时间排序

#QMdiArea.CreationOrder 按照创建时间排序(默认排序方式)
#QMdiAreaStackingOrder 按照堆叠方式排序,最前面的窗口排在列表最后一位
#QMdiArea.ActivationHistoryOrder 按照历史激活时间排序

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.4 列表视图控件、树形视图控件、表格视图控件

1
2
3
4
5
6
7
8
9
10
11
12
13
#PyQt的MVC结构。M表示模型(Model),跟数据定义和处理有关的东西全部由该部分完成。
#V表示视图(View),用来渲染、呈现数据。C表示控制器(Controller),这部分由代理(Delegate,也可以叫作委托)完成,它用来调节数据在视图上的呈现方式,可以实现更高级的功能


#数据模型

# QStringListModel 存储一个字符串列表
# QStandardltemModel 存储QStandardltem类型的数据,可以将QStandardltem看成一只小蜂·小蜜蜂可能带有花(有数据),也可能没有·而QStandardltemModel就是蜂巢,是各个小蜜蜂集合工作的场所
# QFileSystemModel 操作文本文件系统·以前用的是QDirModel,它已经被淘太了,建议用QFileSystemModel来代替
# QSqlQueryModel 操作SQL语句
# QSqlTableModel 操作SQL表
# QSqlRelationalTableModel 和QSqlTableModel-样用来操作SQL表,不过该模型还供外键支持

3.4.1 列表视图控件QListView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
QListView将存储在模型中的数据以列表形式呈现出来,列表中的各项内容一行行从上往下进行排列。
在示例代码3-10中,我们将学习如何使用QListView以及它常用的模型QStringListModel。
"""
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.left_model = QStringListModel() #注释1开始
self.right_model = QStringListModel() #注释1结束

self.left_list = [f'item {i}' for i in range(20)] #注释2开始
self.left_model.setStringList(self.left_list) #只有left_model通过setStringList()方法设置了数据,所以刚开始只有左边的视图有数据显示

self.left_list_view = QListView()
self.right_list_view = QListView()
self.left_list_view.setModel(self.left_model)
self.left_list_view.setEditTriggers(QAbstractItemView.SelectedClicked) #左、右两个视图设置列表各项内容的编辑属性
# QAbstractltemView.NoEditTriggers 不可编辑
# QAbstractltemView.CurrentChanged 选中项发生变换时可进行编辑
# QAbstractltemView.DoubleClicked 双击可进行编辑
# QAbstractltemView.SelectedClicked 单击已选中项时可进行编辑
# QAbstractltemView.EditKeyPressed 在选中项上按Enter键后可进行编辑
# OAbstractltemView.AnyKeyPressed 在选中项上按任何键后可进行编辑
# QAbstractltemView.AllEditTriggers 任何情况下都可进行编辑

self.left_list_view.doubleClicked.connect(self.choose) # 如果双击发生在左边的视图上,被双击项的内容就会显示到右边的视图上;而如果双击发生在右边的视图上,就会删除被双击项。
self.right_list_view.setModel(self.right_model)
self.right_list_view.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.right_list_view.doubleClicked.connect(self.cancel)

h_layout = QHBoxLayout()
h_layout.addWidget(self.left_list_view)
h_layout.addWidget(self.right_list_view)
self.setLayout(h_layout)

def choose(self): # QModelIndex就是货架上贴着的商品标签,该标签记录着商品所在的行列位置以及其他的商品信息
index = self.left_list_view.currentIndex() #获取用户当前双击项的QModelIndex索引对象
data = index.data()

row_count = self.right_model.rowCount() #计算行数
self.right_model.insertRow(row_count)
row_index = self.right_model.index(row_count) #用index()获取到该行的QModelIndex索引对象
self.right_model.setData(row_index, data)

def cancel(self): # 5
index = self.right_list_view.currentIndex() #获取到QModelIndex索引对象
row_numer = index.row() #通过它获取到行号
self.right_model.removeRow(row_numer) #再调用removeRow()删除对应的行

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.4.2 树形视图控件QTreeVie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
相较于QListView,QTreeView适合用来呈现有层级关系的数据,比如用来呈现某路径下的各个文件和目录。
"""
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.model = QFileSystemModel() #注释1开始
self.model.setRootPath('.') #确定目标路径,树形视图显示该路径下的文件和目录
self.model.setReadOnly(True) #调用setReadOnly(False)方法后,我们就可以通过双击来进行重命名操作了。

self.tree_view = QTreeView() #用来给路径展开和收缩操作添加动画
self.tree_view.setModel(self.model)#用来给路径展开和收缩操作添加动画
self.tree_view.setAnimated(True) #用来给路径展开和收缩操作添加动画
self.tree_view.header().setStretchLastSection(True) #header()方法获取树形视图的标题栏,setStretchLastSection(True)方法让标题栏的最后一列拉伸至充满表格
# setHeaderHidden(True) 隐藏标题栏
self.tree_view.doubleClicked.connect(self.show_info)# 3

h_layout = QHBoxLayout()
h_layout.addWidget(self.tree_view)
self.setLayout(h_layout)

def show_info(self):
index = self.tree_view.currentIndex()
self.tree_view.scrollTo(index) #scrollTo()用来将滚动条移动到文件或目录的位置,确保它能够显示出来
self.tree_view.expand(index) #如果单击的是目录,使用expand()方法就会展开它

file_name = self.model.fileName(index) #名称
file_path = self.model.filePath(index) #路径
file_size = self.model.size(index) #大小
print(file_name, file_path, file_size)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.4.3 表格视图控件QTableView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
QTableView是一个使用频率非常高的控件,它以表格形式呈现内容,通常和QStandardItemModel搭配使用。
我们可以在QStandardItemModel模型上设置行列数,并在特定行列位置上添加QStandardItem对象,该对象中包含目标数据。
表格视图会根据QStandardItemModel的属性生成同等行列数的表格来显示数据
"""
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.model = QStandardItemModel() #注释1开始
self.model.setColumnCount(6) #设置行数和列数
self.model.setRowCount(6)
self.model.setHorizontalHeaderLabels(['第1列', '第2列', '第3列','第4列', '第5列', '第6列'])#注释1结束
#setVerticalHeaderLabels() 行标题栏文本
for row in range(6): #注释2开始
for column in range(6):
item = QStandardItem(f'({row}, {column})')
item.setTextAlignment(Qt.AlignCenter) #数据在表格的单元格中居中显示
self.model.setItem(row, column, item)

self.new_items = [QStandardItem(f'(6, {column})') for column in range(6)]
self.model.appendRow(self.new_items)# 接受一个QStandardItem对象列表,调用该方法会在表格末尾新增一行,
#如果要新增一列,则要调用appendColumn()

self.table = QTableView() #表格视图有水平和垂直两种标题栏,分别可用verticalHeader()和horizontalHeader()方法获取到
self.table.setModel(self.model)
self.table.verticalHeader().hide() #针对垂直标题栏,我们调用hide()方法将其隐藏。针对水平标题栏,我们让它的最后一列自适应拉伸,不然留出空白不好看。
self.table.horizontalHeader().setStretchLastSection(True)
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers) #setEditTriggers()用来设置表格中单元格的编辑属性,这里设置为不可编辑
self.table.clicked.connect(self.show_cell_info) #注释3结束

h_layout = QHBoxLayout()
h_layout.addWidget(self.table)
self.setLayout(h_layout)

def show_cell_info(self):
index = self.table.currentIndex() #在槽函数中,我们先获取到单元格的索引对象,再调用data()方法获取到单元格上的数据
data = index.data()
print(data)



if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.5.1 简化版列表视图控件QListWidge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.left_list_widget = QListWidget() #注释1开始
self.right_list_widget = QListWidget()
self.left_list_widget.doubleClicked.connect(self.choose)
self.right_list_widget.doubleClicked.connect(self.cancel)
self.left_list_widget.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.right_list_widget.setEditTriggers(QAbstractItemView.NoEditTriggers)#注释1结束

for i in range(20): #注释2开始
item = QListWidgetItem(f'item {i}')
self.left_list_widget.addItem(item)#注释2结束

h_layout = QHBoxLayout()
h_layout.addWidget(self.left_list_widget)
h_layout.addWidget(self.right_list_widget)
self.setLayout(h_layout)

def choose(self): # 3
item = self.left_list_widget.currentItem()
new_item = QListWidgetItem(item)
self.right_list_widget.addItem(new_item)

def cancel(self): # 4
row = self.right_list_widget.currentRow()
self.right_list_widget.takeItem(row)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.5.2 简化版树形视图控件QTreeWidget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.tree = QTreeWidget()
self.item1 = QTreeWidgetItem() #注释1开始
self.item2 = QTreeWidgetItem()
self.item3 = QTreeWidgetItem()
self.item1.setText(0, '第1章')
self.item2.setText(0, '第1节')
self.item3.setText(0, '第1段') #注释1结束
self.item1.setCheckState(0, Qt.Unchecked) #注 如果一个项调用了setCheckState()方法,那么它前面就会显示一个复选框
self.item2.setCheckState(0, Qt.Unchecked)
self.item3.setCheckState(0, Qt.Unchecked) #注释2结束
self.item1.addChild(self.item2) #添加2是1的子类
self.item2.addChild(self.item3)

self.tree.addTopLevelItem(self.item1) #将最顶层的父项添加到树形视图
self.tree.setHeaderLabel('PyQt教程') #setHeaderLabel()用来设置树形视图的标题栏,如果标题栏存在多列,则可以调用setHeaderLabels()方法。
self.tree.clicked.connect(self.click_slot) #注释3结束

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

def click_slot(self): # 4
item = self.tree.currentItem() #获取当前单击的项
print(item.text(0)) #调用text()获取项上的文本

if item == self.item1:
if self.item1.checkState(0) == Qt.Checked:
self.item2.setCheckState(0, Qt.Checked)
self.item3.setCheckState(0, Qt.Checked)
else:
self.item2.setCheckState(0, Qt.Unchecked)
self.item3.setCheckState(0, Qt.Unchecked)
# click_slot()槽函数通过currentItem()方法获取到当前单击的项,然后调用text()获取项上的文本,记得要传入列号。
# if逻辑判断下的代码实现了全选/全不选的效果,如果用户勾选了最顶层父项左边的复选框,那么子项的复选框也会一并被勾选;
# 反之,则全部取消勾选。

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.5.3 简化版表格视图控件QTableWidget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.table = QTableWidget() #注释1开始
self.table.setColumnCount(6)
self.table.setRowCount(6)
self.table.verticalHeader().hide()
self.table.clicked.connect(self.show_cell_info)
self.table.horizontalHeader().setStretchLastSection(True)
self.table.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.table.setHorizontalHeaderLabels(['第1列', '第2列', '第3列','第4列', '第5列', '第6列'])#注释1结束
for row in range(6):
for column in range(6):
item = QTableWidgetItem(f'({row}, {column})')#注释2开始
item.setTextAlignment(Qt.AlignCenter)
self.table.setItem(row, column, item) #注释2结束

row_count = self.table.rowCount() #获取行数
self.table.setRowCount(row_count+1) #设置总行数
for column in range(6):
self.table.setItem(6, column, QTableWidgetItem(f'(6, {column})'))

h_layout = QHBoxLayout()
h_layout.addWidget(self.table)
self.setLayout(h_layout)

def show_cell_info(self): # 4
item = self.table.currentItem()
print(item.text())

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.6.1 颜色对话框控件QColorDialog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
颜色对话框控件用来让用户选择颜色,选择完毕后颜色就会被设置在目标内容(比如文本、背景等)上
"""


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

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.text_edit = QTextEdit()
self.btn = QPushButton('显示颜色对话框')
self.btn.clicked.connect(self.set_color)

v_layout = QVBoxLayout()
v_layout.addWidget(self.text_edit)
v_layout.addWidget(self.btn)
self.setLayout(v_layout)

def set_color(self): # QColorDialog调用getColor()方法弹出颜色对话框,返回的值是QColor类型的
color = QColorDialog.getColor()
if color.isValid():
print(color.name())
print(color.red(), color.green(), color.blue())
self.text_edit.setTextColor(color)
# 在将颜色设置到文本编辑框之前,需要先调用isValid()方法判断用户是否选择了颜色,
# 如果没有选择,而是单击“关闭”按钮或“Cancel”按钮,那么isValid()就会返回False。
# name()方法用于获取颜色的名称,格式是“#RRGGBB”。red()、green()和blue()分别用来获取红、绿、蓝3个颜色通道的值。

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.6.2 字体对话框控件QFontDialog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.text_edit = QTextEdit()
self.btn = QPushButton('显示字体对话框')
self.btn.clicked.connect(self.set_font)

v_layout = QVBoxLayout()
v_layout.addWidget(self.text_edit)
v_layout.addWidget(self.btn)
self.setLayout(v_layout)

def set_font(self): #
font, is_ok = QFontDialog.getFont() # QFontDialog调用getFont()方法弹出字体对话框
if is_ok: # 返回元组,元组中的第一个元素是QFont类型的,第二个元素是布尔类型的
print(font.family())
print(font.pointSize())
self.text_edit.setFont(font) # 如果值为True,表明用户选择了一种字体,如果没有选择,而是单击“关闭”按钮或“Cancel”按钮,那么is_ok的值就是False。
# family()和pointSize()方法分别用来返回字体的名称和大小


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.6.3 输入对话框控件QInputDialog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.name_line_edit = QLineEdit() #注释1开始
self.gender_line_edit = QLineEdit()
self.age_line_edit = QLineEdit()
self.score_line_edit = QLineEdit()
self.note_text_edit = QTextEdit()

self.name_btn = QPushButton('姓名')
self.gender_btn = QPushButton('性别')
self.age_btn = QPushButton('年龄')
self.score_btn = QPushButton('分数')
self.note_btn = QPushButton('备注') #注释1结束
self.name_btn.clicked.connect(self.get_name)
self.gender_btn.clicked.connect(self.get_gender)
self.age_btn.clicked.connect(self.get_age)
self.score_btn.clicked.connect(self.get_score)
self.note_btn.clicked.connect(self.get_note)

g_layout = QGridLayout()
g_layout.addWidget(self.name_btn, 0, 0)
g_layout.addWidget(self.name_line_edit, 0, 1)
g_layout.addWidget(self.gender_btn, 1, 0)
g_layout.addWidget(self.gender_line_edit, 1, 1)
g_layout.addWidget(self.age_btn, 2, 0)
g_layout.addWidget(self.age_line_edit, 2, 1)
g_layout.addWidget(self.score_btn, 3, 0)
g_layout.addWidget(self.score_line_edit, 3, 1)
g_layout.addWidget(self.note_btn, 4, 0)
g_layout.addWidget(self.note_text_edit, 4, 1)
self.setLayout(g_layout)

def get_name(self): # 2
name, is_ok = QInputDialog.getText(self, '姓名', '请输入姓名')
if is_ok:
self.name_line_edit.setText(name)

def get_gender(self): # 传入一个列表作为下拉列表框的内容选项
gender_list = ['Female', 'Male']
gender, is_ok = QInputDialog.getItem(self, '性别', '请选择性别',gender_list , 1 ,True)
# 数值1表示最初显示下拉列表框中索引为1的选项,也就是'Male'。False表示下拉列表框中的选项不可被编辑。
if is_ok:
self.gender_line_edit.setText(gender)

def get_age(self):
age, is_ok = QInputDialog.getInt(self, '年龄', '请输入年龄', 18 ,0 ,120 )
# 数值18是初始值,0是最小值,120是最大值。
if is_ok:
self.age_line_edit.setText(str(age))

def get_score(self): # 5
score, is_ok = QInputDialog.getDouble(self, '分数', '请输入分数')
# getDouble()方法也可以设置当前值、最小值和最大值,不过需要传入浮点数。
if is_ok:
self.score_line_edit.setText(str(score))

def get_note(self):
note, is_ok = QInputDialog.getMultiLineText(self, '备注', '请输入备注')
if is_ok:
self.note_text_edit.setText(note)



if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

3.6.4 文件对话框控件QFileDialog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
文件对话框控件可以帮助我们快速实现打开文件和保存文件的功能,一般桌面应用都会用到文件对话框控件
"""

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

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.edit = QTextEdit()
self.open_folder_btn = QPushButton('打开文件夹')
self.open_file_btn = QPushButton('打开文件')
self.save_as_btn = QPushButton('另存为')
self.open_folder_btn.clicked.connect(self.open_folder)
self.open_file_btn.clicked.connect(self.open_file)
self.save_as_btn.clicked.connect(self.save_as)

btn_h_layout = QHBoxLayout()
window_v_layout = QVBoxLayout()
btn_h_layout.addWidget(self.open_folder_btn)
btn_h_layout.addWidget(self.open_file_btn)
btn_h_layout.addWidget(self.save_as_btn)
window_v_layout.addWidget(self.edit)
window_v_layout.addLayout(btn_h_layout)
self.setLayout(window_v_layout)

def open_folder(self): # 用来选择一个文件夹,返回值是文件夹路径
folder_path = QFileDialog.getExistingDirectory(self, '打开文件夹', './')
self.edit.setText(folder_path)

def open_file(self): # 2
file_path, filter = QFileDialog.getOpenFileName(self, '打开文件', './', '格式 (*.txt *.log)')# 用来选择单个文件,我们可以往该方法中传入一个过滤器。
if file_path: # 用户无法选择被过滤掉的文件。“格式 (*.txt *.log)”表示用户只能在对话框中选择.txt和.log格式的文件。
with open(file_path, 'r') as f: # 如果要设置多个过滤器,可以用“;;”来进行连接,比如:“格式 (*.txt *.log);;Images (*.png *.jpg)”。
self.edit.setText(f.read())
# getOpenFileName()方法的返回值是一个元组,它的第一个元素是用户选择的文件路径,第二个元素是过滤器。
# 如果要打开多个文件,可以使用getOpenFileNames()。

def save_as(self): # 3
save_path, filter = QFileDialog.getSaveFileName(self, '另存为', './','格式 (*.txt *.log)')
if save_path:
with open(save_path, 'w') as f:
f.write(self.edit.toPlainText())
# getSaveFileName()方法用来获取用户设置的文件保存路径,传入的过滤器“格式 (*.txt *.log)”表示用户只能将文件保存为.txt和.log格式。

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

第4章 深入窗口

在之前的章节中,所有的窗口都是基于QWidget类的。其实在PyQt中,任何一个控件都可以看作一个窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QLineEdit):
def __init__(self):
super(Window, self).__init__()
self.btn = QPushButton('button')

h_layout = QHBoxLayout()
h_layout.addWidget(self.btn)
self.setLayout(h_layout)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.1.1 窗口标题和图标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.setWindowTitle('我的软件') # 设置窗口标题
self.setWindowIcon(QIcon('code.png')) # 设置窗口图标,传入的是一个Qicon对象

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.1.2 窗口大小和位置-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(600, 400) # 传入宽度值和高度值就可以设置窗口的初始显示大小
self.move(0, 0) # 坐标(0, 0)表示窗口显示在计算机桌面左上角
self.setGeometry(0, 0, 600, 400) # resize()和move()方法合并后等效于setGeometry()
# setMinimumwidth() 设置最小宽度
# setMaximumWidth() 设置最大宽度
# setMinimumHeight()设置最小高度
# setMaximumHeight()设置最大高度
# setMinimumSize() 设置最小尺寸
# setMaximumSize() 设置最大尺寸
# width() 宽度
# height() 获取高度
# size() 获取尺寸
# minimumWidth() 获取最小宽度
# maximumWidth() 获取最大宽度
# minimumHeight() 获取最小高度
# maximumHeight() 获取最大高度
# minimumSize() 获取最小尺寸
# maximumSize() 获取最大尺寸
# x() 获取x坐标
# y() 取y坐标
# pos() 获取X和y坐标,返回一个QPoin类型的对象
# geometry() 获取坐标和尺寸,返回一个QRec类型的对象
# frameSize() 获取尺寸 (包含窗口边框)
# frameGeometry() 获取坐标和尺寸(包含窗口边框),返回一个QRec类型的对象
# setFixedSize() 窗口大小固定
# setFixedWidth() 固定窗口宽度
# setFixedHeight() 固定窗口高度
# setFixedSize() 窗口大小固定
# setFixedWidth() 固定窗口宽度
# setFixedHeight() 固定窗口高度

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.1.2 窗口大小和位置-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(200, 200) #注释1开始

desktop = QApplication.desktop() # 获取桌面对象desktop,通过width()和height()方法获取计算机屏幕的宽度和高度
desktop_width = desktop.width()
desktop_height = desktop.height() #注释1结束

window_width = self.frameSize().width()#注释2开始
window_height = self.frameSize().height()

x = desktop_width // 2 - window_width // 2 #之所以用整除运算符“//”,是因为move()方法接收整型值,而不是浮点数。
y = desktop_height // 2 - window_height // 2
self.move(x, y) #注释2结束
# setMinimumwidth() 设置最小宽度
# setMaximumWidth() 设置最大宽度
# setMinimumHeight()设置最小高度
# setMaximumHeight()设置最大高度
# setMinimumSize() 设置最小尺寸
# setMaximumSize() 设置最大尺寸
# width() 取度
# height() 获取高度
# size() 获取尺寸
# minimumWidth() 获取最小宽度
# maximumWidth() 获取最大宽度
# minimumHeight() 获取最小高度
# maximumHeight() 获取最大高度
# minimumSize() 获取最小尺寸
# maximumSize() 获取最大尺寸
# x() 获取x坐标
# y() 取y坐标
# pos() 获取X和y坐标,返回一个QPoin类型的对象
# geometry() 获取坐标和尺寸,返回一个QRec类型的对象
# frameSize() 获取尺寸 (包含窗口边框)
# frameGeometry() 获取坐标和尺寸(包含窗口边框),返回一个QRec类型的对象
# setFixedSize() 窗口大小固定
# setFixedWidth() 固定窗口宽度
# setFixedHeight() 固定窗口高度

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.1.3 其他窗口属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.setWindowOpacity(0.8) # 设置窗口的透明度,传入0~1的数,1表示不透明
self.setWindowFlag(Qt.FramelessWindowHint) # 将窗口的标题栏和边框都去掉
self.setAttribute(Qt.WA_TranslucentBackground) # 让窗口背景完全透明(不会影响到窗口上的控件)

self.another_window = AnotherWindow()

self.btn = QPushButton('显示另一个窗口')
self.btn.clicked.connect(self.another_window.show)

h_layout = QHBoxLayout()
h_layout.addWidget(self.btn)
self.setLayout(h_layout)

class AnotherWindow(QWidget):
def __init__(self):
super(AnotherWindow, self).__init__()
self.setWindowModality(Qt.ApplicationModal) # 模态类型
# Qt.NonModal 设置为非棋态,这是窗口的默认模态类型,表示用户可以在应用中的各个窗口之间随意切换
# Qt.WindowModal 窗口级棋态, 阻塞当前窗口的所有父窗口和祖父窗口,包括与父窗口和祖父窗口同级的其他窗口
# Qt.ApplicationModal 应用级模态, 阻除当前窗口外的所有窗口,用户只有关闭了当前窗口,才能切换到其他的窗口

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.2.1 理解坐标体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

PyQt的坐标体系是统一的。不管是计算机屏幕,还是窗口本身,抑或是窗口上的控件,其
坐标原点(0, 0)都在左上角,且向右为x轴正方向,向下为y轴正方向。另外,锚点也统一位于左上角
"""

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

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(400, 400)
self.move(0, 0) # 窗口位于屏幕的坐标原点

self.edit = QTextEdit(self)
self.edit.move(0, 0) # 文本编辑框位于窗口的坐标原点

self.btn = QPushButton('button', self.edit)
self.btn.move(20, 20) # 按钮则位于文本编辑框上坐标为(20, 20)的位置

# width() 宽度
# height() 获取高度
# size() 获取尺寸
# minimumWidth() 获取最小宽度
# maximumWidth() 获取最大宽度
# minimumHeight() 获取最小高度
# maximumHeight() 获取最大高度
# minimumSize() 获取最小尺寸
# maximumSize() 获取最大尺寸
# x() 获取x坐标
# y() 取y坐标
# pos() 获取X和y坐标,返回一个QPoin类型的对象
# geometry() 获取坐标和尺寸,返回一个QRec类型的对象
# frameSize() 获取尺寸 (包含窗口边框)
# frameGeometry() 获取坐标和尺寸(包含窗口边框),返回一个QRec类型的对象
# setFixedSize() 窗口大小固定
# setFixedWidth() 固定窗口宽度
# setFixedHeight() 固定窗口高度
# setFixedSize() 窗口大小固定
# setFixedWidth() 固定窗口宽度
# setFixedHeight() 固定窗口高度


# 窗口坐标原点并不在标题栏的左上角,而是处于放置控件区域(客户区)的左上角,但是窗口锚点在标题栏的左上角。


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.3.1 窗口关闭事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.is_saved = True # 用来存储当前的保存状态,如果edit文本编辑框内容有更新且文本存在,那么is_saved为False
#当我们单击“保存”按钮后,is_saved变量被设置为True,且项目路径下会出现一个saved.txt文件

self.edit = QTextEdit()
self.edit.textChanged.connect(self.update_save_status)
self.save_btn = QPushButton('保存')
self.save_btn.clicked.connect(self.save)

v_layout = QVBoxLayout()
v_layout.addWidget(self.edit)
v_layout.addWidget(self.save_btn)
self.setLayout(v_layout)

def update_save_status(self):
if self.edit.toPlainText():
self.is_saved = False
else:
self.is_saved = True

def save(self):
self.is_saved = True
with open('saved.txt', 'w') as f:
f.write(self.edit.toPlainText())

def closeEvent(self, event): # 重点是closeEvent(),注意因为是重写,所以函数名称必须一样。
if not self.is_saved:
choice = QMessageBox.question(self, '', '是否保存文本内容?',QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
# 首先判断当前内容是否被保存,如果没有的话,则弹出一个消息框进行询问
if choice == QMessageBox.Yes:
self.save()
event.accept() # 调用event.accept()接受这次关闭操作
elif choice == QMessageBox.No:
event.accept()
else:
event.ignore() # 调用event.ignore()忽略关闭操作

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.3.2 窗口大小调整事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


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

def resizeEvent(self, event): # 1
print('调整前大小:', event.oldSize())
print('调整后大小:', event.size())

# 代码很简单,该事件主要有两个方法:oldSize()和size()。
# 前者用来获取窗口调整前的大小,后者用来获取窗口调整后的大小,两个方法返回的都是QSize对象。
# 窗口只要开始显示,就会触发窗口大小调整事件。

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.3.3 键盘事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
键盘事件分为两种:
键盘按下事件和键盘释放事件,各个事件函数的名称和解释罗列如下。
● keyPressEvent:键盘上的任意键被按下时触发。
● keyReleaseEvent:键盘上的任意键被释放时触发。

"""

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

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

def keyPressEvent(self, event): # 1
if event.key() == Qt.Key_A:
print('a')
if event.text().lower() == 'b': # 如果要获取按键名称,则需要调用text()方法
print('b') # 注意,如果开启了大写字母锁定功能,text()方法会返回大写键名
if event.modifiers()==Qt.ShiftModifier and event.key()==Qt.Key_Q:
print('shift+q')
# modifiers()方法用来获取辅助按键

# QtNoModifier 没有按下辅助按键
# QtShiftModifier Shift键
# Qt.ControlModifier “Ctrl键(macoS系统上是Command键)
# Qt.AltModifier “Alt键(macoS系统上是"option”键)

def keyReleaseEvent(self, event):
print(event.key())
print(event.text())

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.3.4 鼠标事件-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
鼠标事件分为鼠标按下事件、鼠标移动事件、鼠标释放事件和鼠标双击事件。
在每个鼠标事件中我们都可以获取到鼠标指针在窗口或屏幕上的坐标。

● mousePressEvent:鼠标按键被按下时触发。
● mouseMoveEvent:鼠标指针在窗口上移动时触发(鼠标需要被追踪到)。
● mouseReleaseEvent:鼠标按键被释放时触发。
● mouseDoubleClickEvent:在窗口上双击时触发。

"""

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


class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.setMouseTracking(True) # 让窗口始终追踪鼠标
#如果不调用该方法,那么只有在鼠标按键被按下后,窗口才会开始记录鼠标的移动操作,而按键被释放后,窗口就不会进行记录了

def mousePressEvent(self, event): # button()方法获取到当前被按下的鼠标按键。
# 如果用户同时按下多个鼠标按键,可以用buttons()方法获取。
if event.button() == Qt.LeftButton:
print('鼠标左键')
elif event.button() == Qt.MiddleButton:
print('鼠标中键')
elif event.button() == Qt.RightButton:
print('鼠标右键')

def mouseMoveEvent(self, event): # 3
print(event.pos()) # 鼠标指针在窗口位置
print(event.globalPos()) # 鼠标指针在屏幕的坐标位置

def mouseReleaseEvent(self, event):# 注释4开始
print('释放')

def mouseDoubleClickEvent(self, event):# 注释4结束
print('双击')


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.3.4 鼠标事件-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.setWindowFlag(Qt.FramelessWindowHint)
self.start_x = None # 设置两个变量分别用来保存鼠标按键被按下时鼠标指针对应的x和y坐标
self.start_y = None
# 当不释放鼠标左键,并且鼠标指针开始移动时,mouseMoveEvent()事件函数就会不断执行
# 而鼠标指针离窗口左上角的位置也会不断更新并保存在event.x()和event.y()中。

def mousePressEvent(self, event): # 1
if event.button() == Qt.LeftButton:
self.start_x = event.x()
self.start_y = event.y()

def mouseMoveEvent(self, event): # 2
dis_x = event.x() - self.start_x
dis_y = event.y() - self.start_y
self.move(self.x()+dis_x, self.y()+dis_y)

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.3.5 拖放事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
● DragEnterEvent:拖动目标进入窗口时触发。
● DragMoveEvent:在窗口上继续拖动目标时触发。
● DragLeaveEvent:拖动目标离开窗口时触发。
● DropEvent:放下目标时触发。

QMimeData类与MIME(Multipurpose Internet Mail Extension,多用途互联网邮件扩展)相关,
MIME是描述消息内容类型的互联网标准,可以简单理解为对文件扩展名的详细解释。通过该解释,程序就可以知道以何种方式来处理数据。
每个MIME类型由两部分组成,前面是数据的大类,后面定义具体的类,例如扩展名为.png的MIME类型为image/png。
QMimeData类给记录自身MIME类型的数据提供了一个容器,用于专门处理MIME类型的数据。
针对常见的MIME类型,QMimeData类提供了很多方法

判断方法 获取方法 设置方法 MIME类型
hasText() text() setText() text/plain
hasHtml() html() setHtml() text/html
hasUrls() urls() setUrls() text/uri-list
haslmage() imageData() setlmageData() image/*
hasColor() colorData() setColorData() applicationx-color

"""

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

class Window(QLabel): # 继承QLabel,并重写了它的拖放事件
def __init__(self):
super(Window, self).__init__()
self.resize(300, 300)
self.setAcceptDrops(True) # 让窗口或控件接受拖放操作

def dragEnterEvent(self, event): # 3
print('进入')
if event.mimeData().hasUrls(): # event.mimeData()方法获取到当前拖动目标的数据信息
# hasUrls()用来判断数据是否符合text/uri-list类型(即是否为文件)
event.accept() # 符合的话,调用accept()方法接受这次拖放操作

def dragMoveEvent(self, event): # 如果拖动目标继续在窗口中移动的话,控制台就会不断输出“移动”文本。
print('移动')

def dragLeaveEvent(self, event):
print('离开')

def dropEvent(self, event): # 5
print('放下')
url = event.mimeData().urls()[0] # 调用urls()方法获取文件的路径信息,返回一个列表,列表元素都是QUrl类型的对象
file_path = url.toLocalFile() # 通过QUrl对象的toLocalFile()方法可以获取该文件在当前系统上的路径字符串
if file_path.endswith('.png'): # 如果该文件是.png格式的图片,就将它设置在QLabel上
self.setPixmap(QPixmap(file_path))
self.setAlignment(Qt.AlignCenter)
self.setScaledContents(True)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.3.6 绘制事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


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

self.btn1 = QPushButton('移动', self)
self.btn2 = QPushButton('更新', self)
self.btn1.move(0, 0)
self.btn2.move(50, 50)
self.btn1.clicked.connect(lambda: self.btn1.move(100, 100)) # 按钮移动到(100, 100)
self.btn2.clicked.connect(self.update) # 单击“更新”按钮后,窗口会调用update()方法重绘整个窗口。
# 绘制事件通过rect()方法获取窗口上重绘的矩形区域,返回值是QRect类型的

def paintEvent(self, event): # 3
print('paint')
print(event.rect())


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.4.1 主窗口的组成部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()

self.widget = QWidget() #注释1开始
self.edit = QTextEdit()
self.btn = QPushButton('Button')

v_layout = QVBoxLayout()
v_layout.addWidget(self.edit)
v_layout.addWidget(self.btn)
self.widget.setLayout(v_layout) #注释1结束

self.setCentralWidget(self.widget) # 2


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.4.2 停靠窗口类QDockWidget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
"""
从图4-17可以看出停靠窗口一共有4块,即顶部、底部、左侧、右侧。

每块区域上都可以放置一个QDockWidget类型的停靠窗口,我们可以在这些停靠窗口上添加任意控件
"""


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

class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.edit1 = QTextEdit() #注释1开始
self.edit2 = QTextEdit()
self.center_edit = QTextEdit() #注释1结束

self.dock1 = QDockWidget('停靠区域1')
self.dock2 = QDockWidget('停靠区域2')
self.dock1.setWidget(self.edit1)
self.dock2.setWidget(self.edit2)
self.dock1.setAllowedAreas(Qt.RightDockWidgetArea) #注释2开始
self.dock2.setAllowedAreas(Qt.AllDockWidgetAreas)
# setAllowedAreas()用来设置停靠窗口在停靠区域上允许停靠的位置
# 可以传入setAllowedAreas()中的参数:
# Qt.LeftDockWidgetArea 左侧停靠区域
# QtRightDockWidgetArea 右侧停靠区域
# Qt.TopDockWidgetArea 顶部停靠区域
# QtBottomDockWidgetArea 底部停靠区域
# QtAllDockWidgetAreas 全部停靠区域
# QtNoDockWidgetArea 不可停靠区域(不显示)

self.dock1.setFeatures(QDockWidget.DockWidgetFloatable)
self.dock2.setFeatures(QDockWidget.DockWidgetMovable)#注释2结束
# setFeatures()方法用来设置停靠窗口的属性特征。

self.addDockWidget(Qt.RightDockWidgetArea, self.dock1)#注释3开始
self.addDockWidget(Qt.TopDockWidgetArea, self.dock2)#注释3结束
# QMainWindow调用addDockWidget()方法将停靠窗口添加到主窗口的停靠区域上,该方法需要传入停靠位置和停靠窗口对象这两个参数值。
# 同一块停靠区域可以放置多块停靠窗口,此时该区域会显示标签用来切换窗口
self.setCentralWidget(self.center_edit)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.4.3 菜单栏类QMenuBar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
"""
应用中的所有功能不可能全部显示在窗口上,不然会显得非常拥挤,我们应该把部分功能存放在菜单中。
主窗口的菜单栏区域就是用来放置各个菜单的,我们可以通过menuBar()方法获取到菜单栏实例,并调用该实例的addMenu()方法添加菜单。
菜单上的每个命令则通过QAction来添加
"""

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

class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
menu_bar = self.menuBar()
file_menu = menu_bar.addMenu('文件')

open_action = QAction(QIcon('open.ico'), '打开', self)# 程序实例化了3个QAction对象作为文件菜单下的命令,它们的功能分别是打开文件、保存文件和退出程序
save_action = QAction(QIcon('save.ico'), '保存', self)
quit_action = QAction(QIcon('quit.ico'), '退出', self)#注释1结束
open_action.triggered.connect(self.open) # 当用户单击一个QAction对象时,triggered信号会被发射出来。
save_action.triggered.connect(self.save)
quit_action.triggered.connect(self.quit) #注释2结束

file_menu.addAction(open_action) #注释3开始
file_menu.addAction(save_action)
file_menu.addSeparator() # addSeparator()方法用来在菜单中上添加一条分隔线,将部分命令分隔开来,这样在视觉上显得更有条理
# 如果想要添加子菜单,可以对file_menu菜单对象调用addMenu()方法,我们只需要在返回的子菜单对象上添加QAction对象即可,代码如下所示
# sub_menu = file_menu.addMenu('子菜单')
# sub_menu.addAction(QAction(QIcon('xxx.ico'), 'xxx', self))

# 在macOS系统上,菜单栏不在窗口中,而在屏幕左上方。可以调用menu_bar.setNativeMenuBar(False)禁用原生功能,
# 将菜单栏全部显示在标题栏下方。
file_menu.addAction(quit_action) #注释3结束

self.edit = QTextEdit()
self.setCentralWidget(self.edit)

def open(self):
file_path, _ = QFileDialog.getOpenFileName(self, '打开', './', '*.txt')
if file_path:
with open(file_path, 'r') as f:
self.edit.setText(f.read())

def save(self):
text = self.edit.toPlainText()
if text:
with open('saved.txt', 'w') as f:
f.write(text)

def quit(self):
self.close()


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.4.4 工具栏类QToolbar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
"""
我们可以把菜单中的一些常用命令选出来,用图标的方式将其显示在工具栏上,方便用户快速使用。

我们也可以发现工具栏跟停靠区域一样有多个位置可以使用
"""

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

class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.resize(300, 300)
toolbar1 = QToolBar('工具栏1') # 实例化两个QToolBar对象,然后将QAction对象添加到QToolBar对象上
toolbar2 = QToolBar('工具栏2')

open_action = QAction(QIcon('open.ico'), '打开', self)
save_action = QAction(QIcon('save.ico'), '保存', self)
quit_action = QAction(QIcon('quit.ico'), '退出', self)

toolbar1.addAction(open_action)
toolbar1.addAction(save_action)
toolbar1.addSeparator()
toolbar1.addAction(quit_action)
toolbar2.addAction(open_action)
toolbar2.addAction(save_action)
toolbar2.addSeparator()
toolbar2.addAction(quit_action) #注释1结束

toolbar1.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
# 可以传入setAllowedAreas()的参数
# Qt.LeftToolBarArea 左侧工具栏
# Qt.RightToolBarArea 右侧工具栏
# Qt.TopToolBarArea 顶工具栏
# Qt.BottomToolBarArea 底部工具栏
# Qt.AllToolBarAreas 全部区域
# Qt.NoToolBarArea 不显示工具栏
toolbar2.setMovable(False) # 用来设置工具栏是否可以移动(默认是可以移动的

self.addToolBar(Qt.TopToolBarArea, toolbar1) # 调用QMainWindow的addToolBar()方法将工具栏添加到指定区域上
self.addToolBar(Qt.BottomToolBarArea, toolbar2) #注释3结束

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.4.5 状态栏类QStatusBar-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.resize(300, 300)
self.status_bar = QStatusBar() # 实例化一个QStatusBar对象,然后通过setStatusBar()方法将其设置到窗口上
self.setStatusBar(self.status_bar) #注释1结束

self.btn = QPushButton('保存', self)
self.btn.clicked.connect(self.save)

def save(self):
self.status_bar.showMessage('已保存') # 当用户单击“保存”按钮后,状态栏通过showMessage()方法显示“已保存”文本提示用户

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.4.5 状态栏类QStatusBar-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.resize(300, 300)
self.status_bar = QStatusBar()
self.progress_bar = QProgressBar()
self.status_bar.addWidget(self.progress_bar) # 实例化了一个QStatusBar对象后,调用addWidget()方法将进度条控件添加到状态栏上
self.setStatusBar(self.status_bar)

self.btn = QPushButton('计数', self)
self.btn.clicked.connect(self.count)

self.value = 0
self.timer = QTimer()
self.timer.timeout.connect(self.update_progress_bar)

def count(self):
self.value = 0
self.timer.start(50)
self.progress_bar.setValue(0)
self.status_bar.clearMessage()

def update_progress_bar(self): # 单击“计数”按钮后,计时器会每隔50ms更新一次进度条,当value的值为100时,停止计时器,并在状态栏上显示“结束”文本。
self.value += 1
self.progress_bar.setValue(self.value)

if self.value == 100:
self.timer.stop()
self.status_bar.showMessage('结束')


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

4.4.6 程序启动画面类QSplashScreen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
"""
许多大型程序(例如Photoshop)在打开前都会先展示一个启动画面,这是因为程序运行需要一定时间来准备。
用启动画面来显示模块加载进度,这种方式可以提升用户体验。
如果没有启动画面,且双击程序之后很长一段时间窗口都没有出现,用户可能会觉得哪里有问题。
通常我们会将程序启动画面的代码放在程序入口处,位于sys.exit(app.exec())之前
"""

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

class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()

def load(self, splash):
for i in range(101):
time.sleep(0.05)
splash.showMessage(f'加载 {i}%', Qt.AlignBottom|Qt.AlignCenter)

if __name__ == '__main__':
app = QApplication([])

splash = QSplashScreen()
splash.setPixmap(QPixmap('qt.png')) # setPixmap()方法用来设置启动画面上的图片,showMessage()方法用来在启动画面上显示文本
splash.show()
splash.showMessage('加载 0%', Qt.AlignBottom|Qt.AlignCenter)#注释1结束

window = Window()
window.load(splash) # window窗口对象有一个自定义的load()方法,
# 我们在其中调用time.sleep()模拟窗口加载数据和配置的耗时过程。
window.show()
splash.finish(window) # 当窗口对象调用show()显示后,splash对象调用finish(window)关闭启动画面。
sys.exit(app.exec())


# 不过这个程序有个小bug:当启动画面还存在时,如果直接单击的话会将它隐藏起来。
# 我们应该自定义一个MySplashScreen类并重写鼠标事件。
# class MySplashScreen(QSplashScreen):
# def mousePressEvent(self,event):
# pass
# 最后将splash = QSplashScreen()替换成splash = MySplashScreen()就可以了。

6.1 数据库

目前市面上数据库的类型有很多,针对不同类型的数据库,PyQt为我们提供了相应的驱动

1
2
3
4
5
6
7
8
9
10
QDB2          IBM DB2
QIBASE Borland InterBase
QMYSQL MysQL
Qoci Oracle调用接口驱动
QODBC ODBC(包括微软SQLServer)
QPSQL PostgresQL
QSQLITE SQLite3或更高版本
QSQLITE2 sQLite2
QTDs Sybase自适应服务器

6.1.1 数据库连接和关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.db = QSqlDatabase.addDatabase('QSQLITE') # 调用QSqlDatabase类的addDatabase()方法添加QSQLITE数据库驱动,将返回的数据库对象保存在db变量中
self.connect_db()


def connect_db(self):
self.db.setDatabaseName('./info.db') # 调用setDatabaseName()方法选择要使用的数据库。
# 如果数据库文件不存在,则会创建一个。
if not self.db.open():# open()方法打开数据库,如果打开失败,可以使用lastError()获取失败原因并将其显示在消息框上。
error = self.db.lastError().text()
QMessageBox.critical(self, 'Database Connection', error)

def closeEvent(self, event): # 在closeEvent()事件函数中,我们在关闭窗口前先调用close()方法关闭数据库。
self.db.close()
event.accept()

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.1.1 数据库连接和关闭-MySQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.db = QSqlDatabase.addDatabase('QMYSQL')
self.connect_db()

def connect_db(self):
self.db.setHostName('localhost')
self.db.setUserName('root')
self.db.setPassword('password')
self.db.setDatabaseName('info')
if not self.db.open():
error = self.db.lastError().text()
QMessageBox.critical(self, 'Database Connection', error)

def closeEvent(self, event):
self.db.close()
event.accept()

6.2 多线程

在PyQt中,主线程(也可以称为UI线程)负责界面绘制和更新。
当执行某些复杂且耗时的操作时,如果将执行这些操作的代码放在主线程中,界面就会出现停止响应(或卡顿)的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.label = QLabel('0')
self.label.setAlignment(Qt.AlignCenter)
self.btn = QPushButton('计数')
self.btn.clicked.connect(self.count)

v_layout = QVBoxLayout()
v_layout.addWidget(self.label)
v_layout.addWidget(self.btn)
self.setLayout(v_layout)

def count(self): # 我们用while循环来模拟耗时操作。
# 单击“计数”按钮后,可以发现QLabel标签控件没有更新数字,界面停止响应。
# 持续了较长一段时间之后,界面才响应,更新了数字。
num = 0
while num < 10000000:
num += 1
self.label.setText(str(num))

# 针对这种简单的耗时程序,PyQt提供了一种让界面快速响应的方法,
# 我们只需要在while循环中加入这行代码:QApplication.processEvents()。
# 该方法会自动处理线程中一些待处理的事件,比方说用来更新界面的绘制事件。
# 再次运行程序,可以发现界面上的数字是正常更新的。
# 当然,耗时程序还是放在Qthread子线程中比较好,不要放在主线程中。
# 这样不仅方便我们管理代码,而且QThread所提供的多种方法也能让我们实现更好的控制。

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.2.1 使用QThread线程类-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.label = QLabel('0')
self.label.setAlignment(Qt.AlignCenter)
self.btn = QPushButton('计数')
self.btn.clicked.connect(self.count)

v_layout = QVBoxLayout()
v_layout.addWidget(self.label)
v_layout.addWidget(self.btn)
self.setLayout(v_layout)

self.count_thread = CountThread()# 实例化了一个CountThread线程对象,并将它的自定义信号和update_label()槽函数相连接。
self.count_thread.count_signal.connect(self.update_label)

def count(self):
self.count_thread.start() # 当我们单击“计数”按钮后,count_thread线程对象就会调用start()方法开启线程。
# 接着count_signal信号不断将数字发送过来,update_label()槽函数将数字设置到标签控件上。
def update_label(self, num):
self.label.setText(str(num)) #注释1结束

class CountThread(QThread): # 继承了QThread类,并重写了run()函数。
count_signal = pyqtSignal(int) # 这是编写一个线程类的基本操作。在run()函数中。
# 我们通过count_signal自定义信号将当前数据发送出来至 update_label函数中。

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

def run(self):
num = 0
while num < 10000000:
num += 1
self.count_signal.emit(num)#注释2结束

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.2.1 使用QThread线程类-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *


class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.label = QLabel('0')
self.label.setAlignment(Qt.AlignCenter)
self.btn1 = QPushButton('计数')
self.btn2 = QPushButton('停止')
self.btn1.clicked.connect(self.start_counting)
self.btn2.clicked.connect(self.stop_counting)

v_layout = QVBoxLayout()
v_layout.addWidget(self.label)
v_layout.addWidget(self.btn1)
v_layout.addWidget(self.btn2)
self.setLayout(v_layout)

self.count_thread = CountThread() # 注释1开始
self.count_thread.count_signal.connect(self.update_label)

def start_counting(self):
if not self.count_thread.isRunning(): # 判断count_thread线程是否还在运行,这样可以避免重复启动该线程。
self.count_thread.start() # 注释1结束
# 当“停止”按钮被单击后,线程对象调用自定义的stop()方法将flag值变为了False
# 这样程序就会跳出while循环,run()函数也就运行结束了,此时线程也会自动关闭。

def stop_counting(self):
self.count_thread.stop()

def update_label(self, num):
self.label.setText(str(num))

class CountThread(QThread):
count_signal = pyqtSignal(int)

def __init__(self):
super(CountThread, self).__init__()
self.flag = True

def run(self):
num = 0
self.flag = True

while num < 10000000:
if not self.flag:
break

num += 1
self.count_signal.emit(num)
self.msleep(100) # sleep()方法可以让线程休眠,需要传入整型值,传入1表示休眠1s。
# 果要进行毫秒级休眠,可以使用msleep()。如果要进行微秒级休眠,则可以使用usleep()。
def stop(self): # 2
self.flag = False
# 其实QThread线程类本身有让线程停止的方法:exit()、quit()和terminate()。
# 但是前两个方法经常不起作用,第三个办法则不推荐使用,因为它会强制停止线程。
# 如果线程正在保存一些数据的话,那使用terminate()可能会导致数据丢失。

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.2.2 在线程中获取窗口数据信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.label = QLabel('0')
self.label.setAlignment(Qt.AlignCenter)
self.btn1 = QPushButton('计数')
self.btn2 = QPushButton('停止')
self.btn1.clicked.connect(self.start_counting)
self.btn2.clicked.connect(self.stop_counting)

self.spin_box = QSpinBox() # 程序会从这个控件中的数字开始计数
self.spin_box.setRange(0, 10000000)

v_layout = QVBoxLayout()
v_layout.addWidget(self.label)
v_layout.addWidget(self.spin_box)
v_layout.addWidget(self.btn1)
v_layout.addWidget(self.btn2)
self.setLayout(v_layout)

self.count_thread = CountThread(self) # 要想在线程中获取QSpinBox控件上的数据,
# 我们需要在线程实例化时将窗口实例self传入,这样就能在线程中通过该实例获取到窗口上的任何一个控件,
# 最后调用QSpinBox控件的value()方法。
self.count_thread.count_signal.connect(self.update_label)

def start_counting(self):
if not self.count_thread.isRunning():
self.count_thread.start()

def stop_counting(self):
self.count_thread.stop()

def update_label(self, num):
self.label.setText(str(num))

class CountThread(QThread):
count_signal = pyqtSignal(int)
def __init__(self, window): #注释1开始
super(CountThread, self).__init__()
self.flag = True
self.window = window

def run(self):
num = self.window.spin_box.value()#注释1结束
self.flag = True

while num < 10000000:
if not self.flag:
break
num += 1
self.count_signal.emit(num)
self.msleep(100)

def stop(self):
self.flag = False

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.2.3 编写一个简单的爬程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
import requests # 本程序要导入requests模块用于发送网络请求

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.line_edit = QLineEdit() # 输入待爬取的网址
self.text_browser = QTextBrowser() # 用来显示爬取结果
self.btn = QPushButton('爬取') # 用来开启CrawlThread线程

self.line_edit.setPlaceholderText('待爬取的网址')
self.text_browser.setPlaceholderText('爬取结果')
self.btn.clicked.connect(self.crawl)

v_layout = QVBoxLayout()
v_layout.addWidget(self.line_edit)
v_layout.addWidget(self.text_browser)
v_layout.addWidget(self.btn)
self.setLayout(v_layout)

self.crawl_thread = CrawlThread(self)
self.crawl_thread.result_signal.connect(self.show_result)

def crawl(self):
if not self.line_edit.text().strip(): # 如果用户没有输入任何网址,就单击了“爬取”按钮,
# 那窗口会弹出一个消息框提示用户先输入网址。
QMessageBox.critical(self, '错误', "请输入网址!")
return #注释2结束

if not self.crawl_thread.isRunning():
self.crawl_thread.start()

def show_result(self, text): # 3
self.text_browser.setPlainText(text)

class CrawlThread(QThread):
result_signal = pyqtSignal(str) # 这是编写一个线程类的基本操作。在run()函数中。
# 我们通过count_signal将数据通过self.crawl_thread.result_signal.connect(self.show_result)发送出来至 show_result 函数中
def __init__(self, window):
super(CrawlThread, self).__init__()
self.window = window

def run(self): # 3
url= self.window.line_edit.text().strip() # 获取用户输入的网址
result = requests.get(url) # 获取该网址的网页源码
self.result_signal.emit(result.text) # 将源码文本发送出去,最后在show_result()槽函数中将源码文本显示到文本浏览框上

6.3.1 画笔类QPen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

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

self.pen = QPen() # 实例化一个QPen对象
self.pen.setWidth(5) # 设置画笔宽度
self.pen.setColor(Qt.black) # 设置画笔颜色
self.pen.setStyle(Qt.DashLine) # 设置笔线风格
self.pen.setCapStyle(Qt.RoundCap) # 设置笔帽风格
self.pen.setJoinStyle(Qt.MiterJoin) # 设置笔线转折风格

def paintEvent(self, event): # 绘图是在paintEvent()绘制事件函数中进行的。
painter = QPainter(self) # 在实例化一个QPainter对象时,我们传入窗口实例self,
# 表示我们将在当前窗口上进行绘制,窗口这时候就是一个绘制设备。
painter.setPen(self.pen) # 设置好画笔
painter.drawLine(20, 20, 280, 280) # 通过drawLine()和drawRect()方法分别绘制线段和矩形
painter.drawRect(20, 20, 260, 260)
# QPainter类提供了几种常用的绘制方法
# drawArc() 绘制弧
# drawChord() 绘制弦
# drawConvexPolygon() 绘制凸多边形
# drawEllipse() 绘制椭圆
# drawLine() 绘制线段
# drawPath() 绘制自定义路径
# drawPie() 绘制扇形
# drawPixmap() 绘制图月
# drawPoint() 绘制一个点
# drawPolygon() 绘制多边形
# drawPolyline() 绘制多段线
# drawRect() 绘制矩形
# drawRoundedRect() 绘制圆角矩形
# drawText() 绘制文本

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.3.2 画刷类QBrush

画刷就跟油漆桶工具一样,是用来填充的。我们可以设置它的填充颜色和填充风格,也可以设置其用来填充图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

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

self.brush1 = QBrush() #注释1开始
self.brush1.setColor(Qt.red) # 设置画刷的填充颜色
self.brush1.setStyle(Qt.Dense6Pattern) # 设置画刷的填充风格

gradient = QLinearGradient(100, 100, 200, 200) # 线性渐变类
gradient.setColorAt(0.3, QColor(255, 0, 0)) # setColorAt()方法传入两个参数,第一个参数代表颜色开始渐变的位置(大小范围为0~1),第二个参数代表颜色值。
gradient.setColorAt(0.6, QColor(0, 255, 0))
gradient.setColorAt(1.0, QColor(0, 0, 255))
self.brush2 = QBrush(gradient) #注释2结束
# PyQt提供了3种渐变类:线性渐变类QLinearGradient、辐射渐变类QRadialGradient和角度渐变类QConicalGradient。

self.brush3 = QBrush() #注释3开始
self.brush3.setTexture(QPixmap('smile.png'))
# setTexture()方法设置用于填充的图片,此时填充风格自动变为Qt.TexturePattern。

def paintEvent(self, event): # 4
painter = QPainter(self)
painter.setBrush(self.brush1)
painter.drawRect(0, 0, 100, 100)

painter.setBrush(self.brush2)
painter.drawRect(100, 100, 100, 100)

painter.setBrush(self.brush3)
painter.drawRect(200, 200, 100, 100)
# 4 在paintEvent()事件函数中,我们绘制了3个矩形,每绘制完一个后就调用setBrush()方法更换画刷。
if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.3.3 用鼠标在窗口上绘制矩形

绘图软件的一项基本功能是让用户在画板上自由绘图。
在本小节,我们会用鼠标在窗口上绘制任意数量的矩形,好让大家巩固QPainter类的用法并了解用鼠标绘图的原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtPrintSupport import *


class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(500, 500)
self.x1 = None
self.y1 = None
self.x2 = None
self.y2 = None
self.rect_list = []

self.pen = QPen()
self.pen.setWidth(2)
self.pen.setColor(Qt.green)

self.undo_btn = QPushButton('撤销', self) #注释1开始
self.undo_btn.clicked.connect(self.undo_drawing) #注释1结束
self.undo_btn.move(20, 20)

###### 打印 ######
self.printer = QPrinter() # 实例化一个QPrinter打印机对象

self.print_btn = QPushButton('打印', self) # 放置一个“打印”按钮,当用户单击按钮后,print_drawing()槽函数就会启动
self.print_btn.clicked.connect(self.print_drawing)
self.print_btn.move(20, 50) #注释1结束

def undo_drawing(self):
if self.rect_list:
self.rect_list.pop() # 删除rect_list的最后一个元素,也就是最新绘制的矩形。

def mousePressEvent(self, event): # 用x1和y1记录单击时的坐标,也就是当前所绘制矩形左上角的坐标。
if event.button() == Qt.LeftButton:
self.x1 = event.pos().x()
self.y1 = event.pos().y()

def mouseMoveEvent(self, event): #在mouseMoveEvent()事件函数中,用x2和y2记录鼠标指针当前的坐标,将其作为所绘制矩形右下角的坐标。
self.x2 = event.pos().x()
self.y2 = event.pos().y()

def mouseReleaseEvent(self, event): # 将矩形的左上角坐标和宽度、高度添加到了rect_list列表变量中。
if self.x1 and self.y1 and self.x2 and self.y2:
self.rect_list.append((self.x1, self.y1,self.x2-self.x1, self.y2-self.y1))

self.x1 = None
self.y1 = None
self.x2 = None
self.y2 = None
# 添加完毕后,我们要重置x1、y1、x2和y2,否则绘制下一个矩形时,就会使用之前的坐标。

def paintEvent(self, event): # 5
painter = QPainter(self)
painter.setPen(self.pen) # 实例化一个QPainter对象并设置好画笔

if self.x1 and self.y1 and self.x2 and self.y2:
painter.drawText(self.x2, self.y2, '矩形') # 矩形右下角绘制“矩形”文本
painter.drawRect(self.x1, self.y1,self.x2-self.x1, self.y2-self.y1) # 在窗口上实时显示用户当前正在绘制的矩形

for rect in self.rect_list: # 循环rect_list列表,显示之前已经画好的各个矩形
painter.drawRect(rect[0], rect[1], rect[2], rect[3])
self.update()

###### 打印 ######
def print_drawing(self):
print_dialog = QPrintDialog(self.printer) # 将打印机对象传入QPrinterDialog打印对话框中
if print_dialog.exec():
painter = QPainter(self.printer)
painter.setPen(self.pen)
for rect in self.rect_list:
painter.drawRect(rect[0], rect[1], rect[2], rect[3])
# 显示对话框,如果该方法返回值是1,则表示用户单击了对话框上的“打印”按钮;如果该方法返回值是0,则表示用户单击了“撤销”按钮。

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.3.4 打印

有些文本类控件,如QTextEdit和QTextBrowser,本身就提供了print()方法,
我们只需要往该方法中传入QPrinter对象就能够快速绘制要打印的文本内容,不需要使用QPainter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtPrintSupport import * # 打印控件

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.edit = QTextEdit()
self.print_btn = QPushButton('打印')
self.print_btn.clicked.connect(self.print_text)

self.printer = QPrinter()

v_layout = QVBoxLayout()
v_layout.addWidget(self.edit)
v_layout.addWidget(self.print_btn)
self.setLayout(v_layout)

def print_text(self):
print_dialog = QprintDialog(self.printer)
if print_dialog.exec():
self.edit.print(self.printer)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.4.1 属性动画类QPropertyAnimation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(500, 500)

self.size_btn = QPushButton('大小', self)
self.pos_btn = QPushButton('位置', self)
self.color_btn = QPushButton('颜色', self)
self.size_btn.move(100, 20)
self.pos_btn.move(200, 20)
self.color_btn.move(300, 20)
self.size_btn.clicked.connect(self.start_anim)
self.pos_btn.clicked.connect(self.start_anim)
self.color_btn.clicked.connect(self.start_anim)
self.size_anim = QPropertyAnimation(self.size_btn, b'size')# 传入动画作用的目标对象和属性名称
# 注意,属性名称要用字节类型数据,所以要在字符串前面添加b。
# 读者如果想要使用其他的属性,可以使用Python内置的dir()方法,通过它我们能获取到某对象所有的属性和方法。
self.size_anim.setDuration(6000) # 设置动画时长(单位为毫秒)。
self.size_anim.setStartValue(QSize(10, 10)) # 设置动画作用对象的属性初始值
self.size_anim.setEndValue(QSize(100, 300)) # 设置动画作用对象的属性结束值
# 如果是改变大小,则传入QSize类型的值;如果是改变坐标位置,则传入QPoint类型的值;如果是改变颜色,则传入QColor类型的值。
self.size_anim.setLoopCount(2)
# setLoopCount()方法用来设置动画的循环次数。当动画结束后,finished信号就会发射出来。
# 我们将这个信号与delete()槽函数进行连接,相应按钮会在动画结束时被删除。
self.size_anim.finished.connect(self.delete)

self.pos_anim = QPropertyAnimation(self.pos_btn, b'pos')
self.pos_anim.setDuration(5000)
self.pos_anim.setKeyValueAt(0.1, QPoint(200, 100))
self.pos_anim.setKeyValueAt(0.5, QPoint(200, 200))
self.pos_anim.setKeyValueAt(1.0, QPoint(200, 400))
# 使用setKeyValueAt()能够实现更细化的动画控制,第一个传入的参数值为浮点数,范围为0.0~1.0,表示在动画的相应时刻插入一帧
# 假如动画时长为5000ms,那传入0.5就表示在第2500ms时插入一帧,该帧的属性值就是我们传入的第二个参数值。
self.pos_anim.finished.connect(self.delete)

self.color_anim = QPropertyAnimation(self.color_btn, b'color')
self.color_anim.setDuration(5000)
self.color_anim.setStartValue(QColor(0, 0, 0))
self.color_anim.setEndValue(QColor(255, 255, 255))
self.color_anim.finished.connect(self.delete) #注释1结束
# “大小”按钮和“位置”按钮被单击后都是正常运行动画的,但是对“颜色”按钮来说就不行,
# 单击后控制台会提示“you’re trying to animate a non-existing property color of your QObject”。
# 这句话告诉我们QPushButton按钮控件没有颜色属性!
# 我们可以用print(dir(self. color_btn))这行代码输出“颜色”按钮的所有属性,会发现没有“color”

def start_anim(self): # 2
if self.sender() == self.size_btn:
self.size_anim.start()
elif self.sender() == self.pos_btn:
self.pos_anim.start()
else:
self.color_anim.start()
# 除了start(),还有以下几种控制动画的方法。
# ● stop():停止动画。● pause():暂停动画。● resume():继续动画。

def delete(self):
if self.sender() == self.size_anim:
self.size_btn.deleteLater()
elif self.sender() == self.pos_anim:
self.pos_btn.deleteLater()
else:
self.color_btn.deleteLater()

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.4.1 属性动画类QPropertyAnimation-改变按钮颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class ColorButton(QPushButton):
def __init__(self, text=None, parent=None):
super(ColorButton, self).__init__(text, parent)
self._color = QColor()

@pyqtProperty(QColor) # 返回QColor类型的值
def color(self):
return self._color

@color.setter # 2
def color(self, value):
self._color = value
red = value.red()
green = value.green()
blue = value.blue()
self.setStyleSheet(f'background-color: rgb({red}, {green}, {blue})')

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

self.color_btn = ColorButton('颜色', self)
self.color_btn.move(20, 20)
self.color_btn.resize(100, 100)
self.color_btn.clicked.connect(self.start_anim)

self.color_anim = QPropertyAnimation(self.color_btn, b'color')
self.color_anim.setDuration(5000)
self.color_anim.setStartValue(QColor(0, 0, 0))
self.color_anim.setEndValue(QColor(255, 255, 255))
self.color_anim.finished.connect(self.delete)

def start_anim(self):
self.color_anim.start()

def delete(self):
self.color_btn.deleteLater()



if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.4.2 串行动画组类QSequentialAnimationGroup

串行动画组就是指按照动画添加顺序来执行动画。
我们只用实例化QSequentialAnimationGroup类,
然后调用addAnimation()或者insertAnimation()方法把各个属性动画添加到动画组里面就可以了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(500, 500)

self.start_btn = QPushButton('开始', self)
self.stop_btn = QPushButton('停止', self)
self.pause_resume_btn = QPushButton('暂停/继续', self)
self.start_btn.move(20, 20)
self.stop_btn.move(20, 50)
self.pause_resume_btn.move(20, 80)
self.start_btn.clicked.connect(self.control_anim)
self.stop_btn.clicked.connect(self.control_anim)
self.pause_resume_btn.clicked.connect(self.control_anim)

self.plane = QLabel(self)
self.plane.move(200, 400)
self.plane.setPixmap(QPixmap('11.png'))
self.plane.setScaledContents(True)

self.anim1 = QPropertyAnimation(self.plane, b'pos') # 实例化了两个属性动画
self.anim1.setDuration(2000)
self.anim1.setStartValue(QPoint(200, 400))
self.anim1.setEndValue(QPoint(200, 300))
self.anim2 = QPropertyAnimation(self.plane, b'pos')
self.anim2.setDuration(3000)
self.anim2.setStartValue(QPoint(200, 300))
self.anim2.setEndValue(QPoint(100, 200)) #注释1结束

self.anim_group = QSequentialAnimationGroup() #注释2开始
self.anim_group.addAnimation(self.anim1)
self.anim_group.addPause(1000) # 添加了一个暂停1000ms的特殊动画
self.anim_group.addAnimation(self.anim2)
self.anim_group.stateChanged.connect(self.get_info)
print(self.anim_group.totalDuration()) #注释2结束

def get_info(self):
print(self.anim_group.currentAnimation()) # 取当前正在播放的动画对象
print(self.anim_group.currentTime()) # 取当前正在播放的动画时间

def control_anim(self): # 4 开始播放和停止播放动画
if self.sender() == self.start_btn:
self.anim_group.start()
elif self.sender() == self.stop_btn:
self.anim_group.stop()
else:
if self.anim_group.state() == QAbstractAnimation.Paused: # 获取当前的动画状态
self.anim_group.resume() # 继续动画
else:
self.anim_group.pause() # 暂停动画
# 动画状态有3种
# QAbstractAnimation.Stopped 停止状态
# QAbstractAnimation.Paused 暂停状态
# QAbstractAnimation.Running 播放状态

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.4.3 并行动画组类QParallelAnimationGroup

串行动画组就是指按照动画添加顺序来执行动画。
我们只用实例化QSequentialAnimationGroup类,
然后调用addAnimation()或者insertAnimation()方法把各个属性动画添加到动画组里面就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(500, 500)

self.start_btn = QPushButton('开始', self)
self.stop_btn = QPushButton('停止', self)
self.pause_resume_btn = QPushButton('暂停/继续', self)
self.start_btn.move(20, 20)
self.stop_btn.move(20, 50)
self.pause_resume_btn.move(20, 80)
self.start_btn.clicked.connect(self.control_anim)
self.stop_btn.clicked.connect(self.control_anim)
self.pause_resume_btn.clicked.connect(self.control_anim)

self.plane = QLabel(self)
self.plane.move(200, 400)
self.plane.setPixmap(QPixmap('11.png'))
self.plane.setScaledContents(True)

self.anim1 = QPropertyAnimation(self.plane, b'pos')# 属性动画anim1和anim2分别用来修改飞机的位置和大小。
self.anim1.setDuration(2000) # 但在并行动画组中,这两个动画会同时运行。
self.anim1.setStartValue(QPoint(200, 400))
self.anim1.setEndValue(QPoint(200, 300))
self.anim2 = QPropertyAnimation(self.plane, b'size')
self.anim2.setDuration(3000)
self.anim2.setStartValue(QSize(200, 200))
self.anim2.setEndValue(QSize(60, 60))

self.anim_group = QParallelAnimationGroup()
self.anim_group.addAnimation(self.anim1)
self.anim_group.addAnimation(self.anim2)
self.anim_group.stateChanged.connect(self.get_info)
print(self.anim_group.totalDuration())
# 并行动画组没有addPause()方法,因为不能让暂停动画和其他动画一起播放,这样没有意义。
# currentAnimation()方法也不适用,因为同一时间段总是有多个动画。

def get_info(self):
print(self.anim_group.currentTime()) #注释1结束

def control_anim(self): # 4 开始播放和停止播放动画
if self.sender() == self.start_btn:
self.anim_group.start()
elif self.sender() == self.stop_btn:
self.anim_group.stop()
else:
if self.anim_group.state() == QAbstractAnimation.Paused: # 获取当前的动画状态
self.anim_group.resume() # 继续动画
else:
self.anim_group.pause() # 暂停动画
# 动画状态有3种
# QAbstractAnimation.Stopped 停止状态
# QAbstractAnimation.Paused 暂停状态
# QAbstractAnimation.Running 播放状态

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.4.4 时间轴类QTimeline

一个动画由多张静态图片组成,每一张静态图片为一帧。
如果每隔一定时间显示一帧,且时间间隔非常短的话,那这些静态图片就会构成一个连续影像,动画由此而来。
QTimeLine提供了用于控制动画的时间轴,我们可以用它来快速实现动画效果。
示例代码QTimeLine给按钮添加了一段移动动画,同时用QProgressBar显示了动画进度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(500, 130)

self.btn = QPushButton('开始', self)
self.btn.resize(100, 100)
self.btn.move(0, 0)
self.btn.clicked.connect(self.start_anim)

self.progress_bar = QProgressBar(self)
self.progress_bar.setRange(0, 100)
self.progress_bar.resize(500, 20)
self.progress_bar.move(0, 100)


self.time_line = QTimeLine(1000) # 实例化QTimeLine对象时候需传时间值(单位是毫秒)作为动画的运行时长
self.time_line.setFrameRange(0, 100) # 设置动画帧数范围,表示在动画运行时长内要播放多少帧
self.time_line.frameChanged.connect(self.move_btn) # 帧数发生改变时,frameChanged信号就会发射
self.time_line.finished.connect(self.change_direction)
# # QTimeLine默认使用的缓动曲线(Easing Curve)为QEasingCurve.InOutSine
# self.time_line.setEasingCurve(QEasingCurve.OutQuart) # 调用setEasingCurve()方法来修改缓动曲线的类型,比如改成QEasingCurve. OutQuart
# 可以在官方文档中搜索QEasingCurve来查看所有的缓动曲线。

def start_anim(self):
if self.time_line.state() == QTimeLine.NotRunning:
self.time_line.start()

def move_btn(self): # 2
frame = self.time_line.currentFrame() # 获取动画当前的帧数来确定按钮的目标位置和进度条的进度
self.btn.move(frame*4, 0)
self.progress_bar.setValue(frame)

def change_direction(self): # 3
if self.time_line.direction() == QTimeLine.Forward: # 通过direction()获取当前的播放方向
self.time_line.setDirection(QTimeLine.Backward) # setDirection()方法改变播放方向
else:
self.time_line.setDirection(QTimeLine.Forward)
# QTimeLine中的动画状态一共有3种
# QTimeLine.NoRunning 动画未开始或已结束
# QTimeLine.Running 动画正在播放
# QTimeLine.Paused 动画被暂停
if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.5.1 声音类QSound

如果只是想简单地播放一段音频,那用QSound类就够了,我们只需要调用它的play()方法,不过它只能播放.wav格式的音频文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtMultimedia import * # 导入多媒体模块


class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(100, 30)

self.sound = QSound('audio.wav')
self.sound.setLoops(2) # 设置播放音频的循环次数,如果想要无限循环,可以传入QSound.Infinite

self.btn = QPushButton('播放/停止', self)
self.btn.clicked.connect(self.play_or_stop)

def play_or_stop(self): # 2
if self.sound.isFinished(): # 判断音频是否处于结束状态
self.sound.play()
else:
self.sound.stop()



if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.5.2 音效类QSoundEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
QSoundEffect可以用来播放无压缩的音频文件(典型的是.wav文件)。
通过它我们不仅能够以低延迟的方式来播放音频,还能够对音频进行更进一步的操作(比如控制音量)。
该类非常适合用来播放交互音效,如弹出框的提示音、游戏音效等。


import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtMultimedia import * # 导入多媒体模块

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(80, 60)

self.sound_effect = QSoundEffect() # 实例化一个QSoundEffect对象
self.sound_effect.setSource(QUrl.fromLocalFile('click.wav')) # 设置音频源,传入一个QUrl类型的参数。
self.sound_effect.setLoopCount(1) # 设置音频播放的循环次数,如果想要无限循环,可以传入QSoundEffect.Infinite。
self.sound_effect.setVolume(0.8) #设置声音的音量,范围为0.0~1.0

self.btn1 = QPushButton('播放', self)
self.btn2 = QPushButton('关闭声音', self)
self.btn1.move(0, 0)
self.btn2.move(0, 30)
self.btn1.clicked.connect(self.play)
self.btn2.clicked.connect(self.mute_unmute)

def play(self):
self.sound_effect.play()

def mute_unmute(self):
if self.sound_effect.isMuted(): # 判断当前音频是否为静音
self.sound_effect.setMuted(False)
self.btn2.setText('关闭声音')
else:
self.sound_effect.setMuted(True)
self.btn2.setText('开启声音')

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.5.3 媒体播放机类QMediaPlayer-播放视频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtMultimedia import * # 导入多媒体模块
from PyQt5.Qt import QVideoWidget # 导入视频模块

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

video1 = QUrl.fromLocalFile('./video1.mp4')
video2 = QUrl.fromLocalFile('./video2.mp4')
video3 = QUrl.fromLocalFile('./video3.mp4')

self.playlist = QMediaPlaylist() #注释1开始
self.playlist.addMedia(QMediaContent(video1))
self.playlist.addMedia(QMediaContent(video2))
self.playlist.addMedia(QMediaContent(video3))
self.playlist.setCurrentIndex(0)
self.playlist.setPlaybackMode(QMediaPlaylist.Loop)
self.playlist.currentMediaChanged.connect(self.show_info)

self.video_widget = QVideoWidget()

self.player = QMediaPlayer()
self.player.setPlaylist(self.playlist)
self.player.setVideoOutput(self.video_widget)#注释1结束

self.btn1 = QPushButton('上一个', self)
self.btn2 = QPushButton('播放/停止', self)
self.btn3 = QPushButton('下一个', self)
self.btn1.clicked.connect(self.control)
self.btn2.clicked.connect(self.control)
self.btn3.clicked.connect(self.control)

btn_h_layout = QHBoxLayout()
window_v_layout = QVBoxLayout()
btn_h_layout.addWidget(self.btn1)
btn_h_layout.addWidget(self.btn2)
btn_h_layout.addWidget(self.btn3)
window_v_layout.addWidget(self.video_widget)
window_v_layout.addLayout(btn_h_layout)
self.setLayout(window_v_layout)

def show_info(self):
print('索引:', self.playlist.currentIndex())
print('当前媒体:', self.playlist.currentMedia())

def control(self):
print('媒体状态:', self.player.mediaStatus())

if self.sender() == self.btn1:
self.playlist.previous()
elif self.sender() == self.btn2:
if self.player.state() == QMediaPlayer.StoppedState:
self.player.play()
else:
self.player.stop()
else:
self.playlist.next()
# 如果无法播放视频,可能是没有视频解码器,安装LAV Filters就可以了。

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.5.3 媒体播放机类QMediaPlayer-播放音频

QMediaPlayer是一个高级的媒体播放机类,它的功能非常强大,通过它我们既可以播放音频(可以是.mp3格式的文件),也可以播放视频。
该类可以和播放列表类QMediaPlayList一同使用,播放列表用来存放待播放的音频和视频源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtMultimedia import * # 导入多媒体模块

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(320, 50)

audio1 = QUrl.fromLocalFile('./audio1.wav') # 首先将音频源保存在audio1、audio2和audio3变量中
audio2 = QUrl.fromLocalFile('./audio2.mp3')
audio3 = QUrl.fromLocalFile('./audio3.mp3')

self.playlist = QMediaPlaylist() # 实例化一个QMediaPlaylist对象
self.playlist.addMedia(QMediaContent(audio1)) # 将各个音频源添加到播放列表中
self.playlist.addMedia(QMediaContent(audio2)) # 该方法接收一个QMediaContent类型的参数
self.playlist.addMedia(QMediaContent(audio3))
self.playlist.setCurrentIndex(0) # 设置当前要播放的音频,传入0表示播放第一个音频
# 如果需要切换到上一个音频或下一个音频,我们可以直接调用previous()或next()来实现
self.playlist.setPlaybackMode(QMediaPlaylist.Loop)
self.playlist.currentMediaChanged.connect(self.show_info)
# 当一个音频播放完毕,要切换时,currentMediaChanged信号会发射出来

# setPlaybackMode()方法用来设置播放模式
# QMediaPlaylist.CurrentltemOnce 当前音频只播放一次
# QMediaPlaylist.CurrentlteminLoop 单曲循环
# QMediaPlaylist.Sequential 顺序播放
# QMediaPlaylist.Loop 列表循环
# QMediaPlaylist.Random 随机播放

self.player = QMediaPlayer()
self.player.setPlaylist(self.playlist) # 设置媒体播放机要播放的音频列表。
self.player.setVolume(90) # setVolume()用来设置音量,范围为0~100。

self.btn1 = QPushButton('上一个', self)
self.btn2 = QPushButton('播放/停止', self)
self.btn3 = QPushButton('下一个', self)
self.btn1.move(0, 0)
self.btn2.move(90, 0)
self.btn3.move(190, 0)
self.btn1.clicked.connect(self.control)
self.btn2.clicked.connect(self.control)
self.btn3.clicked.connect(self.control)

def show_info(self): # 3
print('当前媒体:', self.playlist.currentMedia()) # 获取媒体对象
print('索引:', self.playlist.currentIndex()) # 获取媒体对象在列表中的索引

def control(self): # 4
print('媒体状态:', self.player.mediaStatus()) # 获取当前音频文件的加载状态
# 共有9种加载状态
# 常量 值 描述
# QMediaPlayer.UnknownMediaStatus 0 未知媒体状态
# QMediaPlayer.NoMedia 1 无媒体文件,QMediaPlayer必于stoppedState播放状态
# MediaPlayer.LoadingMedia 2 正在加载媒体文件,QMediaPlayer可以必于任何状态
# QMediaPlayer.LoadedMedia 3 已加载媒体文件,QMediaPlayer必于stoppedState播放状态
# QMediaPlayer.StalledMedia 4 媒体文件由于缓中不足或其他原因处于卡顿的加载状态,QMediaPlayer必于PlavingState(正在播放)或PausedState(暂停播放)状态
# QMediaPlayer.BufferingMedia 5 在缓中数据,QMediaPlayer必于PlayingState(正在播放)或PausedState (暂停播放)状态
# OMediaPlaver.BufferedMedia 6 正已完成缓冲,QMediaPlayer必于PlayingState (正在播放)或PausedState (暂停播放)状态
# QMediaPlayer.EndOfMedia 7 媒体文件播放结束,QMediaPlayer必于StoppedState (停止播放)状态
# QMediaPlayerInvalidMedia 8 非法的媒体文件,QMediaPlayer必于StoppedState (停止播放)状态

# QMediaPlayer的播放状态有以下3种
# QMediaPlayer.StoppedState 停止播放状态
# QMediaPlayer.PlayingState 正在播放状态
# QMediaPlayer.PausedState 暂停播放状态

if self.sender() == self.btn1:
self.playlist.previous()
elif self.sender() == self.btn2:
if self.player.state() == QMediaPlayer.StoppedState:
self.player.play()
else:
self.player.stop()
else:
self.playlist.next()

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.6.1 了解QWebEngineView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

from PyQt5.QtWebEngineWidgets import QWebEngineView

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.web_view = QWebEngineView() #注释1开始
self.web_view.load(QUrl('https://www.baidu.com')) # 加载一个网页,需要传入一个QUrl类型的参数
self.web_view.loadStarted.connect(self.start) # loadStarted信号会在网页开始加载时发射;
self.web_view.loadProgress.connect(self.progress)
self.web_view.loadFinished.connect(self.finish)
self.web_view.urlChanged.connect(self.show_url)#注释1结束
# loadProgress信号会在网页加载过程中不断发射,连接的progress()槽函数会不断输出当前的加载进度,如果进度为100,则表示加载完毕;
# loadFinished信号会在网页加载结束后发射,在finish()槽函数中,我们可以通过title()获取网页的标题;
# 最后一个urlChanged信号会在网页地址发生改变时发射。

self.btn = QPushButton('更改网址') #注释2开始
self.btn.clicked.connect(self.change_url) #注释2结束

v_layout = QVBoxLayout()
v_layout.addWidget(self.web_view)
v_layout.addWidget(self.btn)
self.setLayout(v_layout)

def start(self):
print('开始加载')

def progress(self, value):
print(value)

def finish(self):
print('加载结束')
print(self.web_view.title())
print(self.web_view.icon())

def show_url(self):
print(self.web_view.url())

def change_url(self):
self.web_view.setUrl(QUrl('https://www.ptpress.com.cn/')) #调用了setUrl()方法显示出新的网页。
# 如果想要显示自定义的HTML页面,可以调用setHtml()方法。


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.6.2 制作一款简单的浏览器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtWebEngineWidgets import QWebEngineView

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.resize(1000, 600)
self.url_input = QLineEdit() #注释1开始
self.back_btn = QPushButton() # 后退
self.forward_btn = QPushButton() # 前进
self.refresh_btn = QPushButton() # 刷新
self.zoom_in_btn = QPushButton() # 放大网页
self.zoom_out_btn = QPushButton() # 缩小网页
self.web_view = QWebEngineView()

self.init_ui()

def init_ui(self):
self.init_widgets()
self.init_signals()
self.init_layouts()

def init_widgets(self):
self.back_btn.setEnabled(False)
self.forward_btn.setEnabled(False)
self.back_btn.setIcon(QIcon('back.png'))
self.forward_btn.setIcon(QIcon('forward.png'))
self.refresh_btn.setIcon(QIcon('refresh.png'))
self.zoom_in_btn.setIcon(QIcon('zoom-in.png'))
self.zoom_out_btn.setIcon(QIcon('zoom-out.png'))
self.url_input.setText('about:blank')
self.url_input.setPlaceholderText('请输入网址')
self.web_view.setUrl(QUrl('about:blank'))

def init_signals(self): # 2
self.back_btn.clicked.connect(self.web_view.back)
self.forward_btn.clicked.connect(self.web_view.forward)
self.refresh_btn.clicked.connect(self.web_view.reload)
self.zoom_in_btn.clicked.connect(self.zoom_in)
self.zoom_out_btn.clicked.connect(self.zoom_out)
self.web_view.loadFinished.connect(self.update_state) # 当网页加载完毕,loadFinished信号就会发射
# 在放大和缩小网页时,先调用zoomFactor()方法获取到当前网页的缩放值,
# 在这个基础上再通过setZoomFactor()方法设置新的缩放值(范围为0.25~5.0)。

def init_layouts(self):
h_layout = QHBoxLayout()
v_layout = QVBoxLayout()
h_layout.addWidget(self.back_btn)
h_layout.addWidget(self.forward_btn)
h_layout.addWidget(self.refresh_btn)
h_layout.addWidget(self.url_input)
h_layout.addWidget(self.zoom_in_btn)
h_layout.addWidget(self.zoom_out_btn)
v_layout.addLayout(h_layout)
v_layout.addWidget(self.web_view)
v_layout.setContentsMargins(0, 8, 0, 0)
self.setLayout(v_layout)

def update_state(self):
url = self.web_view.url().toString() # 将url_input控件中的文本设置为当前所加载的网址。
self.url_input.setText(url)

if self.web_view.history().canGoBack(): # 获取到网页历史对象,它保留了当前用户的浏览记录
self.back_btn.setEnabled(True)
else:
self.back_btn.setEnabled(False)

if self.web_view.history().canGoForward():# 调用该对象的canGoBack()和canGoForward()方法就可以知道当前是否能够后退或前进。
self.forward_btn.setEnabled(True)
else:
self.forward_btn.setEnabled(False)

def zoom_in(self):
zoom_factor = self.web_view.zoomFactor()
self.web_view.setZoomFactor(zoom_factor + 0.1)

def zoom_out(self):
zoom_factor = self.web_view.zoomFactor()
self.web_view.setZoomFactor(zoom_factor - 0.1)

def keyPressEvent(self, event): # 当用户在文本框中输完URL并按“Enter”键后,我们要先判断文本框是否有焦点(部分用户输入完毕后可能会先单击窗口其他地方,导致文本框失去焦点)
if event.key() == Qt.Key_Enter:
if not self.url_input.hasFocus():
return
# 假如不进行判断,那每当用户在窗口上按“Enter”键后,网页就会直接重新加载。
url = self.url_input.text() # 判断用户输入的URL是否以“https://”或“http://”开头,不是的话就先加上“https://”再进行加载。
if url.startswith('https://') or url.startswith('http://'):
self.web_view.load(QUrl(url))
else:
url = 'https://' + url
self.web_view.load(QUrl(url))

self.url_input.setText(url)



if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.7.1 QUdpSocket 开发多人聊天室应用

PyQt提供了QUdpSocket、QTcpSocket、QTcpServer这3个类,它们封装了许多功能,能够帮助我们快速实现基于UDP和TCP的应用程序。
它们相较于Python标准库中的socket模块使用起来也更加方便。这3个类都在QtNetwork模块中,使用前要先导入它们

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtNetwork import *

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

self.browser = QTextBrowser() # 用来显示当前用户发送和接收到的各种信息
self.edit = QTextEdit() # QTextEdit控件用来输入信息
self.edit.setPlaceholderText('请输入消息')
self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.browser)
self.splitter.addWidget(self.edit)
self.splitter.setSizes([200, 100])

self.send_btn = QPushButton('发送')
self.send_btn.clicked.connect(self.send)

v_layout = QVBoxLayout()
v_layout.addWidget(self.splitter)
v_layout.addWidget(self.send_btn)
self.setLayout(v_layout)

self.name = f'用户{id(self)}' # 通过id(self)获取到当前窗口对象的内存地址后,将它作为用户名称保存到name变量中
print(f'我是{self.name}.') #

self.udp = QUdpSocket() #
data = f'{self.name}\n**%%加入%%**' #
self.udp.writeDatagram(data.encode(), QHostAddress('127.0.0.1'), 6666) # 将数据报发送到服务端IP地址(为QHostAddress类型),告诉它用户已加入聊天
self.udp.readyRead.connect(self.receive)
self.browser.append('您已加入聊天。\n') #注释2结束
# PyQt提供了几种常用的IP地址
# QHostAddress.Null 空地址,等同于QHostAddress()
# QHostAddress.LocalHost IPv4本地主机地址,等同于QHostAddress(127.0.0.1)
# QHostAddress.LocalHostlPv6 IPv6本地主机地址,等同于QHostAddress(.:1
# QHostAddress.Broadcast IPv4广播地址,等同于QHostAddress(255.255.255.255
# QHostAddress.AnyIPv4 任何IPv4地址,等同于QHostAdress(0.0.0.0,与该常量绑定的套接字只监听IPV4接
# QHostAddress.AnylPv6 任何IPv6地址,等同于QHostAdress(::),与该常量绑定的套接字只监听IPV6接口
# QHostAddress.Any 任何双协议栈地址,与该常量绑定的套接字可以监听IPV4接口和IPV6接口

def send(self):
if not self.edit.toPlainText():
return

message = self.edit.toPlainText()
data = f'{self.name}\n{message}\n'

self.edit.clear()
self.browser.append(data)
self.udp.writeDatagram(data.encode(), QHostAddress('127.0.0.1'), 6666)

def receive(self):
while self.udp.hasPendingDatagrams(): # 判断是否存在任何待读取的数据报
data_size = self.udp.pendingDatagramSize() # 获取到数据报的大小
data, host, port = self.udp.readDatagram(data_size) # 读取数据,数据报内容、发送者IP地址和发送者端口
if data:
data = data.decode()
self.browser.append(data)

def closeEvent(self, event): # 发送一条信息到服务端,告诉它当前用户已下线。
data = f'{self.name}\n**%%离开%%**'
self.udp.writeDatagram(data.encode(), QHostAddress('127.0.0.1'), 6666)
event.accept()


if __name__ == '__main__':
app = QApplication([])
finance_app = Client()
finance_app.show()
sys.exit(app.exec_())

6.7.2 QTcpSocket和QTcpServer-使用QTcpServer编写服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtNetwork import *

class Server(QWidget):
def __init__(self):
super(Server, self).__init__()
self.resize(400, 200)
self.browser = QTextBrowser()
v_layout = QVBoxLayout()
v_layout.addWidget(self.browser)
self.setLayout(v_layout)

self.tcp = QTcpServer() # 实例化一个QTcpServer对象
if self.tcp.listen(QHostAddress.LocalHost, 6666): # 调用listen()方法对指定IP地址和端口进行监听
# 如果监听正常,则返回True,否则返回False。
self.browser.append('已准备好与客户端进行连接。\n') #
self.tcp.newConnection.connect(self.handle_connection)
# 每当有来自客户端的新的连接请求时,QTcpServer就会发射newConnection信号。
else:
error = self.tcp.errorString() # 可以调用errorString()方法来获取监听失败的原因。
self.browser.append(error)

self.client_set = set()

def handle_connection(self):
sock = self.tcp.nextPendingConnection()
# 调用nextPendingConnection()方法来获取一个连接到客户端的QTcpSocket对象,通过它我们就可以和客户端通信了
self.client_set.add(sock)
sock.readyRead.connect(lambda: self.receive(sock))
sock.disconnected.connect(lambda: self.handle_disconnection(sock))# 如果客户端与服务端断开连接,disconnected信号就会发射。
address, port = self.get_address_and_port(sock)
data = f'{address}:{port}已加入聊天。\n'
self.browser.append(data)
self.send_to_other_clients(sock, data.encode())

def receive(self, sock):
while sock.bytesAvailable():
data_size = sock.bytesAvailable()
data = sock.read(data_size)
self.send_to_other_clients(sock, data)

def handle_disconnection(self, sock):
self.client_set.remove(sock)

address, port = self.get_address_and_port(sock)
data = f'{address}:{port}离开。\n'
self.browser.append(data)
self.send_to_other_clients(sock, data.encode())

def send_to_other_clients(self, current_client, data):
for target in self.client_set:
if target != current_client:
target.write(data)
address, port = self.get_address_and_port(target)
self.browser.append(f'已将消息发送给{address}:{port}。\n')

def get_address_and_port(self, sock):# 过peerAddress()和peerPort()方法分别获取到客户端使用的IP地址和端口。
address = sock.peerAddress().toString() # 获取客户端ip地址
port = sock.peerPort() # 获取客服端端口
return address, port
# 这个修改后的多人聊天室应用的使用方法和6.7.1小节中的一样,也是先运行服务端代码,再运行客户端代码

def closeEvent(self,event):
self.tcp.close()
event.accept()

if __name__ == '__main__':
app = QApplication([])
finance_app = Server()
finance_app.show()
sys.exit(app.exec_())

6.7.2 QTcpSocket和QTcpServer-使用QTcpSocket编写客户端

QTcpSocket和QTcpServer这两个类可以用来开发基于TCP的应用,前者用来开发客户端,后者用来开发服务端。
我们将使用这两个类来开发6.7.1小节中的多人聊天室应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtNetwork import *

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

self.browser = QTextBrowser()
self.edit = QTextEdit()
self.edit.setPlaceholderText('请输入消息')
self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.browser)
self.splitter.addWidget(self.edit)
self.splitter.setSizes([200, 100])

self.send_btn = QPushButton('发送')
self.send_btn.clicked.connect(self.send)

v_layout = QVBoxLayout()
v_layout.addWidget(self.splitter)
v_layout.addWidget(self.send_btn)
self.setLayout(v_layout)

self.name = f'用户{id(self)}'
print(f'我是{self.name}.')

self.tcp = QTcpSocket() # 实例化一个QTcpSockset对象
self.tcp.connectToHost(QHostAddress.LocalHost, 6666) # 连接服务端(三次握手)。
self.tcp.connected.connect(self.handle_connection) # 如果客户端和服务端连接成功,connected信号就会发射出来。
self.tcp.readyRead.connect(self.receive) # 当有新数据等待读取时,readyRead信号就会发射。

def handle_connection(self):
self.browser.append('已连接到服务器!\n')
self.browser.append('您已加入聊天。\n')

def send(self):
if not self.edit.toPlainText():
return

message = self.edit.toPlainText()
data = f'{self.name}\n{message}\n'

self.edit.clear()
self.browser.append(data)
self.tcp.write(data.encode())

def receive(self):
while self.tcp.bytesAvailable(): # 判断是否还有数据等待接收,如果有的话则调用read()方法将其读取出来。
data_size = self.tcp.bytesAvailable()
data = self.tcp.read(data_size)
if data:
self.browser.append(data.decode())

def closeEvent(self, event): # 如果用户关闭了聊天窗口,则调用close()方法关闭连接,释放系统资源。
self.tcp.close()
event.accept()


if __name__ == '__main__':
app = QApplication([])
finance_app = Client()
finance_app.show()
sys.exit(app.exec_())

6.8 QSS-QSS样读取式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtNetwork import *

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

with open('./style.qss', 'r') as f: # 文件中的样式不能有其他注释
qss = f.read()

self.btn = QPushButton('button', self)
self.btn.setStyleSheet(qss)
if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.8 QSS-调用QApplication对象的setStyleSheet()方法将样式作用于整个应用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtNetwork import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.btn = QPushButton('button', self)


if __name__ == '__main__':
with open('style.qss', 'r', encoding='utf-8') as f:
qss = f.read()

app = QApplication([])
app.setStyleSheet(qss)

window = Window()
window.show()
sys.exit(app.exec())

6.8 QSS-改变按钮控件上文本字体的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtNetwork import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
qss = "QPushButton {font-size: 50px;}" # 文本字体大小变为50像素
self.btn = QPushButton('button', self) #
self.btn.setStyleSheet(qss)

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.8.2 QSS基本规则

每个QSS样式都由选择器和声明这两部分组成,前者用来指定样式所作用的控件对象,后者用来指定样式使用的属性和值。
比方说“QPushButton {font-size:50px;}”这个样式,选择器“QPushButton”指定将样式作用于所有QPushButton按钮控件以及继承这个类的控件。
声明部分则指定“font-size”文本字体大小属性,并将值设置成了50像素。
我们可以声明多个属性和值,每对属性和值之间需要用英文分号间隔开来。

QPushButton {
font-size: 50px;
color: red;
}

当然也可以同时指定多个选择器。
QPushButton, QLabel, QLineEdit {
font-size: 50px;
color: red;
}

把选择器部分拆开
QPushButton {
font-size: 50px;
color: red;
}
QLabel {
font-size: 50px;
color: red;
}
QLineEdit {
font-size: 50px;
color: red;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtNetwork import *

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

self.btn = QPushButton('QPushButton')
self.label = QLabel('QLabel')
self.line_edit = QLineEdit()
self.text_edit = QTextEdit()
self.line_edit.setText('QLineEdit')
self.text_edit.setText('QTextEdit') # 1 由于QTextEdit没有在QSS样式的选择器中出现,所以它的文本字体大小和颜色是不会改变的

v_layout = QVBoxLayout()
v_layout.addWidget(self.btn)
v_layout.addWidget(self.label)
v_layout.addWidget(self.line_edit)
v_layout.addWidget(self.text_edit)
self.setLayout(v_layout)


if __name__ == '__main__':
with open('style.qss', 'r', encoding='utf-8') as f:
qss = f.read()

app = QApplication([])
app.setStyleSheet(qss)

window = Window()
window.show()
sys.exit(app.exec())

6.8.3 选择器的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
"""
选择器的类型有很多种,下面总结了几种常用的选择器


类型 示例 描述

通用选择器 * 匹配所有控件
类型选择器 QPushButton 匹配所有QPushButton控件及其子类(即上文中演示的那种)
属性选择器 QPushButton[name="btn"] 匹配所有name属性的值为“bn的QPushButlon控件~=代表匹所有name性的值中包含“bn的QPushButon控件。
QPushButton[name~="btn"] 可以通过etPropert/0方法设属性以及对应的值,而如果要获取某属性值,使用property0方法,传入属性名就可以了
类名选择器 .QPushButton 匹配所有QPushButton控件,但不匹配其子类。也可以这样写:*[class~="QPushButton"]
ID选择器 QPushButton#btn 匹配所有对象名称(ObjectName)为"btn"的QPushButon控件,可以调用setobjectName()方法设置对象名称。虽然不同的控件可以设置相同的对象名称,但是不建议这样做
后代选择器 QWidget.QPushButton 匹配所有QWidget控件中包含(无论是直接包合还是间接包合)的QPushButton控件
子选择器 QWidget>QPushButton 匹配所有QWidget控件中直接包合的QPushButton控件
"""

import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtNetwork import *

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

self.btn1 = QPushButton('button1', self)
self.btn2 = QPushButton('button2', self)
self.btn2.setProperty('name', 'btn') #设置了一个自定义属性name,并将该属性的值设为“btn”,所以property('name')输出的值就是“btn”,“button2”按钮的背景颜色就为绿色。
print(self.btn2.property('name')) #注释1结束

self.line_edit1 = QLineEdit(self)
self.line_edit1.setPlaceholderText('line edit')
self.line_edit2 = SubLineEdit()

self.combo_box = QComboBox(self) # 下拉框
self.combo_box.addItems(['A', 'B', 'C', 'D']) # 下拉列表
self.combo_box.setObjectName('cb') #下拉列表框调用了setObjectName()方法将对象名称设置为“cb”
# 这样就匹配到了QComboBox#cb {color: blue;}

self.group_box = QGroupBox() # label1是直接被添加到QGroupBox中
self.label1 = QLabel('label1') # 匹配的是“QGroupBox QLabel {color:blue;}”和“QGroupBox > QLabel {font: 30px;}”这两个样式
self.label2 = QLabel('label2') # 通过一个QStackWidget被添加到QGroupBox中的,所以算是间接包含,只匹配前者。
self.stack = QStackedWidget()
self.stack.addWidget(self.label2) #而label2则是通过一个QStackWidget被添加到QGroupBox中的

gb_layout = QVBoxLayout()
v_layout = QVBoxLayout()
gb_layout.addWidget(self.label1)
gb_layout.addWidget(self.stack)
self.group_box.setLayout(gb_layout)
v_layout.addWidget(self.btn1)
v_layout.addWidget(self.btn2)
v_layout.addWidget(self.line_edit1)
v_layout.addWidget(self.line_edit2)
v_layout.addWidget(self.combo_box)
v_layout.addWidget(self.group_box)
self.setLayout(v_layout)

class SubLineEdit(QLineEdit):
def __init__(self):
super(SubLineEdit, self).__init__()
self.setPlaceholderText('sub line edit')

# 明明“* {color: red}”这个样式是把所有的文本颜色设为红色,但是有些控件的文本颜色并没有改变,
# 比如QComboBox上的文本颜色就是蓝色。这里就涉及“具体与笼统”的概念,当选择器写得越具体时,
# 选择器的优先程度就越高。通配符*这一选择器写法非常笼统,而之后几个样式的选择器都是指定了控件名称的,
# 比通配符更加具体,所以优先程度更高。再比如这两个样式:

# QPushButton {background-color: blue;}
# QPushButton[name='btn'] {background-color: green;}

# 第一个样式规定所有QPushButton控件及其子类的背景颜色变为蓝色,
# 但第二个样式指定了name属性,比第一个样式更加具体,所以匹配到该选择器的按钮控件背景颜色为绿色,不会遵循第一个样式。

if __name__ == '__main__':
with open('style1.qss', 'r', encoding='utf-8') as f:
qss = f.read()

app = QApplication([])
app.setStyleSheet(qss)

window = Window()
window.show()
sys.exit(app.exec())

6.8.4 子控制器

PyQt提供的原生控件其实可以被细分成不同的子控件,比如QSpinBox数字调节框控件,
它就包含一个单行文本框控件、向上调节按钮控件和向下调节按钮控件。
QSS中有丰富的属性用来修改输入框样式或其中的文本样式,但是这两个调节按钮控件似乎不那么容易被获取到。
此时就应该使用子控制器,它是QSS中独有的(CSS中没有子控制器这一概念),用来设置窗口或控件的子控件样式。
子控制器的出现能够让QSS更深入地改变界面样式。
QSpinBox的向上调节按钮控件和向下调节按钮控件可以分别通过::up-button和::down-button获取到(子控制器用两个冒号::获取)。
示例代码通过子控制器改变了这两个按钮控件的样式,首先我们在style.qss中输入以下内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtNetwork import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.spin_box = QSpinBox(self)
self.spin_box.resize(100, 30)


if __name__ == '__main__':
with open('style2.qss', 'r', encoding='utf-8') as f:
qss = f.read()

app = QApplication([])
app.setStyleSheet(qss)

window = Window()
window.show()
sys.exit(app.exec())

6.8.5 伪状态

控件会根据用户的不同操作呈现出不同的状态,这些状态也被称为“伪状态”。
比方说QPushButton按钮控件,当我们单击按钮时,它处于被按下的状态(pressed)。
如果调用setEnabled(False)禁用按钮,那它就处于禁用状态(disabled)。
PyQt提供了很多伪状态选择器以方便我们对不同状态下的控件样式进行修改,
比如要修改按钮被禁用时的样式,就可以先用:disabled获取到禁用状态(伪状态用一个冒号:获取)。
示例代码通过伪状态选择器改变了按钮在不同状态下的样式,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *
from PyQt5.QtNetwork import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.btn = QPushButton('button', self)
self.btn.clicked.connect(lambda: self.btn.setEnabled(False))

# 我们可以在伪状态前加一个英文格式的感叹号“!”来表示相反的状态,比方说在悬停状态前加一个“!”,像下面这样。
#QPushButton:!hover {
# background-color: red;
# }

if __name__ == '__main__':
with open('style3.qss', 'r', encoding='utf-8') as f:
qss = f.read()

app = QApplication([])
app.setStyleSheet(qss)

window = Window()
window.show()
sys.exit(app.exec())

6.8.6 QSS第三方库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 1.Qt-Material  提供了许多仿Material的界面样式          pip install qt-material
# 导入 from qt_material import apply_stylesheet
# =============================================================================
# class Window(QWidget):
# def __init__(self):
# super(Window, self).__init__()
# self.btn = QPushButton('BUTTON', self)
#  
# if __name__ == '__main__':
# app = QApplication([])
# apply_stylesheet(app, theme='dark_teal.xml')# 1
#  
# window = Window()
# window.show()
# sys.exit(app.exec())
# =============================================================================


# 2.QDarkStyleSheet 它提供了完整的明暗系列主题 pip install qdarkstyle
# 导入 import qdarkstyle
# =============================================================================
# class Window(QWidget):
# def __init__(self):
# super(Window, self).__init__()
# self.btn = QPushButton('button', self)
#  
# if __name__ == '__main__':
# app = QApplication([])
# qss = qdarkstyle.load_stylesheet() #注释1开始
# app.setStyleSheet(qss) #注释1结束
#  
# window = Window()
# window.show()
# sys.exit(app.exec())
# =============================================================================

# #1 程序首先调用load_stylesheet()方法获取QSS样式,然后将它传入QApplication对象的setStyleSheet()方法中。
# 如果要切换为明亮主题,只需要往load_stylesheet()方法中传入qdarkstyle.LightPalette,
# 该方法默认使用的是黑暗主题qdarkstyle.DarkPalette。

6.9.1 国际化-使用translate()方法

当我们给控件设置文本时,会调用setText()方法并传入相应的文本。
为了能让PyQt提取出要翻译的文本,我们要先对各个文本字符串使用QCoreApplication.translate()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
self.combo_box = QComboBox()
self.combo_box.addItems(['English', '中文'])
self.button = QPushButton()
self.label = QLabel()
self.label.setAlignment(Qt.AlignCenter)

v_layout = QVBoxLayout()
v_layout.addWidget(self.combo_box)
v_layout.addWidget(self.button)
v_layout.addWidget(self.label)
self.setLayout(v_layout)

self.retranslateUi()

def retranslateUi(self): # 专门定义了一个retranslateUi()函数来更新控件的文本。
_translate = QCoreApplication.translate # 将QCoreApplication.translate保存到_translate变量中,这样可以让我们更加方便使用它
self.setWindowTitle(_translate('Window', 'Switch')) # _translate()方法接收的第一个参数是待翻译文本所在的类名称,第二个参数是待翻译文本
self.button.setText(_translate('Window', 'Start'))
self.label.setText(_translate('Window', 'Hello World!'))
# 当我们将设计师生成的.ui文件转换为.py文件后,其实也可以看到这个名称为retranslateUi的函数,
# 它是专门用来更新界面上的文本的。

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

6.9.2 制作.ts文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

#跳过

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.1 图形图元类QGraphicsItem-标准图元

第7章 
图形视图框架PyQt提供的图形视图框架可以让我们方便地管理大量的自定义2D图元并与之进行交互。
该框架使用BSP(Binary Space Partitioning,二叉空间剖分)树,能够快速查找图形元素。
因此,就算视图中包含大量的内容,我们也可以在界面上快速地操作它们。除此之外,
该框架还提供了图元放大、缩小、旋转和碰撞检测的相关方法,非常适合用来开发游戏。

图形视图框架主要包含3个类:
图形图元类QGraphicsItem、图形场景类QGraphicsScene和图形视图类QGraphicsView。

用简单的一句话来概括一下三者的关系:
图元是放在场景上的,而场景内容则是通过视图显示出来的。

PyQt标准图元:
● 椭圆图元QGraphicsEllipseItem。
● 直线图元QGraphicsLineItem。
● 路径图元QGraphicsPathItem。
● 图片图元QGraphicsPixmapItem。
● 多边形图元QGraphicsPolygonItem。
● 矩形图元QGraphicsRectItem。
● 纯文本图元QGraphicsSimpleTextItem。
● 富文本图元QGraphicsTextItem。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QGraphicsView): # 场景内容通过视图显示出来,所以我们直接继承QGraphicsView来开发这个窗口
def __init__(self):
super(Window, self).__init__()
self.ellipse = QGraphicsEllipseItem() # 矩形图元
self.ellipse.setRect(0, 100, 50, 100)

self.line = QGraphicsLineItem() # 直线图元
self.line.setLine(100, 100, 100, 200)

self.path = QGraphicsPathItem() # 路径图元
tri_path = QPainterPath()
tri_path.moveTo(150, 100)
tri_path.lineTo(200, 100)
tri_path.lineTo(200, 200)
tri_path.lineTo(150, 100)
tri_path.closeSubpath()
self.path.setPath(tri_path)

self.pixmap = QGraphicsPixmapItem() # 图片图元
self.pixmap.setPixmap(QPixmap('11.png').scaled(100, 100)) # 设置图片图元
self.pixmap.setPos(250, 100) # # 设置图元位置
self.pixmap.setFlags(QGraphicsItem.ItemIsMovable) # 设置图元特征,传入参数QGraphicsItem.ItemIsMovable表示图元能够用鼠标来移动
# 所有图元都可以使用setFlags()来设置特征
# QGraphicsitem.ltemlsMovable 可以移动图元
# QGraphicsltem.ltemlsSelectable 可以选择图元,选择时图元周围会有一圈虚线
# QGraphicsltem.ltemlsFocusable 图元可以接收焦点
# QGraphicsltem.ltemClipsToShape 根据图元形状进行裁剪形状区域之外的区域无法被绘制,也不会触发鼠标事件、拖放事件
# QGraphicsltem.ltemClipsChildrenToShape 根据当前图元的形状进裁剪,该图元的直接或间接子图元都将限制在该形状区域内,无法在形状区域之外的区域进行绘制
# QGraphicsltem.ltemlgnoresParentOpacity 忽略父图元的透明度
# QGraphicsltem.ltemStacksBehindParent 当一个父图元添加子图时,子图元会被放置在父图元下面(默认是置于父图元上面的)。该特征很适合用来制作阴影效果

self.polygon = QGraphicsPolygonItem() # 图片旁边的线图
point1 = QPointF(400.0, 100.0)
point2 = QPointF(420.0, 150.0)
point3 = QPointF(430.0, 200.0)
point4 = QPointF(380.0, 110.0)
point5 = QPointF(350.0, 110.0)
point6 = QPointF(400.0, 100.0)
self.polygon.setPolygon(QPolygonF([point1, point2, point3,point4, point5, point6]))

self.rect = QGraphicsRectItem() # 矩形图元
self.rect.setRect(450, 100, 50, 100)

self.simple_text = QGraphicsSimpleTextItem() # 纯文本图元
self.simple_text.setText('Hello PyQt!')
self.simple_text.setPos(550, 100)

self.rich_text = QGraphicsTextItem() # 富文本图元
self.rich_text.setHtml('<p style="font-size:10px">Hello PyQt!</p>')
self.rich_text.setPos(650, 100)
self.rich_text.setTextInteractionFlags(Qt.TextEditorInteraction)
# 文本图元有一个独有的setTextInteractionFlags()方法,它用来设置文本的交互特征,传入Qt.TextEditorInteraction表示用户能够直接通过双击来编辑文本
# Qt.NoTextlnteraction 无任何交互
# Qt.TextSelectableByMouse 文本可以通过鼠标或键盘选择和复制
# Qt.TextSelectableByKeyboard 文本可以通过键盘上的方向键选择
# Qt.LinksAccessibleByMouse 可以通过单击访问超链接
# Qt.LinksAccessibleByKeyboard 可以通过“Tab”键获取焦点并用“Enter键访问超链接
# Qt.TextEditable 文本能够被完全编辑
# Qt.TextEditorlnteraction 行为与QTextEdit控件相同
# Qt.TextBrowserlnteraction 行为与QTextBrowser控件相同
self.rich_text.setDefaultTextColor(QColor(100, 100, 100)) # 设置文本颜色,只有富文本图元才拥有,纯文本图元无法设置文本颜色

self.graphics_scene = QGraphicsScene() # 图元是要放到场景中的,所以需要实例化一个QGraphicsScene对象
self.graphics_scene.setSceneRect(0, 0, 750, 300) # 设置场景大小
self.graphics_scene.addItem(self.ellipse) # addItem()方法用来添加图元
self.graphics_scene.addItem(self.line)
self.graphics_scene.addItem(self.path)
self.graphics_scene.addItem(self.pixmap)
self.graphics_scene.addItem(self.polygon)
self.graphics_scene.addItem(self.rect)
self.graphics_scene.addItem(self.simple_text)
self.graphics_scene.addItem(self.rich_text)

self.resize(750, 300) # 场景内容通过视图显示出来,所以我们直接继承QGraphicsView来开发这个窗口,resize()方法用来设置视图大小
self.setScene(self.graphics_scene) # setScene()用来确定要显示的场景

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.1.2 图元层级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QGraphicsView):
def __init__(self):
super(Window, self).__init__()
self.rect1 = QGraphicsRectItem() # 程序实例化了3个矩形图元
self.rect1.setRect(0, 0, 200, 200)
self.rect1.setBrush(QBrush(QColor(255, 0, 0)))
self.rect1.setFlags(QGraphicsItem.ItemIsMovable)
self.rect1.setZValue(1.0) # rect1矩形图元对象调用了setZValue()方法改变了自身的层级大小

self.rect2 = QGraphicsRectItem()
self.rect2.setRect(0, 0, 100, 100)
self.rect2.setBrush(QBrush(QColor(0, 255, 0)))
self.rect2.setFlags(QGraphicsItem.ItemIsMovable)

self.rect3 = QGraphicsRectItem()
self.rect3.setRect(0, 0, 50, 50)
self.rect3.setBrush(QBrush(QColor(0, 0, 255)))
self.rect3.setFlags(QGraphicsItem.ItemIsMovable)
# 默认rect3会遮挡rect2,rect2会遮挡rect1。
# 但是rect1矩形图元对象调用了setZValue()方法改变了自身的层级大小,且层级大于rect2和rec3,
# 也就是rect1会遮挡住rect2和rect3。

print(self.rect1.zValue()) # zValue()方法用来获取一个图元的层级大小
print(self.rect2.zValue())
print(self.rect3.zValue())


self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 500,500)
self.graphics_scene.addItem(self.rect1)
self.graphics_scene.addItem(self.rect2)
self.graphics_scene.addItem(self.rect3)

self.resize(500, 500)
self.setScene(self.graphics_scene)
# 添加到场景中的顺序为rect1、rect2、rect3,所以默认rect3会遮挡rect2,rect2会遮挡rect1


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.1.2 图元层级—父图元会被子图元遮挡

父图元会被子图元遮挡,而且父图元无法通过setZValue()方法改变这种遮挡情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QGraphicsView):
def __init__(self):
super(Window, self).__init__()
self.rect1 = QGraphicsRectItem()
self.rect1.setRect(0, 0, 200, 200)
self.rect1.setBrush(QBrush(QColor(255, 0, 0)))
self.rect1.setFlags(QGraphicsItem.ItemIsMovable)
self.rect1.setZValue(2.0)

self.rect2 = QGraphicsRectItem()
self.rect2.setRect(0, 0, 100, 100)
self.rect2.setBrush(QBrush(QColor(0, 255, 0)))
self.rect2.setFlags(QGraphicsItem.ItemIsMovable)
self.rect2.setParentItem(self.rect1) # rect2和rect3通过setParentItem()方法确定rect1为它们的父图元
self.rect2.setZValue(1.0)
# rect2比rect3先被添加到父图元上,所以目前的遮挡情况是:rect3遮挡rect2,rect2遮挡rect1
self.rect3 = QGraphicsRectItem()
self.rect3.setRect(0, 0, 50, 50)
self.rect3.setBrush(QBrush(QColor(0, 0, 255)))
self.rect3.setFlags(QGraphicsItem.ItemIsMovable)
self.rect3.setParentItem(self.rect1) # rect2和rect3通过setParentItem()方法确定rect1为它们的父图元

print(self.rect1.zValue())
print(self.rect2.zValue())
print(self.rect3.zValue())

self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 500,500)
self.graphics_scene.addItem(self.rect1)# 在往场景中添加图元时,我们只需要添加rect1即可,剩下的几个矩形图元都会根据父子关系自动被添加到场景中。

self.resize(500, 500)
self.setScene(self.graphics_scene)
# 可以通过QGraphicsItem.ItemStacksBehindParent来改变父子图元的遮挡情况。

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.1.3 图元变换

学习如何将平移、缩放和旋转这3种变换应用到图元上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QGraphicsView):
def __init__(self):
super(Window, self).__init__()

self.rect1 = QGraphicsRectItem()
self.rect1.setRect(0, 0, 200, 200)
self.rect1.setBrush(QBrush(QColor(255, 0, 0)))
self.rect1.moveBy(100, 100) # 平移图元(图元在场景上的x轴和y轴上的移动距离)
self.rect1.setScale(1.5) # 缩放图元 传入0会将图元缩小成一个点,传入负数则会返回图元翻转和镜像化后的样子
self.rect1.setRotation(45) # 旋转图元 [-360, 360]
self.rect1.setTransformOriginPoint(100,100) # 改变图元的中心点 缩放和旋转的中心点默认为(0, 0),也就是图元的左上角
# setTransformOriginPoint()方法是根据图元的原始大小来设置中心点的,缩放操作不会对该方法产生影响。

self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 500, 500)
self.graphics_scene.addItem(self.rect1)

self.resize(500, 500)
self.setScene(self.graphics_scene)



if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.1.4 图元分组

分组就是指对各个图元进行分类,分到一起的图元会共同行动(选中、移动或复制等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *


class Window(QGraphicsView): # rect1和ellipse1为一组,rect2和ellipse2为另一组
def __init__(self):
super(Window, self).__init__()
self.rect1 = QGraphicsRectItem() # 实例化矩形图元
self.rect2 = QGraphicsRectItem()
self.ellipse1 = QGraphicsEllipseItem() # 实例化椭圆图元
self.ellipse2 = QGraphicsEllipseItem()
self.rect1.setRect(10, 10, 100, 100)
self.rect1.setBrush(QBrush(QColor(255, 0, 0)))
self.rect2.setRect(150, 10, 100, 100)
self.rect2.setBrush(QBrush(QColor(0, 0, 255)))
self.ellipse1.setRect(10, 150, 100, 50)
self.ellipse1.setBrush(QBrush(QColor(255, 0, 0)))
self.ellipse2.setRect(150, 150, 100, 50)
self.ellipse2.setBrush(QBrush(QColor(0, 0, 255)))

self.group1 = QGraphicsItemGroup() # 实例化两个QGraphicsItemGroup分组对象
self.group2 = QGraphicsItemGroup()
self.group1.addToGroup(self.rect1)
self.group1.addToGroup(self.ellipse1)
self.group1.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable)
self.group2.addToGroup(self.rect2)
self.group2.addToGroup(self.ellipse2)
self.group2.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable) #注释1结束

print(self.group1.boundingRect()) #注释2开始
print(self.group2.boundingRect()) #注释2结束
# 选中时,分组边界的大小是由组内的图元整体决定的,边界的位置和大小可以通过boundingRect()方法获取到。

self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 500, 500)
self.graphics_scene.addItem(self.group1) # 调用场景对象的addItem()方法将分组添加到场景中
self.graphics_scene.addItem(self.group2)

self.resize(500, 500)
self.setScene(self.graphics_scene)

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.1.5 碰撞检测

碰撞检测通常会在游戏中出现,比如在《飞机大战》游戏中,程序会对子弹和敌机进行碰撞检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QGraphicsView):
def __init__(self):
super(Window, self).__init__()
self.rect = QGraphicsRectItem() # 界面上有一个矩形图元和一个椭圆图元,两者都可以被移动和选中。
self.ellipse = QGraphicsEllipseItem()
self.rect.setRect(100, 100, 150, 130)
self.rect.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)
self.ellipse.setRect(100, 300, 150, 100)
self.ellipse.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)#注释1结束

self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 500, 500)
self.graphics_scene.addItem(self.rect)
self.graphics_scene.addItem(self.ellipse)

self.resize(500, 500)
self.setScene(self.graphics_scene)

def mouseMoveEvent(self, event): # 2
super(Window, self).mouseMoveEvent(event)
if self.ellipse.collidesWithItem(self.rect, Qt.IntersectsItemShape): # 当ellipse本身的轮廓与rect的轮廓相交时,collidesWithItem()方法就会返回True。
print(self.ellipse.collidingItems(Qt.IntersectsItemShape)) # collidingItems()方法会返回与ellipse图元发生碰撞的图元列表。
# 碰撞的检测方式
# Qt.ContainsItemShape 以形状为范围,当前图元被其他图元完全包含
# Qt.IntersectsItemShape 以形状为范围,当前图元被完全包含或者与其他图元有重圣
# Qt.ContainsItemBoundingRect 以矩形边界为范围,当前图元被其他图元完全包合
# Qt.IntersectsItemBoundingRect 以矩形进界为范围,当前图元被完全包含或者与其他图元有重叠

# 矩形边界就是当我们选中某图元时周围显示的矩形虚线,可以用boundingRect()方法获取到
# 而形状就是图元本身的轮廓,可以用shape()方法获取到

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.1.6 给图元添加信号和动画

QGraphicsItem不继承QObject,所以本身并不能使用信号和槽机制,我们也无法给它添加动画,
不过PyQt提供了QGraphicsObject类好让我们解决这一问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class MyRectItem(QGraphicsObject): # 继承QGraphicsObject
my_signal = pyqtSignal()

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

def boundingRect(self): # 重写
return QRectF(0, 0, 100, 30) # 返回一个QRectF类型的值来确定矩形图元的初始位置和大小

def paint(self, painter, styles, widget=None): # 重写
painter.drawRect(self.boundingRect()) # 调用drawRect()方法将矩形画到界面上。

class Window(QGraphicsView):
def __init__(self):
super(Window, self).__init__()
self.rect = MyRectItem() # 实例化一个MyRectItem对象
self.rect.my_signal.connect(lambda: print('signal and slot')) # 应用信号
self.rect.my_signal.emit()

self.animation = QPropertyAnimation(self.rect, b'pos') # 动画
self.animation.setDuration(3000)
self.animation.setStartValue(QPointF(100, 30))
self.animation.setEndValue(QPointF(100, 200))
self.animation.setLoopCount(-1)
self.animation.start() #注释4结束

self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 500, 500)
self.graphics_scene.addItem(self.rect)

self.resize(500, 500)
self.setScene(self.graphics_scene)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.2 图形场景类QGraphicsScene

我们可以把场景看作一个大容器,它能够容纳大量的图元,并提供相应的方法来对图元进行管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *



if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.2.1 管理图元

QGraphicsScene类提供了快速添加、查找和删除图元的相关方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QGraphicsView):
def __init__(self):
super(Window, self).__init__()
self.graphics_scene = QGraphicsScene() # 实例化场景
self.graphics_scene.setSceneRect(0, 0, 300, 300)
self.graphics_scene.focusItemChanged.connect(self.show_item)# 1
# 场景有一个focusItemChanged信号,当我们选中不同的图元时,该信号就会发射,并传递两个值:
# 第一个是新选中的图元,第二个是之前选中的图元
# 注意,该信号只针对设置了ItemIsFocusable特征的图元。场景也提供了focusItem()方法让我们直接获取到当前新选中的图元。

self.ellipse = self.graphics_scene.addEllipse(50, 100, 50, 100) # 直接调用场景的相应方法来添加不同类型的图元 椭圆图元
self.ellipse.setFlags(QGraphicsItem.ItemIsFocusable)
self.rect = self.graphics_scene.addRect(150, 100, 100, 100) # 矩形图元
self.rect.setFlags(QGraphicsItem.ItemIsFocusable)

print(self.graphics_scene.items())
# items()方法会以列表形式返回场景中的所有图元,且默认按照降序方式(Qt.DescendingOrder)进行排列,即最顶层的图元排在列表最前面。
# 如果要按照升序方式进行排列,只需要往items()方法中传入Qt.AscendingOrder这个值
print(self.graphics_scene.itemsBoundingRect()) # itemsBoundingRect()方法会返回包含所有图元的最小矩形边界

self.resize(300, 300)
self.setScene(self.graphics_scene)

def show_item(self, new_item, old_item):
print(f'new item: {new_item}')
print(f'old item: {old_item}')

def mousePressEvent(self, event): # 鼠标按下事件函数
super(Window, self).mousePressEvent(event)
pos = event.pos() # 获取单击时鼠标指针的位置
item = self.graphics_scene.itemAt(pos, QTransform()) # 获取到当前单击的图元
# 第二个参数是QTransform类型的值,表示应用到视图上的变换,目前只需要传入QTranform()(也就是不应用任何变换)。
print(item)

def mouseDoubleClickEvent(self, event): # 鼠标双击事件函数
super(Window, self).mouseDoubleClickEvent(event)
pos = event.pos()
item = self.graphics_scene.itemAt(pos, QTransform()) # itemAt()获取到当前双击的图元
if item:
self.graphics_scene.removeItem(item) # 用removeItem()方法将其删除



if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.2.2 嵌入控件

我们还可以向场景中添加不同类型的控件,QGraphicsScene场景类专门提供了addWidget()方法用于实现此功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QGraphicsView):
def __init__(self):
super(Window, self).__init__()
self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 220, 100)

self.label = QLabel('label')
self.button = QPushButton('button')

self.label_proxy = self.graphics_scene.addWidget(self.label) # 调用了addWidget()方法将QLabel和QPushButton控件添加到场景中
# 该方法返回一个QGraphicsProxyWidget控件代理对象,它可以被看作场景中的一个图元,而我们可以通过控制代理对象来操作控件。
self.button_proxy = self.graphics_scene.addWidget(self.button)
self.label_proxy.setPos(10, 20)
self.button_proxy.setPos(50, 20)

# 控件的背景是灰色的,与场景的白色背景不搭。可以在程序中添加以下两行代码使控件背景透明化
self.label.setAttribute(Qt.WA_TranslucentBackground)
self.button.setAttribute(Qt.WA_TranslucentBackground)

self.resize(220, 80)
self.setScene(self.graphics_scene)

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.2.2 嵌入控件-使用布局管理器排列控件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QGraphicsView):
def __init__(self):
super(Window, self).__init__()
self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 260, 80)

self.user_label = QLabel('Username:')
self.pass_label = QLabel('Password:')
self.user_line = QLineEdit()
self.pass_line = QLineEdit()
self.user_label.setAttribute(Qt.WA_TranslucentBackground)
self.pass_label.setAttribute(Qt.WA_TranslucentBackground)
self.user_line.setAttribute(Qt.WA_TranslucentBackground)
self.pass_line.setAttribute(Qt.WA_TranslucentBackground)
self.user_label_proxy = self.graphics_scene.addWidget(self.user_label)
self.pass_label_proxy = self.graphics_scene.addWidget(self.pass_label)
self.user_line_proxy = self.graphics_scene.addWidget(self.user_line)
self.pass_line_proxy = self.graphics_scene.addWidget(self.pass_line)

linear_layout1 = QGraphicsLinearLayout() # 使用线性布局管理器,默认水平布局方向
linear_layout2 = QGraphicsLinearLayout()
linear_layout3 = QGraphicsLinearLayout()
linear_layout3.setOrientation(Qt.Vertical) # 通过setOrientation()方法改变布局方向。
# 图形视图框架还提供了QGraphicsGridLayout网格布局管理器和QGraphicsAnchorLayout锚点布局管理器。

linear_layout1.addItem(self.user_label_proxy)
linear_layout1.addItem(self.user_line_proxy)
linear_layout2.addItem(self.pass_label_proxy)
linear_layout2.addItem(self.pass_line_proxy)
linear_layout3.addItem(linear_layout1)
linear_layout3.addItem(linear_layout2)

graphics_widget = QGraphicsWidget() # 将布局设置到QGraphicsWidget对象上
graphics_widget.setLayout(linear_layout3)
self.graphics_scene.addItem(graphics_widget) # 将QGraphicsWidget对象添加到场景

self.resize(260, 80)
self.setScene(self.graphics_scene)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.3 图形视图类QGraphicsView

QGraphicsView用来显示场景内容,它是一个提供了滚动条的视窗。
多个场景可以通过同一个图形视图来切换显示,而同一个场景也可以应用多个图形视图,让我们能够从不同的角度同时进行观察

1
2
3
4
5
6
7
8
9
10
11
12
13
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.3.1 视图和场景的大小关系

在之前的示例代码中,场景和视图的大小都是一样的。如果场景大小大于等于视图大小,
视图则会显示垂直或水平滚动条供用户浏览剩余场景内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window1(QGraphicsView):
def __init__(self):
super(Window1, self).__init__()
self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 300, 300)# 1

self.rect = QGraphicsRectItem()
self.rect.setRect(220, 220, 50, 50)
self.graphics_scene.addItem(self.rect)

self.resize(200, 200) # 2
self.setScene(self.graphics_scene)

# 当场景大小等于视图大小时,如果我们想要去掉滚动条,只需要用QSS把QGraphicsView的边框border属性的值设置为0即可
class Window2(QWidget):
def __init__(self):
super(Window2, self).__init__()
self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 400, 400)

self.rect = QGraphicsRectItem()
self.rect.setRect(0, 0, 50, 50)
self.graphics_scene.addItem(self.rect)

self.graphics_view = QGraphicsView(self)
self.graphics_view.resize(200, 200)
self.graphics_view.setScene(self.graphics_scene)

qss = "QGraphicsView { border: 0px; }"
self.graphics_view.setStyleSheet(qss)

# 如果场景大小小于视图大小,那视图就会默认居中显示场景内容
class Window3(QGraphicsView):
def __init__(self):
super(Window3, self).__init__()
self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 100, 100)

self.rect1 = QGraphicsRectItem()
self.rect1.setRect(0, 0, 50, 50)
self.rect1.setBrush(QBrush(QColor(255, 0, 0)))
self.rect2 = QGraphicsRectItem()
self.rect2.setRect(0, 50, 50, 50)
self.rect2.setBrush(QBrush(QColor(0, 255, 0)))
self.rect3 = QGraphicsRectItem()
self.rect3.setRect(50, 0, 50, 50)
self.rect3.setBrush(QBrush(QColor(0, 0, 255)))
self.rect4 = QGraphicsRectItem()
self.rect4.setRect(50, 50, 50, 50)
self.rect4.setBrush(QBrush(QColor(255, 0, 255)))

self.graphics_scene.addItem(self.rect1)
self.graphics_scene.addItem(self.rect2)
self.graphics_scene.addItem(self.rect3)
self.graphics_scene.addItem(self.rect4)

self.resize(200, 200)
self.setScene(self.graphics_scene)

if __name__ == '__main__':
app = QApplication([])
finance_app = Window3()
finance_app.show()
sys.exit(app.exec_())

7.3.2 视图变换

在视图上执行的平移、缩放和旋转变换会作用在所有图元上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QGraphicsView):
def __init__(self):
super(Window, self).__init__()
self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 300, 300)

self.rect = QGraphicsRectItem()
self.rect.setRect(50, 50, 50, 50)
self.rect.setBrush(QBrush(QColor(255, 0, 0)))
self.ellipse = QGraphicsEllipseItem()
self.ellipse.setRect(100, 100, 50, 50)
self.ellipse.setBrush(QBrush(QColor(0, 255, 0)))

self.graphics_scene.addItem(self.rect)
self.graphics_scene.addItem(self.ellipse)

self.move(10, 10)
self.resize(300, 300)
self.setScene(self.graphics_scene)

def mousePressEvent(self, event): # 调用rotate(10)将视图顺时针旋转10°
self.rotate(10)

def wheelEvent(self, event): # 鼠标滚轮事件函数
if event.angleDelta().y() < 0:
self.scale(0.9, 0.9) # 通过scale()方法改变视图大小
else:
self.scale(1.1, 1.1)
# 平移视图可以使用move()方法,平移视图也就是指改变窗口在计算机屏幕上的位置

if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.4.1 事件传递顺序

现在编写3个类,让它们分别继承QGraphicsView、QGraphicsScene和QGraphicsItem,
然后重写mousePressEvent()事件函数,观察一下事件的传递顺序是怎么样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class ParentItem(QGraphicsRectItem):
def __init__(self):
super(ParentItem, self).__init__()
self.setRect(100, 30, 100, 50)

def mousePressEvent(self, event): # 1
print('event from parent QGraphicsItem')
super().mousePressEvent(event) # 视图→场景→子图元→父图元
# 需要注意的一点是:调用父类的事件处理接口不能省略,否则事件就会停止传递。
# 比如删掉Window类下mousePressEvent()事件函数中的super().mousePressEvent(event),
# 那么控制台就只会输出“event from QGraphicsView”文本了

class ChildItem(QGraphicsRectItem):
def __init__(self):
super(ChildItem, self).__init__()
self.setRect(100, 30, 50, 30)

def mousePressEvent(self, event): # 1
print('event from child QGraphicsItem')
super().mousePressEvent(event)

class Scene(QGraphicsScene):
def __init__(self):
super(Scene, self).__init__()
self.setSceneRect(0, 0, 300, 300)

def mousePressEvent(self, event): # 1
print('event from QGraphicsScene')
super().mousePressEvent(event)

class Window(QGraphicsView):
def __init__(self):
super(Window, self).__init__()
self.resize(300, 300)

self.scene = Scene()
self.setScene(self.scene)

self.parent_item = ParentItem()
self.child_item = ChildItem()
self.child_item.setParentItem(self.parent_item)
self.scene.addItem(self.parent_item)

def mousePressEvent(self, event): # 1
print('event from QGraphicsView')
super().mousePressEvent(event)


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

7.4.2 坐标转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import sys
import time
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtSql import *

class Window(QGraphicsView):
def __init__(self):
super(Window, self).__init__()
self.graphics_scene = QGraphicsScene()
self.graphics_scene.setSceneRect(0, 0, 300, 300)

self.ellipse = self.graphics_scene.addEllipse(50, 100, 50, 100)
self.rect = self.graphics_scene.addRect(150, 100, 100, 100)

self.resize(500, 500)
self.setScene(self.graphics_scene)

def mousePressEvent(self, event):
super(Window, self).mousePressEvent(event)
pos = event.pos() # # 在窗口上单击时,通过event.pos()获取到的坐标是视图内的坐标
# 如果场景大小和视图大小一样的话,那么event.pos()获取到的坐标也可以被看作场景内的坐标
# 但是如果它们的大小不一样,那我们就需要把event.pos()获取到的视图内的坐标转换成场景内的坐标
# 否则我们无法通过QGraphicsScene. itemAt()方法正确获取到场景中的图元
item = self.graphics_scene.itemAt(pos, QTransform())
# itemAt()方法无法通过传给它的视图坐标找到这两个图元,它需要一个场景坐标
print(item)
# 此时,我们应该将这个视图坐标映射到场景中。图形视图框架提供了视图、场景和图元坐标之间的转换方法,以及图元与图元坐标之间的转换方法
# QGraphicsView.mapToScene() 将视图内的坐标转换为场景内的坐标
# QGraphicsView.mapFromScene() 将场景内的坐标转换为视图内的坐标
# QGraphicsltem.mapFromScene() 将场景内的坐标转换为图元内的坐标
# QGraphicsltem.mapToScene() 将图元内的坐标转换为场景内的坐标
# QGraphicsltem.mapToParent() 将子图元内的坐标转换为父图元内的坐标
# QGraphicsltem.mapFromParent() 将父图元内的坐标转换为子图元内的坐标
# QGraphicsltem.mapToltem() 将当前图元内的坐标转换为其他图元内的坐标
# QGraphicsitem.mapFromltem() 将其他图元内的坐标转换为当前图元内的坐标

# 坐标转换代码
# def mousePressEvent(self, event):
# super(Window, self).mousePressEvent(event)
# pos = self.mapToScene(event.pos())
# item = self.graphics_scene.itemAt(pos, QTransform())
# print(item)

# GraphicsView也提供了itemAt()方法,我们只需要给该方法传入视图内的坐标就能获取到图元,
# 不需要传入第二个QTransform类型的参数。


if __name__ == '__main__':
app = QApplication([])
finance_app = Window()
finance_app.show()
sys.exit(app.exec_())

8.1 PyInstaller

PyInstaller的用法非常简单,而且它的打包速度很快。
通过它我们能够将程序打包成Windows、macOS和Linux上的可执行文件。
PyInstaller已经成功运用在一些不常见的系统上,比如AIX、Solaris、FreeBSD和OpenBSD。
PyInstaller官方也在不断更新,所以能够被PyInstaller打包的第三方库也越来越多,“坑”也越来越少。

8.1.1 环境配置

环境配置
pip install pyinstaller

8.1.2 PyInstaller多文件和单文件打包模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 在命令提示符窗口中执行“pyinstaller -v”命令后发现有以下文字时,执行pip uninstall pathlib命令 卸载掉过时的路径包
# The 'pathlib' package is an obsolete backport of a standard library package and is incompatible with PyInstaller. Please remove this package (located in C:\Users\11298\AppData\Local\Programs\Python\Python311\Lib\site-packages) using
# "C:\Users\11298\AppData\Local\Programs\Python\Python311\python.exe" -m pip uninstall pathlib
# then try again.

接着,打开demo文件夹,在路径栏中输入“cmd”并按“Enter”键。在出现的命令提示符窗口中输入以下命令。

pyinstaller hello.py


打包结束后,demo文件夹下出现了build和dist两个文件夹,以及一个.spec文件。

build文件夹中存放着一些编译文件,可以直接删除。
hello.spec是一个打包配置文件,我们也可以通过该文件来打包程序,但效果与命令提示符窗口中执行命令是一样的。

2.单文件打包模式

dist文件夹中只会生成一个hello.exe可执行文件,全部的依赖文件都已经被打包到这个文件中了。
只需要添加一个“-F”就可以使用单文件打包模式。 pyinstaller -F hello.py


采用单文件打包模式生成的hello.exe在运行时,会先花费一点儿时间把依赖文件解压出来,再显示运行结果。
这就是为什么通过单文件打包模式打包出来的程序在运行时会比较慢。

可以使用“--runtime-tmpdir”命令改变_MEI临时文件夹的生成位置,语法格式如下
"""
pyinstaller -F --runtime-tmpdir=/another/path/ hello.py
"""
_MEI文件夹会在程序关闭时自动被删除掉。但是如果程序运行崩溃或者被强制退出(比如使用任务管理器强制退出)的话,
那么这个文件夹是不会自动被删除的!这就会导致磁盘内存被占用,需要我们手动进行删除。

8.1.3 黑框的调试作用

黑框指的是命令提示符窗口。当我们运行可执行文件后,黑框中会显示程序的输出内容。
但是如果可执行文件运行失败,那么黑框中就会显示报错信息。通过这个信息,我们就能知道如何调试程序

调试程序: 打开一个新的命令提示符窗口,然后将hello.exe拖入,按“Enter”键运行

在确保程序运行无误后,我们可以在打包命令中加上“-w”来去掉黑框。
pyinstaller -w hello.py

当我们在打包一个程序时,先不要加“-w”,因为黑框能够告诉我们报错信息。若可执行文件运行成功,且内部功能正常,再加上“-w”重新打包。
如果程序本身就需要用到命令提示符窗口来输入或输出一些内容,那就不用加“-w”了。

8.1.4 给可执行文件加上图标

如果要给可执行文件添加图标,我们可以在打包时加上“-i”命令,并在“-i”后面加上图标文件的路径,使用格式如下。
pyinstaller -i /path/to/xxx.ico hello.py

在macOS系统上打包时,必须要使用.icns格式的图标文件。

一些读者用了“-i”命令后,可执行文件的图标并没有发生改变,可能原因有以下两点。
① 图标文件是无效的。请不要用修改扩展名的方式来获取.ico格式的文件,比如直接把.png改成.ico,我们应该使用专业的格式转换软件(如格式工厂)来改变格式。
② 缓存原因。把可执行文件移动到其他路径下后,就会发现图标是正常显示的。当然也可以重启计算机。

8.1.5 打包资源文件

资源文件(即icon.ico)相对于可执行文件的路径必须要正确,这样程序才能正常运行。

把资源文件手动复制到hello文件夹中的方法只适用于多文件打包模式。

如果用单文件打包模式打包,包含依赖文件的文件夹只有在可执行文件运行后才被解压出来,
而程序会因为找不到资源文件立即报错,所以我们是没有时间去复制的。

在打包时直接把资源文件添加到生成的hello文件夹(单文件打包模式下是_MEI文件夹)中:
pyinstaller –add-data=SRC;DEST hello.py

SRC表示资源文件所在的路径(绝对路径和相对路径都可以),
DEST表示在打包后资源文件在hello文件夹中的相对路径,
在Windows系统上两个路径之间用英文分号“;”隔开,
在macOS和Linux系统上两个路径之间用英文冒号“:”隔开

我们现在用多文件打包模式重新打包示例代码8-2,使用如下打包命令。
pyinstaller –add-data=./icon.ico;. hello.py

icon.ico在当前路径下,所以在SRC处填写“./icon.ico”的话就可以让PyIsntaller找到它。
我们要在打包后将icon.ico放在hello文件夹中,所以在DEST处填写“.”。
打包结束后,可以发现icon.ico出现在了hello文件夹中,程序运行也没有问题

我们可以使用通配符“”来添加多个资源文件。
比如“–add-data=
;./folder/”就表示将当前路径下的所有文件放在hello文件夹下名为folder的文件夹中。
我们也可以添加同种格式的多个文件,比如“–add-data=*.jpg;./image/”就表示将当前路径下的所有.jpg格式的文件放到hello文件夹下名为image的文件夹中。

==============================================================================================================================

现在使用单文件打包模式打包hello.py文件,命令如下。
pyinstaller -F –add-data=./icon.ico;. hello.py

打包结束后,我们运行hello.exe,发现报错了,但是解压出来的_MEI临时文件夹中确实有icon.ico!这是怎么回事?先来看一下这行代码。
win.iconbitmap(‘./icon.ico’) #设置窗口图标

程序会在当前路径下寻找icon.ico文件,但是hello.exe在dist文件夹中,而icon.ico在_MEI文件夹中,所以才会报错。
可以将示例代码8-2修改一下,让程序能够找到_MEI文件夹中的icon.ico,详见示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import tkinter
import sys
import os
 
def res_path(relative_path): #
"""获取资源路径"""
try:
# 获取_MEI文件夹所在路径
base_path = sys._MEIPASS # 通过sys模块的_MEIPASS属性来获取_MEI文件夹的位置
except Exception:
# 没有_MEI文件夹的话使用当前路径
base_path = os.path.abspath(".")

return os.path.join(base_path, relative_path)
 
win = tkinter.Tk()
win.iconbitmap(res_path('./icon.ico')) # 设置窗口图标
win.mainloop()

在用单文件打包模式打包前,需要修改程序代码,将res_path()函数套在各个路径上。

8.1.6 减小打包后的文件大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
PyInstaller会把Python环境和程序使用到的库打包进来,有时候还会打包一些明明没有用到的第三方库,
导致打包所花的时间越来越多,包也越来越大。

第一个减小包体的方法就是使用干净的打包环境。所谓干净,指的是计算机上只安装了程序运行所必需的库。
通常,我们会使用虚拟环境,或者在虚拟机中打包。


第二个方法:如果目前无法在干净的环境中打包,那么可以使用“--exclude-module”命令,
它的作用是在打包时排除指定的库或模块,这样就不会把无关的文件包含进来了,使用格式如下。
pyinstaller --exclude-module=NAME hello.py
在NAME处填写要排除的库或模块名称。

比方说我们现在要打包一个程序,该程序仅仅使用到了操作Excel文件的openpyxl库
from openpyxl import Workbook

# 创建文件对象
wb = Workbook()

# 获取当前正使用的工作表
ws = wb.active

# 为工作表添加一些字段
ws.append(['test1', 'test2', 'test3'])

# 保存为test.xlsx
wb.save('test.xlsx')

假如我们计算机上还装有NumPy和pandas,那这两个没有用到的库也可能会被打包进来。所以我们应该用“--exclude-module”命令排除这两个库,使用以下命令打包。
pyinstaller --exclude-module=numpy --exclude-module=pandas hello.py

第三个方法就是使用UPX工具,它可以进一步压缩可执行文件。我们首先去UPX官网下载对应系统版本的UPX工具。
笔者使用的是Windows 10 64位的计算机,所以需要下载win64版本的UPX

8.1.7 其他常用的命令

1.pyinstaller -h 查看PyInstaller所有命令的解释和用法
2. pyinstaller -n=good hello.py 用来修改生成的可执行文件名称 原来生成的可执行文件名称为hello.exe,现在就变成good.exe了

3.-y
当打包完毕后,我们可能会想修改一下源码然后重新打包。
那么第二次打包时PyInstaller会询问是否要删除之前打包生成的文件,此时需要我们往命令提示符窗口中输入y或者N
如果在打包命令中加上“-y”,那PyInstaller会直接删除上次打包遗留下的文件,不会询问

4.–clean
删除前一次打包留下的缓存文件,不过在删除前PyInstaller会先询问是否确认执行,所以我们可以加上“-y”命令
pyinstaller –clean -y hello.py

5.–hidden-import
我们经常会碰到“ModuleNotFoundError: No module named xxx”这种报错。
出现这种报错的原因无非就两种。
① 没有安装相应的库或模块。
② 已经安装相应的库或模块了,代码中也导入了,但是PyInstaller在打包时没有找到。
如果是第一种原因,我们用pip命令下载相应库或模块就可以解决了。
如果是第二种原因,在打包时添加“–hidden-import”命令就可以解决了,其使用格式如下(xxx就是库或模块的名称)。
pyinstaller –hidden-import=xxx hello.py

6.–noupx代表不使用UPX工具。

7.–runtime-hook当我们双击可执行文件后,程序就会执行。
“–runtime-hook”命令能够让我们在程序运行前先执行一段代码,非常有用。
在hello.py中,我们想要读取data.txt的文本内容。
但是data.txt在data.zip压缩文件中,所以必须先执行extract_data.py中的程序将data.txt解压出来。打包命令如下所示。
pyinstaller –runtime-hook=extract_data.py –add-data=data.zip;. hello.py