1、初探Qt Designer 设计师
打开PyQt5的Qt Designer,会自动弹出新建窗体对话框,对于我们最常用的就是Widget通用窗口类,还有个MainWindows顾名思义主窗口。PyQt5的Widget被分离出来,似乎用来替代Dialog,并将Widget放入了QtWidget模块(库)中,PyQt4是QtGUI。
这是一个Widget和MainWindows,MainWindows默认添加了菜单栏、工具栏和状态栏等。
默认左边是控件栏,提供了很多空间类,我们可以直接拖放到widget中看到效果,点窗体--预览(Ctrl+R)。
每个空间都有自己的名称,提供不同的功能,比如常用的按钮、输入框、单选、文本框等等。
右边是对窗口及控件的各种调整、设置、添加资源(列如:图片)、动作。还可以直接编辑Qt引以为豪的信号槽(signal和slot)。
有了Qt Designer使得我们在程序设计中更快的能开发设计出程序界面,避免了用纯代码来写一个窗口的繁琐,同时PyQt支持界面与逻辑分离,这对于新手来说无疑是个最大的福音,当然要做出华丽的界面还是要学代码的。至少Qt Designer为我们提供了一些解决方法,另外我们也可以通过Qt Designer生成的代码来学习一些窗口控件的用法。
Qt Designer Layouts窗口布局
Qt Designer窗口布局Layouts提供了四种布局方法,他们是:
Vertical Layout 纵向布局
Horizontal Layout 横向布局
Grid Layout 栅格布局
Form Layout 在窗体布局中布局
前三种是我们经常会用到的,我们将布局Layouts拖动到窗体上会有红色框来显示(中间窗体中的四个小红框就是),Layout的一些属性可以通过属性编辑器来控制,一般包括:上下左右边距间隔,空间之间间隔等。
在我们使用布局之前,我们得对层次要有个了解,在程序设计中一般用父子关系来表示。当然有过平面设计经验的童鞋对分层应该有所了解,这里我们还需要将层分成层次。其实就像python中规定的代码缩进量代表不同层次的道理差不多。
从对象查看器中我们可以方便的看出窗体(Form)--布局(Layout)--控件(这里是PushButton按钮)之间的层次关系。Form窗口一般作为顶层显示,然后使用Layout将控件按照我们想要的方式规划开来。
小提示:
通常我们使用栅格布局作为顶层布局,将控件放置好之后可以通过右键--布局--栅格布局,将布局充满整个窗体。我们可以先放入控件,然后ctrl选中多个控件,然后点击工具栏上快速布局工具进行布局。
这里要注意一下,Qt Designer设计出来的文件默认为ui文件,里面包含的类css布局设计语言,如果想要查看代码我们还需要将它转换(编译)成py文件,我们可以使用一条DOS命令来完成D:\Python33\Lib\site-packages\PyQt5\pyuic5.bat main.ui -o frist.py。 我的pycharm经过PyQt5+python3+pycharm开发环境配置的配置。通过下图的操作可以便捷的对UI进行转化
更实用的转换命令可以将当前文件夹下所有ui转换成py文件:
for /f "delims=" %%i in ('dir /b /a-d /s *.ui') do D:\Python33\Lib\site-packages\PyQt5\pyuic5.bat %%i -o %%i.py
PyQt支持用LoadUi方法直接加载ui文件,当然我们通过转换后可以方便学习PyQt窗体控件的源代码。
2、Qt Designer生成源码
下面来分析一下Qt Designer生成的源码。
Qt Designer制作的图形界面为
生成的代码如下
- from PyQt5 import QtCore, QtGui, QtWidgets #导入模块
- class Ui_Form(object): #创建窗口类,继承object
- def setupUi(self, Form):
- Form.setObjectName("Form") #设置窗口名
- Form.resize(400, 300) #设置窗口大小
- self.quitButton = QtWidgets.QPushButton(Form) #创建一个按钮,并将按钮加入到窗口Form中
- self.quitButton.setGeometry(QtCore.QRect(280, 240, 75, 23)) #设置按钮大小与位置
- self.quitButton.setObjectName("quitButton") #设置按钮名
- self.retranslateUi(Form)
- QtCore.QMetaObject.connectSlotsByName(Form) #关联信号槽
- def retranslateUi(self, Form):
- _translate = QtCore.QCoreApplication.translate
- Form.setWindowTitle(_translate("Form", "Test")) #设置窗口标题
- self.quitButton.setText(_translate("Form", "Quit")) #设置按钮显示文字
现在运行这段代码,窗口是不会出现的。如何使窗口出现呢?下面需要添加一段代码
- if __name__=="__main__":
- import sys
- app=QtWidgets.QApplication(sys.argv)
- widget=QtWidgets.QWidget()
- ui=Ui_Form()
- ui.setupUi(widget)
- widget.show()
- sys.exit(app.exec_())
新建一个文件,导入我们设计的untitled .py文件,实现代码与界面分离。
- from PyQt5 import QtWidgets
- from untitled import Ui_Form
- class mywindow(QtWidgets.QWidget):
- def __init__(self):
- super(mywindow,self).__init__()
- self.new=Ui_Form()
- self.new.setupUi(self)
- if __name__=="__main__":
- import sys
- app=QtWidgets.QApplication(sys.argv)
- myshow=mywindow()
- myshow.show()
- sys.exit(app.exec_())
- from PyQt5 import QtWidgets
- from untitled import Ui_Form
- class mywindow(QtWidgets.QWidget,Ui_Form):
- def __init__(self):
- super(mywindow,self).__init__()
- self.setupUi(self)
- if __name__=="__main__":
- import sys
- app=QtWidgets.QApplication(sys.argv)
- myshow=mywindow()
- myshow.show()
- sys.exit(app.exec_())
-
3、Qt Designer信号槽
一些信号槽的基本介绍:信号和槽是一种高级接口,应用于对象之间的通信,它是 QT 的核心特性,也是 QT 区别于其它工具包的重要地方。它为高层次的事件处理自动生成所需要的附加代码。在我们所熟知的很多 GUI 工具包中,窗口小部件 (widget) 都有一个回调函数用于响应它们能触发的每个动作,这个回调函数通常是一个指向某个函数的指针。但是,在 QT 中信号和槽取代了这些凌乱的函数指针,使得我们编写这些通信程序更为简洁明了。
所有从 QObject 或其子类 ( 例如 Qwidget) 派生的类都能够包含信号和槽。当对象改变其状态时,信号就由该对象发射 (emit) 出去,这就是对象所要做的全部事情,它不知道另一端是谁在接收这个信号。这就是真正的信息封装,它确保对象被当作一个真正的软件组件来使用。槽用于接收信号,但它们是普通的对象成员函数。一个槽并不知道是否有任何信号与自己相连接。而且,对象并不了解具体的通信机制。
你可以将很多信号与单个的槽进行连接,也可以将单个的信号与很多的槽进行连接,甚至于将一个信号与另外一个信号相连接也是可能的,这时无论第一个信号什么时候发射系统都将立刻发射第二个信号。总之,信号与槽构造了一个强大的部件编程机制。
说实话对于像我这样的新手来说看着就蛋疼,想学会它没办法,我们还是简化一下概念吧:
所有QObject类都可以使用信号槽,换句话来说继承自pyqt中的类基本上都可以使用信号槽机制。当然非QObject也是可以通过其他一些办法来使用信号槽的。
仅仅有了信号和槽是不行的,我们还需要了解:
信号(Signal)、槽(slot)、连接(connect)、动作事件(action)、发射(emit)、发送者、接受者等等一些列的知识。好吧,别搞的那么复杂行不行,我们还是学学该怎么用吧。
在Qt Designer中为我们提供了一些基本的信号槽方法,我们来看看:
点击工具栏上的“编辑信号/槽”,进入信号槽编辑模式,我们可以直接在发送者(button)上按住鼠标左键不放,拖动到接收者(Form窗体)上。这样就建立起了连接。
接着,会弹出配置连接对话框。
左边是发送者(按钮)的信号(动作事件),右边是接收者(窗体)的槽(动作事件)
如图所示,我信号选择的是clicked,槽选择的是close
我们看一下编译后生成的代码:
self.quitButton.clicked.connect(Form.close)
实现的功能是:当按钮点击之后关闭窗体。
流程:按钮是信号发送者,当点击按钮之后会发送一个信号出去,通过这段代码程序内部的通讯机制知道这个按钮的点击事情被连接到窗体的关闭事件上去了,然后通知接受者窗体,你该运行槽函数close了!
那么我们怎么能执行自己的“槽”呢?
- from PyQt5 import QtWidgets
- from untitled import Ui_Form
- class mywindow(QtWidgets.QWidget,Ui_Form):
- def __init__(self):
- super(mywindow,self).__init__()
- self.setupUi(self)
- self.myButton.clicked.connect(self.myPrint) #槽函数不用加括号
- def myPrint(self): #定义槽
- print("helloWorld")
- if __name__=="__main__":
- import sys
- app=QtWidgets.QApplication(sys.argv)
- myshow=mywindow()
- myshow.show()
- sys.exit(app.exec_())
槽其实就个函数(方法),Qt5中的槽函数不在限定必须是slot,可以是普通的函数、类的普通成员函数、lambda函数等。编译期间就会检查信号与槽是否存在!
信号的connect连接最好放在__init__析构函数里面,这样只会声明一次连接,如果在类方法(函数中)使用的话,要记得disconnect,否则connect会连接多次,导致程序异常。
信号槽函数不用加 (),否则可能会导致连接异常。
4、Qt Designer自定义信号emit及传参
- from PyQt5 import QtWidgets,QtCore
- from untitled import Ui_Form
- import time
- class MyWindow(QtWidgets.QWidget,Ui_Form):
- _signal=QtCore.pyqtSignal(str) #定义信号,定义参数为str类型
- def __init__(self):
- super(MyWindow,self).__init__()
- self.setupUi(self)
- self.myButton.clicked.connect(self.myPrint)
- self._signal.connect(self.mySignal) #将信号连接到函数mySignal
- def myPrint(self):
- self.tb.setText("")
- self.tb.append("正在打印,请稍候")
- self._signal.emit("你妹,打印结束了吗,快回答!")
- def mySignal(self,string):
- print(string)
- self.tb.append("打印结束")
- if __name__=="__main__":
- import sys
- app=QtWidgets.QApplication(sys.argv)
- myshow=MyWindow()
- myshow.show()
- sys.exit(app.exec_())
运行结果
注意:当信号与槽函数的参数数量相同时,它们参数类型要完全一致。信号与槽不能有缺省参数。 当信号的参数与槽函数的参数数量不同时,只能是信号的参数数量多于槽函数的参数数量,且前面相同数量的参数类型应一致,信号中多余的参数会被忽略。此外,在不进行参数传递时,信号槽绑定时也是要求信号的参数数量大于等于槽函数的参数数量。这种情况一般是一个带参数的信号去绑定一个无参数的槽函数。
可以出传递的参数类型有很多种:str、int、list、object、float、tuple、dict等等
5、通用对话框QMessageBox
PyQt5中为我们提供了很多默认信息框QMessageBox,注意为方便使用需要导入模块。
QMessageBox对话框包含类型只是图标不同其他无太大差别:
QMessageBox.information 信息框
QMessageBox.question 问答框
QMessageBox.warning 警告
QMessageBox.ctitical危险
QMessageBox.about 关于
- from PyQt5 import QtWidgets
- from PyQt5.QtWidgets import QMessageBox
- class MyWindow(QtWidgets.QWidget):
- def __init__(self):
- super(MyWindow,self).__init__()
- self.myButton = QtWidgets.QPushButton(self)
- self.myButton.setObjectName("myButton")
- self.myButton.setText("Test")
- self.myButton.clicked.connect(self.msg)
- def msg(self):
- reply = QMessageBox.information(self, #使用infomation信息框
- "标题",
- "消息",
- QMessageBox.Yes | QMessageBox.No)
- if __name__=="__main__":
- import sys
- app=QtWidgets.QApplication(sys.argv)
- myshow=MyWindow()
- myshow.show()
- sys.exit(app.exec_())
if result == reply.Yes
print("Yes")
if result == reply.No
print("No")
选中控件点击右键选择改变样式或change stylesheet
选择border-image,图片随控件变化始终填满控件
background-image,图片作为背景图片
image,图片不改变长宽比,但随控件变化
之后选择图片点击OK
转换成py文件,图片以十六进制转义字符储存
导入文件运行
6、标准输入框QInputDialog
- from PyQt5 import QtWidgets
- from PyQt5.QtWidgets import QLineEdit,QInputDialog
- class MyWindow(QtWidgets.QWidget):
- def __init__(self):
- super(MyWindow,self).__init__()
- self.myButton = QtWidgets.QPushButton(self)
- self.myButton.setObjectName("myButton")
- self.myButton.setText("Test")
- self.myButton.clicked.connect(self.msg)
- def msg(self):
- #后面四个数字的作用依次是 初始值 最小值 最大值 小数点后位数
- doubleNum,ok1 = QInputDialog.getDouble(self, "标题","计数:", 37.56, -10000, 10000, 2)
- #后面四个数字的作用依次是 初始值 最小值 最大值 步幅
- intNum,ok2 = QInputDialog.getInt(self, "标题","计数:", 37, -10000, 10000, 2)
- #第三个参数可选 有一般显示 (QLineEdit.Normal)、密碼显示( QLineEdit. Password)与不回应文字输入( QLineEdit. NoEcho)
- stringNum,ok3 = QInputDialog.getText(self, "标题","姓名:",QLineEdit.Normal, "王尼玛")
- #1为默认选中选项目,True/False 列表框是否可编辑。
- items = ["Spring", "Summer", "Fall", "Winter"]
- item, ok4 = QInputDialog.getItem(self, "标题","Season:", items, 1, True)
- text, ok5 = QInputDialog.getMultiLineText(self, "标题", "Address:", "John Doe\nFreedom Street")
- if __name__=="__main__":
- import sys
- app=QtWidgets.QApplication(sys.argv)
- myshow=MyWindow()
- myshow.show()
- sys.exit(app.exec_())
7、标准文件打开保存框QFileDialog
多个文件打开 QFileDialog.getOpenFileNames()
文件夹选取 QFileDialog.getExistingDirectory()
文件保存 QFileDialog.getSaveFileName()
- from PyQt5 import QtWidgets
- from PyQt5.QtWidgets import QFileDialog
- class MyWindow(QtWidgets.QWidget):
- def __init__(self):
- super(MyWindow,self).__init__()
- self.myButton = QtWidgets.QPushButton(self)
- self.myButton.setObjectName("myButton")
- self.myButton.setText("Test")
- self.myButton.clicked.connect(self.msg)
- def msg(self):
- directory1 = QFileDialog.getExistingDirectory(self,
- "选取文件夹",
- "C:/") #起始路径
- print(directory1)
- fileName1, filetype = QFileDialog.getOpenFileName(self,
- "选取文件",
- "C:/",
- "All Files (*);;Text Files (*.txt)") #设置文件扩展名过滤,注意用双分号间隔
- print(fileName1,filetype)
- files, ok1 = QFileDialog.getOpenFileNames(self,
- "多文件选择",
- "C:/",
- "All Files (*);;Text Files (*.txt)")
- print(files,ok1)
- fileName2, ok2 = QFileDialog.getSaveFileName(self,
- "文件保存",
- "C:/",
- "All Files (*);;Text Files (*.txt)")
- if __name__=="__main__":
- import sys
- app=QtWidgets.QApplication(sys.argv)
- myshow=MyWindow()
- myshow.show()
- sys.exit(app.exec_())
8、QColorDialog与QFontDialog
QColorDialog颜色对话框
QFontDialog字体对话框
- from PyQt5 import QtWidgets
- from PyQt5.QtWidgets import QColorDialog,QFontDialog
- from PyQt5.QtCore import Qt
- from PyQt5.QtGui import QPalette
- class MyWindow(QtWidgets.QWidget):
- def __init__(self):
- super(MyWindow,self).__init__()
- self.myButton = QtWidgets.QPushButton(self)
- self.myButton.setObjectName("myButton")
- self.myButton.setText("Test")
- self.myButton.clicked.connect(self.msg)
- def msg(self):
- color = QColorDialog.getColor(Qt.blue, self, "Select Color")
- if color.isValid():
- print(color.name(),color)
- self.myButton.setPalette(QPalette(color)) #给按钮填充背景色
- self.myButton.setAutoFillBackground(True)
- font, ok = QFontDialog.getFont()
- if ok:
- print(font.key)
- self.myButton.setFont(font)
- if __name__=="__main__":
- import sys
- app=QtWidgets.QApplication(sys.argv)
- myshow=MyWindow()
- myshow.show()
- sys.exit(app.exec_())
9、Qt Designer 主窗口MainWindows
再次回到Qt Designer。
MainWindows 即主窗口,主要包含了菜单栏、工具栏、任务栏等。双击菜单栏上的“在这里输入”,然后录入文字,回车即可。注意要用回车键确认。
对于一级菜单,我们可以通过输入 文件(&F) 编辑(&E) 来加入菜单的快捷按钮,在预览中按alt+m alt+e 可以看到效果.
而子菜单可以通过动作编辑器或者属性编辑器中的shortcut来添加快捷键.
、
双击需要编辑的动作,可以对其进行设置并添加图标、快捷键等等。
工具栏,默认在PyQt5中不显示工具栏,我们可以点击右键来添加工具栏,通过属性编辑器我们可以修改图标大小。而工具栏上面的图标,我们可以通过动作编辑器建立并拖入工具栏中
将ui文件和qrc文件生成对应的python源码,给菜单中的打开添加个文件对话框- from PyQt5 import QtWidgets
- from untitled import Ui_MainWindow
- from PyQt5.QtWidgets import QFileDialog
- class MyWindow(QtWidgets.QMainWindow,Ui_MainWindow):
- def __init__(self):
- super(MyWindow,self).__init__()
- self.setupUi(self)
- self.fileOpen.triggered.connect(self.openMsg) #菜单的点击事件是triggered
- def openMsg(self):
- file,ok=QFileDialog.getOpenFileName(self,"打开","C:/","All Files (*);;Text Files (*.txt)")
- self.statusbar.showMessage(file) #在状态栏显示文件地址
- if __name__=="__main__":
- import sys
- app=QtWidgets.QApplication(sys.argv)
- myshow=MyWindow()
- myshow.show()
- sys.exit(app.exec_())
10、主窗口动态加载Widget
编写代码
- from PyQt5 import QtWidgets
- from MainForm import Ui_MainForm
- from Children import Ui_Form
- from PyQt5.QtWidgets import QFileDialog
- class MainForm(QtWidgets.QMainWindow,Ui_MainForm):
- def __init__(self):
- super(MainForm,self).__init__()
- self.setupUi(self)
- self.child=ChildrenForm() #self.child = children()生成子窗口实例self.child
- self.fileOpen.triggered.connect(self.openMsg) #菜单的点击事件是triggered
- self.fileClose.triggered.connect(self.close)
- self.actionTst.triggered.connect(self.childShow) #点击actionTst,子窗口就会显示在主窗口的MaingridLayout中
- def childShow(self):
- self.MaingridLayout.addWidget(self.child) #添加子窗口
- self.child.show()
- def openMsg(self):
- file,ok=QFileDialog.getOpenFileName(self,"打开","C:/","All Files (*);;Text Files (*.txt)")
- self.statusbar.showMessage(file) #在状态栏显示文件地址
- class ChildrenForm(QtWidgets.QWidget,Ui_Form):
- def __init__(self):
- super(ChildrenForm,self).__init__()
- self.setupUi(self)
- if __name__=="__main__":
- import sys
- app=QtWidgets.QApplication(sys.argv)
- myshow=MainForm()
- myshow.show()
- sys.exit(app.exec_()
11、pyqt线程间通信
PyQt的窗体控件类已经有很多的内置信号,开发者也可以添加自己的自定义信号,信号槽有如下特点:
- 一个信号可以连接到许多插槽。
- 一个信号也可以连接到另一个信号。
- 信号参数可以是任何Python类型。
- 一个插槽可以连接到许多信号。
- 连接可能会直接(即同步)或排队(即异步)。
- 连接可能会跨线程。
- 信号可能会断开
(以上几条特点翻译于官方文档),接下来,我将以若干个实例,来体现以上几个特点。
(1)内置信号槽的使用
- from PyQt5.QtWidgets import *
- from PyQt5.QtCore import *
- def sinTest():
- btn.setText("按钮文本改变")
- app = QApplication([])
- main = QWidget()
- main.resize(200,100)
- btn = QPushButton("按钮文本",main)
- ##按钮btn的内置信号连接名为sinTest的槽
- btn.clicked.connect(sinTest)
- main.show()
- app.exec_()
(2)自定义信号槽的使用
- class SinClass(QObject):
- ##声明一个无参数的信号
- sin1 = pyqtSignal()
- ##声明带一个int类型参数的信号
- sin2 = pyqtSignal(int)
- ##声明带一个int和str类型参数的信号
- sin3 = pyqtSignal(int,str)
- ##声明带一个列表类型参数的信号
- sin4 = pyqtSignal(list)
- ##声明带一个字典类型参数的信号
- sin5 = pyqtSignal(dict)
- ##声明一个多重载版本的信号,包括了一个带int和str类型参数的信号,以及带str参数的信号
- sin6 = pyqtSignal([int,str], [str])
- def __init__(self,parent=None):
- super(SinClass,self).__init__(parent)
- ##信号连接到指定槽
- self.sin1.connect(self.sin1Call)
- self.sin2.connect(self.sin2Call)
- self.sin3.connect(self.sin3Call)
- self.sin4.connect(self.sin4Call)
- self.sin5.connect(self.sin5Call)
- self.sin6[int,str].connect(self.sin6Call)
- self.sin6[str].connect(self.sin6OverLoad)
- ##信号发射
- self.sin1.emit()
- self.sin2.emit(1)
- self.sin3.emit(1,"text")
- self.sin4.emit([1,2,3,4])
- self.sin5.emit({"name":"codeio","age":"25"})
- self.sin6[int,str].emit(1,"text")
- self.sin6[str].emit("text")
- def sin1Call(self):
- print("sin1 emit")
- def sin2Call(self,val):
- print("sin2 emit,value:",val)
- def sin3Call(self,val,text):
- print("sin3 emit,value:",val,text)
- def sin4Call(self,val):
- print("sin4 emit,value:",val)
- def sin5Call(self,val):
- print("sin5 emit,value:",val)
- def sin6Call(self,val,text):
- print("sin6 emit,value:",val,text)
- def sin6OverLoad(self,val):
- print("sin6 overload emit,value:",val)
- sin = SinClass()
sin1 emit
sin2 emit,value: 1
sin3 emit,value: 1 text
sin4 emit,value: [1, 2, 3, 4]
sin5 emit,value: {'age': '25', 'name': 'codeio'}
sin6 emit,value: 1 text
sin6 overload emit,value: text
- from PyQt5.QtWidgets import *
- from PyQt5.QtCore import *
- class SinClass(QObject):
- ##声明一个无参数的信号
- sin1 = pyqtSignal()
- ##声明带一个int类型参数的信号
- sin2 = pyqtSignal(int)
- def __init__(self,parent=None):
- super(SinClass,self).__init__(parent)
- ##信号sin1连接到sin1Call和sin2Call这两个槽
- self.sin1.connect(self.sin1Call)
- self.sin1.connect(self.sin2Call)
- ##信号sin2连接到信号sin1
- self.sin2.connect(self.sin1)
- ##信号发射
- self.sin1.emit()
- self.sin2.emit(1)
- ##断开sin1、sin2信号与各槽的连接
- self.sin1.disconnect(self.sin1Call)
- self.sin1.disconnect(self.sin2Call)
- self.sin2.disconnect(self.sin1)
- ##信号sin1和sin2连接同一个槽sin1Call
- self.sin1.connect(self.sin1Call)
- self.sin2.connect(self.sin1Call)
- ##信号再次发射
- self.sin1.emit()
- self.sin2.emit(1)
- def sin1Call(self):
- print("sin1 emit")
- def sin2Call(self):
- print("sin2 emit")
- sin = SinClass()
sin1 emit
sin2 emit
sin1 emit
sin2 emit
sin1 emit
sin1 emit
(4)多线程信号槽通信
- from PyQt5.QtWidgets import *
- from PyQt5.QtCore import *
- class Main(QWidget):
- def __init__(self, parent = None):
- super(Main,self).__init__(parent)
- ##创建一个线程实例并设置名称、变量、信号槽
- self.thread = MyThread()
- self.thread.setIdentity("thread1")
- self.thread.sinOut.connect(self.outText)
- self.thread.setVal(6)
- def outText(self,text):
- print(text)
- class MyThread(QThread):
- sinOut = pyqtSignal(str)
- def __init__(self,parent=None):
- super(MyThread,self).__init__(parent)
- self.identity = None
- def setIdentity(self,text):
- self.identity = text
- def setVal(self,val):
- self.times = int(val)
- ##执行线程的run方法
- self.start()
- def run(self):
- while self.times > 0 and self.identity:
- ##发射信号
- self.sinOut.emit(self.identity+" "+str(self.times))
- self.times -= 1
- app = QApplication([])
- main = Main()
- main.show()
- app.exec_()
thread1 6
thread1 5
thread1 4
thread1 3
thread1 2
thread1 1
12、初识pyqt多线程操作
首先来看一个例子:
- # coding=utf-8
- __author__ = 'a359680405'
- from PyQt5.QtCore import *
- from PyQt5.QtGui import *
- from PyQt5.QtWidgets import *
- global sec
- sec=0
- def setTime():
- global sec
- sec+=1
- lcdNumber.display(sec) #LED显示数字+1
- def work():
- timer.start(1000) #计时器每秒计数
- for i in range(2000000000):
- pass
- timer.stop()
- app=QApplication([])
- top=QWidget()
- layout=QVBoxLayout(top) #垂直布局类QVBoxLayout;
- lcdNumber=QLCDNumber() #加个显示屏
- layout.addWidget(lcdNumber)
- button=QPushButton("测试")
- layout.addWidget(button)
- timer=QTimer()
- timer.timeout.connect(setTime) #每次计时结束,触发setTime
- button.clicked.connect(work)
- top.show()
- app.exec()
有经验的开发者立即指出,这里需要使用线程。这是因为 Qt 中所有界面都是在 UI 线程中(也被称为主线程,就是执行了QApplication::exec()的线程),在这个线程中执行耗时的操作(比如那个循环),就会阻塞 UI 线程,从而让界面停止响应。界面停止响应,用户体验自然不好,不过更严重的是,有些窗口管理程序会检测到你的程序已经失去响应,可能会建议用户强制停止程序,这样一来你的程序可能就此终止,任务再也无法完成。所以,为了避免这一问题,我们要使用 QThread 开启一个新的线程:
- # coding=utf-8
- __author__ = 'a359680405'
- from PyQt5.QtCore import *
- from PyQt5.QtGui import *
- from PyQt5.QtWidgets import *
- global sec
- sec=0
- class WorkThread(QThread):
- trigger = pyqtSignal()
- def __int__(self):
- super(WorkThread,self).__init__()
- def run(self):
- for i in range(203300030):
- pass
- self.trigger.emit() #循环完毕后发出信号
- def countTime():
- global sec
- sec+=1
- lcdNumber.display(sec) #LED显示数字+1
- def work():
- timer.start(1000) #计时器每秒计数
- workThread.start() #计时开始
- workThread.trigger.connect(timeStop) #当获得循环完毕的信号时,停止计数
- def timeStop():
- timer.stop()
- print("运行结束用时",lcdNumber.value())
- global sec
- sec=0
- app=QApplication([])
- top=QWidget()
- layout=QVBoxLayout(top) #垂直布局类QVBoxLayout;
- lcdNumber=QLCDNumber() #加个显示屏
- layout.addWidget(lcdNumber)
- button=QPushButton("测试")
- layout.addWidget(button)
- timer=QTimer()
- workThread=WorkThread()
- button.clicked.connect(work)
- timer.timeout.connect(countTime) #每次计时结束,触发setTime
- top.show()
- app.exec()
我增加了一个WorkerThread类。WorkerThread继承自QThread类,重写了其run()函数。可以认为,run()函数就是新的线程需要执行的代码。在这里就是要执行这个循环,然后发出计算完成的信号。而在按钮点击的槽函数中,使用work()中的workThread.start()函数启动一个线程(注意,这里不是run()函数)。再次运行程序,你会发现现在界面已经不会被阻塞了
13、PyQt 线程相关类
正如前面所说,要使用QThread开始一个线程,我们可以创建它的一个子类,然后覆盖其QThread.run()函数:
- class Thread(QThread):
- def __init__(self):
- super(Thread,self).__init__()
- def run(self):
- pass #线程相关代码
然后我们这样新建一个新的线程
- thread=Thread()
- thread.start()
QRunnable是我们要介绍的第二个类。这是一个轻量级的抽象类,用于开始一个另外线程的任务。这种任务是运行过后就丢弃的。由于这个类是抽象类,我们需要继承QRunnable,然后重写其纯虚函数QRunnable.run():
- class Task(QRunnable):
- def __init__(self):
- super(QRunnable,self).__init__()
- def run(self):
- pass #线程相关代码
- task=Task()
- QThreadPool.globalInstance().start(task)
QtConcurrent是我们要介绍的最后一个对象。这是一个高级 API,构建于QThreadPool之上,用于处理大多数通用的并行计算模式:map、reduce 以及 filter。它还提供了QtConcurrent.run()函数,用于在另外的线程运行一个函数。注意,QtConcurrent是一个命名空间而不是一个类,因此其中的所有函数都是命名空间内的全局函数。
不同于QThread和QRunnable,QtConcurrent不要求我们使用低级同步原语:所有的QtConcurrent都返回一个QFuture对象。这个对象可以用来查询当前的运算状态(也就是任务的进度),可以用来暂停/回复/取消任务,当然也可以用来获得运算结果。注意,并不是所有的QFuture对象都支持暂停或取消的操作。比如,由QtConcurrent.run()返回的QFuture对象不能取消,但是由QtConcurrent.mappedReduced()返回的是可以的。QFutureWatcher类则用来监视QFuture的进度,我们可以用信号槽与QFutureWatcher进行交互(注意,QFuture也没有继承QObject)。
下面我们可以对比一下上面介绍过的三种类:
特性 | QThread |
QRunnable |
QtConcurrent |
高级 API | ✘ | ✘ | ✔ |
面向任务 | ✘ | ✔ | ✔ |
内建对暂停/恢复/取消的支持 | ✘ | ✘ | ✔ |
具有优先级 | ✔ | ✘ | ✘ |
可运行事件循环 | ✔ | ✘ | ✘ |
14、pyqt事件循环
进一步说,对于一个类,如果不同的实例可以被不同线程同时使用而不受影响,就说这个类是可重入的;如果这个类的所有成员函数都可以被不同线程同时调用而不受影响,即使这些调用针对同一个对象,那么我们就说这个类是线程安全的。由此可以看出,线程安全的语义要强于可重入。接下来,我们从事件开始讨论。在 PyQt 中,事件由一个普通对象表示(QEvent或其子类)。这是事件与信号的一个很大区别:事件总是由某一种类型的对象表示,针对某一个特殊的对象,而信号则没有这种目标对象。所有QObject的子类都可以通过覆盖QObject::event()函数来控制事件的对象。
事件可以由程序生成,也可以在程序外部生成。例如:
QKeyEvent和QMouseEvent对象表示键盘或鼠标的交互,通常由系统的窗口管理器产生;
QTimerEvent事件在定时器超时时发送给一个QObject,定时器事件通常由操作系统发出;
QChildEvent在增加或删除子对象时发送给一个QObject,这是由 Qt 应用程序自己发出的。
需要注意的是,与信号不同,事件并不是一产生就被分发。事件产生之后被加入到一个队列中(这里的队列含义同数据结构中的概念,先进先出),该队列即被称为事件队列。事件分发器遍历事件队列,如果发现事件队列中有事件,那么就把这个事件发送给它的目标对象。这个循环被称作事件循环。事件循环的伪代码描述大致如下所示:
- while (is_active)
- {
- while (!event_queue_is_empty) {
- dispatch_next_event();
- }
- wait_for_more_events();
- }
伪代码里面的while会遍历整个事件队列,发送从队列中找到的事件;wait_for_more_events()函数则会阻塞事件循环,直到又有新的事件产生。我们仔细考虑这段代码,在wait_for_more_events()函数所得到的新的事件都应该是由程序外部产生的。因为所有内部事件都应该在事件队列中处理完毕了。因此,我们说事件循环在wait_for_more_events()函数进入休眠,并且可以被下面几种情况唤醒:
- 窗口管理器的动作(键盘、鼠标按键按下、与窗口交互等);
- 套接字动作(网络传来可读的数据,或者是套接字非阻塞写等);
- 定时器;
- 由其它线程发出的事件(我们会在后文详细解释这种情况)。
在类 UNIX 系统中,窗口管理器(比如 X11)会通过套接字(Unix Domain 或 TCP/IP)向应用程序发出窗口活动的通知,因为客户端就是通过这种机制与 X 服务器交互的。如果我们决定要实现基于内部的socketpair(2)函数的跨线程事件的派发,那么窗口的管理活动需要唤醒的是:
- 套接字 socket
- 定时器 timer
至于为什么需要事件循环,我们可以简单列出一个清单:
- 组件的绘制与交互:QWidget.paintEvent()会在发出QPaintEvent事件时被调用。该事件可以通过内部QWidget.update()调用或者窗口管理器(例如显示一个隐藏的窗口)发出。所有交互事件(键盘、鼠标)也是类似的:这些事件都要求有一个事件循环才能发出。
- 定时器:长话短说,它们会在select(2)或其他类似的调用超时时被发出,因此你需要允许 Qt 通过返回事件循环来实现这些调用。
- 网络:所有低级网络类(QTcpSocket、QUdpSocket以及QTcpServer等)都是异步的。当你调用read()函数时,它们仅仅返回已可用的数据;当你调用write()函数时,它们仅仅将写入列入计划列表稍后执行。只有返回事件循环的时候,真正的读写才会执行。注意,这些类也有同步函数(以waitFor开头的函数),但是它们并不推荐使用,就是因为它们会阻塞事件循环。高级的类,例如QNetworkAccessManager则根本不提供同步 API,因此必须要求事件循环。
- QApplication.exec()
- […]
- QWidget.event()
- Button.mousePressEvent()
- Button.clicked()
- […]
- Worker.doWork()
在事件就此卡住时,组件也不会更新自身(因为QPaintEvent对象还在队列中),也不会有其它什么交互发生(还是同样的原因),定时器也不会超时并且网络交互会越来越慢直到停止。也就是说,前面我们大费周折分析的各种依赖事件循环的活动都会停止。这时候,需要窗口管理器会检测到你的应用程序不再处理任何事件,于是告诉用户你的程序失去响应。这就是为什么我们需要快速地处理事件,并且尽可能快地返回事件循环。
现在,重点来了:我们不可能避免业务逻辑中的耗时操作,那么怎样做才能既可以执行那些耗时的操作,又不会阻塞事件循环呢?一般会有三种解决方案:第一,我们将任务移到另外的线程(正如我们上一章看到的那样,不过现在我们暂时略过这部分内容);第二,我们手动强制运行事件循环。想要强制运行事件循环,我们需要在耗时的任务中一遍遍地调用Application.processEvents()函数。Application.processEvents()函数会发出事件队列中的所有事件,并且立即返回到调用者。仔细想一下,我们在这里所做的,就是模拟了一个事件循环;另外一种解决方案我们在前面的章节提到过:使用QEventLoop类重新进入新的事件循环。通过调用QEventLoop.exec()函数,我们重新进入新的事件循环,给QEventLoop.quit()槽函数发送信号则退出这个事件循环。
15、PyQt信号和槽传递额外参数
使用Pyqt编程过程中,经常会遇到给槽函数传递额外参数的情况。但是信号-槽机制只是指定信号如何连接到槽,信号定义的参数被传递给槽,而额外的参数(用户定义)不能直接传递。
而传递额外参数又是很有用处。你可能使用一个槽处理多个组件的信号,有时要传递额外的信息。
一种方法是使用lambda表达式。
- from PyQt4.QtCore import *
- from PyQt4.QtGui import *
- class MyForm(QMainWindow):
- def __init__(self, parent=None):
- super(MyForm, self).__init__(parent)
- button1 = QPushButton('Button 1')
- button2 = QPushButton('Button 1')
- button1.clicked.connect(lambda: self.on_button(1))
- button2.clicked.connect(lambda: self.on_button(2))
- layout = QHBoxLayout()
- layout.addWidget(button1)
- layout.addWidget(button2)
- main_frame = QWidget()
- main_frame.setLayout(layout)
- self.setCentralWidget(main_frame)
- def on_button(self, n):
- print('Button {0} clicked'.format(n))
- if __name__ == "__main__":
- import sys
- app = QApplication(sys.argv)
- form = MyForm()
- form.show()
- app.exec_()
解释一下,on_button是怎样处理从两个按钮传来的信号。我们使用lambda传递按钮数字给槽,也可以传递任何其他东西---甚至是按钮组件本身(假如,槽打算把传递信号的按钮修改为不可用)
第2个方法是使用functools里的partial函数。
- button1.clicked.connect(partial(self.on_button, 1))
- button2.clicked.connect(partial(self.on_button, 2))
哪个办法好一点?这个属于风格的问题。个人观点,喜欢lambda,条理清楚,而且灵活。
《Rapid GUI Program with Python and QT》 P143例子。
- from PyQt4.QtCore import *
- from PyQt4.QtGui import *
- from functools import partial
- import sys
- class Bu1(QWidget):
- def __init__(self, parent=None):
- super(Bu1, self).__init__(parent)
- #水平盒式布局
- layout = QHBoxLayout()
- #显示
- self.lbl = QLabel('no button is pressed')
- #循环5个按钮
- for i in range(5):
- but = QPushButton(str(i))
- layout.addWidget(but)
- #信号和槽连接
- but.clicked.connect(self.cliked)
- #使用封装,lambda
- but = QPushButton('5')
- layout.addWidget(but)
- but.clicked.connect(lambda: self.on_click('5'))
- #使用个who变量,结果不正常,显示 False is pressed
- #but.clicked.connect(lambda who="5": self.on_click(who))
- #使用封装,partial函数
- but = QPushButton('6')
- layout.addWidget(but)
- but.clicked.connect(partial(self.on_click, '6'))
- layout.addWidget(self.lbl)
- #设置布局
- self.setLayout(layout)
- #传递额外参数
- def cliked(self):
- bu = self.sender()
- if isinstance(bu, QPushButton):
- self.lbl.setText('%s is pressed' % bu.text())
- else:
- self.lbl.setText('no effect')
- def on_click(self, n):
- self.lbl.setText('%s is pressed' % n)
- if __name__ == '__main__':
- app = QApplication(sys.argv)
- bu =Bu1()
- bu.show()
-
app.exec_()
16.QT Designer界面和逻辑分离 实例
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'untitled1.ui'
#
# Created by: PyQt5 UI code generator 5.5
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object): #创建窗体类MainWindow,继承object
def setupUi(self, MainWindow): #创建setupUi函数
MainWindow.setObjectName("MainWindow") #设置窗体名
MainWindow.resize(434, 390) #设置窗体的大小
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.label = QtWidgets.QLabel(self.centralwidget)
self.label.setGeometry(QtCore.QRect(80, 100, 54, 12))
self.label.setObjectName("label")
self.pushButton = QtWidgets.QPushButton(self.centralwidget)
self.pushButton.setGeometry(QtCore.QRect(70, 170, 75, 23))
self.pushButton.setObjectName("pushButton")
#MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 434, 23))
self.menubar.setObjectName("menubar")
#MainWindow.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(MainWindow)
self.statusbar.setObjectName("statusbar")
#MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow) # 加载retranslateUi函数,只是做一系列的 setText、translate 等调用
QtCore.QMetaObject.connectSlotsByName(MainWindow) #关联信号槽
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.label.setText(_translate("MainWindow", "文本测试"))
self.pushButton.setText(_translate("MainWindow", "按钮"))
__author__ = 'zd'
from PyQt5 import QtWidgets,QtCore
from PtQt import Ui_MainWindow
import sys,time
class mydesignshow(QtWidgets.QWidget,Ui_MainWindow): #同时继承QWidget和Ui_MainWindow
_mysignal = QtCore.pyqtSignal() #定义信号
_mysignal1 = QtCore.pyqtSignal(str) #定义含参数信号
def __init__(self): # 析构函数,实例化后默认加载
super(mydesignshow,self).__init__() #超级加载
self.setupUi(self)
self.pushButton.clicked.connect(self.prn) #调用槽函数prn,注意槽函数不需要加()
self._mysignal.connect(self.mysignalslot) #将信号连接至槽
self._mysignal1.connect(self.mysignalslot1) #将信号连接至槽
def prn(self): #定义槽函数
print("按钮按下信号")
time.sleep(1)
print("延时1秒")
self._mysignal.emit() #发射信号
self._mysignal1.emit("有参信号") #发射带参信号
def mysignalslot(self): #定义自己的槽函数
print("无参信号")
def mysignalslot1(self,parameter): #定义自己的槽函数
print(parameter)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv) #程序入口
myshow = mydesignshow() #实例化
myshow.show() #调用继承QWidget中的show方法
sys.exit(app.exec_())