0%

PyQt5高级布局与美化

还记得我之前说过布局什么的用Qt Designer就行了,还强力推荐过。用了一段时间后,现在想来,Qt Designer就是个垃圾。布局什么的,还是使用代码进行管理吧,其实也不难。而且也不用分两个文件,一个写QWidget,另一个写QMainWindow了。在QMainWindow中创建一个QWidget的实例,并且加入即可。

本文收录:

  • 如何美化布局,使界面更优美
  • StackLayout布局管理,模仿常用软件功能,使界面更人性化
  • Qspliter,像一个IDE一个自由拖动
  • 综合实例,实际开发一个软件,用到以上功能。

软件主体功能:调用天气网的API,爬取数据,将数据放到软件的QTableWidget中,预测未来天气,对天气走向进行绘图pyqtgraph。功能简易,但用到了Qt开发中的大部分控件和常见功能。

软件结果如下图所示:

代码如下:

本文全部代码下载链接,建议在这里下载代码,因为需要辅助文件

天气API参考:
https://www.sojson.com/api/weather.html

高级布局管理

QHBoxLayout, QVBoxLayout, QGridLayout, 这几个基本的布局管理的入门教程如下,也是我目前看到过的最好的:
https://www.learnpyqt.com/courses/start/layouts/

此外,还有网上盛传的布局美化代码,网上清一色是这个:

虽然也不知道谁是原创,只要一搜索:PyQt5美化开发,全部都是这个代码。

我知道写的美它好,但我不想抄袭。不过从以上网址的代码确实学到了点东西,那就是设立总体布局和子布局,子布局管理控件,总体布局管理子布局,这样布置出来的界面是很好看的。

接下来,一点一点的写出来我上面那个天气预报的软件。

需要导入的库如下:

1
2
3
4
5
6
7
8
9
10
11
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (QApplication, QMainWindow, QDesktopWidget, QStyleFactory, QWidget,
QGridLayout, QHeaderView, QTableWidgetItem, QMessageBox, QFileDialog,
QSlider, QLabel, QLineEdit, QPushButton, QTableWidget)
from PyQt5.QtGui import QPalette, QColor, QBrush
from PyQt5.QtCore import Qt
from pyqtgraph import GraphicsLayoutWidget
import pyqtgraph as pg
import numpy as np
import pyqtgraph.exporters as pe
import qdarkstyle, requests, sys, time, random, json, datetime, re

整体布局

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
class MainUi(QMainWindow):
def __init__(self):
super().__init__()

# 设置绘图背景
pg.setConfigOption('background', '#19232D')
pg.setConfigOption('foreground', 'd')
pg.setConfigOptions(antialias = True)

# 窗口居中显示
self.center()

self.init_ui()

# 设置城市的编号
self.code = ""

# 多次查询时,查询近5天, 受限于 API 接口提供的数量
self.num = 5

# return request json file
self.rep = ""

# 创建绘图面板
self.plt = []
# 控制绘图的文件名称
self.filename = 1
# 默认的状态栏
# 可以设置其他按钮点击 参考多行文本显示 然而不行
self.status = self.statusBar()
self.status.showMessage("我在主页面~")

# 标题栏
self.setWindowTitle("天气查询软件")

def init_ui(self):

# self.setFixedSize(960,700)

# 创建窗口主部件
self.main_widget = QWidget()
# 创建主部件的网格布局
self.main_layout = QGridLayout()
# 设置窗口主部件布局为网格布局
self.main_widget.setLayout(self.main_layout)

# 创建左侧部件
self.left_widget = QWidget()
self.left_widget.setObjectName('left_widget')
# 创建左侧部件的网格布局层
self.left_layout = QGridLayout()
# 设置左侧部件布局为网格
self.left_widget.setLayout(self.left_layout)

# 创建右侧部件
self.right_widget = QWidget()
self.right_widget.setObjectName('right_widget')
self.right_layout = QGridLayout()
self.right_widget.setLayout(self.right_layout)

# 左侧部件在第0行第0列,占12行5列
self.main_layout.addWidget(self.left_widget, 0, 0, 12, 5)
# 右侧部件在第0行第6列,占12行7列
self.main_layout.addWidget(self.right_widget, 0, 5, 12, 7)
# 设置窗口主部件
self.setCentralWidget(self.main_widget)

def center(self):
'''
获取桌面长宽
获取窗口长宽
移动
'''
screen = QDesktopWidget().screenGeometry()
size = self.geometry()
self.move((screen.width() - size.width()) / 2, (screen.height() - size.height()) / 2)

def main():
app = QApplication(sys.argv)
gui = MainUi()
gui.show()
sys.exit(app.exec_())

if __name__ == '__main__':
main()

现在只能执行出一个小窗口来,没关系,继续下面的东西。

左侧添加控件

这个时候我们再来添加左边的按钮控件。代码缩进添加至def init_ui(self):中。

提醒:在布局的时候,buttonclicked connect的时候找不到逻辑函数报错,加入逻辑函数就不会错了。逻辑函数在之后会加入。所以,现在只看布局,就别点按钮以至于报错了。

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
# function button 
self.single_query = QPushButton("查询今日")
self.single_query.clicked.connect(self.request_weather)
self.single_query.setEnabled(False)
# self.single_query.setFixedSize(400, 30)
self.btn_tempa = QPushButton("温度预测(可绘图)")
self.btn_tempa.clicked.connect(self.request_weather)
self.btn_tempa.setEnabled(False)
self.btn_wind = QPushButton("风力预测(可绘图)")
self.btn_wind.clicked.connect(self.request_weather)
self.btn_wind.setEnabled(False)
self.btn_stawea = QPushButton("综合天气预测")
self.btn_stawea.clicked.connect(self.request_weather)
self.btn_stawea.setEnabled(False)
self.left_layout.addWidget(self.single_query, 2, 0, 1, 5)
self.left_layout.addWidget(self.btn_tempa, 3, 0, 1, 5)
self.left_layout.addWidget(self.btn_wind, 4, 0, 1, 5)
self.left_layout.addWidget(self.btn_stawea, 5, 0, 1, 5)

# lineEdit to input a city
self.city_line = QLineEdit()
self.city_line.setPlaceholderText("输入城市回车确认")
self.city_line.returnPressed.connect(self.match_city)
self.left_layout.addWidget(self.city_line, 1, 0, 1, 5)

# save figure and quit window
self.save_fig = QPushButton("保存绘图")
self.save_fig.setEnabled(False)
self.save_fig.clicked.connect(self.fig_save)
self.left_layout.addWidget(self.save_fig, 6, 0, 1, 5)

self.load = QPushButton("写日记")
self.left_layout.addWidget(self.load, 7, 0, 1, 5)

self.quit_btn = QPushButton("退出")
self.quit_btn.clicked.connect(self.quit_act)
self.left_layout.addWidget(self.quit_btn, 8, 0, 1, 5)

添加完按钮控件后,加入QtableWidget控件用于观察数据。

1
2
3
4
# tablewidgt to view data
self.query_result = QTableWidget()
self.left_layout.addWidget(self.query_result, 9, 0, 2, 5)
self.query_result.verticalHeader().setVisible(False)

右侧窗口布局

最后完成右侧窗口的布局即可。

1
2
3
4
5
6
7
self.label = QLabel("预测天气情况绘图展示区")
self.right_layout.addWidget(self.label, 0, 6, 1, 7)

self.plot_weather_wind = GraphicsLayoutWidget()
self.plot_weather_temp = GraphicsLayoutWidget()
self.right_layout.addWidget(self.plot_weather_temp, 1, 6, 4, 7)
self.right_layout.addWidget(self.plot_weather_wind, 6, 6, 4, 7)

整体美化

1
2
3
4
self.setWindowOpacity(0.9) # 设置窗口透明度
self.main_layout.setSpacing(0)
# 美化风格
self.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())

逻辑函数

此外,还需要单独引入import read_citycode, get_weather,这两个单独成两个文件,和主文件放在一起。

read_citycode以字典的形式返回每个城市对应的ID。json文件在上面提供的压缩包内,上面提到的天气API网址内也有。

有的逻辑函数需要引入外部文件,否则会报错。因此,简易使用上文提供的代码下载链接,下载代码执行。直接copy这里的代码是不能执行的,因为需要外部文件的支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
import json

def read_code(filename):
city_ = {}
with open(filename, 'r') as f:
temp = json.loads(f.read())
for city in temp:
if 'city_code' in city:
key = city['city_name']
value = city['city_code']
city_[key] = value

sorted(city_.keys())
return city_

get_weather是对网址发送具体城市的天气请求,获取对应的结果,结果形式为json

1
2
3
4
5
6
7
8
9
import requests

def run(url):
try:
rep = requests.get(url)
rep.encoding = 'utf-8'
return rep
except:
print("network error")

以上两份代码分别整理到两个文件中,命名为read_citycode.pyget_weather.py。文件结构图如下:

布局完成之后,开始增加功能,所以我把所有的逻辑函数放上来。因为之前布局的时候,buttonclicked connect的时候找不到逻辑函数报错,加入逻辑函数就不会错了。

退出窗口函数:

1
2
3
4
5
6
7
# 退出按钮
def quit_act(self):
# sender 是发送信号的对象
sender = self.sender()
print(sender.text() + '键被按下')
qApp = QApplication.instance()
qApp.quit()

窗口居中函数:

1
2
3
4
5
6
7
8
9
10
def center(self):
'''
获取桌面长宽
获取窗口长宽
移动
'''
screen = QDesktopWidget().screenGeometry()
size = self.geometry()
self.move((screen.width() - size.width()) / 2, (screen.height() - size.height()) / 2)

获取城市ID函数

根据json文件,将获得的输入城市转换为ID,完成进一步查询。因为API的url只能传入ID,不可传入城市名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 读取 json 文件, 获得城市的code
def match_city(self):
# 输入城市后才能点击绘图
self.btn_tempa.setEnabled(True)
self.btn_wind.setEnabled(True)
self.single_query.setEnabled(True)
self.btn_stawea.setEnabled(True)
# 在外部json文件中 读取所有城市的 code
city = read_citycode.read_code("最新_city.json")
line_city = self.city_line.text()
# code与输入的城市对比, 如果有, 返回code, 如果没有则默认为北京
if line_city in city.keys():
self.code = city[line_city]
else:
self.code = "101010100"
self.city_line.setText("北京")
Qreply = QMessageBox.about(self, "你犯了一个粗误", "输入城市无效,请示新输入,否则默认为北京")

请求天气

在获得城市的ID之后,便能生成url,请求API返回天气。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 按照城市的code, 请求一个城市的天气 返回 json 形式
def request_weather(self):
root_url = "http://t.weather.sojson.com/api/weather/city/"
url = root_url + str(self.code)
self.rep = get_weather.run(url)
sender = self.sender()
if sender.text() == "查询今日":
self.query(1, 5, '温度', '风向', '风力', 'PM2.5', '天气描述')
if sender.text() == "温度预测(可绘图)":
self.btn_tempa.setEnabled(False)
self.query(self.num, 2, '日期', '温度')
if sender.text() == "风力预测(可绘图)":
self.btn_wind.setEnabled(False)
self.query(self.num, 2, '日期', '风力')
if sender.text() == "综合天气预测":
self.query(self.num, 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
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# 查询, 可以接受多个参数, 更加灵活的查询
def query(self, row_num, col_num, *args):
# true value
tempature = self.rep.json()['data']['wendu']
wind_power = self.rep.json()['data']['forecast'][0]['fl']
wind_direction = self.rep.json()['data']['forecast'][0]['fx']
pm = self.rep.json()['data']['pm25']
type_ = self.rep.json()['data']['forecast'][0]['type']
# forecast value
pre_tempature = []
pre_wind_power = []
pre_wind_direction = []
pre_pm = []
pre_type_ = []

for i in range(self.num):
pre_tempature.append(str(self.rep.json()['data']['forecast'][i]['low']))
pre_wind_power.append(str(self.rep.json()['data']['forecast'][i]['fl']))
pre_wind_direction.append(str(self.rep.json()['data']['forecast'][i]['fx']))
pre_type_.append(str(self.rep.json()['data']['forecast'][i]['type']))


# 设置当前查询结果的行列
self.query_result.setRowCount(row_num)
# 否则不会显示
self.query_result.setColumnCount(col_num)
# 表头自适应伸缩
self.query_result.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
# 按照 传入的参数设置表头, 因为每次查询的表头都不一样
ls = [i for i in args]
self.query_result.setHorizontalHeaderLabels(ls)

if col_num > 2 and row_num == 1:
item = QTableWidgetItem(str(tempature) + "℃")
# 设置单元格文本颜色
item.setForeground(QBrush(QColor(144, 182, 240)))
self.query_result.setItem(0, 0, item)

item = QTableWidgetItem(str(wind_direction))
item.setForeground(QBrush(QColor(144, 182, 240)))
self.query_result.setItem(0, 1, item)

item = QTableWidgetItem(str(wind_power))
item.setForeground(QBrush(QColor(144, 182, 240)))
self.query_result.setItem(0, 2, item)

item = QTableWidgetItem(str(pm))
item.setForeground(QBrush(QColor(144, 182, 240)))
self.query_result.setItem(0, 3, item)

item = QTableWidgetItem(str(type_))
item.setForeground(QBrush(QColor(144, 182, 240)))
self.query_result.setItem(0, 4, item)

if col_num > 2 and row_num > 1:
for i in range (0, self.num):
item = QTableWidgetItem("最" + str(pre_tempature[i]))
# 设置单元格文本颜色
item.setForeground(QBrush(QColor(144, 182, 240)))
self.query_result.setItem(i, 0, item)

item = QTableWidgetItem(str(pre_wind_direction[i]))
item.setForeground(QBrush(QColor(144, 182, 240)))
self.query_result.setItem(i, 1, item)

item = QTableWidgetItem(str(pre_wind_power[i]))
item.setForeground(QBrush(QColor(144, 182, 240)))
self.query_result.setItem(i, 2, item)

item = QTableWidgetItem(str(pre_type_[i]))
item.setForeground(QBrush(QColor(144, 182, 240)))
self.query_result.setItem(i, 3, item)

if col_num == 2 and row_num > 1:
date = self.get_date(addDays=self.num)
key = 0
for i in date:
item = QTableWidgetItem(i)
item.setForeground(QBrush(QColor(144, 182, 240)))
self.query_result.setItem(key, 0, item)
key += 1
if self.query_result.horizontalHeaderItem(1).text() == '温度':
key = 0
for i in pre_tempature:
item = QTableWidgetItem("最" + i)
item.setForeground(QBrush(QColor(144, 182, 240)))
self.query_result.setItem(key, 1, item)
key += 1
if self.query_result.horizontalHeaderItem(1).text() == '风力':
key = 0
for i in pre_wind_power:
item = QTableWidgetItem(i)
item.setForeground(QBrush(QColor(144, 182, 240)))
self.query_result.setItem(key, 1, item)
key += 1

# 只有两列的时候才可以绘制图像,
if col_num < 4:
ls, y = [], []
x = np.linspace(1, self.num, self.num)
# 将 treeview 里面的结果以数字的形式返回到列表中, 用于绘图
for row in range(self.num):
str1 = str(self.query_result.item(row, 1).text())
ls.extend(re.findall(r'\d+(?:\.\d+)?', str1))
if len(ls) == 5:
y = [float(i) for i in ls]
if len(ls) == 10:
lt = [float(i) for i in ls]
for i in range (0, len(lt), 2):
y.append((lt[i] + lt[i + 1]) / 2)
if len(ls) == 5:
y = [float(i) for i in ls]
else:
y = [float(i) for i in ls[0:5]]
# 获取 treeview 的标题, 以得到绘图时的标得
if self.query_result.horizontalHeaderItem(1).text() == '温度':
title_ = "近期一个月温度变化(预测)"
if self.query_result.horizontalHeaderItem(1).text() == '风力':
title_ = "近期一个月风力变化(预测)"
# 绘图时先清空面板 否则会新加一列,效果不好
# 且 pyqtgraph 的新加一列有bug, 效果不是很好 下次使用 matplotlib
if title_ == "近期一个月风力变化(预测)":
self.plot_weather_wind.clear()
bg1 = pg.BarGraphItem(x=x, height=y, width=0.3, brush=QColor(137, 232, 165))
self.plt1 = self.plot_weather_wind.addPlot(title = title_)
self.plt1.addItem(bg1)
if title_ == "近期一个月温度变化(预测)":
self.plot_weather_temp.clear()
self.plt = self.plot_weather_temp.addPlot(title = title_)
bg2 = pg.BarGraphItem(x=x, height=y, width=0.3, brush=QColor(32, 235, 233))
self.plt.addItem(bg2)
# 绘图后才可以保存图片
self.save_fig.setEnabled(True)

保存图片与显示日期

逻辑如下:

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
# 保存图片成功时的提醒
def pic_messagebox(self):
string = '第' + str(self.filename) + '张图片.png'
Qreply = QMessageBox.information(self, string, "已经成功保存图片到当前目录, 关闭软件后请及时拷贝走")

# 保存图片的设置 pyqtgraph 保存无法设置图片路径
def fig_save(self):
ex = pe.ImageExporter(self.plt.scene())
filename = '第' + str(self.filename) + '张图片.png'
self.filename += 1
ex.export(fileName = filename)
self.pic_messagebox()

def get_date(self, dateFormat="%Y-%m-%d", addDays=0):
ls = []
timeNow = datetime.datetime.now()
key = 0
if (addDays != 0) and key < addDays - 1:
for i in range (addDays):
anotherTime = timeNow + datetime.timedelta(days = key)
anotherTime.strftime(dateFormat)
ls.append(str(anotherTime)[0:10])
key += 1
else:
anotherTime = timeNow

return ls

最终结果如下所示,以上代码见开头下载链接的weather.py文件。

以上,我们见识到了层次布局管理后,软件的界面效果是可以接受的。

那么问题来了,我想要像某个IDE或者编辑器,比如Pycharm或者Vscode一样,主界面下的框能拖动自由改变大小。接下来实现这个。

QSplitter 布局

这个实现比较容易,需要实例化对象,加入frame,在添加至主布局中即可。参考:
http://zetcode.com/gui/pyqt5/widgets2/

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
from PyQt5.QtWidgets import (QWidget, QHBoxLayout, QFrame, 
QSplitter, QStyleFactory, QApplication)
from PyQt5.QtCore import Qt
import sys

class Example(QWidget):

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

self.initUI()


def initUI(self):

hbox = QHBoxLayout(self)

topleft = QFrame(self)
topleft.setFrameShape(QFrame.StyledPanel)

topright = QFrame(self)
topright.setFrameShape(QFrame.StyledPanel)

bottom = QFrame(self)
bottom.setFrameShape(QFrame.StyledPanel)

splitter1 = QSplitter(Qt.Horizontal)
splitter1.addWidget(topleft)
splitter1.addWidget(topright)

splitter2 = QSplitter(Qt.Vertical)
splitter2.addWidget(splitter1)
splitter2.addWidget(bottom)

hbox.addWidget(splitter2)
self.setLayout(hbox)

self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('QSplitter')
self.show()


if __name__ == '__main__':

app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())

之后,在frame里面添加控件,便可以随心所欲的玩耍。

实例

代码见本文开头给出的下载链接中的weather1.py

StackLayout布局

脑部网易云音乐的界面,是不是点击左侧的按钮后,右侧的窗口也会跟着改变。而实现这个技术,就需要StackLayout布局的管理。

一个Demo, stacklayout添加完控件后,会自动设立索引,只需要按照索引访问对应项即可,以下代码实现了最简单的界面切换。

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

from PyQt5 import QtWidgets
import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QDesktopWidget, QStyleFactory, QWidget,
QGridLayout, QHeaderView, QTableWidgetItem, QMessageBox, QFileDialog,
QStackedLayout, QFrame, QLabel, QLineEdit, QPushButton, QTableWidget, QVBoxLayout,
QHBoxLayout, QSplitter)
from PyQt5.QtGui import QPalette, QColor, QBrush

class Color(QWidget):

def __init__(self, color, *args, **kwargs):
super(Color, self).__init__(*args, **kwargs)
self.setAutoFillBackground(True)

palette = self.palette()
palette.setColor(QPalette.Window, QColor(color))
self.setPalette(palette)

class MainWindow(QMainWindow):

def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)

self.setWindowTitle("My Awesome App")

pagelayout = QVBoxLayout()
button_layout = QHBoxLayout()
layout = QStackedLayout()

pagelayout.addLayout(button_layout)
pagelayout.addLayout(layout)

for n, color in enumerate(['red','green','blue','yellow']):
btn = QPushButton( str(color) )
btn.pressed.connect( lambda n=n: layout.setCurrentIndex(n) )
button_layout.addWidget(btn)
layout.addWidget(Color(color))

widget = QWidget()
widget.setLayout(pagelayout)
self.setCentralWidget(widget)


def main():
app = QApplication(sys.argv)
gui = MainWindow()
gui.show()
sys.exit(app.exec_())

if __name__ == '__main__':
main()

实例

那么反过来思考以下,如果一个软件要综合Stacklayout,splitter等,要怎么实现呢。以下给一个例子,综合以上所有的内容,实现更加综合的布局管理。(以下代码copy后可以单独运行,不需要外部文件,也没有在上文的下载链接中给出)

坑 1:整体界面resize前,先要设置splitter的大小,否则无效。
坑 2:TreeView和QWebEngineView不能放在同一个layout中,否则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
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
#!/bin/bash
import sys, hashlib
import qdarkstyle

from PyQt5.QtWidgets import (QApplication, QSplitter, QGridLayout, QHBoxLayout, QPushButton,
QTreeWidget, QFrame, QLabel, QHBoxLayout, QMainWindow,
QStackedLayout, QWidget, QVBoxLayout, QLineEdit, QRadioButton,
QTreeWidgetItem, QDesktopWidget)
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtWebEngineWidgets import QWebEngineView

class MainWindow(QMainWindow):

def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)

# 设置窗口名称
self.setWindowTitle("华北理工数学建模协会比赛查询")

# 设置状态栏
self.status = self.statusBar()
self.status.showMessage("我在主页面~")

# 设置初始化的窗口大小
self.resize(600, 400)

# 最开始窗口要居中显示
self.center()

# 设置窗口透明度
self.setWindowOpacity(0.9) # 设置窗口透明度

# 设置窗口样式
self.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5())

# 设置整体布局 左右显示
pagelayout = QGridLayout()

# 左侧开始布局
# 创建左侧部件
top_left_frame = QFrame(self)
top_left_frame.setFrameShape(QFrame.StyledPanel)
# 左边按钮为垂直布局
button_layout = QVBoxLayout(top_left_frame)

# 登录按钮
verifyid_btn = QPushButton(top_left_frame)
verifyid_btn.setFixedSize(100, 30), verifyid_btn.setText("确认身份")
button_layout.addWidget(verifyid_btn)
# 输入用户名 密码按钮
user_btn = QPushButton(top_left_frame)
user_btn.setFixedSize(100, 30), user_btn.setText("登录")
button_layout.addWidget(user_btn)
# 申请账号 按钮
registor_btn = QPushButton(top_left_frame)
registor_btn.setFixedSize(100, 30), registor_btn.setText("申请帐号")
button_layout.addWidget(registor_btn)
# 录入信息按钮
input_btn = QPushButton(top_left_frame)
input_btn.setFixedSize(100, 30), input_btn.setText("录入信息")
button_layout.addWidget(input_btn)
# 查询按钮
query_btn = QPushButton(top_left_frame)
query_btn.setFixedSize(100, 30), query_btn.setText("查询信息")
button_layout.addWidget(query_btn)
# 建模之家 按钮
friend_btn = QPushButton(top_left_frame)
friend_btn.setFixedSize(100, 30), friend_btn.setText("建模园地")
button_layout.addWidget(friend_btn)
# 退出按钮
quit_btn = QPushButton(top_left_frame)
quit_btn.setFixedSize(100, 30), quit_btn.setText("退出")
button_layout.addWidget(quit_btn)

# 左下角为空白 必须要有布局,才可以显示至内容中
bottom_left_frame = QFrame(self)
blank_label = QLabel(bottom_left_frame)
blank_layout = QVBoxLayout(bottom_left_frame)
blank_label.setText("建模学子的博客")
blank_label.setFixedHeight(20)
blank_layout.addWidget(blank_label)
self.webEngineView = QWebEngineView(bottom_left_frame)
self.webEngineView.close()
blank_layout.addWidget(self.webEngineView)

# 右侧开始布局 对应按钮布局
right_frame = QFrame(self)
right_frame.setFrameShape(QFrame.StyledPanel)
# 右边显示为stack布局
self.right_layout = QStackedLayout(right_frame)

# 确认身份
# 管理员身份
radio_btn_admin = QRadioButton(right_frame)
radio_btn_admin.setText("我是管理员,来输入数据的")
# 游客身份
radio_btn_user = QRadioButton(right_frame)
radio_btn_user.setText("我是游客,就来看看")
# 以处置布局管理器管理
radio_btn_layout = QVBoxLayout() # 这里没必要在传入frame,已经有布局了
radio_btn_widget = QWidget(right_frame)
radio_btn_layout.addWidget(radio_btn_admin)
radio_btn_layout.addWidget(radio_btn_user)
radio_btn_widget.setLayout(radio_btn_layout)
self.right_layout.addWidget(radio_btn_widget)

# 登录界面
user_line = QLineEdit(right_frame)
user_line.setPlaceholderText("输入账号:")
user_line.setFixedWidth(400)
password_line = QLineEdit(right_frame)
password_line.setPlaceholderText("请输入密码:")
password_line.setFixedWidth(400)
login_layout = QVBoxLayout()
login_widget = QWidget(right_frame)
login_widget.setLayout(login_layout)
login_layout.addWidget(user_line)
login_layout.addWidget(password_line)
self.right_layout.addWidget(login_widget)

# 申请帐号
registor_id = QLineEdit(right_frame)
registor_id.setPlaceholderText("请输入新帐号:")
registor_id.setFixedWidth(400)
registor_psd = QLineEdit(right_frame)
registor_psd.setPlaceholderText("请输入密码:")
registor_psd.setFixedWidth(400)
registor_confirm = QLineEdit(right_frame)
registor_confirm.setPlaceholderText("请确认密码:")
registor_confirm.setFixedWidth(400)
registor_confirm_btn = QPushButton("确认提交")
registor_confirm_btn.setFixedSize(100, 30)
registor_layout = QVBoxLayout()
register_widget = QWidget(right_frame)
register_widget.setLayout(registor_layout)
registor_layout.addWidget(registor_id)
registor_layout.addWidget(registor_psd)
registor_layout.addWidget(registor_confirm)
registor_layout.addWidget(registor_confirm_btn)
self.right_layout.addWidget(register_widget)

# 建模园地 使用 TreeView 水平布局 应该读取数据库
self.friend_tree = QTreeWidget(right_frame)
self.friend_tree.setColumnCount(3) # 一列
self.friend_tree.setHeaderLabels(['年级', '人员', '友情链接']) # 设置标题
root = QTreeWidgetItem(self.friend_tree) # 设置根节点
self.friend_tree.setColumnWidth(2, 400) # 设置宽度
# 设置子节点
root.setText(0, "年级") # 0 表示位置
root.setText(1, "姓名")
root.setText(2, "网址")
child_16 = QTreeWidgetItem(root)
child_16.setText(0, "16级")

child_ljw = QTreeWidgetItem(child_16)
child_ljw.setText(1, "刘佳玮")
child_ljw.setText(2, "https://muyuuuu.github.io")

child_17 = QTreeWidgetItem(root)
child_17.setText(0, "17级")

child_lqr = QTreeWidgetItem(child_17)
child_lqr.setText(1, "李秋然")
child_lqr.setText(2, "https://dgimoyeran.github.io")

friend_widget = QWidget(right_frame)
friend_layout = QVBoxLayout()
friend_widget.setLayout(friend_layout)
friend_layout.addWidget(self.friend_tree)
self.right_layout.addWidget(friend_widget)

self.url = '' # 后期会获取要访问的url

# 三分界面,可拖动
self.splitter1 = QSplitter(Qt.Vertical)
top_left_frame.setFixedHeight(250)
self.splitter1.addWidget(top_left_frame)
self.splitter1.addWidget(bottom_left_frame)

self.splitter2 = QSplitter(Qt.Horizontal)
self.splitter2.addWidget(self.splitter1)
# 添加右侧的布局
self.splitter2.addWidget(right_frame)

# 窗口部件添加布局
widget = QWidget()
pagelayout.addWidget(self.splitter2)
widget.setLayout(pagelayout)
self.setCentralWidget(widget)

# 函数功能区
verifyid_btn.clicked.connect(self.show_verifyid_page)
user_btn.clicked.connect(self.show_login_page)
registor_btn.clicked.connect(self.show_register_page)
friend_btn.clicked.connect(self.show_friend_page)
self.friend_tree.clicked.connect(self.show_firend_web)
quit_btn.clicked.connect(self.quit_act)

def init(self):
# 刚开始要管理浏览器,否则很丑
self.webEngineView.close()
# 注意先后顺序,resize 在前面会使代码无效
self.splitter1.setMinimumWidth(150)
self.splitter2.setMinimumWidth(250)
self.resize(600, 400)

# TreeView 的点击事件
def show_firend_web(self):
item = self.friend_tree.currentItem()
if item.text(2)[:4] == "http":
self.url = item.text(2)
self.resize(1800, 1200)
self.webEngineView.show()
self.splitter1.setFixedWidth(1400)
self.webEngineView.load(QUrl(self.url))

# 展示树形结构
def show_friend_page(self):
self.init()
self.center()
self.right_layout.setCurrentIndex(3)

# 显示注册帐号的页面
def show_register_page(self):
self.init()
self.center()
self.right_layout.setCurrentIndex(2)

# 显示登录的页面
def show_login_page(self):
self.init()
self.center()
self.right_layout.setCurrentIndex(1)

# stacklayout 布局,显示验证身份的页面
def show_verifyid_page(self):
self.init()
self.center()
self.right_layout.setCurrentIndex(0)

# 设置窗口居中
def center(self):
'''
获取桌面长宽
获取窗口长宽
移动
'''
screen = QDesktopWidget().screenGeometry()
size = self.geometry()
self.move((screen.width() - size.width()) / 2, (screen.height() - size.height()) / 2)

# 退出按钮 有信息框的提示 询问是否确认退出
def quit_act(self):
# sender 是发送信号的对象
sender = self.sender()
print(sender.text() + '键被按下')
qApp = QApplication.instance()
qApp.quit()


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

结语

PyQt5 这方面的文档,国内的资源很少。而且很单一(因为普遍都在抄袭,或者说没有特意的写出来)。

出了问题去StackOverflow上或者谷歌一下,能搜出来很多、很有效的解决方案,至少国内的搜索引擎还做不到能快速的搜索出有效的科技文档来。

感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章