PyQt与matplotlib-画图软件(二):主界面设计

PyQt

This article was last updated on <span id="expire-date"></span> days ago, the information described in the article may be outdated.

前言

昨天终于把软件给写完了,算是我有史以来做过的最大的项目了吧,今天终于能有空闲时间思考一下这个系列的博客该怎么填了。我打算贴出部分代码来展示如何从头写出一个完整的 GUI 程序,但是由于我们需要进行软著和专利的申请,绘图的核心代码就不放出了。但是各位看官放心😋,贴出的代码足够编译出一个完整的 GUI 程序。

温馨提示:文章的最末尾有本节的完整代码

UI 设计

UI 设计上我把重心放到了 PPI 以及 RHI 两个子页面上,主页面 (form.ui) 仅仅展示了三个按钮

使用 Qt Creator 设计起来非常简单,仅需要几个按钮以及数个弹簧 (spacer) 即可

  1. 按钮和弹簧全部添加完成以后,将按钮与垂直弹簧间隔排列,并全部选中,对其使用垂直布局
  2. 然后水平排列水平弹簧和垂直布局,全选,使用水平布局
  3. 最后选中最高级的 MainWindow 使用栅格布局,即可让所有部件对齐并平铺填满窗口。

这里添加弹簧的原因是要让弹簧来填满按钮与按钮、按钮与边界之间的间隙,如不加弹簧在使用栅格布局时,按钮的长和宽会被自动拉长以填满窗口,使得 UI 异常难看。

关于布局和弹簧

使用水平或垂直布局会使同一布局内的部件有相同的长或者宽 (除非你设置了部件的最大长度和宽度),对于布局外的部件来讲,会将一个布局视为一个整体,因此这里设置水平布局时只添加了两个水平弹簧。

弹簧会最大压缩部件的尺寸,所以这里垂直布局的水平边长几乎与第一个按钮的长度一致。(几乎是因为布局的长宽会略微大于部件)

Qt Creator 右侧设置好部件的类名称后可使用 pyuic5 命令将 form.ui 文件转换为 form.py 文件,方便后续使用。

1
pyuic5 form.ui -o form.py

这里我设置的类名分别为

请忽略这里驼峰和蛇形命名的混用,我也不知道我为什么命名会这么混乱🐶

按钮 类名
PPI及组合反射率绘制 PPIPlotButton
RHI 及任意剖面绘制 RHIPlotButton
关闭程序 exit_button

初始化界面

.ui 生成的文件我们不好直接修改 (会被覆盖掉),若要进一步设置各种事件,需要新定义一个类。这里不会使用类的继承,而是使用 Qt 中的函数 setupUi

首先新建一个 mainwindow.py 文件,然后导入 form.py 中的类,最后创建一个类并继承与 form.py 中的类相同的 Qt 类,并使用 setupUi 初始化 UI

1
2
3
4
5
6
7
8
from PyQt5.QtWidgets import QMainWindow
from form import Ui_MainWindow

class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)

若要运行程序,需要继续加上以下代码

1
2
3
4
5
6
7
8
if __name__ == "__main__":
import sys
from PyQt5.QtWidgets import QApplication

app = QApplication([])
widget = MainWindow()
widget.show()
sys.exit(app.exec())

现在程序可以运行了,但是点击按钮不会触发任何事件,因为我们还没有为按钮绑定槽函数。

绑定槽函数

首先新建两个新的 UI 界面吧,分别当作 PPI 和 RHI 的绘图界面。整个文件夹分布如下

1
2
3
4
5
6
Lily
|--src--ppi--ppi_form.ui
| |
| ---rhi--rhi_form.ui
|--form.ui(form.py)
---mainwindow.py

同样使用 pyuic 命令将 .ui 文件转换成 .py 文件,然后分别新建 ppi.py 以及 rhi.py 对界面进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ppi.py
from PyQt5.QtWidgets import QMainWindow
from src.ppi.ppi_form import Ui_PPIWindow

class PPIWindow(QMainWindow):
def __init__(self):
super(PPIWindow, self).__init__()
self.ui = Ui_PPIWindow()
self.ui.setupUi(self)

# rhi.py
from PyQt5.QtWidgets import QMainWindow
from src.rhi.rhi_form import Ui_RHIWindow

class RHIWindow(QMainWindow):
def __init__(self):
super(RHIWindow, self).__init__()
self.ui = Ui_RHIWindow()
self.ui.setupUi(self)
关于 Python 包引入的问题

ppi.py 以及 rhi.py 两个文件其实分别在 src/ppisrc/rhi 中,但是这里 import 的方式仍为 from src.xxx.xxx import xxx,这与 Python 的 模块缓存 有一定联系,这样写的好处是仅搜索 sys.modules 即可找到对应的包,并且由于 src 模块已经被导入过 (第一次被导入将在 mainwindow.py 中发生),这样可以保证 import 不会出错。

现在我们可以在 mainwindow.py 中引入这两个部件并配置按钮的槽函数了,以下是配置过后的代码

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
from PyQt5.QtWidgets import QMainWindow
from form import Ui_MainWindow

from src.ppi.ppi import PPIWindow # 引入部件
from src.rhi.rhi import RHIWindow


class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ppi_window = None
self.rhi_window = None
self.init_slot()

def init_slot(self):
"""
bind function to slot
:return:
"""
self.ui.PPIPlotButton.clicked.connect(self.show_ppi) # 注意!这里是传入函数名,而非执行函数
self.ui.RHIPlotButton.clicked.connect(self.show_rhi)
self.ui.exit_button.clicked.connect(self.close) # 绑定关闭按钮功能

def show_ppi(self):
"""
create ppi window and show it
:return:
"""
# 这里会关闭已经初始化过的窗口,并创建新的窗口,以达到只显示一个窗口的目的
# 但是需要注意,这样做会丢失所有在旧窗口上做过的更改
if self.ppi_window is not None:
self.ppi_window.deleteLater()
self.ppi_window = PPIWindow()
self.ppi_window.show()

def show_rhi(self):
"""
create rhi window and show it
:return:
"""
if self.rhi_window is not None:
self.rhi_window.deleteLater()
self.rhi_window = RHIWindow()
self.rhi_window.show()


if __name__ == "__main__":
import sys
from PyQt5.QtWidgets import QApplication

app = QApplication([])
widget = MainWindow()
widget.show()
sys.exit(app.exec())

现在运行程序,点击按钮就可触发对应事件了。

快捷键及其他事件

接下来我们为应用绑定快捷键,方便程序的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 文件:mainwindow.py
# 首先导入需要的 Qt 部件
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QShortcut
# 然后在类中实例化快捷键类并绑定对应的函数
# 这里省略部分已有的代码
class MainWindow(QMainWindow):
def __init__(self):
#...
# 省略前面的代码
#...
self.bind_short_cut()

def bind_short_cut(self):
"""
bing shortcut to app
:return:
"""
self.close_short_cut = QShortcut(QKeySequence('Ctrl+Q'), self)
self.close_short_cut.activated.connect(self.close)

ppi.pyrhi.py 的快捷键同理,这里不再详述。运行程序,使用对应快捷键,窗口会随之关闭。为了防止用户误触快捷键导致所做的修改丢失,这里使用 QMessageBox 加一个确认框用来确认关闭窗口。

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
# 文件:mainwindow.py
# 导入 QMessageBox
from PyQt5.QtWidgets import QMessageBox
# 定义函数
def close_confirm_box(parent):
"""
return True if Yes or False if No.
be careful! close window will lose all results
:return:
"""
# QMessageBox.Yes | QMessageBox.No 的意思是添加两个按钮:是 和 否
# 第五个参数指定了默认的按钮是哪一个,这里设置默认为 否
res = QMessageBox.question(parent,
'关闭窗口',
'关闭窗口会导致所有结果丢失,确认关闭?',
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)
if res == QMessageBox.Yes:
return True
else:
return False

# 在类中设置相应的槽函数
class MainWindow(QMainWindow):
...
# 省略前面的代码
...
def closeEvent(self, QCloseEvent):
# 重写 closeEvent 函数
if close_confirm_box(self):
QCloseEvent.accept()
else:
QCloseEvent.ignore()

现在重新使用快捷键或者直接尝试关闭窗口时,将弹出确认对话框进行确认

全部代码

mainwindow.py

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
from PyQt5.QtWidgets import QMainWindow, QShortcut, QMessageBox
from PyQt5.QtGui import QKeySequence
from form import Ui_MainWindow

from src.ppi.ppi import PPIWindow # 引入部件
from src.rhi.rhi import RHIWindow


def close_confirm_box(parent):
"""
return True if Yes or False if No.
be careful! close window will lose all results
:return:
"""
# QMessageBox.Yes | QMessageBox.No 的意思是添加两个按钮:是 和 否
# 第五个参数指定了默认的按钮是哪一个,这里设置默认为 否
res = QMessageBox.question(parent,
'关闭窗口',
'关闭窗口会导致所有结果丢失,确认关闭?',
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)
if res == QMessageBox.Yes:
return True
else:
return False


class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.close_short_cut = None
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ppi_window = None
self.rhi_window = None
self.init_slot()
self.bind_short_cut()

def init_slot(self):
"""
bind function to slot
:return:
"""
self.ui.PPIPlotButton.clicked.connect(self.show_ppi) # 注意!这里是传入函数名,而非执行函数
self.ui.RHIPlotButton.clicked.connect(self.show_rhi)
self.ui.exit_button.clicked.connect(self.close) # 绑定关闭按钮功能

def show_ppi(self):
"""
create ppi window and show it
:return:
"""
if self.ppi_window is not None:
self.ppi_window.deleteLater()
self.ppi_window = PPIWindow()
self.ppi_window.show()

def show_rhi(self):
"""
create rhi window and show it
:return:
"""
if self.rhi_window is not None:
self.rhi_window.deleteLater()
self.rhi_window = RHIWindow()
self.rhi_window.show()

def bind_short_cut(self):
"""
bing shortcut to app
:return:
"""
self.close_short_cut = QShortcut(QKeySequence('Ctrl+Q'), self)
self.close_short_cut.activated.connect(self.close)

def closeEvent(self, QCloseEvent):
# 重写 closeEvent 函数
if close_confirm_box(self):
QCloseEvent.accept()
else:
QCloseEvent.ignore()


if __name__ == "__main__":
import sys
from PyQt5.QtWidgets import QApplication

app = QApplication([])
widget = MainWindow()
widget.show()
sys.exit(app.exec())

ppi.py

1
2
3
4
5
6
7
8
9
from PyQt5.QtWidgets import QMainWindow
from src.ppi.ppi_form import Ui_PPIWindow


class PPIWindow(QMainWindow):
def __init__(self):
super(PPIWindow, self).__init__()
self.ui = Ui_PPIWindow()
self.ui.setupUi(self)

rhi.py

1
2
3
4
5
6
7
8
9
from PyQt5.QtWidgets import QMainWindow
from src.rhi.rhi_form import Ui_RHIWindow


class RHIWindow(QMainWindow):
def __init__(self):
super(RHIWindow, self).__init__()
self.ui = Ui_RHIWindow()
self.ui.setupUi(self)

Author: Syize

Permalink: https://blog.syize.cn/2022/07/18/Lily-radar-painter-2/

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Syizeのblog

Comments