前言
配置环境
正式开发
开发前准备
界面制作
踩坑日记一
逻辑与界面分离
代码编写
踩坑日记二
踩坑日记三
踩坑日记四
程序封装
最终效果
后记
本人最近接到一个小任务,用python完成一个垃圾分类小程序。功能要求如下:用户可以通过点击不同的垃圾桶判断屏幕上出现的垃圾种类,判断对错,普及垃圾分类知识。
说来有趣,本人也是刚刚转行到程序员行列,各种知识学习都还不够,只能一边做一边学,网上找各种教程学习,也是踩了不少坑,不过好在最后花了一点时间把任务完成了。作为一名初学者,觉得还是有必要记录下辛酸历程,为后面的学习之路做个铺垫。话不多说,开整!
首先介绍一下工作环境:
首先就是安装各种环境,这里不多赘述,其他博主已经介绍的很详细了(但是本人并没有在pycharm中安装QTdesigner和ui转py的外部工具,这个后面会讲到)要注意的是python版本的问题在安装PyQT5的时候命令不一样
pip install PyQt5
pip3 install PyQt5
第一步是理清思路(这点很重要,敲黑板划重点):
这里的思路包括整个项目的进行流程,界面之间的跳转逻辑,功能实现中的函数逻辑。后面都会详细讲到,这里就不讲那么多,但是再次强调这一步真的很重要,且对于逻辑能力不强的同学来说,好记性不如烂笔头。QAQ
鉴于之前接触过C++的QT可视化界面开发,所以本人第二步选择制作出所有的界面。鉴于QT相对来说比渐简单,拖拖拽拽就可以形成还不错的界面,这里就简单介绍一下部分控件的属性设置。
先来看看本人最后完成的界面(由于本次项目要求不高,所以界面并没有进行太多的美化,各位大佬可以利用QSS等对界面进行美化)
这里就不讲具体制作,只选择讲几个我觉得比较重要的属性。
1、最小最大尺寸
很好理解的属性,这里笔者建议想要获得更好的视觉效果最好是将界面的最最小最大尺寸都进行设置,这样将会避免因为拖动造成的不必要的界面美观问题。
2、label的部分属性
笔者界面的背景选择了label来进行设置,在label中显示图片的方法就是在pixmap属性中选择对应的图片。这里笔者遇到了第一个坑,在QT中是需要加入资源文件才能调用图片的,但是在后续的应用程序打包过程中,笔者发现qrc资源文件在打包时有更多繁琐的步骤,所以在返回来重新检查的时候笔者发现,只要在py代码中将图片使用绝对地址调用就不存在这个问题,简单好用。所以在这里笔者就不介绍资源文件的使用了,网上有很多教程,但是我个人觉得新手还是不要使用了,土方法:使用绝对路径有时候已经够用了。(更正:qrc文件在打包时需要转化为py文件才能打包,而如果使用了绝对路径的话换一台电脑就会因为路径不同而造成图片打不开,素以还是建议大家使用qrc资源文件,添加方法网络上有。qrc转py文件的方式为:在目录文件夹运行下面一行代码
pyrcc5 -o ui_rc.py ui.qrc
完成转化后就可以在后面直接进行封装。但是请注意ui文件转换的py文件中需导入资源文件<运用本人所介绍的命令行转化的会自动导入>)scaledContents属性是让内容自适应label标签的大小,建议大家勾选。alignment属性是决定内容的对齐方式,分别由水平和垂直方向,简单易懂。
3、控件的名字
建议大家在QT设计师里面制作页面时候就将控件的名称制定好,不要使用它默认的名字,例如label1,label2,label3,pushbutton1等等。最好是能统一命名并且记录下来,不然在后面的代码部分将会十分痛苦(包括各个文件的命名,养成好习惯,英语不好就用自己能懂的方式)。一般可以直接在对象查看器中进行修改。
其实窗口的名字也是可以直接进行设置的,就是这个属性,如果你也和笔者一样是个强迫症,建议你修改一下hhhhhhhh
4、布局方式
布局方式的选择主要看各位的习惯,QT中有四种布局方式可以选择,网上教程也很多,这里不赘述
5、其他属性
至于其他属性,笔者不多赘述,想了解的自行百度,本次没有用到。(笔者好像什么也没讲,hhhhh,重点不在这)
6、信号与槽
信号(Signal)和槽(Slot)是Qt中的核心机制,也是在PyQt编程中对象之间进行通信的机制。在QT中,每个继承自QObject的对象都可以使用信号和槽机制来进行通信。 信号和槽函数通过object.signal.connect()方法来连接。当一个QObject对象发射信号,与之相连接的槽函数将会自动执行。
信号和槽是可以在QTdesigner中设置的,缺点是自带的槽函数并不多无法实现想要的功能,所以笔者没有选择这么做,原因是方便后面的代码逻辑操作。
到此,界面的设计就基本上结束了,这也是本次项目中最为简单的一部分,下面进入踩坑高发区。
在将界面导出为ui文件后,面临着第一大问题,怎么将ui文件转化问可编辑的py文件,网上介绍了很多方法,例如pycharm中导入外部工具,vscode插件以及命令行操作。这些方式笔者都尝试过,其实并没有谁好谁不好,结果都是一样的。但是有这么几个问题,第一,pycharm利用外部工具,配置方法网上有,但是笔者一直没有成功,也找不出来是什么原因,遂放弃。第二,vscode插件,这个是比较简单方式,但是缺点是要切换到另一个软件操作,有时候并不是很方便,遂放弃。最后笔者采用了命令行的操作方。详细介绍一下:其实操作系统上正确配置python环境之后,会自带有转化命令——pyuic5。下面介绍一下怎么转化:
pyuic5 -o main.py main.ui
-o是操作参数,表示要生成一个文件
main.py是要生成的.py文件
main.ui是在此之前用Qt生成的包含UI设计的.ui文件
一定要在ui文件的目录下运行(可以在cmd中利用cd命令转到文件夹,也可以直接在打开的文件夹地址行输入cmd直接打开当前目录下的cmd命令)
直接运行此行就可以在目录中生成对应的py文件(其实也可以在后面在加上-x,表示直接生成可执行文件,会帮你将ui文件直接转化为可以运行的代码,但是笔者并没有这么选择,这里就涉及到逻辑与界面分离的思想,后面会讲到)
这是一种思想,其实将逻辑代码直接写到ui生成的py文件中也不是不行,但是我们要注意到,如果我们需要更改ui设计,就需要重新将ui文件转化为py文件,这时候之前写的逻辑代码就会被覆盖掉,得不偿失,采用复制粘贴也不是不行。但是笔者建议,采用逻辑与代码分离的思想来做。将所有的界面文件都不做更改,另外新建一个py文件来调用之前ui界面文件,将功能写到这个文件中,需要时调用其他的界面文件即可。
到此,我们已经做完了所有准备工作,生成了5个界面ui文件并且转化为py文件,新建了一个main.py来进行逻辑功能的实现,调用其他的界面文件实现界面。
话不多说直接上代码:
import sys,os
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
#导入UI界面
from welcome import Ui_welcome
from main import Ui_Form
from knowledge import Ui_knowledge
from right import Ui_right
from mistake import Ui_mistake
#标识说明:0=可回收垃圾、1=厨余垃圾、2=有害垃圾、3=其他垃圾#
#比对库
data = {"碎玻璃酒瓶":0,"苹果核":1,"易拉罐":0,"塑料瓶":0,"香蕉皮":1,"鸡蛋壳":1,"污损垃圾袋":2,
"废纸":0,"一次性塑料杯":2,"螺丝刀":0,"旧衣服":3,"纸尿裤":3,"烟头":3,"大骨棒":3,
"电池":2,"过期药品":2,"节能灯":2,"水银温度计":2,"杀虫剂":2,"灰土":3}
datas = ["水银温度计","碎玻璃酒瓶","纸尿裤", "苹果核", "易拉罐", "塑料瓶", "污损垃圾袋",
"杀虫剂", "一次性塑料杯", "螺丝刀", "旧衣服","废纸", "烟头", "大骨棒",
"电池","鸡蛋壳", "过期药品","香蕉皮" "节能灯", "灰土"]
#开始页
class MainWindow(QDialog,Ui_welcome):
def __init__(self):
super(MainWindow,self).__init__()
self.setupUi(self)
self.join.clicked.connect(self.join_main)#将信号连接到自定义槽函数join_main
self.exit3.clicked.connect(lambda :self.close())#退出按钮连接到close函数
def join_main(self):#定义槽函数
self.main = MainWindow1()
self.main.show()#打开主界面
self.close()#关闭欢迎页
#主界面
class MainWindow1(QMainWindow,Ui_Form):
def __init__(self):
super(MainWindow1,self).__init__()
self.setupUi(self)
self.index = 0
self.knowledge.clicked.connect(self.join_knowledge)#将信号连接到自定义槽函数join_knowledge
self.start.clicked.connect(self.start_game)#将信号连接到自定义槽函数start_game
#将各个按钮连接到函数game
# 厨余 1
self.PB1.clicked.connect(lambda :self.game(1))
# 可回收 0
self.PB2.clicked.connect(lambda :self.game(0))
# 有害 2
self.PB3.clicked.connect(lambda :self.game(2))
# 其他 3
self.PB4.clicked.connect(lambda :self.game(3))
def game(self,index):
#声明数据库,以便于在函数内使用
global data,datas
ans = self.lineEdit.text() #将文本框里面的内容赋值给ans 方便下面判断对错
#正确 弹出正确窗口
if index == data[ans]:#与数据库中对应键值对进行比对(自定义字典)
self.main1 = MainWindow3()
self.main1.a.connect(self.next)#将信号源连接到下一题 next
self.main1.show()
else:
self.main1 = MainWindow4()#回答错误将不会切换下一题 仅仅弹出答错界面
self.main1.show()
#下一题
def next(self,index):
global datas,data
self.index +=1 #自增
self.lineEdit.setText(datas[self.index]) #显示下一题
def start_game(self):
global data,datas
self.lineEdit.setText(datas[0])#显示题目
def join_knowledge(self):
self.main= MainWindow2()
self.main.show()
#知识界面
class MainWindow2(QDialog,Ui_knowledge):
def __init__(self):
super(MainWindow2,self).__init__()
self.setupUi(self)
#正确界面
class MainWindow3(QDialog,Ui_right):
a = pyqtSignal(str)
def __init__(self):
super(MainWindow3,self).__init__()
self.setupUi(self)
self.exit2.clicked.connect(self.chuan)
def chuan(self):
self.a.emit("正确")
self.close()
#错误界面
class MainWindow4(QDialog,Ui_mistake):
def __init__(self):
super(MainWindow4,self).__init__()
self.setupUi(self)
self.exit1.clicked.connect(self.chuan)
def chuan(self):
self.close()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
以上就是我的main.py 文件,代码都进行了注释,应该还是比较好懂的。大致介绍一下:
1、导入库函数和其他界面文件
#导入库函数
import sys,os
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
#导入UI界面
from welcome import Ui_welcome
from main import Ui_Form
from knowledge import Ui_knowledge
from right import Ui_right
from mistake import Ui_mistake
2、建立数据库
#标识说明:0=可回收垃圾、1=厨余垃圾、2=有害垃圾、3=其他垃圾#
#比对库
data = {"碎玻璃酒瓶":0,"苹果核":1,"易拉罐":0,"塑料瓶":0,"香蕉皮":1,"鸡蛋壳":1,"污损垃圾袋":2,
"废纸":0,"一次性塑料杯":2,"螺丝刀":0,"旧衣服":3,"纸尿裤":3,"烟头":3,"大骨棒":3,
"电池":2,"过期药品":2,"节能灯":2,"水银温度计":2,"杀虫剂":2,"灰土":3}
#题库
datas = ["水银温度计","碎玻璃酒瓶","纸尿裤", "苹果核", "易拉罐", "塑料瓶", "污损垃圾袋",
"杀虫剂", "一次性塑料杯", "螺丝刀", "旧衣服","废纸", "烟头", "大骨棒",
"电池","鸡蛋壳", "过期药品","香蕉皮" "节能灯", "灰土"]
3、创建新类并写入功能(部分注释在后续代码中不做解释)
#开始页
#界面
class MainWindow(QDialog,Ui_welcome):#继承
def __init__(self):#初始化
super(MainWindow,self).__init__()#实例化窗口
self.setupUi(self)#写入UI界面文件
self.join.clicked.connect(self.join_main)#将信号连接到自定义槽函数join_main
self.exit3.clicked.connect(lambda :self.close())#退出按钮连接到close函数
#功能实现
def join_main(self):#定义槽函数
self.main = MainWindow1()
self.main.show()#打开主界面
self.close()#关闭欢迎页
4、主界面和功能实现
#主界面
class MainWindow1(QMainWindow,Ui_Form):
def __init__(self):
super(MainWindow1,self).__init__()
self.setupUi(self)
self.index = 0
self.knowledge.clicked.connect(self.join_knowledge)#将信号连接到自定义槽函数join_knowledge
self.start.clicked.connect(self.start_game)#将信号连接到自定义槽函数start_game
#将各个按钮连接到函数game
# 厨余 1
self.PB1.clicked.connect(lambda :self.game(1))
# 可回收 0
self.PB2.clicked.connect(lambda :self.game(0))
# 有害 2
self.PB3.clicked.connect(lambda :self.game(2))
# 其他 3
self.PB4.clicked.connect(lambda :self.game(3))
#游戏功能实现
def game(self,index):
global data,datas#声明数据库,以便于在函数内使用
ans = self.lineEdit.text() #将文本框里面的内容赋值给ans 方便下面判断对错
#正确 弹出正确窗口
if index == data[ans]:#与数据库中对应键值对进行比对(自定义字典)
self.main1 = MainWindow3()
self.main1.a.connect(self.next)#将信号源连接到下一题 next
self.main1.show()
else:
self.main1 = MainWindow4()#回答错误将不会切换下一题 仅仅弹出答错界面
self.main1.show()
#下一题
def next(self,index):
global datas,data
self.index +=1 #自增
self.lineEdit.setText(datas[self.index]) #显示下一题
def start_game(self):
global data,datas
self.lineEdit.setText(datas[0])#显示题目
def join_knowledge(self):
self.main= MainWindow2()
self.main.show()
5、其他页面
#知识界面
class MainWindow2(QDialog,Ui_knowledge):
def __init__(self):
super(MainWindow2,self).__init__()
self.setupUi(self)
#正确界面
class MainWindow3(QDialog,Ui_right):
a = pyqtSignal(str)
def __init__(self):
super(MainWindow3,self).__init__()
self.setupUi(self)
self.exit2.clicked.connect(self.chuan)
def chuan(self):
self.a.emit("正确")
self.close()
#错误界面
class MainWindow4(QDialog,Ui_mistake):
def __init__(self):
super(MainWindow4,self).__init__()
self.setupUi(self)
self.exit1.clicked.connect(self.chuan)
def chuan(self):
self.close()
6、主函数
#固定写法,主函数
if __name__ == "__main__":
app = QApplication(sys.argv)#创建APP
window = MainWindow()#定义窗口
window.show()#使用show方法
sys.exit(app.exec_())##循环等待退出
文中还有很多细节没有写出来,尤其是功能实现那一块,鉴于笔者刚开始学习,计划整理一下思路,后面再出一篇文章来细讲功能实现,这里就不浪费篇幅了。
到此,我们的程序开发已经进行了绝大部分,后面就是不断的调试优化,这里就不讲过程了
给大家讲讲踩坑日记
前面讲到,一定要对控件的和槽函数的命名有足够的耐心,因为在笔者编写代码时,并没有出现一大堆同名的按钮控件和函数名,使得编写还比较顺利,但是可以想象,如果大家都长得一样,是很容易出错,并且还不容易找到错误的。这里讲一个例子
self.knowledge.clicked.connect(self.join_knowledge)
def join_knowledge(self):
self.main= MainWindow2()
self.main.show()
这个槽函数将打开知识界面,但是在笔者第一次编写时,函数的命名和按钮控件的名字一样,本以为可以运行,但是却意外报错,后将函数名进行了修改后就可以正常运行,这里我也不知道是什么原因,但是还是给各位提醒一下,有大佬知道的话可以在评论区告知我。
上文讲到,QT中的资源文件使用并不方便,在使用时不仅需要转化ui文件,还需要转化qrc文件,在后面打包成exe文件时,还有更多麻烦,所以在这里大家可以看到,笔者都是使用的图片的绝对路径来进行调用的,可以给大家作为参考。
上文还讲到,ui文件转化为py的问题,这里在啰嗦两句,如果你采用的是跟笔者一样的方式
pyuic5 -o try.py right.ui
转化出来的代码就会是这样的
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_right(object):
def setupUi(self, right):
#############
#相同部分省略#
#############
如果你采用的是
pyuic5 -o try.py right.ui -x
转化出来的代码就会是这样的
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_right(object):
def setupUi(self, right):
#############
#相同部分省略#
#############
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
right = QtWidgets.QDialog()
ui = Ui_right()
ui.setupUi(right)
right.show()
sys.exit(app.exec_())
两者在UI部分是没有差别的,差别在于-x为了使得转化出来的文件可以直接运行,是给文件自动加上了主函数的,这就是界面逻辑统一的思想,但是我们这次采用的是分离的思想,所以我么不能用这种方式,不然main.py在调用时会报错。
我们的代码写完后,可以运行,是因为我们电脑上有程序运行所需要的环境和各种库,但是我们写出来的程序不能只在我们电脑上运行,那其他人电脑上不可能都安装了python和各种库,也不可能让他们先安装在运行,这样就失去了程序的意义。所以我们要将程序打包成一个exe文件,这样在其它电脑上就可以直接打开运行了。所以接下来我们介绍打包封装。
打包我们同样用到了一个模块:pyinstaller 可以采用pip命令进行安装,不多赘述
打包前,我们需要将所有的源码,,资源文件,图片放在同一个文件夹,在当前文件夹下运行cmd命令
pyinstaller -F -w -i logo.ico main.py
-F是,表示要生成一整个文件包含所有用到的东西(笔者选择这样,因为项目比较小,不会有很大的包。但是如果项目比较大不建议这么做,因为生成的文件太大了。可以选择-D 部署为一个文件夹)
-w 使用Windows子系统执行,当程序启动的时候不会打开命令行(只对Windows有效)
-i 将目录下的ico文件设置为exe图标(这里注意一定要是ico格式的图片,这里给大家介绍一个网站可以转化为ico,灰常好用,见下面)(-o后面就是ico文件的名字)
最后就是将要生成的exe文件的py文件名(如果你的项目是逻辑洁敏分离,并且在main.py中调用其他文件的,你可以和我一样操作,只将这个main.py打包就行了,他会帮你把其他的文件也包含进去的)
http://www.ico51.cn/
执行操作后将会在目录下生成两个文件夹,其中dist文件夹中的exe文件就是我们最后生成的可执行文件啦。
垃圾分类小程序
到这,基本上就完成了整个项目,其实说起来好像很简单,但是还是包含了很多辛酸泪的,笔者作为一个初学者,这段时间几乎是住在了CSDN和Github,不断地查资料,尝试,失败,推到重来。但是好在没有选择放弃,可能这个项目对于大佬来说很容易,几个小时就能写完,但是笔者既然选择写下来就是为了鼓励自己不断地学习,希望有朝一日能够成为大佬hhhhh。
在这里还有几局话想对想转行做程序员的同学说的,笔者之前是学通信的,说实话编程基础一般,数据库、数据结构都没有学过,转行过来也才发现隔行如隔山,所以真的需要足够的兴趣和大量的学习、练习。做一名程序猿,还要能要坐得住,这是我认为最重要的,基础,编程语言什么的都可以靠学习,但是耐心只能靠自己。
第一次做项目,第一次写博客,文笔有限,诸君海涵。
路漫漫其修远兮,吾将上下而求索。
(另外全部源文件,有需要的可以私聊我XX )