为啥做这个

应导师的要求,要给项目做个简单的GUI,用于糊弄甲方;某师兄虽然会但是显然不想接锅

想着自己原来也做过,正好借这个机会再学学怎么做GUI,以后自己的程序也能做出来好看的GUI唬人

以下为演示视频,如果糊了就是bilibili的问题,可以点进去看

开发环境

笑死我了,这也能叫开发环境的

虽然之前我的第一个GUI是用Tkinter开发的来着,但是啊,一言难尽的丑呢~

而且开发了一半才知道大家伙儿都不用这玩意儿

像极了学地球物理的我(话说我才感觉这英文字体太花里胡哨了,考虑换一个吧orz)

下载完之后,就用designer设计界面了

不过这么多能用的组件,一下也不知道什么能用,再分享一个链接,可以对着找自己想要的效果

我的

长啥样儿?

因为我的博客都在laptop上更新,但是环境搭建在办公室

今儿回宿舍一看,pyqt5之前安装过一次,但是现在环境冲突,搞得乱七八糟

干脆也不想整了,所以这里没有designer的图了

放个半成品

真是偷懒的设计呢把需要的东西全给堆在一起就完了要是我自己的GUI,肯定先加点图标、背景图,然后把按钮都给换成有趣的,再输出一堆乱七八糟的语句

实现

读取一个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#将[select_file]函数绑定在名叫[read_sgy]的按钮上(在designer里命名)
ui.read_sgy.clicked.connect(select_file)

def select_file(self):
global data_in
global freq,channels,time_lenth
global file
file, filetype = QFileDialog.getOpenFileName(MainWindow, "选取文件", "./","*.*")
if file.endswith('sgy'):
data_in,freq, channels, times_lenth=get_data_parameters(file)
ui.textBrowser.append(f'{file}已经被读入')
ui.textBrowser.append(f'一共有{channels}道,时间采样率为{freq},长度为{times_lenth}')
print(file)
else:
ui.textBrowser.append(f'Unsupported type: {file}')
print('Unsupported type')

这个函数会读取指定的文件,并且通过全局变量存储数组以降低代码复杂度。这里添加了一些其他的输出值,虽然没啥用,但是有也不是一点用没有。不过其实可以写个质量控制啥的,try函数什么的(对于我自己来说,我不需要用这种函数,对于潜在客户,啊,谁管他们啊~~)。

获取一个路径&保存文件

感谢二师兄给的输出函数
1
2
3
4
5
6
7
8
9
10
11
12
13
ui.trans2sac.clicked.connect(st2sac)
def st2sac():
global file
#这个函数会弹出弹窗,选择文件夹
directory = QFileDialog.getExistingDirectory(None,"选取文件夹","./")
st = read(file)
print(directory)
sacname='_sac'
ui.textBrowser.append(f'正在处理......')
for i,tr in enumerate(st):
tr.write(directory+'/'+'%s'%i + sacname + '.sac')
ui.textBrowser.append(f'{file}已经被拆分并写入{directory}')
return 1

这里就不演示了,没啥好看的

参数设定&预处理

这里用了几个输入窗口来获取参数,这应该是最简单的办法(其实滑块我也会了,但是没必要是吧)

但是在滤波方法上加了个多选框,这都是在designer里直接拽进来就行了,自己打几个字就成

最后在需要取参数时,使用.text()函数就好了

1
2
3
4
short=int(ui.shortwin.text())
long=int(ui.longwin.text())
fre_min=int(ui.low_f.text())
fre_max=int(ui.hign_f.text())

画图

有时候不得不说,偷懒真是第一生产力

本来想着用pyqt5新建一个窗口内嵌matplotlib的办法来画图,但是突然想到为啥不能直接让matplotlib弹出来窗口呢?试了一下还真可以,而且发现matplotlib可以用按钮进行交互,ok那就这么搞了~

以下为臭长的代码,包含了三个画图函数,我用注释来强调一下需要注意的地方

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
# 这里为什么要用全局函数呢?
# 因为一开始写完这个函数时,发现按钮都不能点
# 一顿面向搜索引擎debug后,发现了知道但是从来没有关心过的事儿
# 如果函数运行后没有返回值,那局部变量就丢进垃圾桶了

# 如果我们不把这几个存在于matplotlib窗口中的按钮以某种形式存起来
# 那按钮就会失效(可以快进到最后三行看❤❤❤❤❤)
global b1,b2,b3,s1
ind='0'
def new_figure():
global data_in,b1,b2,b3,s1
global data_f
f1,ax=plt.subplots(1)
class Button_handlers():
#这里如果定义 ind=0,后续使用self.ind=1等语句时,ind的值不会变化
global ind
def showf(self, event):
global ind
ind='1'
ax.cla()
ax.imshow(data_f,aspect='auto',vmax=sfreq.val,vmin=-sfreq.val)
def showo(self, event):
global ind
ind='0'
ax.cla()
ax.imshow(data_in,aspect='auto',vmax=sfreq.val,vmin=-sfreq.val)
def showsl(self, event):
global ind
ind='2'
ax.cla()
ax.imshow(data_sl,aspect='auto')
def fresh(self,event):
if ind=='0':
self.showo(event)
elif ind=='1':
self.showf(event)
else:
print('-')
ax.imshow(data_in,aspect='auto')

#滤波按钮
#这个语句用来定义按钮的位置
buttonf = plt.axes([0.3, 0.03, 0.1, 0.05])
#这个语句用来绑定按钮位置与样式,创建按钮
button1 = Button(buttonf, 'Filter',color='khaki', hovercolor='yellow')
#这个语句用来捆绑按钮的功能
button1.on_clicked(Button_handlers().showf)

#原始波形按钮
buttono = plt.axes([0.1, 0.03, 0.1, 0.05])
button2 = Button(buttono, 'Raw',color='khaki', hovercolor='yellow')

button2.on_clicked(Button_handlers().showo)
#长短窗按钮
buttonsl = plt.axes([0.5, 0.03, 0.1, 0.05])
button3 = Button(buttonsl, 'with S/L',color='khaki', hovercolor='yellow')
button3.on_clicked(Button_handlers().showsl)

#滑块
#这个功能纯属我看到了,就想试试,是用来改变imshow的最大值最小值的
#定义和按钮差不多,on_changed()函数指定值变化时的操作,不定义也没关系
axcolor = 'lightgoldenrodyellow'
axfreq = plt.axes([0.1, 0.12, 0.8, 0.03], facecolor=axcolor)
sfreq = Slider(axfreq, 'Vmax', 0.0, 1000.0,valfmt='% .2f', valinit=0, valstep=0.01)
sfreq.on_changed(Button_handlers().fresh)
sfreq.reset()
#滑块起始值
sfreq.set_val(500.0)

plt.subplots_adjust(bottom=0.3)
plt.show()
#####################❤❤在这里,把我们的按钮存起来❤❤######################
b1,b2,b3=button1,button2,button3
s1=sfreq
return button1,button2,button3,sfreq

后续实现的内容

  • 到时拾取需要增加上去,不过顶多就是点几个点罢了
  • 加个波形图上去
  • 可能需要输出拾取的结果,我想的直接放到matplotlib那里更合适一点
  • 批处理是一件非常值得考虑的事情,但是又不是我用,先不做了~
  • 换个好看的录屏软件
2022-05-02新增的内容

限制用户行为

笑话[酒吧]:
  • 一个测试工程师走进一家酒吧,要了一杯啤酒;
  • 一个测试工程师走进一家酒吧,要了一杯咖啡;
  • 一个测试工程师走进一家酒吧,要了 0.7 杯啤酒;
  • 一个测试工程师走进一家酒吧,要了-1 杯啤酒;
  • 一个测试工程师走进一家酒吧,要了 2^32 杯啤酒;
  • 一个测试工程师走进一家酒吧,要了一杯洗脚水;
  • 一个测试工程师走进一家酒吧,要了一杯蜥蜴;
  • 一个测试工程师走进一家酒吧,要了一份 asdfQwer@24dg!&*(@;
  • 一个测试工程师走进一家酒吧,什么也没要;
  • 一个测试工程师走进一家酒吧,又走出去又从窗户进来又从后门出去从下水道钻进来;
  • 一个测试工程师走进一家酒吧,又走出去又进来又出去又进来又出去,最后在外面把老板打了一顿;
  • 一个测试工程师走进一家酒吧,要了一杯烫烫烫的锟斤拷;
  • 一个测试工程师走进一家酒吧,要了 NaN 杯 Null;
  • 一个测试工程师冲进一家酒吧,要了 500T 啤酒咖啡洗脚水野猫狼牙棒奶茶;
  • 一个测试工程师把酒吧拆了;
  • 一个测试工程师化装成老板走进一家酒吧,要了 500 杯啤酒并且不付钱;
  • 一万个测试工程师在酒吧门外呼啸而过;
  • 一个测试工程师走进一家酒吧,要了一杯啤酒’;DROP TABLE 酒吧;
  • 测试工程师们满意地离开了酒吧。
  • 然后一名顾客点了一份炒饭,酒吧炸了。

按照墨菲定律(不是),用户一定会沿着与设计理想不同的方向使用软件,所以适当的控制用户行为还是必要的,这里加入了对按钮的限制:

  1. 不导入文件时,无法进行后续操作(因为文件数组并没建立)
  2. 不滤波无法拾取,这是为了保证处理质量
  3. 不拾取无法画图及保存结果,这是防止相关代码收到空数组

image-20220502103114343image-20220502103405091image-20220502103709869

实现代码:

1
2
ui./*item——name*/.setEnabled(0)
#之后在合适的位置启用就行

波形图界面

image-20220502104024945

滑动条的实现

image-20220502104155378image-20220502104240693

这里采用了调整y轴范围的方法来进行上下滑动,同理可以进行左右滑动,但是没必要。虽然上下滑动可以用放大镜代替,但是有个滑动条要友好的多,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
def ax_begin(event):
global num,begin,tr
begin=s_begin.val
print(begin,num)
ax.set_ylim(begin,begin+num*tr)
axcolor = 'lightgoldenrodyellow'
axfreq = plt.axes([0.93, 0.1, 0.025, 0.8], facecolor=axcolor)
s_begin = Slider(axfreq, 'Begin line', 0.0, data_in.shape[0]-50,valfmt='% 2d', valinit=0, valstep=1,orientation='vertical')
s_begin.on_changed(ax_begin)
s_begin.reset()
s_begin.set_val(0)

间隔条和密度条

image-20220502104728148密度条可以调整界面中的道数,使得显示范围更大,方便看出波场的变化。

image-20220502104921856

间隔条允许跳过部分道的波形显示,这能够进一步获得更大的视野,代价是道间距变大。

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
    def ax_num(event):
global num,begin,tr
num=s_num.val
print(begin,num)
ax.set_ylim(begin,begin+num*tr)
#调整间隔涉及到归一化的问题,所以简单的做法是重新画
def ax_tr(event):
global num,begin,tr,pickings
global ln1,ln2,ln3,ln4,lns,index_check
ax.cla()
tr=int(s_tr.val)
for i in range(data_in.shape[0]//tr):
l1,=ax.plot(tr*i+tr*0.5*data_in[i*tr]/data_in[i*tr].max(),color='black',linewidth=1)
ln1.append(l1)
l2,=ax.plot(tr*i+tr*0.5*data_f[i*tr]/data_f[i*tr].max(),color='blue',linewidth=1)
ln2.append(l2)
l3,=ax.plot(tr*i+tr*0.5*data_sl[i*tr]/5,color='red',linewidth=1)
ln3.append(l3)
l4=short_line(ax,pickings[i],tr*i,tr)
ln4.append(l4)
lns=[ln1,ln2,ln3,ln4]
ax.set_ylim(begin,begin+num*tr)
ax.set_xlim(0,time_lenth)
for i in range(4):
for l in lns[i]:
l.set_visible(index_check[i])
##调节窗口中数量
axcolor = 'lightgoldenrodyellow'
axfreq = plt.axes([0.01, 0.5, 0.025, 0.4], facecolor=axcolor)
s_num = Slider(axfreq, 'Lines Num', 0.0, 100,valfmt='% 2d', valinit=0, valstep=1,orientation='vertical')
s_num.on_changed(ax_num)
s_num.reset()
s_num.set_val(20)
#调节窗口中间隔
axcolor = 'lightgoldenrodyellow'
axfreq = plt.axes([0.1, 0.5, 0.025, 0.4], facecolor=axcolor)
s_tr = Slider(axfreq, 'Space', 1, 50,valfmt='% 2d', valinit=0, valstep=1,orientation='vertical')
s_tr.on_changed(ax_tr)
s_tr.reset()
s_tr.set_val(1)

显示内容的变化

image-20220502105624709

image-20220502105724276

image-20220502105814559

这个功能看起来很好用,甚至让我觉得我日常应该也能用得到这个gui了,但是实现起来非常简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#这里设置全局变量,可以保存状态,并且指示多选框选中和弃用时,对应的显示状态
index_check=[1,0,0,0]
#复选框
rax = plt.axes([0.05, 0.1, 0.1, 0.3])
# 创建复选框的标签列表,标签自动对应曲线的标签
labels = ['Raw','Filter','S/L','Picking']
# 根据对应曲线可见状态初始化复选框初始状态
visibility = index_check
# 构造复选框实例
check = CheckButtons(rax, labels, visibility)
check.on_clicked(func)
def func(label):
global ln1,ln2,ln3,lns,index_check
labels = ['Raw','Filter','S/L','Picking']
index = labels.index(label)
index_check[index]=1*(index_check[index]==0)
for l in lns[index]:
l.set_visible(not l.get_visible())
plt.draw()# 不加这句代码,则屏幕不会主动刷新

完整的波形图界面代码

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
def new_figure2():
global s2,s3,s4,c1
global data_in,data_f,data_sl
global num,begin,time_lenth
global time
global ln1,ln2,ln3,ln4,lns,index_check
global pickings
ui.textBrowser.append(f'=============================================================')
ui.textBrowser.append(f'正在画波形图,请稍后')
f1,ax=plt.subplots(figsize=(10,10))
plt.subplots_adjust(left=0.2, right=0.9)
def ax_num(event):
global num,begin,tr
num=s_num.val
print(begin,num)
ax.set_ylim(begin,begin+num*tr)
def ax_begin(event):
global num,begin,tr
begin=s_begin.val
print(begin,num)
ax.set_ylim(begin,begin+num*tr)
def ax_tr(event):
global num,begin,tr,pickings
global ln1,ln2,ln3,ln4,lns,index_check
ax.cla()
tr=int(s_tr.val)
for i in range(data_in.shape[0]//tr):
l1,=ax.plot(tr*i+tr*0.5*data_in[i*tr]/data_in[i*tr].max(),color='black',linewidth=1)
ln1.append(l1)
l2,=ax.plot(tr*i+tr*0.5*data_f[i*tr]/data_f[i*tr].max(),color='blue',linewidth=1)
ln2.append(l2)
l3,=ax.plot(tr*i+tr*0.5*data_sl[i*tr]/5,color='red',linewidth=1)
ln3.append(l3)
l4=short_line(ax,pickings[i],tr*i,tr)
ln4.append(l4)
lns=[ln1,ln2,ln3,ln4]
ax.set_ylim(begin,begin+num*tr)
ax.set_xlim(0,time_lenth)
for i in range(4):
for l in lns[i]:
l.set_visible(index_check[i])
def func(label):
global ln1,ln2,ln3,lns,index_check
labels = ['Raw','Filter','S/L','Picking']
index = labels.index(label)
index_check[index]=1*(index_check[index]==0)
for l in lns[index]:
l.set_visible(not l.get_visible())
plt.draw()
##调节窗口中数量
axcolor = 'lightgoldenrodyellow'
axfreq = plt.axes([0.01, 0.5, 0.025, 0.4], facecolor=axcolor)
s_num = Slider(axfreq, 'Lines Num', 0.0, 100,valfmt='% 2d', valinit=0, valstep=1,orientation='vertical')
s_num.on_changed(ax_num)
s_num.reset()
s_num.set_val(20)
#调节起始点
axcolor = 'lightgoldenrodyellow'
axfreq = plt.axes([0.93, 0.1, 0.025, 0.8], facecolor=axcolor)
s_begin = Slider(axfreq, 'Begin line', 0.0, data_in.shape[0]-50,valfmt='% 2d', valinit=0, valstep=1,orientation='vertical')
s_begin.on_changed(ax_begin)
s_begin.reset()
s_begin.set_val(0)
#复选框
rax = plt.axes([0.05, 0.1, 0.1, 0.3])
# 创建复选框的标签列表,标签自动对应曲线的标签
labels = ['Raw','Filter','S/L','Picking']
# 根据对应曲线可见状态初始化复选框初始状态
visibility = index_check
# 构造复选框实例
check = CheckButtons(rax, labels, visibility)
check.on_clicked(func)
#调节窗口中间隔
axcolor = 'lightgoldenrodyellow'
axfreq = plt.axes([0.1, 0.5, 0.025, 0.4], facecolor=axcolor)
s_tr = Slider(axfreq, 'Space', 1, 50,valfmt='% 2d', valinit=0, valstep=1,orientation='vertical')
s_tr.on_changed(ax_tr)
s_tr.reset()
s_tr.set_val(1)
s2,s3,s4=s_begin,s_num,s_tr
c1=check
plt.show()
return 1

后日谈

这个GUI做到这里基本就已经成型了,其实可以增加更多的功能来丰富,但是属实没必要。不过在写这个GUI时我学到了不少pyqt5的基础语法,这还是让我有所受益的,也许在我完成了地震检测、定位以及去噪的工作后,我可以用pyqt5集成一个DAS的多功能终端,想来还是挺酷的。

不过这个GUI还是有不少可以改进的地方的,比如应该加入try语法,吧错误信息打印到log栏里,而不是直接退出,但是还是那句话,这个GUI的设计也不是我用,随意一点好了~

这个程序不算难,直接分享了

ui源代码

主程序源代码

虽然理论上不需要,但是使用前还是应该先给我打个招呼~