前言
上期教程我们介绍了 简单的代码封装。
这期教程来进行更加有用的实战案例,以求加深对 Qt 运用的理解。
基础运行环境搭建
下面我们重新建一个类,来实现近似于 Qt Designer 的 Label 双击效果。
首先我们先新建一个 Python 脚本,可以名为 My_Label.py ,将脚本放到 我的文档/maya/scripts 目录下。
使用 Qt 的模块来兼容老版本的 Maya (需要将 Qt.py
放入到 我的文档/maya/scripts 目录下)
My_Lable 文件下的代码
1 2 3 4 5 6 7 8 9
| from Qt.QtCore import * from Qt.QtGui import * from Qt.QtWidgets import *
class My_Label(QLabel): def __init__(self): super(My_Label,self).__init__() self.setText("My_Label")
|
Maya 脚本编辑器下的代码
1 2 3 4 5
| from My_Label import My_Label reload(My_Label) a = My_Label() a.show()
|
双击事件
下面我们来实现一下双击事件。
由于 QLabel 不存在双击事件的信号槽(可以通过虚函数添加一个自定义的信号槽)
因此我们需要借助虚函数来实现双击效果,而这个函数是 mouseDoubleClickEvent
另外还需要添加一个 QLineEdit 来实现双击修改的效果。
我们来继续完善 My_Lable 文件下的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| from PySide2.QtCore import * from PySide2.QtGui import * from PySide2.QtWidgets import *
class My_Label(QLabel): def __init__(self): super(My_Label,self).__init__() self.setText("My_Label") self.edit = QLineEdit(self) self.edit.setVisible(False)
def mouseDoubleClickEvent(self,event): self.edit.setVisible(True)
|
似乎效果还相差甚远呀。我们先来理清楚要实现的需求,然后一步一步去完成。
- 双击添加鼠标左键判断 (目前鼠标三键的双击都可以触发函数)
- 根据当前组件大小调整 QLineEdit 大小
- 点击任意位置修改 My_Label 上的内容
- 点击任意位置隐藏 QLineEdit
- 添加 enable 函数来开启双击交互效果
双击添加鼠标左键判断
事件判断可以通过 虚函数传入的 QMouseEvent
进行处理
在API文档可以看到 QMouseEvent
有 button()
方法,可以获取到当前的按键。
我们可以将获取到的信息打印出来。
可以看到打印出来的是 MidButton
LeftButton
RightButton
这些都对应到当前的鼠标点击按键。
可以查找 API文档
Qt 内置了以上这些按钮的触发。
通过判断传入的 event 就可以判断是否是鼠标左键点击。
1 2 3 4 5
| def mouseDoubleClickEvent(self,event): if event.button() == Qt.MouseButton.LeftButton: self.edit.setVisible(True)
|
QLineEdit 大小调整
QLineEdit 大小固定在 Label 上,是因为没有给它添加 Layout
这种状态就和 Qt Designer 没有添加 QWidget 的组件一样,属于随意摆放的组件。
因此我们将它添加到 Layout 中就可以解决大小的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| def __init__(self): super(My_Label,self).__init__() self.setText("My_Label")
self.edit = QLineEdit() self.edit.setVisible(False)
self.layout = QVBoxLayout(self) self.layout.addWidget(self.edit)
|
修改 My_Lable 内容
隐藏组件需要获取一个事件,当我们点击其它位置的时候可以触发的事件。
我们可以去查 QLineEdit 的 API文档
可以找到 QLineEdit 有 editingFinished
信号槽,
借助这个信号槽我们可以实现部分效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| def __init__(self): super(My_Label,self).__init__() self.setText("My_Label")
self.edit = QLineEdit() self.edit.setVisible(False) self.edit.editingFinished.connect(self.__inputComplete)
self.layout = QVBoxLayout(self) self.layout.addWidget(self.edit)
def __inputComplete(self): '''函数前添加双下划线让方法作为私有方法,不提供给外部调用''' self.edit.setVisible(False) self.setText(self.edit.text())
|
这样只要在 edit 上输入内容,然后按键盘 enter
键就可以实现隐藏 edit 并且将 edit 的内容添加到 My_Label 上
但是这样无法实现点击任意位置隐藏 edit 的效果。
其实也不全然只能通过按 enter
键才可以隐藏, editingFinished
信号槽和 focus 事件相关,只要 focus 转移也可以实现隐藏。
我们可以将 Maya 中的运行代码做些修改,多添加一些组件进行测试。
Maya 运行代码
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
| import My_Label reload(My_Label) from My_Label import My_Label from PySide2.QtCore import * from PySide2.QtWidgets import * from PySide2.QtGui import *
win = QWidget() win.resize(300,150)
layout = QVBoxLayout(win)
my_label = My_Label() my_label.setText("My_Label") label = QLabel() label.setText("QLabel") edit = QLineEdit()
layout.addWidget(my_label) layout.addWidget(label) layout.addWidget(edit)
win.show()
|
My_Label 代码
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
| from Qt.QtCore import * from Qt.QtGui import * from Qt.QtWidgets import *
class My_Label(QLabel):
editable = False def __init__(self): super(My_Label,self).__init__() self.setText("My_Label")
self.edit = QLineEdit() self.edit.setVisible(False) self.edit.editingFinished.connect(self.__inputComplete)
self.layout = QVBoxLayout(self) self.layout.addWidget(self.edit)
def __inputComplete(self): '''函数前添加双下划线让方法作为私有方法,不提供给外部调用''' self.edit.setVisible(False) self.setText(self.edit.text())
def mouseDoubleClickEvent(self,event): if event.button() == Qt.MouseButton.LeftButton: self.edit.setVisible(True)
|
隐藏 QLineEdit - 方案一
上面虽然实现了 组件focus跳转的时候 隐藏自身。
却依旧无法实现点击窗口任意位置隐藏 QLineEdit。
我们要实现的是 如果没有点击到组件自身就要隐藏的效果。
如果学习过前端 JS 的事件处理就会知道,通过 document
的点击可以获取到全局的点击,还可以通过 preventPropagation
来阻止冒泡从而实现不点击自身的事件反馈。
然而我并没有在 Qt 中找到简单的实现方法。
因此下面提到的方法会比较复杂,如果看不懂的话可以暂行跳过这个部分。
我们先来分析要实现点击任意位置需要怎样的条件。
首先我们需要获取一个全局事件的方法,因为组件当中的虚函数只有在组件被触发时才奏效,组件自身无法实现全局响应的。
但是 Qt 的全局事件点击是怎么处理的呢?
一开始我经过了大量的搜索也没有找到 Qt 的全局事件处理的相关方法,因此只能退而求其次,给所有的组件添加一个处理点击的回调函数来实现这个功能。
也就是说 通过一个函数遍历窗口中所有的组件,然后给每个组件的 mousePressEvent 添加点击处理函数,这样就可以实现伪全局的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from functools import partial
def __eventHandler(self,widget,event): if self.edit.isVisible() and widget != self.edit: self.__inputComplete() def setPressEvent(self,win): if hasattr(win,"mousePressEvent"): win.mousePressEvent = partial(self.__eventHandler,win,win.mousePressEvent) for child in win.children(): self.setPressEvent(child)
|
由于组件的children是通过嵌套方式存储在各个组件的的 children() 方法当中的,因此不能用简单的 for 循环来实现所有组件的遍历。
这里需要使用递归的方式来实现。 递归-百度百科
递归,就是在运行的过程中调用自己。
-> 我们创建了一个 setPressEvent 方法,不断在children for循环中调用自己,就可以实现遍历一个组件下所有的子组件。
这里点击函数是通过复写的形式实现的。
国外的大大其实非常不推荐使用这种方式来改写函数的方法,一些代码处理工具没有办法识别这种修改,代码的可读性也会大打折扣。 参考链接
正确的写法应该通过类来继承。
但是这里所有的child都已经实例化了。
再加上并不是所有的组件都有触发点击的信号槽的,而继承QWidget的组件都有 mousePressEvent
虚函数的,因此我只想到了这种不太好的操作。
在函数内部不好获取当前调用的 __eventHandler 是哪个组件触发的,我们可以将触发的组件作为参数传入到函数中。
复写函数是无法修改传参的,那这种效果要如何实现呢?
我们可以利用python自带的包 from functools import partial
patial 来实现,同样在使用信号槽的时候也可以用这种黑科技实现槽函数的传参。 partial内核原理分析
其实上述方案是有 BUG 的,由于复写函数的原因,如果有多个 My_Label 实例存在的话就会互相覆盖。
我最初想到的方案就是类似于 super 的方法来继承这个方法的调用。
当然因为这里已经实例化当然无法调用 super 方法,因此我想到的是直接 调用在 __eventHandler
第一行加上 widget.mousePressEvent(event)
然而这样执行的话会报错!!!
原因是 mousePressEvent 已经被复写了,所以在这里直接调用就会陷入递归循环。
所以这里的处理必须是执行被复写之前的函数,因此我想到的方案是通过传参的形式加进来。
下面是完整代码。
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
| from Qt.QtCore import * from Qt.QtGui import * from Qt.QtWidgets import * from functools import partial
class My_Label(QLabel):
editable = False def __init__(self): super(My_Label,self).__init__() self.setText("My_Label")
self.edit = QLineEdit() self.edit.setVisible(False) self.edit.editingFinished.connect(self.__inputComplete)
self.layout = QVBoxLayout(self) self.layout.addWidget(self.edit)
def __inputComplete(self): '''函数前添加双下划线让方法作为私有方法,不提供给外部调用''' self.edit.setVisible(False) self.setText(self.edit.text())
def mouseDoubleClickEvent(self,event): if event.button() == Qt.MouseButton.LeftButton: self.edit.setVisible(True) def __eventHandler(self,widget,pressEvent,event): pressEvent(event) if self.edit.isVisible() and widget != self.edit: self.__inputComplete() def setPressEvent(self,win): if hasattr(win,"mousePressEvent"): win.mousePressEvent = partial(self.__eventHandler,win,win.mousePressEvent) for child in win.children(): self.setPressEvent(child)
|
maya 的运行上可以多添加一些 My_Label 来测试多个实例的运行情况。
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
| import My_Label reload(My_Label) from My_Label import My_Label from PySide2.QtCore import * from PySide2.QtWidgets import * from PySide2.QtGui import *
win = QWidget() win.resize(300,150)
layout = QVBoxLayout(win)
my_label = My_Label() my_label2 = My_Label()
label = QLabel() label.setText("QLabel") edit = QLineEdit()
layout.addWidget(my_label) layout.addWidget(edit) layout.addWidget(label) layout.addWidget(my_label2)
win.show()
my_label.setPressEvent(win) my_label2.setPressEvent(win)
|
可以看到这个方案初步实现了 Qt Designer 的双击修改,点击任意位置确认修改的效果。
但是使用起来会有很多的限制,是非常不方便的。
setPressEvent(win)
函数必须要添加了所有的组件之后才可以触发,否则递归处理就会缺少了部分组件而导致BUG。
交互以及UI细节上依旧存在差别(在下一个方案中提供解决方案)
另外这里也还没有完成处理判断,一旦调用了 setPressEvent
就无法取消组件的效果。
当然这个无法取消的问题是可以添加额外的判断来修复的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| def setEditable(self,editable,parent=None): if parent == None: if self.parent() != None: parent = self.parent() else: raise Exception("parent is None")
if editable: win = parent.window() self.__setPressEvent(win) self.editable = True else: self.editable = False
|
这部分的代码就是我仿造 PyQt 内核的设置函数的编辑方法,可以实现传入布尔参数来开启和禁用事件调用。
也可以通过 QWidget
下的 window
方法来获取最顶层的窗口。
当然执行这个方法的时候,如果组件没有依附到任何窗口上,那么就无法递归,也就无法实现全局点击事件的效果,因此我抛出了错误。
以上方案是我最初写组件的实现方案。
在写教程的过程中,我觉得这种实现方式非常不优雅,自己很不甘心就这么留着坑不填,于是我重新研究了实现方案。
扩展说明
隐藏 QLineEdit - 方案二
回到最初想要实现的效果,最让我疑惑的是 Qt 内部的全局事件处理。
尽管虚函数是满足相关条件之后才会被触发的,但是根据 Qt 的事件处理机制。
应该有一个专门的地方来处理所有的被调用的事件才对的,于是我以这个为突破口,在网上有查了不少资料。
功夫不负有心人,我还是找到了一些有用的线索。 参考
参考提问者自己提出的解决方案,可以知道 QApplication 有 notify 方法,这个方法就是一个全局事件处理的相关函数。
于是我尝试继承一个 自己的 QApplication 类来实现效果。
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
| from PySide2.QtCore import * from PySide2.QtGui import * from PySide2.QtWidgets import *
class MyApp(QApplication): def notify(self, receiver, event): if event.type() == 2: print (receiver) return super(MyApp, self).notify(receiver, event)
if __name__ == "__main__": import sys app = MyApp(sys.argv) win = QWidget() layout = QVBoxLayout(win)
label = QLabel() label.setText('test') edit = QLineEdit() combo = QComboBox()
layout.addWidget(label) layout.addWidget(combo) layout.addWidget(edit) win.show()
app.exec_()
|
经过测试可以看到 notify 确实是一个全局事件处理器,而且 PyQt4 也可以正确调用(event.type()
需要改为数字进行识别)
然而兴奋瞬间就被冷水当头了,因为 QApplication 只能存在一个。
所以在 Maya 环境下 QApplication 已经是实例了,这也就是为什么 在 Maya 中运行不能声明 QApplication() 参考这里
所以这里也只能采取上面的方法,用覆盖的方法。
但是 QApplication 的实例要怎么获取出来呢?
这个时候真是柳暗花明又一村呀!! 没想到 QApplication 就有 instance()
方法可以直接获取到当前运行的实例。
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
| from PySide2.QtCore import * from PySide2.QtGui import * from PySide2.QtWidgets import * from functools import partial class My_Label(QLabel): editable = False def __init__(self): super(My_Label,self).__init__()
self.edit = QLineEdit() self.edit.setVisible(False) self.edit.setFrame(False) self.edit.editingFinished.connect(self.__inputComplete)
self.setText("My_Label") self.edit.setText("My_Label")
self.layout = QVBoxLayout(self) self.layout.setContentsMargins(0,0,0,0) self.layout.addWidget(self.edit)
app = QApplication.instance() app.notify = partial(self.__notifyClickEvent,app.notify)
def __notifyClickEvent(self, notify, receiver, event): if self.editable: if event.type() == QEvent.Type.MouseButtonPress: if type(receiver) != QWindow: if receiver != self.edit: self.__inputComplete() return notify(receiver, event)
def mouseDoubleClickEvent(self,event): if event.button() == 1 and self.editable: self.edit.setVisible(True) self.edit.setText(self.text()) self.edit.selectAll() self.edit.setFocus()
def __inputComplete(self): self.edit.setVisible(False) self.setText(self.edit.text())
def setEditable(self,editable): self.editable = True if editable else False
if __name__ == "__main__": import sys app = QApplication(sys.argv) win = QWidget() layout = QVBoxLayout(win)
bar = QLabel() bar.setText('test') label = My_Label() label.setEditable(True) edit = QLineEdit() combo = QComboBox()
layout.addWidget(bar) layout.addWidget(label) layout.addWidget(combo) layout.addWidget(edit) win.show()
app.exec_()
|
这里改写了双击事件的处理,让双击效果更接近 Qt Designer 的全选选择效果。
取消了 QLineedit 的边框更贴近 Qt Designer 的效果
之前在不拉动窗口双击会看不到 edit 组件,那是因为要放大窗口才可以看到,可以将 layout 的外边框设置为 0 即可。
在函数实现上,其实上面使用的方法和复写 mousePressEvent
一样的
只是复写了 QApplication 的 notify
方法而已。
由于复写的方法来自于 组件的类方法 ,因此之后也可以利用组件的属性作为判断条件。
这样我们只需要设置 setEditable
函数就可以达到开启和关闭双击效果。
上述的代码在 PySide2 上可以执行,但是在 PyQt4 下无法实现效果。
原因是 notify 函数并没有像 PySide2 那样调用起来。
更令人沮丧的是 Maya2018 的 PySide2 也无法将 notify 函数顺利跑起来。
扩展说明
- 如何在 maya 运行 Stackoverflow 里面的 PyQt 代码 参考这里
隐藏 QLineEdit - 方案三
上面的方案几乎是最完美的解决方案了,然而在临门一脚的时候却失败了。
难道就没有方法实现全局调用了吗?
我又开始在网上寻找答案,最后偶然间在这个Maya官方的技术博客中有了灵感。
我最初在方案一的阶段的时候,确实有尝试过 installEventFilter
的方法,然而这个方法和 所有的虚函数一样,如果安装在组件上的话,只能对组件进行过滤。
但是通过 方案二 的尝试之后,我猛然发现,如果 installEventFilter
放在 QApplication 上,岂不美哉。
不过说到底 installEventFilter
要怎么使用呢?
可以参考API文档
installEventFilter
需要传入 QObject 参数,不过这个东西 QWidget 是继承过来的。
因此可以直接将类实例传进去。
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
| from Qt.QtCore import * from Qt.QtGui import * from Qt.QtWidgets import * from functools import partial
class My_Label(QLabel):
editable = False def __init__(self): super(My_Label,self).__init__()
self.edit = QLineEdit() self.edit.setVisible(False) self.edit.editingFinished.connect(self.__inputComplete) self.edit.setStyleSheet("border:None")
self.layout = QVBoxLayout(self) self.layout.setContentsMargins(0,0,0,0) self.layout.addWidget(self.edit)
app = QApplication.instance() app.installEventFilter(self)
def eventFilter(self,receiver,event): if event.type() == 2 and self.editable and self.edit.isVisible(): if receiver == self: self.__inputComplete() return False
def __inputComplete(self): self.edit.setVisible(False) self.setText(self.edit.text())
def keyPressEvent(self,e): if e.key() == Qt.Key_Escape: self.__inputComplete()
def mouseDoubleClickEvent(self,event): if event.button() == 1 and self.editable: self.edit.setVisible(True) self.edit.setText(self.text()) self.edit.selectAll() self.edit.setFocus()
def setEditable(self,editable): self.editable = True if editable else False
|
可以看到这次实现了在maya当中双击修改QLabel的效果,交互感受和 Qt Designer 是一样的。
这里通过 edit 的 keyPressEvent 可以添加 Esc 键触发完成的效果。
另外这里的代码没有使用 setFrame(False)
的方法来取消边框。
而是使用了样式表的 setStyleSheet("border:None")
取消边框,可以实现相同的效果。
总结
这期教程我们介绍了 Qt Designer 的双击效果的实现效果
并且经过了我自己的大量探索,才最终实现了一个全局事件处理的效果。
本期教程前面不算很复杂,后面的全局事件的处理还是有点难度,如果是初学者需要好好消化。
这一次组件封装就实现了 MEL 语言绝对实现不了的效果,终于体现出了 Qt 的强大之处了。
在末尾我也引出了下一期教程要讲的主题 qss 样式。
敬请期待第五期教程吧。